Internal change

PiperOrigin-RevId: 517952945
Change-Id: I152c004412fefc87f5d674256658fd7b88bf9950
diff --git a/.java.policy b/.java.policy
new file mode 100644
index 0000000..68a8e72
--- /dev/null
+++ b/.java.policy
@@ -0,0 +1,4 @@
+grant {
+permission java.sql.SQLPermission "callAbort";
+permission java.sql.SQLPermission "setNetworkTimeout";
+};
diff --git a/.mvn/jvm.config b/.mvn/jvm.config
new file mode 100644
index 0000000..e2a50e0
--- /dev/null
+++ b/.mvn/jvm.config
@@ -0,0 +1 @@
+--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..fd38483
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,115 @@
+os: linux
+dist: bionic
+language: java
+services:
+  - docker
+  - mysql
+jdk: openjdk11
+addons:
+  hosts:
+    - mariadb.example.com
+    - mariadb1.example.com
+    - mariadb2.example.com
+    - mariadb3.example.com
+
+before_install:
+  - git clone https://github.com/mariadb-corporation/connector-test-machine.git
+  # Load cached docker images
+  - if [[ -d $HOME/docker ]]; then ls $HOME/docker/*.tar.gz | xargs -I {file} sh -c "zcat {file} | docker load"; fi
+
+cache:
+  directories:
+    - $HOME/docker
+    - $HOME/.m2
+
+env: local=0 RUN_LONG_TEST=true
+
+install:
+  - |-
+    case $TRAVIS_OS_NAME in
+      windows)
+        choco install openjdk11 maven
+        export PATH=$(cmd.exe //c "refreshenv > nul & C:\Progra~1\Git\bin\bash -c 'echo \$PATH' ")
+        connector-test-machine/launch.bat -t "$srv" -v "$v" -d testj
+        ;;
+      linux)
+        source connector-test-machine/launch.sh -t "$srv" -v "$v" -d testj -l "$local" -p "$packet"
+        ;;
+    esac
+
+stages:
+  - Minimal
+  - name: Enterprise
+    if: type = push AND fork = false
+  - Community
+
+jobs:
+  allow_failures:
+    - env: srv=build
+    - env: srv=xpand TEST_DB_OTHER="&initSql=SET NAMES UTF8"
+  include:
+    - stage: Minimal
+      env: srv=mariadb v=10.6 packet=8
+      name: "CS 10.6"
+    - env: srv=mariadb-es v=10.6
+      name: "ES 10.6"
+      if: type = push AND fork = false
+
+    - stage: Enterprise
+      env: srv=mariadb-es v=10.4
+      name: "ES 10.4"
+    - env: srv=mariadb-es v=10.5
+      name: "ES 10.5"
+    - env: srv=maxscale
+      name: "Maxscale"
+    - env: srv=xpand TEST_DB_OTHER="&initSql=SET NAMES UTF8"
+      name: "Xpand"
+    - env: srv=skysql
+      name: "SkySQL"
+    - env: srv=skysql-ha
+      name: "SkySQL with replication"
+
+    - stage: Community
+      env: srv=mariadb v=10.6
+      os: windows
+      language: shell
+      name: "CS 10.6 - Windows"
+    - env: srv=mariadb v=10.3 local=1
+      name: "CS 10.3"
+    - env: srv=mariadb v=10.4 local=1
+      name: "CS 10.4"
+    - env: srv=mariadb v=10.5 local=1
+      name: "CS 10.5"
+    - env: srv=mariadb v=10.7 local=1
+      name: "CS 10.7"
+    - env: srv=mariadb v=10.8 local=1
+      name: "CS 10.8"
+    - env: srv=mariadb v=10.9 local=1
+      name: "CS 10.9"
+    - env: srv=mariadb v=10.6 packet=40
+      jdk: openjdk17
+      name: "CS 10.6 - openjdk 17"
+    - env: srv=mariadb v=10.6 packet=8
+      name: "CS 10.6 - packet 8M"
+    - env: srv=mariadb v=10.6 local=1 BENCH=1
+      name: "Benchmarks"
+    - env: srv=galera v=10.8
+      name: "Galera 10.8"
+    - env: srv=mysql v=5.7
+      name: "MySQL 5.7"
+    - env: srv=mysql v=8.0
+      name: "MySQL 8.0"
+    - env: srv=build
+      name: "CS build"
+
+script:
+  - mvn clean -Dmaven.test.skip
+  - if [ -n "$BENCH" ] ; then mvn package -P bench -Dmaven.test.skip -Dmaven.javadoc.skip=true ; fi
+  - if [ -n "$BENCH" ] ; then java -Duser.country=US -Duser.language=en -DTEST_PORT=$TEST_DB_PORT -DTEST_HOST=$TEST_DB_HOST -DTEST_USERNAME=$TEST_DB_USER -DTEST_PASSWORD=$TEST_DB_PASSWORD -jar target/benchmarks.jar; fi
+  - if [ -z "$BENCH" ] ; then MAVEN_SKIP_RC=true MAVEN_OPTS="-Xmx2g" mvn clean test -DjobId=${TRAVIS_JOB_ID}; fi
+
+after_script:
+  - bash <(curl -s https://codecov.io/bash)
+
+after_failure:
+  - if [ "$srv" == "maxscale" ] ; then docker-compose -f ${COMPOSE_FILE} exec -u root maxscale tail -500 /var/log/maxscale/maxscale.log; fi
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..5e65f42
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,1184 @@
+# Change Log
+
+## [3.1.2](https://github.com/mariadb-corporation/mariadb-connector-j/tree/3.1.2) (Jan 2023)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.1.1...3.1.2)
+
+* CONJ-1040 possible ConcurrentModificationException when connecting
+* CONJ-1041 possible ArrayIndexOutOfBoundsException
+
+## [2.7.8](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.7.8) (Jan 2023)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.7.7...2.7.8)
+
+* CONJ-1039 setQueryTimeout not honored by CallableStatement for procedures depending on security context
+* CONJ-1041 possible ArrayIndexOutOfBoundsException
+* CONJ-1023 set missing SSL capability in handshake after SSL exchanges
+
+
+## [3.1.1](https://github.com/mariadb-corporation/mariadb-connector-j/tree/3.1.1) (Jan 2023)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.1.0...3.1.1)
+
+- 3.0.10 bug fix:
+  - CONJ-1023	Connector/J doesn't set SSL cap bit in Handshake Response Packet
+  - CONJ-1026	timezone=auto option failure on non-fixed-offset zone machine
+  - CONJ-1032	Compatibility for deprecated arguments is case sensitive now
+- CONJ-1036	org.mariadb.jdbc.client.socket.impl.PacketWriter.writeAscii() broken in 3.1.0
+
+
+## [3.0.10](https://github.com/mariadb-corporation/mariadb-connector-j/tree/3.0.10) (Jan 2023)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.0.9...3.0.10)
+
+* CONJ-1023	Connector/J doesn't set SSL cap bit in Handshake Response Packet
+* CONJ-1026	timezone=auto option failure on non-fixed-offset zone machine
+* CONJ-1032	Compatibility for deprecated arguments is case sensitive now
+
+## [3.1.0](https://github.com/mariadb-corporation/mariadb-connector-j/tree/3.1.0) (Nov 2022)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.0.9...3.1.0)
+
+##### Notable Changes
+* CONJ-899 Support UUID Object
+* CONJ-916 when a failover occurs, log replayed transaction
+* CONJ-917 deprecated options use must be logged
+* CONJ-992 load balance distribution
+* CONJ-1008	default value for socket option useReadAheadInput
+* CONJ-1009	improve performance reading big result-set
+* CONJ-1014	avoid creating array when receiving server packet
+* CONJ-1015	pipelining sending multiple packet to socket
+
+##### Bugs Fixed
+* CONJ-1020	java 11 option setting ignored
+
+## [3.0.9](https://github.com/mariadb-corporation/mariadb-connector-j/tree/3.0.9) (Nov 2022)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.0.8...3.0.9)
+* 2.7.7 merge
+* CONJ-1012	stored procedure register output parameter as null if set before registerOutParameter command
+* CONJ-1017 Calendar possible race condition, cause wrong timestamp setting
+
+## [2.7.7](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.7.7) (Nov 2022)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.7.6...2.7.7)
+
+* CONJ-1021 GSSAPI authentication might result in connection reset
+* CONJ-1019 DatabaseMetaData.getImportedKeys should return real value for PK_NAME column
+* CONJ-1016 avoid splitting BULK command into multiple commands in case of prepareStatement.setNull() use
+* CONJ-1011 correcting possible NPE when using statement.cancel() that coincide with statement.close() in another thread
+* CONJ-1007 Socket file descriptors are leaked after connecting with unix socket if DB is not up running
+
+## [3.0.8](https://github.com/mariadb-corporation/mariadb-connector-j/tree/3.0.8) (Sept 2022)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.0.7...3.0.8)
+
+##### Notable Changes
+* small performance improvement
+  * [CONJ-1010] improve client side prepared parameter parameter substitution
+  
+##### Bugs Fixed
+* [CONJ-997] regression in 3.x when using option galeraAllowedState resulting in an IndexOutOfBoundsException
+* [CONJ-1002] 2nd failover reconnection ignores default database/schema setting when not set by connection string
+* [CONJ-1003] replication configuration always use 1st replica on 3.0
+* [CONJ-996] BatchUpdateException doesn't inherited the SQLState & vendorCode from the cause SQL exception
+* [CONJ-1006] disabling cachePrepStmts with useServerPrepStmts might result in Exception
+* [CONJ-1007] Socket file descriptors are leaked after connecting with unix socket if DB is not up running
+* [CONJ-1010] improve client side prepare statement parameter substitution
+* [CONJ-999] setting createDatabaseIfNotExist option use on read-only server will refuse connection on 3.0
+
+
+## [3.0.7](https://github.com/mariadb-corporation/mariadb-connector-j/tree/3.0.7) (Jul 2022)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.0.6...3.0.7)
+
+* [CONJ-993] SQLDataException reading DATA_TYPE on DatabaseMetaData.getTypeInfo() after 3.0.4		
+* [CONJ-986] Permit specific Statement.setLocalInfileInputStream for compatibility 
+* [CONJ-987] Version 3.0.0 returns String for VARBINARY instead of byte[] as 2.7.6 did		
+* [CONJ-989] Binary column read as String
+* [CONJ-990] Setting timezone=UTC result in SQLSyntaxErrorException		
+* [CONJ-991] Regression: binary(16) is returned as String by getObject()		
+* [CONJ-994] Version 3.x rejects previously accepted boolean string parameter for BOOLEAN field
+
+## [3.0.6](https://github.com/mariadb-corporation/mariadb-connector-j/tree/3.0.6) (Jun 2022)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.0.5...3.0.6)
+
+* [CONJ-953] PreparedStatement.getGeneratedKeys() returns rows when no keys are generated in insert
+* [CONJ-975] ArrayIndexOutOfBoundsException when attempt to getTime() from ResultSet
+* [CONJ-976] Improve use of pipelining when allowLocalInfile is enabled
+* [CONJ-979] ResultSet.getObject() returns Byte instead of Boolean for tinyint(1)
+* [CONJ-980] Permit setObject with java.util.Date parameter
+* [CONJ-984] Permit executing initial command with new option `initSql`
+* [CONJ-985] ResultSet.getObject() returns ByteSet instead of Byte[] for BIT
+
+## [3.0.5](https://github.com/mariadb-corporation/mariadb-connector-j/tree/3.0.5) (may 2022)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.0.4...3.0.5)
+
+* [CONJ-956] ArrayIndexOutOfBoundsException when alias length > 250		
+* [CONJ-947] value after milliseconds precision lost when timestamp is encoded		
+* [CONJ-949] keep clientCertificateKeyStoreUrl and clientCertificateKeyStoreUrl aliases
+* [CONJ-950] metadata TEXT/TINYTEXT/MEDIUMTEXT/LONGTEXT wrong column type and length
+* [CONJ-954] java.time.OffsetDateTime not supported
+* [CONJ-958] compatibility with 2.7: now loop through hosts when multiple host without failover mode	
+* [CONJ-959] java.time.Instant not supported		
+* [CONJ-961] LOAD DATA LOCAL INFILE was disable by default		
+* [CONJ-962] resultset for negative TIME value return erronous LocalDateTime values		
+* [CONJ-965] better error message when not loading serverSslCert file
+* [CONJ-967] clearParameters() breaks validity when using output parameters in stored procedures		
+* [CONJ-969] org.mariadb.jdbc.ClientPreparedStatement is missing a toString implementation, useful for logging
+
+
+## [3.0.4](https://github.com/mariadb-corporation/mariadb-connector-j/tree/3.0.4) (Mar 2022)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.0.3...3.0.4)
+
+* [CONJ-915] javadoc addition
+* [CONJ-921] DatabaseMetadata#getTables with null value for tableNamePattern throws Syntax error
+* [CONJ-922] DECIMAL overflow for long/int/short not throwing exception
+* [CONJ-924] NULL column type might result in java.lang.IllegalArgumentException: Unexpected datatype NULL
+* [CONJ-926] Client restrict authentication to 'mysql_native_password,client_ed25519,auth_gssapi_client' if restrictedAuth parameter is not set
+* [CONJ-924] NULL column test correction
+* [CONJ-923] correctly return 64 bits generated id / updated rows
+* [CONJ-933] load-balancing failover doesn't timeout
+* [CONJ-935] Connection.getMetaData() returns MariaDbClob instead of String
+* [CONJ-937] metadata getColumnTypeName wrong return type
+* [CONJ-934] MariaDbDataSource is sensitive to the order of setting of username and password
+* [CONJ-932] Login packet now use recommended length encoded value for connection attributes
+* [CONJ-925] missing OSGI infos
+* [CONJ-945] ensure retry is limited by retriesAllDown
+* [CONJ-940] Permit updating rows when not having primary info on metadata (Xpand)
+* [CONJ-939] add Xpand testing
+
+
+## [3.0.3](https://github.com/mariadb-corporation/mariadb-connector-j/tree/3.0.3) (Jan 2022)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.0.2-rc...3.0.3)
+
+* [CONJ-908] correct Connection.prepareStatement(String sql, int[] columnIndexes/String[] columnNames) to return generated keys
+* [CONJ-909] adding createDatabaseIfNotExist option for 2.x compatibility
+* [CONJ-910] permit jdbc:mysql scheme when connection string contains "permitMysqlScheme" for compatibility
+* [CONJ-913] Avoid executing additional command on connection for faster connection creation
+* [CONJ-912] remove security manager code (JEP 411)
+* [CONJ-911] enable keep-alive by default
+* failover improvement. some specific commands not in transaction are considered to be replayed in case of failover, like PING, PREPARE, ROLLBACK, ...
+* CONJ-705 parameter metadata get parameter count even when query cannot be prepared
+* prepareStatement.addBatch must initialize with previous set
+* Connection.prepareStatement(String sql, int[] columnIndexes/String[] columnNames) must return generated keys
+* setting "transaction read only" only for replica
+* keeping option interactiveClient for compatibility
+* adding option `transactionReplaySize` to control redo cache size
+* only set skip metadata connection flag when using binary protocol
+* permit getString on a binary object
+* compression correction for multi-packet
+* COM_RESET_CONNECTION expect a response (ERR_Packet or OK_Packet)
+* [CONJ-901] ArrayIndexOutOfBoundsException on StandardReadableByteBuf.readByte error
+
+## [3.0.2-rc](https://github.com/mariadb-corporation/mariadb-connector-j/tree/3.0.2-rc) (31 Aug 2021)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.0.1-beta...3.0.2-rc)
+
+* CONJ-879 Java 9 module full support
+  *  Aws IAM credential now use sdk v2 authentication that support java 9 modularity 
+* CONJ-896 Ensure pool connections validation when a socket fail
+* CONJ-897 Ensure having connection's thread id in Exception / debug logs
+
+minor: 
+* Ensure travis testing for PR/fork 
+
+## [3.0.1-beta](https://github.com/mariadb-corporation/mariadb-connector-j/tree/3.0.1-beta) (29 Jul 2021)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.0.0-alpha...3.0.1-beta)
+
+* CONJ-879 Provide JPMS module descriptor
+* CONJ-880 metadata query performance correction
+* CONJ-884 MariaDbPoolDataSource leaks connections when the mariadb server restarts
+* CONJ-885 org.mariadb.jdbc.internal.util.pool.Pool swallows SQLException during addConnection
+* CONJ-891 getImportedKeys with null catalog restrict result to current database		
+* CONJ-894 Adding useMysqlMetadata for 2.7 compatibility
+
+
+## [2.7.4](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.7.4) (29 Jul 2021)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.7.3...2.7.4)
+
+* CONJ-890	getImportedKeys/getTables regression returning an empty resultset for null/empty catalog
+* CONJ-863	Ensure socket state when SocketTimeout occurs
+* CONJ-873	IndexOutOfBoundsException when executing prepared queries using automatic key generation in parallel
+* CONJ-884	MariaDbPoolDataSource leaks connections when the mariadb server restarts
+* CONJ-893	DatabaseMetaData.getColumns regression causing TINYINT(x) with x > 1 to return BIT type in place of TINYINT
+* CONJ-889	CallableStatement using function throw wrong error on getter
+
+
+## [3.0.0](https://github.com/mariadb-corporation/mariadb-connector-j/tree/3.0.0) (3 May 2021)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.7.2...3.0.0)
+
+This version is a total rewrite of java driver. 
+
+* complete rewrite, code clarification, reduced size (15%), more than 90% coverage tested.
+* Encoding/decoding implementation are now registred by Codec, permitting codec registry implementation
+  * example support of Geometry Object 
+* Permit authentication plugin restriction by option `restrictedAuth`
+* performance improvement:
+  * Prepare and execution are now using pipelining when using option `useServerPrepStmts`
+  * performance enhancement with MariaDB 10.6 server when using option `useServerPrepStmts`, skipping metadata (see https://jira.mariadb.org/browse/MDEV-19237)
+
+
+correction: 
+* CONJ-864 includeThreadDumpInDeadlockExceptions always includes the thread dump, even when it is not a deadlock exception
+* CONJ-858 Properties parameter that differ from string not taken in account
+
+### Easy logging
+If using slf4J, just enabled package "org.mariadb.jdbc" log.
+
+level ERROR will log connection error
+level WARNING will log query errors
+level DEBUG will log queries
+level TRACE will log all exchanges with server.
+
+If not using slf4J, console will be used. 
+If really wanting to use JDK logger, System property "mariadb.logging.fallback" set to JDK will indicate to use common logging. 
+
+
+### Failover
+Failover implementation now permit redoing transaction : 
+when creating a transaction, all command will be cached, and can be replayed in case of failover. 
+
+This functionality can be enabled using option `transactionReplay`.
+
+This is not enabled by default, because this required that application to avoid using non-idempotent commands. 
+
+example:
+```sql
+START TRANSACTION;
+select next_val(hibernate_sequence);
+INSERT INTO myCar(id, name) VALUE (?, ?) //with parameters: 1, 'car1'
+INSERT INTO myCarDetail(id, carId, name) VALUE (?, ?, ?) //with parameters: 2, 1, 'detail1'
+INSERT INTO myCarDetail(id, carId, name) VALUE (?, ?, ?) //with parameters: 3, 2, 'detail2'
+COMMIT;
+```
+
+### Allow setup of TCP_KEEPIDLE, TCP_KEEPCOUNT, TCP_KEEPINTERVAL
+
+Equivalent options are `tcpKeepIdle`, `tcpKeepCount`, `tcpKeepInterval`
+Since available only with java 11, setting this option with java < 11 will have no effect. 
+
+## [2.7.3](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.7.3) (12 May 2021)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.7.2...2.7.3)
+
+* CONJ-619	Multiple batch update fails after LOAD DATA LOCAL INFILE
+* CONJ-854	LOAD XML INFILE breaks when using LOCAL
+* CONJ-855	throwing more specific exception for updatable result-set that can not be updated by ResultSet
+* CONJ-857	Remove use of mysql.proc table, relying on information_schema.parameters
+* CONJ-864	includeThreadDumpInDeadlockExceptions always includes the thread dump, even when it is not a deadlock exception
+* CONJ-866	long binary parsing improvement
+* CONJ-871	OSGi: Missing Import-Package in Connector/J bundle (javax.sql.rowset.serial)
+* CONJ-878	option serverSslCert file location
+* CONJ-880	metadata query performance correction
+* CONJ-858	Properties.put with object that differ from String supported even if use is not recommended
+* CONJ-861	executeBatch must not clear last parameter value.
+* CONJ-883	using unix socket, hostname is not mandatory anymore
+
+
+## [2.7.2](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.7.2) (29 Jan. 2021)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.7.1...2.7.2)
+
+* CONJ-847 NPE at UpdatableResultSet#close
+* CONJ-849 driver now doesn't close connection caused java.io.NotSerializableException as a result of incorrect data bind to a prepared statement parameter
+* CONJ-850 MariaDbResultSetMetaData#getPrecision(int) now returns correct length for character data
+* CONJ-851 metadata getBestRowIdentifier incompatibility with MySQL 8 correction
+* CONJ-853 Support Aurora cluster custom endpoints
+* CONJ-852 ON DUPLICATE KEY detection failed when using new line
+
+
+## [2.7.1](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.7.1) (23 Nov. 2020)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.7.0...2.7.1)
+
+* CONJ-834 use of BULK batch is conditioned by capability, not checking server version
+* CONJ-835 GSS Imports set in OSGI Bundle
+* CONJ-839 Wrong exception message when rewriteBatchedStatements is enabled
+* CONJ-841 ResultSetMetaData::getColumnTypeName() returns incorrect type name for LONGTEXT
+* CONJ-842 Byte array parameters are now send as long data
+* CONJ-837 prepared statement cache leak on ResultSet CONCUR_UPDATABLE concurrency
+* CONJ-843 ParameterMetaData::getParameterType for CallableStatement parameter return expected "BINARY" value for BINARY type
+
+minor:
+* CONJ-845 test suite now test SkySQL with replication setting
+* CONJ-838 have a 'replica' alias for 'slave' connection option
+
+
+## [2.7.0](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.7.0) (24 Sep. 2020)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.6.2...2.7.0)
+
+* CONJ-805	maxFieldSize string truncation occurs on bytes' length, not character length
+* CONJ-807	Correcting possible Get Access Denied error if using multiple classloader
+* CONJ-810	normalization of resultset getDate/getTime of timestamp field.
+* CONJ-812	DatabaseMetadata.getBestRowIdentifier and getMaxProcedureNameLength correction
+* CONJ-813	setConfiguration not being called on classes that extend ConfigurableSocketFactory
+* CONJ-816	Table with primary key with DEFAULT function can be inserted for 10.5 servers
+* CONJ-817	Switched position of REMARKS and PROCEDURE_TYPE in the getProcedures result
+* CONJ-820	MySQLPreparedStatement.setObject can now handle java.lang.Character type
+* CONJ-828	new option `ensureSocketState` to ensure protocol state
+* CONJ-829	Option to cache callablestatement is now disabled by default
+* CONJ-830	connector now throw a better error if SSL is mandatory and server doesn't support SSL
+* CONJ-814	Small possible improvement of getCrossReference, getExportedKeys and getImportedKey
+* CONJ-825	XAResource.isSameRM implementation
+
+## [2.6.2](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.6.2) (23 Jul. 2020)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.6.1...2.6.2)
+
+* CONJ-804 - Automatic charset correction
+* CONJ-809 - SelectResultSet's (ResultSet)MetaData always indicates all columns to be readonly
+* CONJ-802 - Version parsing depending on Classloader might result in connection Exception
+
+
+## [2.6.1](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.6.1) (23 Jun. 2020)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.6.0...2.6.1)
+
+* CONJ-781 - DatabaseMetaData.supportsMultipleResultSets() now return correctly true.
+* CONJ-791 - Using CallableStatement.getTimestamp() can't get data correctly
+* CONJ-705 - ParameterMetadata now return parameterCount() even if no information
+* CONJ-775 - avoid a NPE for malformed "jdbc:mariadb:///" connection string.
+* CONJ-776 - Temporal Data Tables are not listed in metadata
+* CONJ-785 - corrected escape sequence for multiple backslash escape
+* CONJ-786 - Connection.setReadOnly(true ) with option `assureReadOnly` now force read only connection even for mono server*
+* CONJ-795 - permit resultset.getRow() for TYPE_FORWARD_ONLY when streaming
+* CONJ-797 - Connector set UTF8mb4 equivalent in case of server configured with UTF8mb3 collation
+* CONJ-800 - implement Statement setEscapeProcessing to avoid escape
+* CONJ-801 - possible race condition using resultset getter using label
+* CONJ-778 - Missing import org.osgi.service.jdbc in Import-Package clause of the OSGi manifest
+* CONJ-779 - Logic error in stop() method of OSGi bundle activator
+* CONJ-780 - Logic error in implementation of OSGi DataSourceFactory (MariaDbDataSourceFactory)
+* CONJ-788 - resultset metadata always indicate that column is writable even if not
+* CONJ-789 - ensure connection reference removal on (prepared)Statement close
+* CONJ-782 - SkySQL testing
+
+## [2.6.0](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.6.0) (19 Mar. 2020)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.5.4...2.6.0)
+
+* CONJ-768 - Check Galera allowed state when connecting when option `galeraAllowedState` is set, and not only on validation
+* CONJ-759 - on failover, catalog changed might not be set when automatically recreating a connection.
+* CONJ-761 - remove unnecessary dependencies for fedora tar creation
+* CONJ-763 - Custom SocketFactory now can change options
+* CONJ-764 - DatabaseMetaData.getExportedKeys should return "PRIMARY" for PK_NAME column
+* CONJ-765 - Allow MariaDbDatabaseMetaData#getExportedKeys to return the exported keys for all tables
+* CONJ-766 - Adding a socket timeout until complete authentication, to avoid hangs is server doesn't support pipelining
+* CONJ-767 - permit using Aurora RO endpoint
+* CONJ-771 - enablePacketDebug must not reset stack on failover
+* CONJ-772 - JDBC Conversion Function support parsing correction
+
+## [2.5.4](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.5.4) (27 Jan. 2020)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.5.3...2.5.4)
+
+* CONJ-756 - Logging correction when using enablePacketDebug option
+* CONJ-755 - permits avoiding setting session_track_schema with new option `trackSchema`
+
+## [2.5.3](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.5.3) (07 Jan. 2020)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.5.2...2.5.3)
+
+* CONJ-752 - Manifest file wrong entry - thanks to Christoph Läubrich
+* CONJ-750 - protocol error when not setting database with maxscale
+* CONJ-747 - JDBC Conversion Function fast-path skipped, always using longer implementation
+
+## [2.5.2](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.5.2) (22 Nov. 2019)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.5.1...2.5.2)
+
+* CONJ-745 - use pool reset only for corrected COM_RESET_CONNECTION
+* CONJ-743 - byte signed value wrong serialization for text protocol
+* CONJ-742 - ensure plugin using Driver classloader
+
+
+## [2.5.1](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.5.1) (15 Oct. 2019)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.5.0...2.5.1)
+
+* CONJ-736 - OSGI compliance
+* CONJ-737 - Error packet caching_sha2_password not handled when not having a password
+* CONJ-738 - PAM authentication multiple exchanges permitting multiple step in connection string
+* CONJ-735 - Multi insert regression correction returning multi generated keys
+
+
+## [2.5.0](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.5.0) (02 Oct. 2019)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.4.4...2.5.0)
+
+* CONJ-663 - Client authentication plugins are now defined as services. The driver has 2 new plugins `caching_sha2_password` and `sha256_password plugin` for MySQL compatibility
+* CONJ-733 - Credential service: AWS IAM authentication
+* CONJ-727 - Support configuration of custom SSLSocketFactory
+* CONJ-561 - JDBC 4.3 partial implementation java.sql.Statement methods isSimpleIdentifier, enquoteIdentifier, enquoteLiteral and enquoteNCharLiteral
+* CONJ-692 - ConnectionPoolDataSource interface addition to MariaDbPoolDataSource
+* CONJ-563 - closing possible option batch thread on driver registration.
+* CONJ-732 - Driver getPropertyInfo returns no options' information when url is empty
+* CONJ-734 - DatabaseMetaData.getSchemaTerm now return "schema", not empty string
+
+## [2.4.4](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.4.4) (14 Sep. 2019)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.4.3...2.4.4)
+
+* CONJ-724 - Do not ignore the Calendar parameter in ResultSet#getTime(int, Calendar)
+* CONJ-725 - Connection Failure when using PAM authenticated user on 10.4 MariaDB server
+* CONJ-729 - master-slave regression: commit on read-only server Executed only when there is an active transaction on master connection
+* CONJ-726 - removing possible NPE after failover on aurora cluster
+
+## [2.4.3](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.4.3) (02 Jul. 2019)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.4.2...2.4.3)
+
+* CONJ-717 - conversion function support for other data type than default MariaDB conversion type
+* CONJ-722 - Permit suppression of result-set metadata getTableName for oracle compatibility
+* CONJ-719 - Saving values using Java 8 LocalTime does not store fractional parts of seconds
+* CONJ-716 - Correcting possible NPE on non thread safe NumberFormat (logging)
+
+## [2.4.2](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.4.2) (17 Jun. 2019)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.4.1...2.4.2)
+
+Security
+* CONJ-679 - parse Query when receiving LOAD LOCAL INFILE
+
+Bugs
+* CONJ-703 - ClassNotFoundException when trying to connect using two-authentication in an OSGI environment.
+* CONJ-711 - Xid format id is unsigned integer, currently sending as signed value.
+* CONJ-700 - autoReconnect=true on Basic Failover doesn't reconnect
+* CONJ-707 - failover might throw an unexpected exception with using "failover"/"sequential" configuration on socket error
+* CONJ-709 - includeThreadDumpInDeadlockExceptions is thrown only if option includeInnodbStatusInDeadlockExceptions is set
+* CONJ-710 - Throw complete stackTrace when having an exception on XA Commands
+* CONJ-714 - Error on connection on galera server when in detached mode.
+* CONJ-701 - typo in error message in SelectResultSet.java
+
+## [2.4.1](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.4.1) (15 Mar. 2019)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.4.0...2.4.1)
+
+Evolutions
+* misc - enabled running of 'SHOW ENGINE INNODB STATUS' for error code 1213 (@mtykhenko)
+* misc - reduce mutex using select @@innodb_read_only for aurora (@matsuzayaws)
+
+Bugs
+* misc - updating checkstyle version dependency
+* misc - permit using SSL on localsocket
+* CONJ-687 - addition of option "useMysqlMetadata" to permit MySQL meta compatibility
+* misc - java PID using java 9 ProcessHandle if existing, relying on JNA if present
+* CONJ-682 - internal pool correction: when receiving an RST during connection validation, the pool will end up throwing connection timeout exception in place of reusing another connection.
+
+## [2.4.0](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.4.0) (28 Jan. 2019)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.3.0...2.4.0)
+
+Evolutions
+* CONJ-675 - permit multiple alternative authentication methods for the same user (future MariaDB 10.4 feature)
+* 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-659 - improve text performance reading date/time/timestamp resultset
+* CONJ-670 - ability to Refresh SSL certificate
+
+New options
+
+|Option|Description|
+|---|---|
+|useReadAheadInput|use a buffered inputSteam that read socket available data. <br /><i>Default: true</i>|
+|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|
+
+Bugs
+* CONJ-646 - possible NullPointerException when connection lost to database using aurora configuration with one node
+* CONJ-672 - batch using multi-send can hang when using query timeout
+* CONJ-544 - disable SSL session resumption when using SSL
+* CONJ-589 - correcting Clob.length() for utf8mb4
+* CONJ-649 - datasource connectTimeout URL parameter is not honoured
+* CONJ-650 - Correction on resultset.getObject(columnName, byte[].class) when value is NULL
+* CONJ-665 - old MySQL (<5.5.3) doesn't support utf8mb4, using utf8 on 3 bytes as connection charset by default
+* CONJ-671 - MariaDb bulk threads occupy full cpu(99%) while db connections broken
+* CONJ-673 - abording a connection while fetching a query still does read whole resultset
+* CONJ-669 - SQLSyntaxErrorException when querying on empty column name
+* CONJ-674 - make dumpQueriesOnException = false by default as per documentation
+
+minor:
+* CONJ-644 - small optimization when validating galera connection
+* CONJ-625 - add coverage test
+* CONJ-654 - DatabaseMetaData.getDriverName() returns connector/J with a lowercase c
+
+
+## [2.3.0](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.3.0) (06 Sep. 2018)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.2.6...2.3.0)
+
+#### [CONJ-398] Improve deadlock debugging capabilities
+
+MariaDB has now 2 new options to permit identifying deadlock :
+New options
+
+|Option|Description|
+|---|---|
+|includeInnodbStatusInDeadlockExceptions|add "SHOW ENGINE INNODB STATUS" result to exception trace when having a deadlock exception.<br /><i>//Default: false</i>|
+|includeThreadDumpInDeadlockExceptions|add thread dump to exception trace when having a deadlock exception.<br /><i>Default: false</i>|
+
+#### [CONJ-639] the option "enabledSslProtocolSuites" now include TLSv1.2 by default
+previous default value was "TLSv1, TLSv1.1", disabling TLSv1.2 by default, due to a corrected issue (MDEV-12190) with servers using YaSSL - not openSSL. Server error was .
+Now, the default value is "TLSv1, TLSv1.1, TLSv1.2". So TLSv1.2 can be use directly.
+Connecting MySQL community server use YaSSL without correction, and connection might result in SSLException: "Unsupported record version Unknown-0.0".
+
+
+#### [CONJ-642] disable the option "useBulkStmts" by default
+Using useBulkStmts permit faster batch, but cause one major issue : Batch return -1 = SUCCESS_NO_INFO
+
+Different option use this information for optimistic update, and cannot confirm if update succeed or not.
+This option still makes sense, since for big batch is way faster, but will not be activated by default.
+
+
+##= Minor change:
+* CONJ-628 - optimization to read metadata faster
+* CONJ-637 - java.sql.Driver class implement DriverPropertyInfo[] getPropertyInfo, permitting listing options on querying tools
+* CONJ-639 - enabledSslProtocolSuites does not include TLSv1.2 by default
+* CONJ-641 - update maven test dependencies for java 10 compatibility
+* CONJ-643 - PreparedStatement::getParameterMetaData always returns VARSTRING as type resulting in downstream libraries interpreting values wrongly
+
+##= Bug correction:
+* CONJ-616 - correction on possible NPE on getConnection when using failover configuration and master is down, not throwing a proper exception
+* CONJ-636 - Error in batch might throw a NPE and not the proper Exception
+
+
+## [2.2.6](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.2.6) (19 Jul. 2018)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.2.5...2.2.6)
+
+minor change:
+* CONJ-623 - Increase connection logging when Primary node connection fails
+* CONJ-384 - Permit knowing affected rows number, not only real changed rows
+
+New options
+|=useAffectedRows|default correspond to the JDBC standard, reporting real affected rows. if
+enable, will report "affected" rows. example : if enabled, an update command that doesn't change a row value will still be "affected", then report.<br /><i>Default: false. Since 2.2.6</i>
+
+Bug correction:
+* CONJ-624 - MariaDbPoolDataSource possible NPE on configuration getter
+* CONJ-623 - Increase connection logging when Primary node connection fails
+* CONJ-622 - The option "connectTimeout" must take in account DriverManager.getLoginTimeout() when set
+* CONJ-621 - wrong escaping when having curly bracket in table/field name
+* CONJ-618 - Client preparestatement parsing error on escaped ' / " in query
+
+
+## [2.2.5](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.2.5) (28 May. 2018)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.2.4...2.2.5)
+
+minor change:
+* CONJ-602 - Add server hostname to connection packet for proxy
+* CONJ-604 - handle support for mysql 8.0 tx_isolation replacement by transaction_isolation
+
+Bug correction:
+* CONJ-613 - Connection using "replication" Parameters fail when no slave is available
+* CONJ-595 - Create option to configure DONOR/DESYNCED Galera nodes to be unavailable for load-balancing
+* CONJ-605 - Newlines where breaking calling stored procedures
+* CONJ-609 - Using getDate with function DATE_ADD() with parameter using string format where return wrong result using binary protocol
+* CONJ-610 - Option "allowMasterDownConnection" improvement on connection validation and Exceptions on master down
+
+## [2.2.4](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.2.4) (04 May. 2018)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.2.3...2.2.4)
+
+Minor changes:
+* CONJ-580 - Some options are missing in documentation like default 'autocommit' value
+* CONJ-597 - Internal exchanges send utf8mb4 with server even if default server collation is not utf8/utf8mb4
+* CONJ-600 - Upgrading non-mandatory Waffle dependency to 1.9.0 (windows GSSAPI authentication)
+* CONJ-575 - test addition to ensure YaSSL downgrade TLSv1.2 protocol to TLSv1.1
+
+## [2.2.3](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.2.3) (08 Mar. 2018)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.2.2...2.2.3)
+
+Bug correction:
+* CONJ-583 - possible hang indefinitely using master/slave configuration and failover occur
+* CONJ-586 - erroneous transaction state when first command result as error
+* CONJ-587 - using allowMasterDownConnection option can lead to NPE when using setReadOnly()
+* CONJ-588 - using option 'allowMasterDownConnection' won't permit connecting if master is down
+* CONJ-534 - Connection.isValid() must be routed to Master and Slave connections to avoid any server timeout
+
+## [2.2.2](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.2.2) (20 Feb. 2018)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.2.1...2.2.2)
+
+Bug correction:
+* CONJ-564 - Never ever throw an instance of java.lang.Error
+* CONJ-579 - Keywords missing from DatabaseMetaData.getSQLKeywords()
+* CONJ-567 - UrlParser.initialUrl gets overwritten
+* CONJ-571 - Permit java 9 serialization filtering
+* CONJ-574 - forcing using toLowerCase/toUpperCase with Locale.ROOT
+* CONJ-560 - Automatic module name for java 9
+* CONJ-578 - windows testing using all mariadb server
+* CONJ-570 - Add tests for 10.3.3 INVISIBLE column
+
+## [2.2.1](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.2.1) (22 Dec. 2017)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.2.0...2.2.1)
+
+* CONJ-501 - provide support for authentication plugin ed25519
+
+Bug correction:
+* CONJ-529 - failover : the driver will pause for 250ms if no servers are available before attempting to reconnect another time
+* CONJ-548 - don't use COM_STMT_BULK_EXECUTE for INSERT ... SELECT statements
+* CONJ-549 - correction on connection reset when using MariaDbPoolDataSource with options useServerPrepStmts and useResetConnection enabled
+* CONJ-555 - failover caused by client timeout must not reuse connection
+* CONJ-558 - removing extra ".0" to resultset.getString() value for FLOAT/DOUBLE fields
+* CONJ-550 - fetching state correction when reusing statement without having read all results
+* CONJ-553 - RejectedExecutionException was thrown when having large amount of concurrent batches
+
+
+
+## [2.2.0](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.2.0) (08 Nov. 2017)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.1.2...2.2.0)
+
+Evolutions:
+#### CONJ-522 - Pool datasource implementation
+
+MariaDB has now 2 different Datasource implementation :
+* MariaDbDataSource : Basic implementation. A new connection each time method getConnection() is called.
+* MariaDbPoolDataSource : Connection pooling implementation. MariaDB Driver will keep a pool of connection and borrow Connections when asked for it.
+
+New options
+
+|Option|Description|
+|---|---|
+|pool|Use pool. This option is useful only if not using a DataSource object, but only connection object. <br /><i>Default: false. since 2.2.0</i>|
+|poolName|Pool name that will permit to identify thread.<br />default: auto-generated as MariaDb-pool-<pool-index> <i>since 2.2.0</i>|
+|maxPoolSize| The maximum number of physical connections that the pool should contain. <br /><i>Default: 8. since 2.2.0</i>|
+|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.<br /><i>Default: maxPoolSize value. Since 2.2.0</i>|
+|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.<br /><i>Default: 1000 (in milliseconds). Since 2.2.0</i>|
+|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 <br /><i>Default: 600 in seconds (=10 minutes), minimum value is 60 seconds. Since 2.2.0</i>|
+|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.<br /><i>Default: false. Since 2.2.0</i>|
+|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<br /><i>Default: false. Since 2.2.0</i>|
+
+Other evolutions:
+* CONJ-530 - Permit Connection.abort() forcing killing the connection, even if connection is stuck in another thread
+* CONJ-531 - permit cancelling streaming result-set using Statement.cancel.
+* CONJ-495 - Improve reading result-set data
+* CONJ-510 - allow execution of read-only statements on slaves when master is down
+
+
+Bug :
+* CONJ-532 - correction Statement.getMoreResults() for multi-queries
+* CONJ-533 - PrepareStatement.setTime() may insert incorrect time according to current timezone, time and option "useLegacyDatetimeCode"
+* CONJ-535 - correction on numerical getter for big BIT data type fields
+* CONJ-541 - Fix behavior of ResultSet#relative when crossing result set boundaries
+
+
+Misc:
+* CONJ-469 - Improve Blob/Clob implementation (avoiding array copy from result-set row)
+* CONJ-539 - better message when server close connection
+* misc - resultset.findColumn method use column name if alias not found
+* misc - default option "connectTimeout" value to 30 seconds (was 0 = no timeout)
+* misc - ensure that enablePacketDebug option works when timer tick is big
+
+
+
+## [2.1.2](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.1.2) (24 Sep. 2017)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.1.1...2.1.2)
+
+Bug :
+* CONJ-525 - Batch result-set return array correction when DELETE statement when bulk option is used
+* CONJ-526 - better error message getting metadata information when SQL syntax is wrong
+* CONJ-527 - Resultset.last() return wrong value if resultset has only one result
+* CONJ-528 - Error executing LOAD DATA LOCAL INFILE when file is larger than max_allowed_packet
+
+
+## [2.1.1](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.1.1) (05 Sep. 2017)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.1.0...2.1.1)
+
+Bug :
+* CONJ-519 - Updatable result-set possible NPE when same field is repeated.
+* CONJ-514 - ResultSet method wasNull() always return true after a call on a "null-date" field binary protocol handling
+* CONJ-516 - Permit using updatable result-set when fetching
+* CONJ-511 - Add legacy SSL certificate Hostname verification with CN even when SAN are set
+* CONJ-515 - Improve MariaDB driver stability in case JNA errors
+
+misc :
+* correct typo in error message when setting wrong parameter
+* add trace to HostnameVerifier implementation
+* handling connection error when no database is provided
+
+
+## [2.1.0](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.1.0) (29 Jul. 2017)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.0.3...2.1.0)
+
+##= CONJ-422 : verification of SSL Certificate Name Mismatch
+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.
+
+A new option "disableSslHostnameVerification" permit to deactivate this validation.
+|=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.<br />//Default: false. Since 2.1.0//
+
+##= CONJ-400 - Galera validation
+When configuration with multi-master, Connection.isValid() will not only validate connection, but primary state.
+A connection to a node that is not in primary mode will return false (then for pool, connection will be discarded)
+
+##= CONJ-322 - ResultSet.update* methods implementation
+ResultSet.update* methods aren't implemented
+statement using ResultSet.CONCUR_UPDATABLE must be able to update record.
+exemple:
+{{{
+Statement stmt = con.createStatement(
+ResultSet.TYPE_SCROLL_INSENSITIVE,
+ResultSet.CONCUR_UPDATABLE);
+ResultSet rs = stmt.executeQuery("SELECT age FROM TABLE2");
+// rs will be scrollable, will not show changes made by others,
+// and will be updatable
+while(rs.next()){
+//Retrieve by column name
+int newAge = rs.getInt(1) + 5;
+rs.updateDouble( 1 , newAge );
+rs.updateRow();
+}
+}}}
+
+
+##= CONJ-389 - faster batch insert
+Use dedicated [COM_STMT_BULK_EXECUTE |https://mariadb.com/kb/en/mariadb/com_stmt_bulk_execute/] protocol for batch insert when possible.
+(batch without Statement.RETURN_GENERATED_KEYS and streams) to have faster batch.
+(significant only if server MariaDB &ge; 10.2.7)
+
+A new option "useBulkStmts" permit to deactivate this functionality.
+|=useBulkStmts| Use dedicated COM_STMT_BULK_EXECUTE protocol for batch insert when possible. (batch without Statement.RETURN_GENERATED_KEYS and streams) to have faster batch. (significant only if server MariaDB &ge; 10.2.7)<br />//Default: true. Since 2.1.0//
+
+
+other evolution
+* CONJ-508 - Connection.getCatalog() optimisation for 10.2+ server using new session_track_schema capabilities
+* CONJ-492 - Failover handle automatic reconnection on KILL command
+
+Bug
+* CONJ-502 - isolation leak when using multiple pools on same VM on failover
+* CONJ-503 - regression on aurora Connection.isReadOnly()
+* CONJ-505 - correcting issue that ended throwing "Unknown prepared statement handler given to mysqld_stmt_execute"
+* CONJ-496 - return rounded numeric when querying on a decimal field in place of throwing an exception for compatibility
+
+
+## [2.0.3](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.0.3) (27 Jun. 2017)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.0.2...2.0.3)
+
+Bug
+* CONJ-473 - when useServerPrepStmts is not set, the PREPARE statement must not be cached.
+* CONJ-494 - Handle PrepareStatement.getParameterMetaData() if query cannot be PREPAREd
+* CONJ-497 - escape string correction for big query
+
+
+## [2.0.2](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.0.2) (05 Jun. 2017)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.0.1...2.0.2)
+
+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-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
+* CONJ-479 - ArrayIndexOutOfBoundsException on connect to MySQL 5.1.73
+* CONJ-480 - Access denied error on connect to MySQL 5.1.73
+* CONJ-483 - Wrong content of DEFERRABILITY column in MariaDbDatabaseMetaData
+* CONJ-487 - No timeout exception on Client PrepareStatement
+* CONJ-489 - javax.transaction.xa.XAException message error truncated ( near '0x )
+
+Task
+* CONJ-478 - Change CI tests to use maxscale 2.1 version
+* CONJ-482 - Connection.setNetworkTimeout don't throw exception if no executor
+* CONJ-488 - Use java.net.URL to read keyStore and trustStore again
+
+## [2.0.1](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.0.1) (10 May. 2017)
+[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/2.0.0-RC...2.0.1)
+
+* CONJ-467 - changing database metadata to 4.2
+* CONJ-460 - Query that contain multiqueries with fetch and EOF deprecation failed
+* CONJ-464 - Using of "slowQueryThresholdNanos" option with value > Integer.MAX_VALUE results in class cast exception
+* CONJ-452 - correcting inline ssl server certificate parsing
+* CONJ-461 - LAST_INSERT_ID() validation check correction for rewrite statement
+* CONJ-465 - new option "enablePacketDebug"
+
+New Options :
+|=enablePacketDebug|Driver will save the last 16 MySQL packet exchanges (limited to first 1000 bytes).<br />Hexadecimal value of this packet will be added to stacktrace when an IOException occur.<br />This options has no performance incidence (< 1 microseconds per query) but driver will then take 16kb more memory.//Default: true. Since 1.6.0//|
+
+* CONJ-468 - autoIncrementIncrement value loaded during connection, avoiding a query for first statement for rewrite
+
+
+## [2.0.0-RC](https://github.com/mariadb-corporation/mariadb-connector-j/tree/2.0.0-RC) (20 Apr. 2017)
+
+Release candidate version.
+
+Java 8 is now minimum required version.
+
+##= CONJ-318 : Handle CLIENT_DEPRECATE_EOF flag
+Implement some protocol changes that permit to save some bytes.(part of https://jira.mariadb.org/browse/MDEV-8931).
+
+##= CONJ-341 : handle SERVER_SESSION_STATE_CHANGE status flag
+With server with version MariaDB 10.2, MySQL 5.7, ensure driver state :
+- driver does now always get current database, even database is changed by query.
+- when using rewriteBatchedStatements does return correct autoincrement ids even when session variable @auto_increment_increment has change during session.
+
+
+##= CONJ-393 : improve setQueryTimeout to use SET STATEMENT max_statement_time
+
+Previous implementation of query timeout handling (using Statement.setQueryTimeout) will create an additional thread with a scheduler.
+When timeout is reached, a temporary connection will be created to permit executing "KILL QUERY <current connection id>", then closing the temporary connection.
+When query ended before timeout, the scheduled task will be canceled.
+
+If server is > 10.1.2, query timeout will be handle server side using "SET MAX_STATEMENT_TIME FOR" command.
+
+##= [CONJ-315]
+
+Closing a Statement that was fetching a result-set (using Statement.setFetchSize) and all rows where not read at the time of closing, a kill query command
+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 sent 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
+Driver execute different command on connection. Those queries are now send using pipeline (all queries are sent, then only all results are reads).
+
+New Options :
+|=usePipelineAuth|Fast connection creation.//Default: true. Since 2.0.0//|
+
+##= [CONJ-368]
+Parsing row result optimisation to avoid creating byte array to the maximum for faster results and less memory use.
+
+##= Remaining JDBC 4.2 missing implementation :
+- CONJ-414 - support for large update count [CONJ-414]
+- CONJ-409 - PrepareStatement.setObject(...) support for with java 8 temporal object.
+- CONJ-411 - support for Statement maxFieldSize
+
+##= Misc
+
+* CONJ-443 - NullpointerException when making concurrent procedure calls
+* CONJ-391 - Improve connection using SELECT in place of SHOW to avoid creating a mutex server side.
+* CONJ-402 - tcpKeepAlive option now default to true.
+* CONJ-448 - QueryException: Incorrect arguments to mysqld_stmt_execute on inserting an "emptyString"-Lob with JPA
+* CONJ-451 - Respect type parameter of ResultSet.getObject with type
+* CONJ-455 - MetaData : tinyInt1isBit doesn't work properly in TINYINT(1) column that is marked as UNSIGNED
+* CONJ-450 - NPE on setClientInfo if value is an empty string
+* CONJ-457 - trustStore : Retain leading slash when trust store beings with 'file:///'
+* CONJ-160 - ConnectionPool test using hikariCP
+* CONJ-307 - valid connector java 9 early access
+* CONJ-402 - make tcpKeepAlive option default to true
+* CONJ-411 - Implement Statement maxFieldSize
+* CONJ-449 - Permit CallableStatement streaming
+
+
+## 1.5.9
+* CONJ-212 : Implement password encoding charset option
+* CONJ-423 : Permit to have MySQL driver and MariaDB driver in same classpath
+* CONJ-431 : multi-values queries return only one generated key
+* CONJ-434 : 1.5.8 regression : ResultSet returns duplicate entries when using fetchsize
+* CONJ-437 : ResultSet.getString on field with ZEROFILL doesn't have the '0' leading chars when using binary protocol
+* CONJ-435 : avoid "All pipe instances are busy" exception on multiple connections to the same named pipe
+* CONJ-446 : ResultSet first() throw an exception for scroll type if TYPE_FORWARD_ONLY when streaming
+* CONJ-440 : handle very big COM_STMT_SEND_LONG_DATA packet (1Gb)
+* CONJ-429 : ResultSet.getDouble/getFloat may throw a NumberFormatException
+* CONJ-438 : using option rewriteBatchedStatements, permit rewrite when query has column/table that contain 'select' keyword.
+
+## 1.5.8
+* CONJ-424 : getGeneratedKeys() on table without generated key failed on second execution
+* CONJ-412 : Metadata take in account tinyInt1isBit in method columnTypeClause
+* CONJ-418 : ResultSet.last() isLast() afterLast() and isAfterLast() correction when streaming
+* CONJ-415 : ResultSet.absolute() should not always return true
+* CONJ-392 : Aurora cluster endpoint detection fails when time_zone doesn't match system_time_zone
+* CONJ-425 : CallableStatement getObject class according to java.sql.Types value
+* CONJ-426 : Allow executeBatch to be interrupted
+* CONJ-420 : High CPU usage against Aurora after 2 hours inactivity
+
+## 1.5.7
+* CONJ-407 : handling failover when packet > max_allowed_packet reset the connection state.
+* CONJ-403 : possible NPE on ResultSet.close() correction
+* CONJ-405 : Calendar instance not cleared before being used in ResultSet.getTimestamp
+
+
+## 1.5.6
+* CONJ-399 : resultSet getLong() for BIGINT column fails if value is Long.MIN_VALUE in Text protocol
+* CONJ-395 : Aurora does not randomise selection of read replicas
+* CONJ-392 : Aurora cluster endpoint detection timezone issue
+* CONJ-394 : mysql_native_password plugin authentication fail when default-auth set
+* CONJ-388 : handle timestamp '0000-00-00 00:00:00' getString()
+* CONJ-380 : add maxscale in CI
+* CONJ-391 : Use SELECT in place of SHOW command on connection
+* CONJ-396 : handling multiple resultSet correctly (was failing if more than 2)
+
+## 1.5.5
+* CONJ-386 : Disabling useBatchMultiSend option for Aurora, since can hang connection.
+* CONJ-385 : Store procedure with resultSet get wrong getUpdateCount() value (0 in place of -1)
+* CONJ-383 : permit OldAuthSwitchRequest protocol (compatibility with 5.5 server using plugin)
+* CONJ-382 : Client sockets remain option when server close socket when maximum connections number has been reached
+* CONJ-381 : Metadata getProcedureColumns precision's information corrected for date/timestamp/datetime
+* CONJ-379 : Metadata TINYTEXT type return Types.LONGVARCHAR instead of Types.VARCHAR
+* CONJ-376 : Maxscale compatibility : Permit protocol compression only if server permit it
+* CONJ-375 : Load data infile with large files fails with OutOfMemoryError
+* CONJ-370 : use KeyStore default property when not using keyStore option
+* CONJ-369 : Encoding on clob column when useServerPrepStmts=true
+* CONJ-362 : fix a possible race condition MariaDbPooledConnection
+
+## 1.5.4
+* CONJ-363 : Connection.getClientInfo implementation correction to follow JDBC rules
+* CONJ-361 : PrepareStatement setString() with empty string correction.
+* CONJ-360 : replacing ManagementFactory.getRuntimeMXBean() that cause possible slow connection depending on JVM / environment
+* CONJ-359 : Metadata getColumns(...) resultSet doesnt have "IS_GENERATEDCOLUMN" info
+
+## 1.5.3
+* CONJ-358 : Permit using private key with password that differ from keyStore password
+* CONJ-356 : secure connection : use KeyStore private key and associate public keys certificates only
+* CONJ-342 : Empty clientCertificateKeyStoreUrl option correction
+* CONJ-353 : IBM jdk compatibility issue
+* CONJ-354 : Streaming issue when using procedures in PrepareStatement/Statement
+* CONJ-345 : Regression with using COLLATE keyword in PrepareStatement query
+* CONJ-352 : metadata correction on getPrecision() for numeric fields
+* CONJ-350 : make prepare fallback to client prepare if query cannot be prepared
+
+## 1.5.2
+Release version
+* CONJ-331 : clearWarnings() now throw exception on closed connection
+* CONJ-299 : PreparedStatement.setObject(Type.BIT, "1") registered as true.
+* CONJ-293 : permit named pipe connection without host
+* CONJ-333 : ResultSet.getString() of PreparedStatement return NULL When TIME column value=00:00:00
+
+RC corrections
+* CONJ-335 : Pool connection may fail to connect with good user
+* CONJ-332 : option enabledSslCipherSuites rely on java supportedCipherSuites (replacing enabledCipherSuites)
+* UTF-8 conversion correction
+
+## 1.5.1
+Release candidate version
+### Evolution
+#### Aurora host auto-discovery
+(CONJ-325)
+
+Aurora now auto discover nodes from cluster endpoint.
+
+##### Aurora endpoints
+
+Every aurora instance has a specific endpoint, i.e. a 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 previous version, 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, like : <br />
+{{{
+jdbc:mariadb:aurora://a.yyy.zzz.rds.amazonaws.com.com,b.yyy.zzz.rds.amazonaws.com.com/db
+}}}
+This kind of url string will still work, but now, recommended url string has to use only cluster endpoint :<br/>
+{{{
+jdbc:mariadb:aurora://xxx.cluster-yyy.zzz.rds.amazonaws.com/db
+}}}
+
+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 discard 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)
+
+
+### Bugfix
+* CONJ-329 and CONJ-330 : rewriteBatchedStatements execute single query exceptions correction.
+  <br /><br />
+
+## 1.5.0
+Release candidate version
+### Use native SSPI windows implementation
+CONJ-295.<br />
+
+Java kerberos implementation is not well implemented with windows :
+* need a Windows registry entry (HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa\Kerberos\Parameters\AllowTGTSessionKey) so windows shared current ticket to java.
+* java kinit must be executed to create a Ticket.
+* restriction when client with local admin rights
+* ...
+
+[see openJDK issue](https://bugs.openjdk.java.net/browse/JDK-6722928) for more information
+
+Kerberos GSSAPI implementation on Windows in now based on [Waffle](https://github.com/dblock/waffle) that support windows SSPI based on [JNA](https://github.com/java-native-access/jna).<br />
+if waffle-jna (and dependencies) is on classpath, native implementation will automatically be used.
+
+This removes all those problems
+
+### Support for TLSv1.1 and TLSv1.2
+CONJ-249/CONJ-301<br />
+
+Driver before version 1.5 support only TLSv1.<br />
+Default supported protocol are now TLSv1 and TLSv1.1, other protocols can be activated by options.
+
+MariaDB and MySQL community server permit TLSv1 and TLSv1.1.<br />
+MariaDB server from version 10.0.15 is using the openSSL library permitting TLSv1.2 (>= 5.5.41 for the 5.5 branch).
+//YaSSL doesn't support TLSv1.2, so if MariaDB server is build from sources with YaSSL, only TLSv1 and TLSv1.1 will be available, even for version > 10.0.15//
+
+TLSv1.2 can be enabled by setting option {{{enabledSslProtocolSuites}}} to values {{{"TLSv1, TLSv1.1, TLSv1.2"}}}.
+
+A new option {{{enabledSslCipherSuites}}} permit setting specific cipher.
+
+New Options :
+|=enabledSslProtocolSuites|Force TLS/SSL protocol to a specific set of TLS versions (comma separated list). <br />Example : "TLSv1, TLSv1.1, TLSv1.2"<br />//Default: TLSv1, TLSv1.1. Since 1.5.0//|
+|=enabledSslCipherSuites|Force TLS/SSL cipher (comma separated list).<br /> Example : "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384, TLS_DHE_DSS_WITH_AES_256_GCM_SHA384"<br />//Default: use JRE ciphers. Since 1.5.0//|
+
+### Performance improvement
+[CONJ-291]<br />
+
+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).
+* there was some memory wastage when query return big resultset (> 10kb), slowing query.
+* ...
+
+[CONJ-320]
+Send X well established MySQL protocol without reading results, and read those X results afterwhile.
+Basically that permit to avoid a lot of 'ping-pong' between driver and server.
+
+New Options :
+|=useBatchMultiSend|PreparedStatement.executeBatch() will send many QUERY before reading result packets.//Default: true. Since 1.5.0//|
+|=useBatchMultiSendNumber|When using useBatchMultiSend, indicate maximum query that can be sent at a time.<br />//Default: 100. Since 1.5.0//|
+
+### Prepare + execute in one call
+CONJ-296
+
+When using MySQL/MariaDB prepared statement, there will be 3 exchanges with server :
+* PREPARE - Prepares statement for execution.
+* EXECUTE - Executes a prepared statement preparing by a PREPARE statement.
+* DEALLOCATE PREPARE - Releases a prepared statement.
+
+See [Server prepare documentation](https://mariadb.com/kb/en/mariadb/prepare-statement/) for more
+information.
+
+PREPARE and DEALLOCATE PREPARE are 2 additional client-server round-trip.
+Since MariaDB 10.2, a new functionality named COM-MULTI to permitting to send different task to server in one round-trip.
+Driver is using this functionality to PREPARE and EXECUTE in one client-server round-trip.
+
+### Client logging
+
+Client logging can be enabled, permitting to log query information, execution time and different failover information.
+This implementation need the standard SLF4J dependency.
+
+New Options :
+|=log|Enable log information. require Slf4j version > 1.4 dependency.<br />//Default: false. Since 1.5.0//|
+|=maxQuerySizeToLog|Only the first characters corresponding to this options size will be displayed in logs<br />//Default: 1024. Since 1.5.0//|
+|=slowQueryThresholdNanos|Will log query with execution time superior to this value (if defined )<br />//Default: 1024. Since 1.5.0//|
+|=profileSql|log query execution time.<br />//Default: false. Since 1.5.0//|
+
+
+### "LOAD DATA INFILE" Interceptors
+CONJ-305
+LOAD DATA INFILE The fastest way to load many datas is using query [LOAD DATA INFILE](https://mariadb.com/kb/en/mariadb/load-data-infile/).
+<br />Problem is 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 asked from server so client will send a Local file to this proxy.
+* If someone has can execute query from client, he can have access to any file on client (according to the rights of the user running the client process).
+
+See [load-data-infile documentation](./documentation/use-mariadb-connector-j-driver.creole#load-data-infile) for more information.
+
+Interceptors can now filter LOAD DATA LOCAL INFILE query's file location to validate path / file name.
+Those interceptors:
+* Must implement interface {{{org.mariadb.jdbc.LocalInfileInterceptor}}}.
+* Use [[http://docs.oracle.com/javase/7/docs/api/java/util/ServiceLoader.html|ServiceLoader]] implementation, so interceptors classes must be listed in file META-INF/services/org.mariadb.jdbc.LocalInfileInterceptor.
+
+Example:
+{{{
+package org.project;
+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");
+}
+}
+}}}
+file META-INF/services/org.mariadb.jdbc.LocalInfileInterceptor must exist with content {{{org.project.LocalInfileInterceptorImpl}}}.
+
+You can get rid of defining the META-INF/services file using [[https://github.com/google/auto/tree/master/service|google auto-service]] framework, permitting to use annotation {{{@AutoService(LocalInfileInterceptor.class)}}} that will register the implementation as a service automatically.
+
+Using the previous example:
+{{{
+@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");
+}
+}
+}}}
+
+
+
+### Minor evolution
+* CONJ-260 : Add jdbc nString, nCharacterStream, nClob implementation
+
+### Bugfix
+* CONJ-316 : Wrong Exception thrown for ScrollType TYPE_SCROLL_INSENSITIVE
+* CONJ-298 : Error on Callable function exception when no parameter and space before parenthesis
+* CONJ-314 : Permit using Call with Statement / Prepare Statement
+  <br /><br /><br />
+## 1.4.6
+* CONJ-293] Permit named pipe connection without host
+* CONJ-309] Possible NPE on aurora when failover occur during connection initialisation
+* CONJ-312] NPE while loading a null from TIMESTAMP field using binary protocol
+* misc] batch with one parameter correction (using rewriteBatchedStatements option)
+
+## 1.4.5
+* CONJ-297] Useless memory consumption when using Statement.setQueryTimeout
+* CONJ-294] PrepareStatement on master reconnection after a failover
+* CONJ-288] using SHOW VARIABLES to replace SELECT on connection to permit connection on a galera non primary node
+* CONJ-290] Timestamps format error when using prepareStatement with options useFractionalSeconds and useServerPrepStmts
+
+## 1.4.4
+* CONJ-289] PrepareStatement on master reconnection after a failover
+* CONJ-288] using SHOW VARIABLES to replace SELECT on connection to permit connection on a galera non primary node
+
+## 1.4.3
+
+* CONJ-284] Cannot read autoincremented IDs bigger than Short.MAX_VALUE
+* CONJ-283] Parsing correction on MariaDbClientPreparedStatement - syntax error on insert values
+* CONJ-282] Handling YEARs with binary prepareStatement
+* CONJ-281] Connector/J is incompatible with Google App Engine correction
+* CONJ-278] Improve prepared statement on failover
+
+## 1.4.2
+
+* CONJ-275] Streaming result without result throw "Current position is before the first row"
+
+
+## 1.4.1
+
+
+* CONJ-274] correction to permit connection to MySQL 5.1 server
+* CONJ-273] correction when using prepareStatement without parameters and option rewriteBatchedStatements to true
+* CONJ-270] permit 65535 parameters to server preparedStatement
+* CONJ-268] update license header
+* misc] when option rewriteBatchedStatements is set to true, correction of packet separation when query size > max_allow_packet
+* misc] performance improvement for select result.
+
+## 1.4.0
+
+### Complete implementation of fetch size.
+CONJ-26
+JDBC allows to specify the number of rows fetched for a query, and this number is referred to as the fetch size
+Before version 1.4.0, query were loading all results or row by row using Statement.setFetchSize(Integer.MIN_VALUE).
+Now it's possible to set fetch size according to your need.
+Loading all results for large result sets is using a lot of memory. This functionality permit to save memory without having performance decrease.
+
+### Memory footprint improvement
+CONJ-125
+Buffers have been optimized to reduced memory footprint
+
+### CallableStatement  performance improvement.
+CONJ-209
+Calling function / procedure performance is now optimized according to query. Depending on queries, difference can be up to 300%.
+
+### Authentication evolution
+CONJ-251 Permit now new authentication possibility : [[https://mariadb.com/kb/en/mariadb/pam-authentication-plugin/|PAM authentication]], and GSSAPI/SSPI authentication.
+
+GSSAPI/SSPI authentication authentication plugin for MariaDB permit a passwordless login.
+
+On Unix systems, GSSAPI is usually synonymous with Kerberos authentication. Windows has slightly different but very similar API called SSPI, that along with Kerberos, also supports NTLM authentication.
+See more detail in [[https://github.com/mariadb-corporation/mariadb-connector-j/blob/master/documentation/plugin/GSSAPI|GSSAPI/SSPI configuration]]
+
+### Connection attributes
+CONJ-217
+Driver information informations are now send to [[https://mariadb.com/kb/en/mariadb/performance-schema-session_connect_attrs-table/|connection attributes tables]] (performance_schema must be activated).
+A new option "connectionAttributes" permit to add client specifics data.
+
+For example when connecting with the following connection string {{{"jdbc:mariadb://localhost:3306/testj?user=root&connectionAttributes=myOption:1,mySecondOption:'jj'"}}},
+if performance_schema is activated, information about this connection will be available during the time this connection is active :
+{{{
+select * from performance_schema.session_connect_attrs where processList_id = 5
++----------------+-----------------+---------------------+------------------+
+| PROCESSLIST_ID | ATTR_NAME       | ATTR_VALUE          | ORDINAL_POSITION |
++----------------+-----------------+---------------------+------------------+
+|5               |_client_name     |MariaDB connector/J  |0                 |
+|5               |_client_version  |1.4.0-SNAPSHOT       |1                 |
+|5               |_os              |Windows 8.1          |2                 |
+|5               |_pid             |14124@portable-diego |3                 |
+|5               |_thread          |5                    |4                 |
+|5               |_java_vendor     |Oracle Corporation	 |5                 |
+|5               |_java_version    |1.7.0_79	         |6                 |
+|5               |myOption         |1	                 |7                 |
+|5               |mySecondOption   |'jj'                 |8                 |
++----------------+-----------------+---------------------+------------------+
+}}}
+
+
+## Minor evolution
+* CONJ-210 : adding a "jdbcCompliantTruncation" option to force truncation warning as SQLException.
+* CONJ-211 : when in master/slave configuration, option "assureReadOnly" will ensure that slaves are in read-only mode ( forcing transaction by a query "SET SESSION TRANSACTION READ ONLY").
+* CONJ-213 : new option "continueBatchOnError". Permit to continue batch when an exception occur : When executing a batch and an error occur, must the batch stop immediatly (default) or finish remaining batch before throwing exception.
+
+## Bugfix
+* CONJ-236 : Using a parametrized query with a smallint -1 does return the unsigned value
+* CONJ-250 : Tomcat doesn't stop when using Aurora failover configuration
+* CONJ-260 : Add jdbc nString, nCharacterStream, nClob implementation
+* CONJ-269 : handle server configuration autocommit=0
+* CONJ-271 : ResultSet.first() may throw SQLDataException: Current position is before the first row
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..952fe34
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,90 @@
+# Contributing
+
+Each pull request should address a single issue, and contain both the fix and 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 [MariaDB JIRA](https://mariadb.atlassian.net/secure/RapidBoard.jspa?projectKey=CONJ). 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.
+
+* At least one GPG Key see https://help.github.com/en/articles/generating-a-new-gpg-key
+* [Oracle JDK 8](http://www.oracle.com/technetwork/java/javase/downloads/index.html) ( with [JCE policies](http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html) 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.  
+
+## Branch signification
+
+* master : correspond to the last released version
+* develop : Develop new features for upcoming releases (compatible with java 8)
+* develop-jre6 : maintenance branch compatible with java 6 / 7 
+
+# 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 
+    
+```script
+    mvn test
+```
+    
+You can change this parameter by adding -DdbUrl parameter. like : 
+ 
+```script
+    mvn test -DdbUrl=jdbc:mariadb://127.0.0.1:3306/testj?user=root&password=*****
+```
+    
+You can launch a specific test by adding -Dtest
+
+```script
+    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 : 
+  
+```script
+    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
+and if your work is on develop-jre6 branch
+* openjdk6
+* openjdk7
+
+server :
+* MariaDB 5.5
+* MariaDB 10.0
+* MariaDB 10.1
+* MariaDB 10.2
+* MySQL 5.6
+* MySQL 5.7
+
+max_allowed_packet : (this permit to check multipacket send)
+* 8M
+* 20M
+* 40M
+
+For that, you have to go on [travis website](https://travis-ci.org), 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/LICENSE b/LICENSE
new file mode 100644
index 0000000..118b572
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,502 @@
+                 GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+                  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                            NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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 the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..bb05156
--- /dev/null
+++ b/README.md
@@ -0,0 +1,63 @@
+<p align="center">
+  <a href="https://mariadb.com/">
+    <img src="https://mariadb.com/kb/static/images/logo-2018-black.png">
+  </a>
+</p>
+
+# MariaDB java connector
+
+MariaDB java connector is a JDBC 4.2 compatible driver, used to connect applications developed in Java to MariaDB and MySQL databases. MariaDB Connector/J is LGPL licensed.
+
+Tracker link <a href="https://jira.mariadb.org/projects/CONJ/issues/">https://jira.mariadb.org/projects/CONJ/issues/</a>
+
+## Status
+[![Linux Build](https://travis-ci.com/mariadb-corporation/mariadb-connector-j.svg?branch=master)](https://app.travis-ci.com/github/mariadb-corporation/mariadb-connector-j)
+[![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)
+[![codecov][codecov-image]][codecov-url]
+
+## Obtaining the driver
+
+For java 8+ :
+
+The driver (jar) can be downloaded from [mariadb connector download](https://mariadb.com/downloads/#connectors)
+or maven : 
+```script
+<dependency>
+	<groupId>org.mariadb.jdbc</groupId>
+	<artifactId>mariadb-java-client</artifactId>
+	<version>3.1.2</version>
+</dependency>
+```
+
+
+Development snapshot are available on sonatype nexus repository
+```script
+<repositories>
+    <repository>
+        <id>sonatype-nexus-snapshots</id>
+        <name>Sonatype Nexus Snapshots</name>
+        <url>https://oss.sonatype.org/content/repositories/snapshots</url>
+    </repository>
+</repositories>
+
+<dependencies>
+    <dependency>
+        <groupId>org.mariadb.jdbc</groupId>
+        <artifactId>mariadb-java-client</artifactId>
+        <version>3.1.3-SNAPSHOT</version>
+    </dependency>
+</dependencies>
+```
+
+## Documentation
+
+For a Getting started guide, API docs, recipes,  etc. see the 
+* [About MariaDB connector/J](https://mariadb.com/kb/en/about-mariadb-connector-j/)
+* [Install driver](https://mariadb.com/kb/en/installing-mariadb-connectorj/)
+* [Changelog](/CHANGELOG.md)
+* [Failover and high-availability](https://mariadb.com/kb/en/failover-and-high-availability-with-mariadb-connector-j/)
+
+
+[codecov-image]:https://codecov.io/gh/mariadb-corporation/mariadb-connector-j/branch/master/graph/badge.svg
+[codecov-url]:https://codecov.io/gh/mariadb-corporation/mariadb-connector-j
diff --git a/bnd.bnd b/bnd.bnd
new file mode 100644
index 0000000..f189fda
--- /dev/null
+++ b/bnd.bnd
@@ -0,0 +1,21 @@
+Export-Package: org.mariadb.jdbc;org.mariadb.jdbc.client;org.mariadb.jdbc.client.util;org.mariadb.jdbc.client.socket;org.mariadb.jdbc.message;org.mariadb.jdbc.type;org.mariadb.jdbc.export;org.mariadb.jdbc.plugin;org.mariadb.jdbc.plugin.codec;org.mariadb.jdbc.plugin.authentication.standard;org.mariadb.jdbc.plugin.authentication.addon;org.mariadb.jdbc.plugin.credential.aws;org.mariadb.jdbc.plugin.credential.env;org.mariadb.jdbc.plugin.credential.system;org.mariadb.jdbc.plugin.tls.main
+
+Import-Package: \
+    javax.naming,\
+    javax.naming.ldap,\
+    javax.management,\
+    javax.sql,\
+    org.slf4j;resolution:=optional, \
+    javax.net;resolution:=optional,\
+    javax.net.ssl;resolution:=optional,\
+    com.sun.jna;resolution:=optional,\
+    com.sun.jna.platform.win32;resolution:=optional,\
+    javax.transaction.xa;resolution:=optional,\
+    waffle.windows.auth;resolution:=optional,\
+    waffle.windows.auth.impl;resolution:=optional,\
+    org.ietf.jgss;resolution:=optional,\
+    javax.security.auth.login;resolution:=optional,\
+    javax.security.auth.x500;resolution:=optional,\
+    software.amazon.awssdk.*;resolution:=optional
+
+-fixupmessages: "Classes found in the wrong directory:";is:=ignore
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..a107d78
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,462 @@
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.mariadb.jdbc</groupId>
+    <artifactId>mariadb-java-client</artifactId>
+    <packaging>jar</packaging>
+    <name>mariadb-java-client</name>
+    <version>3.1.2</version>
+    <description>JDBC driver for MariaDB and MySQL</description>
+    <url>https://mariadb.com/kb/en/mariadb/about-mariadb-connector-j/</url>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <jmh.version>1.34</jmh.version>
+        <junit.version>5.8.2</junit.version>
+        <junit-pioneer.version>1.5.0</junit-pioneer.version>
+        <assertj.version>3.22.0</assertj.version>
+        <osgi.version>6.0.0</osgi.version>
+        <osgi.compendium.version>5.0.0</osgi.compendium.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <logback.version>1.4.4</logback.version>
+        <jacoco.version>0.8.8</jacoco.version>
+        <waffle-jna.version>3.2.0</waffle-jna.version>
+        <mysql-connector-java.version>8.0.31</mysql-connector-java.version>
+        <bnd-maven-plugin.version>6.3.1</bnd-maven-plugin.version>
+    </properties>
+
+    <licenses>
+        <license>
+            <name>LGPL-2.1</name>
+        </license>
+    </licenses>
+
+    <organization>
+        <name>mariadb.com</name>
+        <url>https://mariadb.com</url>
+    </organization>
+
+    <developers>
+        <developer>
+            <id>mariadbJdbcDevelopers</id>
+            <name>mariadb jdbc developers</name>
+            <url>http://mariadb.org/</url>
+        </developer>
+    </developers>
+
+    <scm>
+        <connection>scm:git:git://github.com/mariadb-corporation/mariadb-connector-j.git</connection>
+        <url>https://github.com/mariadb-corporation/mariadb-connector-j</url>
+        <developerConnection>scm:git:git@github.com:MariaDB/mariadb-connector-j.git
+        </developerConnection>
+    </scm>
+
+    <issueManagement>
+        <system>JIRA</system>
+        <url>https://mariadb.atlassian.net/browse/CONJ</url>
+    </issueManagement>
+
+    <distributionManagement>
+        <snapshotRepository>
+            <id>ossrh</id>
+            <url>https://oss.sonatype.org/content/repositories/snapshots</url>
+        </snapshotRepository>
+    </distributionManagement>
+
+    <dependencyManagement>
+        <dependencies>
+
+            <dependency>
+                <groupId>org.junit</groupId>
+                <artifactId>junit-bom</artifactId>
+                <version>${junit.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+            <dependency>
+                <groupId>software.amazon.awssdk</groupId>
+                <artifactId>bom</artifactId>
+                <version>2.18.10</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+
+    </dependencyManagement>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>true</filtering>
+            </resource>
+        </resources>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.10.1</version>
+                <configuration>
+                    <showWarnings>true</showWarnings>
+                    <compilerArgs>
+                        <arg>-Xlint:all,-options,-path,-processing</arg>
+                    </compilerArgs>
+                    <source>1.8</source>
+                    <target>1.8</target>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>compile-java-8</id>
+                        <goals>
+                            <goal>compile</goal>
+                        </goals>
+                    </execution>
+                    <execution>
+                        <id>compile-java-9</id>
+                        <phase>compile</phase>
+                        <goals>
+                            <goal>compile</goal>
+                        </goals>
+                        <configuration>
+                            <release>9</release>
+                            <source>9</source>
+                            <target>9</target>
+                            <compileSourceRoots>
+                                <compileSourceRoot>${project.basedir}/src/main/java9</compileSourceRoot>
+                            </compileSourceRoots>
+                            <multiReleaseOutput>true</multiReleaseOutput>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>compile-java-11</id>
+                        <phase>compile</phase>
+                        <goals>
+                            <goal>compile</goal>
+                        </goals>
+                        <configuration>
+                            <release>11</release>
+                            <source>11</source>
+                            <target>11</target>
+                            <compileSourceRoots>
+                                <compileSourceRoot>${project.basedir}/src/main/java11</compileSourceRoot>
+                            </compileSourceRoots>
+                            <multiReleaseOutput>true</multiReleaseOutput>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <version>3.4.1</version>
+                <configuration>
+                    <source>1.8</source>
+                    <excludePackageNames>
+                        org.mariadb.jdbc.client,org.mariadb.jdbc.codec,org.mariadb.jdbc.message,org.mariadb.jdbc.util,org.mariadb.jdbc.plugin.authentication.standard.ed25519,org.mariadb.jdbc.plugin.authentication.standard.ed25519.*
+                    </excludePackageNames>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>attach-javadocs</id>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>3.3.0</version>
+                <configuration>
+                    <archive>
+                        <manifestEntries>
+                            <Multi-Release>true</Multi-Release>
+                            <Export-Package>org.mariadb.jdbc</Export-Package>
+                            <Import-Package>
+                                javax.naming,javax.naming.ldap,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.ietf.jgss;resolution:=optional,javax.security.auth.login;resolution:=optional,javax.security.auth.x500;resolution:=optional,software.amazon.awssdk.*;resolution:=optional,org.slf4j;resolution:=optional
+                            </Import-Package>
+                        </manifestEntries>
+                        <manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
+                    </archive>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+                <version>3.2.1</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.sonatype.plugins</groupId>
+                <artifactId>nexus-staging-maven-plugin</artifactId>
+                <version>1.6.13</version>
+                <extensions>true</extensions>
+                <executions>
+                    <execution>
+                        <phase>deploy</phase>
+                    </execution>
+                </executions>
+                <configuration>
+                    <serverId>ossrh</serverId>
+                    <nexusUrl>https://oss.sonatype.org/</nexusUrl>
+                    <autoReleaseAfterClose>false</autoReleaseAfterClose>
+                    <stagingProgressTimeoutMinutes>15</stagingProgressTimeoutMinutes>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-gpg-plugin</artifactId>
+                <version>3.0.1</version>
+                <executions>
+                    <execution>
+                        <phase>verify</phase>
+                        <goals>
+                            <goal>sign</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>com.coveo</groupId>
+                <artifactId>fmt-maven-plugin</artifactId>
+                <version>2.13</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>format</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>3.0.0-M7</version>
+            </plugin>
+
+            <plugin>
+                <groupId>biz.aQute.bnd</groupId>
+                <artifactId>bnd-maven-plugin</artifactId>
+                <version>${bnd-maven-plugin.version}</version>
+                <configuration>
+                    <bndfile>bnd.bnd</bndfile>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>bnd-process</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+        </plugins>
+    </build>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>com.github.waffle</groupId>
+            <artifactId>waffle-jna</artifactId>
+            <version>${waffle-jna.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <version>${assertj.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.junit-pioneer</groupId>
+            <artifactId>junit-pioneer</artifactId>
+            <version>${junit-pioneer.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>2.0.3</version>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <version>${logback.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+            <version>${osgi.version}</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+            <version>${osgi.compendium.version}</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>software.amazon.awssdk</groupId>
+            <artifactId>rds</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+    </dependencies>
+
+
+    <profiles>
+        <profile>
+            <id>default</id>
+            <activation>
+                <activeByDefault>true</activeByDefault>
+            </activation>
+
+            <build>
+                <plugins>
+
+                    <plugin>
+                        <groupId>org.jacoco</groupId>
+                        <artifactId>jacoco-maven-plugin</artifactId>
+                        <version>${jacoco.version}</version>
+                        <configuration>
+                            <excludes>
+                                <exclude>META-INF/**</exclude>
+                            </excludes>
+                        </configuration>
+                        <executions>
+                            <execution>
+                                <id>jacoco-initialize</id>
+                                <goals>
+                                    <goal>prepare-agent</goal>
+                                </goals>
+                            </execution>
+                            <execution>
+                                <id>report</id>
+                                <phase>test</phase>
+                                <goals>
+                                    <goal>report</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                    </plugin>
+
+                </plugins>
+            </build>
+        </profile>
+
+        <profile>
+            <id>bench</id>
+            <dependencies>
+                <dependency>
+                    <groupId>org.openjdk.jmh</groupId>
+                    <artifactId>jmh-core</artifactId>
+                    <version>${jmh.version}</version>
+                </dependency>
+                <dependency>
+                    <groupId>org.openjdk.jmh</groupId>
+                    <artifactId>jmh-generator-annprocess</artifactId>
+                    <version>${jmh.version}</version>
+                </dependency>
+
+                <dependency>
+                    <groupId>mysql</groupId>
+                    <artifactId>mysql-connector-java</artifactId>
+                    <version>${mysql-connector-java.version}</version>
+                </dependency>
+
+            </dependencies>
+
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.codehaus.mojo</groupId>
+                        <artifactId>build-helper-maven-plugin</artifactId>
+                        <version>3.0.0</version>
+                        <executions>
+                            <execution>
+                                <id>add-source</id>
+                                <phase>generate-sources</phase>
+                                <goals>
+                                    <goal>add-source</goal>
+                                </goals>
+                                <configuration>
+                                    <sources>
+                                        <source>src/benchmark/java</source>
+                                    </sources>
+                                </configuration>
+                            </execution>
+                            <execution>
+                                <id>add-resource</id>
+                                <phase>generate-resources</phase>
+                                <goals>
+                                    <goal>add-resource</goal>
+                                </goals>
+                                <configuration>
+                                    <resources>
+                                        <resource>
+                                            <directory>src/benchmark/resources</directory>
+                                        </resource>
+                                    </resources>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-shade-plugin</artifactId>
+                        <version>3.2.4</version>
+                        <configuration>
+                            <finalName>benchmarks</finalName>
+                            <transformers>
+                                <transformer
+                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                                    <mainClass>org.openjdk.jmh.Main</mainClass>
+                                </transformer>
+                            </transformers>
+                        </configuration>
+                        <executions>
+                            <execution>
+                                <phase>package</phase>
+                                <goals>
+                                    <goal>shade</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
+</project>
diff --git a/src/benchmark/README.md b/src/benchmark/README.md
new file mode 100644
index 0000000..1a5158f
--- /dev/null
+++ b/src/benchmark/README.md
@@ -0,0 +1,33 @@
+<p align="center">
+  <a href="https://mariadb.com/">
+    <img src="https://mariadb.com/kb/static/images/logo-2018-black.png">
+  </a>
+</p>
+
+# Benchmark
+
+How to run : 
+```script
+mvn clean package -P bench -DskipTests
+
+# run all benchmarks
+nohup java -Duser.country=US -Duser.language=en -jar target/benchmarks.jar > log.txt
+
+# run a specific benchmark
+java -Duser.country=US -Duser.language=en -jar target/benchmarks.jar "Select_100_cols"
+```
+
+Configuration by system properties :
+* TEST_HOST: Hostname. default "localhost"
+* TEST_PORT: port. default 3306
+* TEST_USERNAME: user name. default "root"
+* TEST_PASSWORD: password. default ""
+* TEST_DATABASE: database. default "testj"
+* TEST_OTHER: permit adding connection string options. default ""
+
+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_100_cols"
+```
+
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 0000000..8a43e13
--- /dev/null
+++ b/src/benchmark/java/org/mariadb/jdbc/Common.java
@@ -0,0 +1,200 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.*;
+
+@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 {
+
+  // conf
+  public static final String host = System.getProperty("TEST_HOST", "localhost");
+  public static final int port = Integer.parseInt(System.getProperty("TEST_PORT", "3306"));
+  public static final String username = System.getProperty("TEST_USERNAME", "root");
+  public static final String password = System.getProperty("TEST_PASSWORD", "");
+  public static final String database = System.getProperty("TEST_DATABASE", "testj");
+  public static final String other = System.getProperty("TEST_OTHER", "");
+
+  static {
+    new SetupData();
+  }
+
+  @State(Scope.Thread)
+  public static class MyState {
+
+    // connections
+    protected Connection connectionText;
+    protected Connection connectionTextRewrite;
+
+    protected Connection connectionBinary;
+
+    protected Connection connectionBinaryNoPipeline;
+    protected Connection connectionBinaryNoCache;
+
+    @Param({"mysql", "mariadb"})
+    String driver;
+
+    @Setup(Level.Trial)
+    public void createConnections() 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 =
+            "jdbc:%s://%s:%s/%s?user=%s&password=%s&sslMode=DISABLED&useServerPrepStmts=%s&cachePrepStmts=%s&serverTimezone=UTC%s";
+        String jdbcUrlText =
+            String.format(
+                jdbcBase, driver, host, port, database, username, password, false, false, other);
+        String jdbcUrlBinary =
+            String.format(
+                jdbcBase, driver, host, port, database, username, password, true, true, other);
+
+        connectionText =
+            ((java.sql.Driver) Class.forName(className).getDeclaredConstructor().newInstance())
+                .connect(jdbcUrlText, new Properties());
+        String jdbcUrlTextRewrite =
+            String.format(
+                jdbcBase,
+                driver,
+                host,
+                port,
+                database,
+                username,
+                password,
+                false,
+                false,
+                "&rewriteBatchedStatements=true&useBulkStmts=false" + other);
+        connectionTextRewrite =
+            ((java.sql.Driver) Class.forName(className).getDeclaredConstructor().newInstance())
+                .connect(jdbcUrlTextRewrite, new Properties());
+        connectionBinary =
+            ((java.sql.Driver) Class.forName(className).getDeclaredConstructor().newInstance())
+                .connect(jdbcUrlBinary, new Properties());
+
+        String jdbcUrlBinaryNoCache =
+            String.format(
+                jdbcBase,
+                driver,
+                host,
+                port,
+                database,
+                username,
+                password,
+                true,
+                false,
+                "&prepStmtCacheSize=0" + other);
+
+        connectionBinaryNoCache =
+            ((java.sql.Driver) Class.forName(className).getDeclaredConstructor().newInstance())
+                .connect(jdbcUrlBinaryNoCache, new Properties());
+
+        String jdbcUrlBinaryNoCacheNoPipeline =
+            String.format(
+                jdbcBase,
+                driver,
+                host,
+                port,
+                database,
+                username,
+                password,
+                true,
+                true,
+                "&prepStmtCacheSize=0&cachePrepStmts=false&disablePipeline=true" + other);
+        connectionBinaryNoPipeline =
+            ((java.sql.Driver) Class.forName(className).getDeclaredConstructor().newInstance())
+                .connect(jdbcUrlBinaryNoCacheNoPipeline, new Properties());
+      } catch (SQLException e) {
+        e.printStackTrace();
+        throw new RuntimeException(e);
+      }
+    }
+
+    @TearDown(Level.Trial)
+    public void doTearDown() throws SQLException {
+      connectionText.close();
+      connectionBinary.close();
+      connectionTextRewrite.close();
+      connectionBinaryNoCache.close();
+      connectionBinaryNoPipeline.close();
+    }
+  }
+
+  public static class SetupData {
+    static {
+      try {
+        try (Connection conn =
+            DriverManager.getConnection(
+                String.format(
+                    "jdbc:mariadb://%s:%s/%s?user=%s&password=%s",
+                    host, port, database, username, password))) {
+          Statement stmt = conn.createStatement();
+          try {
+            stmt.executeQuery("INSTALL SONAME 'ha_blackhole'");
+          } catch (SQLException e) {
+            // eat
+          }
+          stmt.executeUpdate("DROP TABLE IF EXISTS testBlackHole");
+          stmt.executeUpdate("DROP TABLE IF EXISTS test100");
+
+          try {
+            stmt.executeUpdate(
+                "CREATE TABLE testBlackHole (id INT, t VARCHAR(256)) ENGINE = BLACKHOLE");
+          } catch (SQLException e) {
+            stmt.executeUpdate("CREATE TABLE testBlackHole (id INT, t VARCHAR(256))");
+          }
+
+          StringBuilder sb = new StringBuilder("CREATE TABLE test100 (i1 int");
+          StringBuilder sb2 = new StringBuilder("INSERT INTO test100 value (1");
+          for (int i = 2; i <= 100; i++) {
+            sb.append(",i").append(i).append(" int");
+            sb2.append(",").append(i);
+          }
+          sb.append(")");
+          sb2.append(")");
+          stmt.executeUpdate(sb.toString());
+          stmt.executeUpdate(sb2.toString());
+
+          stmt.execute("DROP TABLE IF EXISTS perfTestTextBatch");
+          try {
+            stmt.execute("INSTALL SONAME 'ha_blackhole'");
+          } catch (SQLException e) {
+          }
+
+          String createTable =
+              "CREATE TABLE perfTestTextBatch (id MEDIUMINT NOT NULL AUTO_INCREMENT,t0 text,"
+                  + " PRIMARY KEY (id)) COLLATE='utf8mb4_unicode_ci'";
+          try {
+            stmt.execute(createTable + " ENGINE = BLACKHOLE");
+          } catch (SQLException e) {
+            stmt.execute(createTable);
+          }
+        }
+      } catch (SQLException e) {
+        e.printStackTrace();
+      }
+    }
+  }
+}
diff --git a/src/benchmark/java/org/mariadb/jdbc/Do_1.java b/src/benchmark/java/org/mariadb/jdbc/Do_1.java
new file mode 100644
index 0000000..ef6466d
--- /dev/null
+++ b/src/benchmark/java/org/mariadb/jdbc/Do_1.java
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.sql.Statement;
+import org.openjdk.jmh.annotations.Benchmark;
+
+public class Do_1 extends Common {
+
+  @Benchmark
+  public int run(MyState state) throws Throwable {
+    try (Statement st = state.connectionText.createStatement()) {
+      return st.executeUpdate("DO 1");
+    }
+  }
+}
diff --git a/src/benchmark/java/org/mariadb/jdbc/Do_1000_params.java b/src/benchmark/java/org/mariadb/jdbc/Do_1000_params.java
new file mode 100644
index 0000000..a31af30
--- /dev/null
+++ b/src/benchmark/java/org/mariadb/jdbc/Do_1000_params.java
@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import org.openjdk.jmh.annotations.Benchmark;
+
+public class Do_1000_params extends Common {
+
+  private static final String sql;
+
+  static {
+    StringBuilder sb = new StringBuilder("do ?");
+    for (int i = 1; i < 1000; i++) {
+      sb.append(",?");
+    }
+    sql = sb.toString();
+  }
+
+  @Benchmark
+  public int text(MyState state) throws Throwable {
+    return run(state.connectionText);
+  }
+
+  //    @Benchmark
+  //    public int binary(MyState state) throws Throwable {
+  //        return run(state.connectionBinary);
+  //    }
+
+  private int run(Connection con) throws Throwable {
+
+    try (PreparedStatement st = con.prepareStatement(sql)) {
+      for (int i = 1; i <= 1000; i++) {
+        st.setInt(i, i);
+      }
+      return st.executeUpdate();
+    }
+  }
+}
diff --git a/src/benchmark/java/org/mariadb/jdbc/Insert_batch.java b/src/benchmark/java/org/mariadb/jdbc/Insert_batch.java
new file mode 100644
index 0000000..400c2b9
--- /dev/null
+++ b/src/benchmark/java/org/mariadb/jdbc/Insert_batch.java
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.openjdk.jmh.annotations.Benchmark;
+
+public class Insert_batch extends Common {
+
+  static final List<String> chars = new ArrayList<>();
+
+  static {
+    chars.addAll(Arrays.asList("123456789abcdefghijklmnop\\Z".split("")));
+    chars.add("😎");
+    chars.add("🌶");
+    chars.add("🎤");
+    chars.add("🥂");
+  }
+
+  public static String randomString(int length) {
+    StringBuilder result = new StringBuilder();
+    for (int i = length; i > 0; --i)
+      result.append(chars.get(Math.round((int) Math.random() * (chars.size() - 1))));
+    return result.toString();
+  }
+
+  @Benchmark
+  public int[] binary(MyState state) throws Throwable {
+    return run(state.connectionBinary);
+  }
+
+  @Benchmark
+  public int[] rewrite(MyState state) throws Throwable {
+    return run(state.connectionTextRewrite);
+  }
+
+  private int[] run(Connection con) throws Throwable {
+    String s = randomString(100);
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO perfTestTextBatch(t0) VALUES (?)")) {
+      for (int i = 0; i < 100; i++) {
+        prep.setString(1, s);
+        prep.addBatch();
+      }
+      return prep.executeBatch();
+    }
+  }
+}
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 0000000..32ce59d
--- /dev/null
+++ b/src/benchmark/java/org/mariadb/jdbc/Select_1.java
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.sql.ResultSet;
+import java.sql.Statement;
+import org.openjdk.jmh.annotations.Benchmark;
+
+public class Select_1 extends Common {
+
+  @Benchmark
+  public int run(MyState state) throws Throwable {
+    try (Statement st = state.connectionText.createStatement()) {
+      ResultSet rs = st.executeQuery("select 1");
+      rs.next();
+      return rs.getInt(1);
+    }
+  }
+}
diff --git a/src/benchmark/java/org/mariadb/jdbc/Select_1000_Rows.java b/src/benchmark/java/org/mariadb/jdbc/Select_1000_Rows.java
new file mode 100644
index 0000000..8a298a8
--- /dev/null
+++ b/src/benchmark/java/org/mariadb/jdbc/Select_1000_Rows.java
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import org.openjdk.jmh.annotations.Benchmark;
+
+public class Select_1000_Rows extends Common {
+  private static final String sql =
+      "select seq, 'abcdefghijabcdefghijabcdefghijaa' from seq_1_to_1000";
+
+  @Benchmark
+  public int text(MyState state) throws Throwable {
+    return run(state.connectionText);
+  }
+
+  @Benchmark
+  public int binary(MyState state) throws Throwable {
+    return run(state.connectionBinary);
+  }
+
+  private int run(Connection con) throws Throwable {
+    try (PreparedStatement st = con.prepareStatement(sql)) {
+      ResultSet rs = st.executeQuery();
+      int i = 0;
+      while (rs.next()) {
+        i = rs.getInt(1);
+        rs.getString(2);
+      }
+      return i;
+    }
+  }
+}
diff --git a/src/benchmark/java/org/mariadb/jdbc/Select_100_cols.java b/src/benchmark/java/org/mariadb/jdbc/Select_100_cols.java
new file mode 100644
index 0000000..94c6114
--- /dev/null
+++ b/src/benchmark/java/org/mariadb/jdbc/Select_100_cols.java
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.sql.*;
+import java.sql.Connection;
+import org.openjdk.jmh.annotations.Benchmark;
+
+public class Select_100_cols extends Common {
+
+  @Benchmark
+  public int[] text(MyState state) throws Throwable {
+    return run(state.connectionText);
+  }
+
+  @Benchmark
+  public int[] binary(MyState state) throws Throwable {
+    return run(state.connectionBinary);
+  }
+
+  @Benchmark
+  public int[] binaryNoCache(MyState state) throws Throwable {
+    return run(state.connectionBinaryNoCache);
+  }
+
+  @Benchmark
+  public int[] binaryNoPipeline(MyState state) throws Throwable {
+    return run(state.connectionBinaryNoPipeline);
+  }
+
+  private int[] run(Connection con) throws Throwable {
+
+    try (PreparedStatement prep = con.prepareStatement("select * FROM test100")) {
+      ResultSet rs = prep.executeQuery();
+      rs.next();
+      int[] objs = new int[100];
+      for (int i = 0; i < 100; i++) {
+        objs[i] = rs.getInt(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 0000000..3dbbecb
--- /dev/null
+++ b/src/benchmark/resources/logback-test.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+
+  <appender class="ch.qos.logback.core.ConsoleAppender" name="STDOUT">
+    <encoder>
+      <pattern>%d{yyyy-MM-dd} | %d{HH:mm:ss.SSS} | %-20.20thread | %5p | %logger{25} | %m%n
+      </pattern>
+    </encoder>
+  </appender>
+
+  <logger level="ERROR" name="org.mariadb.jdbc"/>
+
+  <root level="WARN">
+    <appender-ref ref="STDOUT"/>
+  </root>
+
+</configuration>
\ No newline at end of file
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 0000000..92d0e3d
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/BaseCallableStatement.java
@@ -0,0 +1,2832 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+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.export.ExceptionFactory;
+import org.mariadb.jdbc.util.ParameterList;
+
+/** Common methods for function/stored procedure */
+public abstract class BaseCallableStatement extends ServerPreparedStatement
+    implements CallableStatement {
+
+  /** Database name */
+  protected final String databaseName;
+
+  /** Procedure name */
+  protected final String procedureName;
+
+  /** parameter metadata */
+  protected CallableParameterMetaData parameterMetaData = null;
+
+  /** Declared output parameters */
+  protected final Set<Integer> outputParameters = new HashSet<>();
+
+  /** output parameter result */
+  private Result outputResult = null;
+
+  /**
+   * Constructor
+   *
+   * @param sql sql command
+   * @param con connection
+   * @param lock thread safe lock
+   * @param databaseName database name
+   * @param procedureName procedure name
+   * @param canUseServerTimeout indicate if server support server timeout
+   * @param canUseServerMaxRows indicate if server support server max rows
+   * @param canCachePrepStmts can cache server prepared result
+   * @param resultSetType resultset type
+   * @param resultSetConcurrency resultset concurrency
+   * @param defaultFetchSize default fetch size
+   * @throws SQLException if prepare fails
+   */
+  public BaseCallableStatement(
+      String sql,
+      Connection con,
+      ReentrantLock lock,
+      String databaseName,
+      String procedureName,
+      boolean canUseServerTimeout,
+      boolean canUseServerMaxRows,
+      boolean canCachePrepStmts,
+      int resultSetType,
+      int resultSetConcurrency,
+      int defaultFetchSize)
+      throws SQLException {
+    super(
+        sql,
+        con,
+        lock,
+        canUseServerTimeout,
+        canUseServerMaxRows,
+        canCachePrepStmts,
+        Statement.RETURN_GENERATED_KEYS,
+        resultSetType,
+        resultSetConcurrency,
+        defaultFetchSize);
+    this.databaseName = databaseName;
+    this.procedureName = procedureName;
+  }
+
+  /**
+   * Indicate if callable statement is a function or a stored procedure
+   *
+   * @return indicate if is a function
+   */
+  public abstract boolean isFunction();
+
+  /**
+   * Output result without output parameters
+   *
+   * @param i index
+   * @throws SQLException if any exception
+   */
+  protected void outputResultFromRes(int i) throws SQLException {
+    this.outputResult = (Result) this.results.remove(this.results.size() - i);
+    this.outputResult.next();
+  }
+
+  /**
+   * Registers the OUT parameter in ordinal position <code>parameterIndex</code> to the JDBC type
+   * <code>sqlType</code>. All OUT parameters must be registered before a stored procedure is
+   * executed.
+   *
+   * <p>The JDBC type specified by <code>sqlType</code> for an OUT parameter determines the Java
+   * type that must be used in the <code>get</code> method to read the value of that parameter.
+   *
+   * <p>If the JDBC type expected to be returned to this output parameter is specific to this
+   * particular database, <code>sqlType</code> should be <code>java.sql.Types.OTHER</code>. 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 <code>java.sql.Types</code>. If the parameter is
+   *     of JDBC type <code>NUMERIC</code> or <code>DECIMAL</code>, the version of <code>
+   *     registerOutParameter</code> 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</code>
+   * @throws SQLFeatureNotSupportedException if <code>sqlType</code> is a <code>ARRAY</code>, <code>
+   *     BLOB</code>, <code>CLOB</code>, <code>DATALINK</code>, <code>JAVA_OBJECT</code>, <code>
+   *     NCHAR</code>, <code>NCLOB</code>, <code>NVARCHAR</code>, <code>LONGNVARCHAR</code>, <code>
+   *     REF</code>, <code>ROWID</code>, <code>SQLXML</code> or <code>STRUCT</code> 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);
+    if (!parameters.containsKey(parameterIndex - 1))
+      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 <code>parameterIndex</code> to be of JDBC type
+   * <code>sqlType</code>. All OUT parameters must be registered before a stored procedure is
+   * executed.
+   *
+   * <p>The JDBC type specified by <code>sqlType</code> for an OUT parameter determines the Java
+   * type that must be used in the <code>get</code> method to read the value of that parameter.
+   *
+   * <p>This version of <code>registerOutParameter</code> should be used when the parameter is of
+   * JDBC type <code>NUMERIC</code> or <code>DECIMAL</code>.
+   *
+   * @param parameterIndex the first parameter is 1, the second is 2, and so on
+   * @param sqlType the SQL type code defined by <code>java.sql.Types</code>.
+   * @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</code>
+   * @throws SQLFeatureNotSupportedException if <code>sqlType</code> is a <code>ARRAY</code>, <code>
+   *     BLOB</code>, <code>CLOB</code>, <code>DATALINK</code>, <code>JAVA_OBJECT</code>, <code>
+   *     NCHAR</code>, <code>NCLOB</code>, <code>NVARCHAR</code>, <code>LONGNVARCHAR</code>, <code>
+   *     REF</code>, <code>ROWID</code>, <code>SQLXML</code> or <code>STRUCT</code> 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);
+  }
+
+  @Override
+  public void clearParameters() throws SQLException {
+    checkNotClosed();
+    parameters = new ParameterList();
+    outputParameters.stream().forEach(index -> parameters.set(index - 1, Parameter.NULL_PARAMETER));
+  }
+
+  /**
+   * Retrieves whether the last OUT parameter read had the value of SQL <code>NULL</code>. 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 <code>null</code> or not.
+   *
+   * @return <code>true</code> if the last parameter read was SQL <code>NULL</code>; <code>false
+   *     </code> otherwise
+   * @throws SQLException if a database access error occurs or this method is called on a closed
+   *     <code>CallableStatement</code>
+   */
+  @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 <code>CHAR</code>, <code>VARCHAR</code>, or <code>
+   * LONGVARCHAR</code> parameter as a <code>String</code> in the Java programming language.
+   *
+   * <p>For the fixed-length type JDBC <code>CHAR</code>, the <code>String</code> object returned
+   * has exactly the same value the SQL <code>CHAR</code> 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 <code>NULL</code>, the result is <code>null
+   *     </code>.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @see #setString
+   */
+  @Override
+  public String getString(int parameterIndex) throws SQLException {
+    checkNotClosed();
+    checkOutputResult();
+    return outputResult.getString(idxToOutIdx(parameterIndex));
+  }
+
+  /**
+   * Retrieves the value of the designated JDBC <code>BIT</code> or <code>BOOLEAN</code> parameter
+   * as a <code>boolean</code> 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 <code>NULL</code>, the result is <code>false
+   *     </code>.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @see #setBoolean
+   */
+  @Override
+  public boolean getBoolean(int parameterIndex) throws SQLException {
+    checkNotClosed();
+    checkOutputResult();
+    return outputResult.getBoolean(idxToOutIdx(parameterIndex));
+  }
+
+  /**
+   * Retrieves the value of the designated JDBC <code>TINYINT</code> parameter as a <code>byte
+   * </code> 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 <code>NULL</code>, the result is <code>0
+   *     </code>.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @see #setByte
+   */
+  @Override
+  public byte getByte(int parameterIndex) throws SQLException {
+    checkNotClosed();
+    checkOutputResult();
+    return outputResult.getByte(idxToOutIdx(parameterIndex));
+  }
+
+  /**
+   * Retrieves the value of the designated JDBC <code>SMALLINT</code> parameter as a <code>short
+   * </code> 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 <code>NULL</code>, the result is <code>0
+   *     </code>.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @see #setShort
+   */
+  @Override
+  public short getShort(int parameterIndex) throws SQLException {
+    checkNotClosed();
+    checkOutputResult();
+    return outputResult.getShort(idxToOutIdx(parameterIndex));
+  }
+
+  /**
+   * Retrieves the value of the designated JDBC <code>INTEGER</code> parameter as an <code>int
+   * </code> 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 <code>NULL</code>, the result is <code>0
+   *     </code>.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @see #setInt
+   */
+  @Override
+  public int getInt(int parameterIndex) throws SQLException {
+    checkNotClosed();
+    checkOutputResult();
+    return outputResult.getInt(idxToOutIdx(parameterIndex));
+  }
+
+  /**
+   * Retrieves the value of the designated JDBC <code>BIGINT</code> parameter as a <code>long</code>
+   * 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 <code>NULL</code>, the result is <code>0
+   *     </code>.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @see #setLong
+   */
+  @Override
+  public long getLong(int parameterIndex) throws SQLException {
+    checkNotClosed();
+    checkOutputResult();
+    return outputResult.getLong(idxToOutIdx(parameterIndex));
+  }
+
+  /**
+   * Retrieves the value of the designated JDBC <code>FLOAT</code> parameter as a <code>float</code>
+   * 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 <code>NULL</code>, the result is <code>0
+   *     </code>.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @see #setFloat
+   */
+  @Override
+  public float getFloat(int parameterIndex) throws SQLException {
+    checkNotClosed();
+    checkOutputResult();
+    return outputResult.getFloat(idxToOutIdx(parameterIndex));
+  }
+
+  /**
+   * Retrieves the value of the designated JDBC <code>DOUBLE</code> parameter as a <code>double
+   * </code> 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 <code>NULL</code>, the result is <code>0
+   *     </code>.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @see #setDouble
+   */
+  @Override
+  public double getDouble(int parameterIndex) throws SQLException {
+    checkNotClosed();
+    checkOutputResult();
+    return outputResult.getDouble(idxToOutIdx(parameterIndex));
+  }
+
+  /**
+   * Retrieves the value of the designated JDBC <code>NUMERIC</code> parameter as a <code>
+   * java.math.BigDecimal</code> object with <i>scale</i> 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 <code>NULL</code>, the result is <code>null
+   *     </code>.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @see #setBigDecimal
+   * @deprecated use <code>getBigDecimal(int parameterIndex)</code> or <code>
+   *     getBigDecimal(String parameterName)</code>
+   */
+  @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 <code>BINARY</code> or <code>VARBINARY</code>
+   * parameter as an array of <code>byte</code> 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 <code>NULL</code>, the result is <code>null
+   *     </code>.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @see #setBytes
+   */
+  @Override
+  public byte[] getBytes(int parameterIndex) throws SQLException {
+    checkNotClosed();
+    checkOutputResult();
+    return outputResult.getBytes(idxToOutIdx(parameterIndex));
+  }
+
+  /**
+   * Retrieves the value of the designated JDBC <code>DATE</code> parameter as a <code>java.sql.Date
+   * </code> object.
+   *
+   * @param parameterIndex the first parameter is 1, the second is 2, and so on
+   * @return the parameter value. If the value is SQL <code>NULL</code>, the result is <code>null
+   *     </code>.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @see #setDate
+   */
+  @Override
+  public Date getDate(int parameterIndex) throws SQLException {
+    checkNotClosed();
+    checkOutputResult();
+    return outputResult.getDate(idxToOutIdx(parameterIndex));
+  }
+
+  /**
+   * Retrieves the value of the designated JDBC <code>TIME</code> parameter as a <code>java.sql.Time
+   * </code> object.
+   *
+   * @param parameterIndex the first parameter is 1, the second is 2, and so on
+   * @return the parameter value. If the value is SQL <code>NULL</code>, the result is <code>null
+   *     </code>.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @see #setTime
+   */
+  @Override
+  public Time getTime(int parameterIndex) throws SQLException {
+    checkNotClosed();
+    checkOutputResult();
+    return outputResult.getTime(idxToOutIdx(parameterIndex));
+  }
+
+  /**
+   * Retrieves the value of the designated JDBC <code>TIMESTAMP</code> parameter as a <code>
+   * java.sql.Timestamp</code> object.
+   *
+   * @param parameterIndex the first parameter is 1, the second is 2, and so on
+   * @return the parameter value. If the value is SQL <code>NULL</code>, the result is <code>null
+   *     </code>.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @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 <code>Object</code> in the Java
+   * programming language. If the value is an SQL <code>NULL</code>, the driver returns a Java
+   * <code>null</code>.
+   *
+   * <p>This method returns a Java object whose type corresponds to the JDBC type that was
+   * registered for this parameter using the method <code>registerOutParameter</code>. By
+   * registering the target JDBC types as <code>java.sql.Types.OTHER</code>, 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 <code>java.lang.Object</code> 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 <code>CallableStatement</code>
+   * @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 <code>NUMERIC</code> parameter as a <code>
+   * java.math.BigDecimal</code> 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 <code>NULL</code>, the
+   *     result is <code>null</code>.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @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 <code>parameterIndex</code> and uses
+   * <code>map</code> for the custom mapping of the parameter value.
+   *
+   * <p>This method returns a Java object whose type corresponds to the JDBC type that was
+   * registered for this parameter using the method <code>registerOutParameter</code>. By
+   * registering the target JDBC types as <code>java.sql.Types.OTHER</code>, 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 <code>java.lang.Object</code> 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 <code>CallableStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @see #setObject
+   * @since 1.2
+   */
+  @Override
+  public Object getObject(int parameterIndex, Map<String, Class<?>> map) throws SQLException {
+    checkNotClosed();
+    checkOutputResult();
+    return outputResult.getObject(idxToOutIdx(parameterIndex), map);
+  }
+
+  /**
+   * Retrieves the value of the designated JDBC <code>REF(&lt;structured-type&gt;)</code> 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 <code>Ref</code> object in the Java programming language. If
+   *     the value was SQL <code>NULL</code>, the value <code>null</code> is returned.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.2
+   */
+  @Override
+  public Ref getRef(int parameterIndex) throws SQLException {
+    throw exceptionFactory().notSupported("Method ResultSet.getRef not supported");
+  }
+
+  /**
+   * Retrieves the value of the designated JDBC <code>BLOB</code> 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 <code>Blob</code> object in the Java programming language. If
+   *     the value was SQL <code>NULL</code>, the value <code>null</code> is returned.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @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 <code>CLOB</code> parameter as a <code>java.sql.Clob
+   * </code> 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 <code>Clob</code> object in the Java programming language. If
+   *     the value was SQL <code>NULL</code>, the value <code>null</code> is returned.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @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 <code>ARRAY</code> 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 <code>Array</code> object in the Java programming language.
+   *     If the value was SQL <code>NULL</code>, the value <code>null</code> is returned.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.2
+   */
+  @Override
+  public Array getArray(int parameterIndex) throws SQLException {
+    checkNotClosed();
+    checkOutputResult();
+    throw exceptionFactory().notSupported("Method ResultSet.getArray not supported");
+  }
+
+  /**
+   * Retrieves the value of the designated JDBC <code>DATE</code> parameter as a <code>java.sql.Date
+   * </code> object, using the given <code>Calendar</code> object to construct the date. With a
+   * <code>Calendar</code> object, the driver can calculate the date taking into account a custom
+   * timezone and locale. If no <code>Calendar</code> 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 <code>Calendar</code> object the driver will use to construct the date
+   * @return the parameter value. If the value is SQL <code>NULL</code>, the result is <code>null
+   *     </code>.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @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 <code>TIME</code> parameter as a <code>java.sql.Time
+   * </code> object, using the given <code>Calendar</code> object to construct the time. With a
+   * <code>Calendar</code> object, the driver can calculate the time taking into account a custom
+   * timezone and locale. If no <code>Calendar</code> 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 <code>Calendar</code> object the driver will use to construct the time
+   * @return the parameter value; if the value is SQL <code>NULL</code>, the result is <code>null
+   *     </code>.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @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 <code>TIMESTAMP</code> parameter as a <code>
+   * java.sql.Timestamp</code> object, using the given <code>Calendar</code> object to construct the
+   * <code>Timestamp</code> object. With a <code>Calendar</code> object, the driver can calculate
+   * the timestamp taking into account a custom timezone and locale. If no <code>Calendar</code>
+   * 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 <code>Calendar</code> object the driver will use to construct the timestamp
+   * @return the parameter value. If the value is SQL <code>NULL</code>, the result is <code>null
+   *     </code>.
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @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 <code>
+   * registerOutParameter</code> should be used for a user-defined or <code>REF</code> output
+   * parameter. Examples of user-defined types include: <code>STRUCT</code>, <code>DISTINCT</code>,
+   * <code>JAVA_OBJECT</code>, and named array types.
+   *
+   * <p>All OUT parameters must be registered before a stored procedure is executed.
+   *
+   * <p>For a user-defined parameter, the fully-qualified SQL type name of the parameter should also
+   * be given, while a <code>REF</code> 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</code> parameters.
+   *
+   * <p>Although it is intended for user-defined and <code>REF</code> 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</code> type, the <i>typeName</i> parameter is ignored.
+   *
+   * <p><B>Note:</B> 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 <code>CallableStatement</code>
+   * @throws SQLFeatureNotSupportedException if <code>sqlType</code> is a <code>ARRAY</code>, <code>
+   *     BLOB</code>, <code>CLOB</code>, <code>DATALINK</code>, <code>JAVA_OBJECT</code>, <code>
+   *     NCHAR</code>, <code>NCLOB</code>, <code>NVARCHAR</code>, <code>LONGNVARCHAR</code>, <code>
+   *     REF</code>, <code>ROWID</code>, <code>SQLXML</code> or <code>STRUCT</code> 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 <code>parameterName</code> to the JDBC type <code>sqlType
+   * </code>. All OUT parameters must be registered before a stored procedure is executed.
+   *
+   * <p>The JDBC type specified by <code>sqlType</code> for an OUT parameter determines the Java
+   * type that must be used in the <code>get</code> method to read the value of that parameter.
+   *
+   * <p>If the JDBC type expected to be returned to this output parameter is specific to this
+   * particular database, <code>sqlType</code> should be <code>java.sql.Types.OTHER</code>. The
+   * method {@link #getObject} retrieves the value.
+   *
+   * @param parameterName the name of the parameter
+   * @param sqlType the JDBC type code defined by <code>java.sql.Types</code>. If the parameter is
+   *     of JDBC type <code>NUMERIC</code> or <code>DECIMAL</code>, the version of <code>
+   *     registerOutParameter</code> 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</code>
+   * @throws SQLFeatureNotSupportedException if <code>sqlType</code> is a <code>ARRAY</code>, <code>
+   *     BLOB</code>, <code>CLOB</code>, <code>DATALINK</code>, <code>JAVA_OBJECT</code>, <code>
+   *     NCHAR</code>, <code>NCLOB</code>, <code>NVARCHAR</code>, <code>LONGNVARCHAR</code>, <code>
+   *     REF</code>, <code>ROWID</code>, <code>SQLXML</code> or <code>STRUCT</code> 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("parameter name 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("parameter name %s not found", parameterName));
+  }
+
+  /**
+   * Registers the parameter named <code>parameterName</code> to be of JDBC type <code>sqlType
+   * </code>. All OUT parameters must be registered before a stored procedure is executed.
+   *
+   * <p>The JDBC type specified by <code>sqlType</code> for an OUT parameter determines the Java
+   * type that must be used in the <code>get</code> method to read the value of that parameter.
+   *
+   * <p>This version of <code>registerOutParameter</code> should be used when the parameter is of
+   * JDBC type <code>NUMERIC</code> or <code>DECIMAL</code>.
+   *
+   * @param parameterName the name of the parameter
+   * @param sqlType SQL type code defined by <code>java.sql.Types</code>.
+   * @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</code>
+   * @throws SQLFeatureNotSupportedException if <code>sqlType</code> is a <code>ARRAY</code>, <code>
+   *     BLOB</code>, <code>CLOB</code>, <code>DATALINK</code>, <code>JAVA_OBJECT</code>, <code>
+   *     NCHAR</code>, <code>NCLOB</code>, <code>NVARCHAR</code>, <code>LONGNVARCHAR</code>, <code>
+   *     REF</code>, <code>ROWID</code>, <code>SQLXML</code> or <code>STRUCT</code> 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 <code>
+   * registerOutParameter</code> 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.
+   *
+   * <p>All OUT parameters must be registered before a stored procedure is executed.
+   *
+   * <p>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.
+   *
+   * <p>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.
+   *
+   * <p><B>Note:</B> When reading the value of an out parameter, you must use the <code>getXXX
+   * </code> 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 <code>CallableStatement</code>
+   * @throws SQLFeatureNotSupportedException if <code>sqlType</code> is a <code>ARRAY</code>, <code>
+   *     BLOB</code>, <code>CLOB</code>, <code>DATALINK</code>, <code>JAVA_OBJECT</code>, <code>
+   *     NCHAR</code>, <code>NCLOB</code>, <code>NVARCHAR</code>, <code>LONGNVARCHAR</code>, <code>
+   *     REF</code>, <code>ROWID</code>, <code>SQLXML</code> or <code>STRUCT</code> 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 <code>DATALINK</code> parameter as a <code>
+   * java.net.URL</code> object.
+   *
+   * @param parameterIndex the first parameter is 1, the second is 2,...
+   * @return a <code>java.net.URL</code> object that represents the JDBC <code>DATALINK</code> 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 <code>CallableStatement</code>, 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 <code>java.net.URL</code> object. The driver
+   * converts this to an SQL <code>DATALINK</code> 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 <code>CallableStatement</code> 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 <code>NULL</code>.
+   *
+   * <p><B>Note:</B> You must specify the parameter's SQL type.
+   *
+   * @param parameterName the name of the parameter
+   * @param sqlType the SQL type code defined in <code>java.sql.Types</code>
+   * @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</code>
+   * @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 <code>boolean</code> value. The driver converts
+   * this to an SQL <code>BIT</code> or <code>BOOLEAN</code> 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 <code>CallableStatement</code>
+   * @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 <code>byte</code> value. The driver converts
+   * this to an SQL <code>TINYINT</code> 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 <code>CallableStatement</code>
+   * @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 <code>short</code> value. The driver converts
+   * this to an SQL <code>SMALLINT</code> 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 <code>CallableStatement</code>
+   * @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 <code>int</code> value. The driver converts
+   * this to an SQL <code>INTEGER</code> 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 <code>CallableStatement</code>
+   * @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 <code>long</code> value. The driver converts
+   * this to an SQL <code>BIGINT</code> 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 <code>CallableStatement</code>
+   * @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 <code>float</code> value. The driver converts
+   * this to an SQL <code>FLOAT</code> 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 <code>CallableStatement</code>
+   * @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 <code>double</code> value. The driver converts
+   * this to an SQL <code>DOUBLE</code> 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 <code>CallableStatement</code>
+   * @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 <code>java.math.BigDecimal</code> value. The driver
+   * converts this to an SQL <code>NUMERIC</code> 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 <code>CallableStatement</code>
+   * @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 <code>String</code> value. The driver converts
+   * this to an SQL <code>VARCHAR</code> or <code>LONGVARCHAR</code> value (depending on the
+   * argument's size relative to the driver's limits on <code>VARCHAR</code> 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 <code>CallableStatement</code>
+   * @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 <code>VARBINARY</code> or <code>LONGVARBINARY</code> (depending on the argument's size
+   * relative to the driver's limits on <code>VARBINARY</code> 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 <code>CallableStatement</code>
+   * @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 <code>java.sql.Date</code> value using the default
+   * time zone of the virtual machine that is running the application. The driver converts this to
+   * an SQL <code>DATE</code> 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 <code>CallableStatement</code>
+   * @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 <code>java.sql.Time</code> value. The driver
+   * converts this to an SQL <code>TIME</code> 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 <code>CallableStatement</code>
+   * @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 <code>java.sql.Timestamp</code> value. The driver
+   * converts this to an SQL <code>TIMESTAMP</code> 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 <code>CallableStatement</code>
+   * @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 <code>LONGVARCHAR</code> parameter, it
+   * may be more practical to send it via a <code>java.io.InputStream</code>. 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.
+   *
+   * <p><B>Note:</B> 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 <code>CallableStatement</code>
+   * @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 <code>LONGVARBINARY</code> parameter, it
+   * may be more practical to send it via a <code>java.io.InputStream</code> object. The data will
+   * be read from the stream as needed until end-of-file is reached.
+   *
+   * <p><B>Note:</B> 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 <code>CallableStatement</code>
+   * @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.
+   *
+   * <p>The given Java object will be converted to the given targetSqlType before being sent to the
+   * database.
+   *
+   * <p>If the object has a custom mapping (is of a class implementing the interface <code>SQLData
+   * </code>), the JDBC driver should call the method <code>SQLData.writeSQL</code> to write it to
+   * the SQL data stream. If, on the other hand, the object is of a class implementing <code>Ref
+   * </code>, <code>Blob</code>, <code>Clob</code>, <code>NClob</code>, <code>Struct</code>, <code>
+   * java.net.URL</code>, or <code>Array</code>, the driver should pass it to the database as a
+   * value of the corresponding SQL type.
+   *
+   * <p>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 <code>CallableStatement</code>
+   * @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.
+   *
+   * <p>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 <code>CallableStatement</code>
+   * @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.
+   *
+   * <p>The JDBC specification specifies a standard mapping from Java <code>Object</code> types to
+   * SQL types. The given argument will be converted to the corresponding SQL type before being sent
+   * to the database.
+   *
+   * <p>Note that this method may be used to pass database- specific abstract data types, by using a
+   * driver-specific Java type.
+   *
+   * <p>If the object is of a class implementing the interface <code>SQLData</code>, the JDBC driver
+   * should call the method <code>SQLData.writeSQL</code> to write it to the SQL data stream. If, on
+   * the other hand, the object is of a class implementing <code>Ref</code>, <code>Blob</code>,
+   * <code>Clob</code>, <code>NClob</code>, <code>Struct</code>, <code>java.net.URL</code>, or
+   * <code>Array</code>, the driver should pass it to the database as a value of the corresponding
+   * SQL type.
+   *
+   * <p>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.
+   *
+   * <p><b>Note:</b> Not all databases allow for a non-typed Null to be sent to the backend. For
+   * maximum portability, the <code>setNull</code> or the <code>
+   * setObject(String parameterName, Object x, int sqlType)</code> method should be used instead of
+   * <code>setObject(String parameterName, Object x)</code>.
+   *
+   * @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 <code>CallableStatement</code> or if
+   *     the given <code>Object</code> 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 <code>Reader</code> object, which is the given
+   * number of characters long. When a very large UNICODE value is input to a <code>LONGVARCHAR
+   * </code> parameter, it may be more practical to send it via a <code>java.io.Reader</code>
+   * 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.
+   *
+   * <p><B>Note:</B> 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 <code>java.io.Reader</code> 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 <code>CallableStatement</code>
+   * @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 <code>java.sql.Date</code> value, using the given
+   * <code>Calendar</code> object. The driver uses the <code>Calendar</code> object to construct an
+   * SQL <code>DATE</code> value, which the driver then sends to the database. With a a <code>
+   * Calendar</code> object, the driver can calculate the date taking into account a custom
+   * timezone. If no <code>Calendar</code> 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 <code>Calendar</code> 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 <code>CallableStatement</code>
+   * @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 <code>java.sql.Time</code> value, using the given
+   * <code>Calendar</code> object. The driver uses the <code>Calendar</code> object to construct an
+   * SQL <code>TIME</code> value, which the driver then sends to the database. With a a <code>
+   * Calendar</code> object, the driver can calculate the time taking into account a custom
+   * timezone. If no <code>Calendar</code> 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 <code>Calendar</code> 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 <code>CallableStatement</code>
+   * @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 <code>java.sql.Timestamp</code> value, using the
+   * given <code>Calendar</code> object. The driver uses the <code>Calendar</code> object to
+   * construct an SQL <code>TIMESTAMP</code> value, which the driver then sends to the database.
+   * With a a <code>Calendar</code> object, the driver can calculate the timestamp taking into
+   * account a custom timezone. If no <code>Calendar</code> 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 <code>Calendar</code> 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 <code>CallableStatement</code>
+   * @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 <code>NULL</code>. This version of the method <code>
+   * setNull</code> 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.
+   *
+   * <p><B>Note:</B> 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.
+   *
+   * <p>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 <code>java.sql.Types</code>
+   * @param typeName the fully-qualified name of an SQL user-defined type; ignored if the parameter
+   *     is not a user-defined type or SQL <code>REF</code> 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 <code>CallableStatement</code>
+   * @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 <code>CHAR</code>, <code>VARCHAR</code>, or <code>LONGVARCHAR
+   * </code> parameter as a <code>String</code> in the Java programming language.
+   *
+   * <p>For the fixed-length type JDBC <code>CHAR</code>, the <code>String</code> object returned
+   * has exactly the same value the SQL <code>CHAR</code> 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 <code>NULL</code>, the result is <code>null
+   *     </code>.
+   * @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</code>
+   * @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 <code>BIT</code> or <code>BOOLEAN</code> parameter as a <code>
+   * boolean</code> in the Java programming language.
+   *
+   * @param parameterName the name of the parameter
+   * @return the parameter value. If the value is SQL <code>NULL</code>, the result is <code>false
+   *     </code>.
+   * @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</code>
+   * @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 <code>TINYINT</code> parameter as a <code>byte</code> in the Java
+   * programming language.
+   *
+   * @param parameterName the name of the parameter
+   * @return the parameter value. If the value is SQL <code>NULL</code>, the result is <code>0
+   *     </code>.
+   * @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</code>
+   * @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 <code>SMALLINT</code> parameter as a <code>short</code> in the
+   * Java programming language.
+   *
+   * @param parameterName the name of the parameter
+   * @return the parameter value. If the value is SQL <code>NULL</code>, the result is <code>0
+   *     </code>.
+   * @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</code>
+   * @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 <code>INTEGER</code> parameter as an <code>int</code> in the Java
+   * programming language.
+   *
+   * @param parameterName the name of the parameter
+   * @return the parameter value. If the value is SQL <code>NULL</code>, the result is <code>0
+   *     </code>.
+   * @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</code>
+   * @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 <code>BIGINT</code> parameter as a <code>long</code> in the Java
+   * programming language.
+   *
+   * @param parameterName the name of the parameter
+   * @return the parameter value. If the value is SQL <code>NULL</code>, the result is <code>0
+   *     </code>.
+   * @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</code>
+   * @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 <code>FLOAT</code> parameter as a <code>float</code> in the Java
+   * programming language.
+   *
+   * @param parameterName the name of the parameter
+   * @return the parameter value. If the value is SQL <code>NULL</code>, the result is <code>0
+   *     </code>.
+   * @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</code>
+   * @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 <code>DOUBLE</code> parameter as a <code>double</code> in the
+   * Java programming language.
+   *
+   * @param parameterName the name of the parameter
+   * @return the parameter value. If the value is SQL <code>NULL</code>, the result is <code>0
+   *     </code>.
+   * @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</code>
+   * @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 <code>BINARY</code> or <code>VARBINARY</code> parameter as an
+   * array of <code>byte</code> values in the Java programming language.
+   *
+   * @param parameterName the name of the parameter
+   * @return the parameter value. If the value is SQL <code>NULL</code>, the result is <code>null
+   *     </code>.
+   * @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</code>
+   * @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 <code>DATE</code> parameter as a <code>java.sql.Date</code>
+   * object.
+   *
+   * @param parameterName the name of the parameter
+   * @return the parameter value. If the value is SQL <code>NULL</code>, the result is <code>null
+   *     </code>.
+   * @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</code>
+   * @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 <code>TIME</code> parameter as a <code>java.sql.Time</code>
+   * object.
+   *
+   * @param parameterName the name of the parameter
+   * @return the parameter value. If the value is SQL <code>NULL</code>, the result is <code>null
+   *     </code>.
+   * @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</code>
+   * @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 <code>TIMESTAMP</code> parameter as a <code>java.sql.Timestamp
+   * </code> object.
+   *
+   * @param parameterName the name of the parameter
+   * @return the parameter value. If the value is SQL <code>NULL</code>, the result is <code>null
+   *     </code>.
+   * @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</code>
+   * @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 <code>Object</code> in the Java programming language.
+   * If the value is an SQL <code>NULL</code>, the driver returns a Java <code>null</code>.
+   *
+   * <p>This method returns a Java object whose type corresponds to the JDBC type that was
+   * registered for this parameter using the method <code>registerOutParameter</code>. By
+   * registering the target JDBC types as <code>java.sql.Types.OTHER</code>, this method can be used
+   * to read database-specific abstract data types.
+   *
+   * @param parameterName the name of the parameter
+   * @return A <code>java.lang.Object</code> 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 <code>CallableStatement</code>
+   * @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 <code>NUMERIC</code> parameter as a <code>java.math.BigDecimal
+   * </code> 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 <code>NULL</code>, the
+   *     result is <code>null</code>.
+   * @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</code>
+   * @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 <code>parameterName</code> and uses
+   * <code>map</code> for the custom mapping of the parameter value.
+   *
+   * <p>This method returns a Java object whose type corresponds to the JDBC type that was
+   * registered for this parameter using the method <code>registerOutParameter</code>. By
+   * registering the target JDBC types as <code>java.sql.Types.OTHER</code>, 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 <code>java.lang.Object</code> 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 <code>CallableStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @see #setObject
+   * @since 1.4
+   */
+  @Override
+  public Object getObject(String parameterName, Map<String, Class<?>> map) throws SQLException {
+    return outputResult.getObject(idxToOutIdx(nameToIndex(parameterName)), map);
+  }
+
+  /**
+   * Retrieves the value of a JDBC <code>REF(&lt;structured-type&gt;)</code> parameter as a {@link
+   * Ref} object in the Java programming language.
+   *
+   * @param parameterName the name of the parameter
+   * @return the parameter value as a <code>Ref</code> object in the Java programming language. If
+   *     the value was SQL <code>NULL</code>, the value <code>null</code> 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 <code>CallableStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.4
+   */
+  @Override
+  public Ref getRef(String parameterName) throws SQLException {
+    throw exceptionFactory().notSupported("Method ResultSet.getRef not supported");
+  }
+
+  /**
+   * Retrieves the value of a JDBC <code>BLOB</code> parameter as a {@link Blob} object in the Java
+   * programming language.
+   *
+   * @param parameterName the name of the parameter
+   * @return the parameter value as a <code>Blob</code> object in the Java programming language. If
+   *     the value was SQL <code>NULL</code>, the value <code>null</code> 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 <code>CallableStatement</code>
+   * @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 <code>CLOB</code> parameter as a <code>java.sql.Clob</code>
+   * object in the Java programming language.
+   *
+   * @param parameterName the name of the parameter
+   * @return the parameter value as a <code>Clob</code> object in the Java programming language. If
+   *     the value was SQL <code>NULL</code>, the value <code>null</code> 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 <code>CallableStatement</code>
+   * @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 <code>ARRAY</code> parameter as an {@link Array} object in the
+   * Java programming language.
+   *
+   * @param parameterName the name of the parameter
+   * @return the parameter value as an <code>Array</code> object in Java programming language. If
+   *     the value was SQL <code>NULL</code>, the value <code>null</code> 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 <code>CallableStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.4
+   */
+  @Override
+  public Array getArray(String parameterName) throws SQLException {
+    throw exceptionFactory().notSupported("Method ResultSet.getArray not supported");
+  }
+
+  /**
+   * Retrieves the value of a JDBC <code>DATE</code> parameter as a <code>java.sql.Date</code>
+   * object, using the given <code>Calendar</code> object to construct the date. With a <code>
+   * Calendar</code> object, the driver can calculate the date taking into account a custom timezone
+   * and locale. If no <code>Calendar</code> object is specified, the driver uses the default
+   * timezone and locale.
+   *
+   * @param parameterName the name of the parameter
+   * @param cal the <code>Calendar</code> object the driver will use to construct the date
+   * @return the parameter value. If the value is SQL <code>NULL</code>, the result is <code>null
+   *     </code>.
+   * @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</code>
+   * @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 <code>TIME</code> parameter as a <code>java.sql.Time</code>
+   * object, using the given <code>Calendar</code> object to construct the time. With a <code>
+   * Calendar</code> object, the driver can calculate the time taking into account a custom timezone
+   * and locale. If no <code>Calendar</code> object is specified, the driver uses the default
+   * timezone and locale.
+   *
+   * @param parameterName the name of the parameter
+   * @param cal the <code>Calendar</code> object the driver will use to construct the time
+   * @return the parameter value; if the value is SQL <code>NULL</code>, the result is <code>null
+   *     </code>.
+   * @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</code>
+   * @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 <code>TIMESTAMP</code> parameter as a <code>java.sql.Timestamp
+   * </code> object, using the given <code>Calendar</code> object to construct the <code>Timestamp
+   * </code> object. With a <code>Calendar</code> object, the driver can calculate the timestamp
+   * taking into account a custom timezone and locale. If no <code>Calendar</code> object is
+   * specified, the driver uses the default timezone and locale.
+   *
+   * @param parameterName the name of the parameter
+   * @param cal the <code>Calendar</code> object the driver will use to construct the timestamp
+   * @return the parameter value. If the value is SQL <code>NULL</code>, the result is <code>null
+   *     </code>.
+   * @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</code>
+   * @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 <code>DATALINK</code> parameter as a <code>java.net.URL</code>
+   * object.
+   *
+   * @param parameterName the name of the parameter
+   * @return the parameter value as a <code>java.net.URL</code> object in the Java programming
+   *     language. If the value was SQL <code>NULL</code>, the value <code>null</code> 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 <code>CallableStatement</code>, 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 <code>ROWID</code> parameter as a <code>
+   * java.sql.RowId</code> object.
+   *
+   * @param parameterIndex the first parameter is 1, the second is 2,...
+   * @return a <code>RowId</code> object that represents the JDBC <code>ROWID</code> value is used
+   *     as the designated parameter. If the parameter contains an SQL <code>NULL</code>, then a
+   *     <code>null</code> 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 <code>CallableStatement</code>
+   * @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 <code>ROWID</code> parameter as a <code>
+   * java.sql.RowId</code> object.
+   *
+   * @param parameterName the name of the parameter
+   * @return a <code>RowId</code> object that represents the JDBC <code>ROWID</code> value is used
+   *     as the designated parameter. If the parameter contains an SQL <code>NULL</code>, then a
+   *     <code>null</code> 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 <code>CallableStatement</code>
+   * @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 <code>java.sql.RowId</code> object. The driver
+   * converts this to an SQL <code>ROWID</code> 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 <code>CallableStatement</code>
+   * @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 <code>String</code> object. The driver converts this
+   * to an SQL <code>NCHAR</code> or <code>NVARCHAR</code> or <code>LONGNVARCHAR</code>
+   *
+   * @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
+   *     <code>CallableStatement</code>
+   * @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 <code>Reader</code> object. The <code>Reader</code> 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
+   *     <code>CallableStatement</code>
+   * @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 <code>java.sql.NClob</code> object. The object implements
+   * the <code>java.sql.NClob</code> interface. This <code>NClob</code> object maps to an SQL <code>
+   * NCLOB</code>.
+   *
+   * @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
+   *     <code>CallableStatement</code>
+   * @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 <code>Reader</code> object. The <code>reader</code> must
+   * contain the number of characters specified by length otherwise a <code>SQLException</code> will
+   * be generated when the <code>CallableStatement</code> is executed. This method differs from the
+   * <code>setCharacterStream (int, Reader, int)</code> method because it informs the driver that
+   * the parameter value should be sent to the server as a <code>CLOB</code>. When the <code>
+   * setCharacterStream</code> 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 <code>LONGVARCHAR</code> or a
+   * <code>CLOB</code>
+   *
+   * @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 <code>CallableStatement</code>
+   * @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 <code>Inputstream</code>
+   * must contain the number of characters specified by length, otherwise a <code>SQLException
+   * </code> will be generated when the <code>CallableStatement</code> is executed. This method
+   * differs from the <code>setBinaryStream (int, InputStream, int)</code> method because it informs
+   * the driver that the parameter value should be sent to the server as a <code>BLOB</code>. When
+   * the <code>setBinaryStream</code> 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 <code>LONGVARBINARY
+   * </code> or a <code>BLOB</code>
+   *
+   * @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 <code>CallableStatement</code>
+   * @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 <code>Reader</code> object. The <code>reader</code> must
+   * contain the number of characters specified by length otherwise a <code>SQLException</code> will
+   * be generated when the <code>CallableStatement</code> is executed. This method differs from the
+   * <code>setCharacterStream (int, Reader, int)</code> method because it informs the driver that
+   * the parameter value should be sent to the server as a <code>NCLOB</code>. When the <code>
+   * setCharacterStream</code> 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 <code>LONGNVARCHAR</code> or a
+   * <code>NCLOB</code>
+   *
+   * @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 <code>CallableStatement</code>
+   * @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 <code>NCLOB</code> parameter as a <code>
+   * java.sql.NClob</code> 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 <code>NClob</code> object in the Java programming language. If
+   *     the value was SQL <code>NULL</code>, the value <code>null</code> 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 <code>
+   *     CallableStatement</code>
+   * @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 <code>NCLOB</code> parameter as a <code>java.sql.NClob</code>
+   * object in the Java programming language.
+   *
+   * @param parameterName the name of the parameter
+   * @return the parameter value as a <code>NClob</code> object in the Java programming language. If
+   *     the value was SQL <code>NULL</code>, the value <code>null</code> 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
+   *     <code>CallableStatement</code>
+   * @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 <code>java.sql.SQLXML</code> object. The driver
+   * converts this to an <code>SQL XML</code> value when it sends it to the database.
+   *
+   * @param parameterName the name of the parameter
+   * @param xmlObject a <code>SQLXML</code> object that maps an <code>SQL XML</code> 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 <code>CallableStatement</code> or
+   *     the <code>java.xml.transform.Result</code>, <code>Writer</code> or <code>OutputStream
+   *     </code> has not been closed for the <code>SQLXML</code> 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 <code>SQL XML</code> parameter as a <code>java.sql.SQLXML
+   * </code> object in the Java programming language.
+   *
+   * @param parameterIndex index of the first parameter is 1, the second is 2, ...
+   * @return a <code>SQLXML</code> object that maps an <code>SQL XML</code> value
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @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 <code>SQL XML</code> parameter as a <code>java.sql.SQLXML
+   * </code> object in the Java programming language.
+   *
+   * @param parameterName the name of the parameter
+   * @return a <code>SQLXML</code> object that maps an <code>SQL XML</code> 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 <code>CallableStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.6
+   */
+  @Override
+  public SQLXML getSQLXML(String parameterName) throws SQLException {
+    throw exceptionFactory().notSupported("SQLXML are not supported");
+  }
+
+  private ExceptionFactory exceptionFactory() {
+    return con.getExceptionFactory().of(this);
+  }
+
+  /**
+   * Retrieves the value of the designated <code>NCHAR</code>, <code>NVARCHAR</code> or <code>
+   * LONGNVARCHAR</code> parameter as a <code>String</code> in the Java programming language.
+   *
+   * <p>For the fixed-length type JDBC <code>NCHAR</code>, the <code>String</code> object returned
+   * has exactly the same value the SQL <code>NCHAR</code> 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 <code>String</code> object that maps an <code>NCHAR</code>, <code>NVARCHAR</code> or
+   *     <code>LONGNVARCHAR</code> value
+   * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or
+   *     this method is called on a closed <code>CallableStatement</code>
+   * @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 <code>NCHAR</code>, <code>NVARCHAR</code> or <code>
+   * LONGNVARCHAR</code> parameter as a <code>String</code> in the Java programming language.
+   *
+   * <p>For the fixed-length type JDBC <code>NCHAR</code>, the <code>String</code> object returned
+   * has exactly the same value the SQL <code>NCHAR</code> value had in the database, including any
+   * padding added by the database.
+   *
+   * @param parameterName the name of the parameter
+   * @return a <code>String</code> object that maps an <code>NCHAR</code>, <code>NVARCHAR</code> or
+   *     <code>LONGNVARCHAR</code> 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 <code>CallableStatement</code>
+   * @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 <code>java.io.Reader</code> object in the
+   * Java programming language. It is intended for use when accessing <code>NCHAR</code>,<code>
+   * NVARCHAR</code> and <code>LONGNVARCHAR</code> parameters.
+   *
+   * @param parameterIndex the first parameter is 1, the second is 2, ...
+   * @return a <code>java.io.Reader</code> object that contains the parameter value; if the value is
+   *     SQL <code>NULL</code>, the value returned is <code>null</code> 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 <code>CallableStatement</code>
+   * @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 <code>java.io.Reader</code> object in the
+   * Java programming language. It is intended for use when accessing <code>NCHAR</code>,<code>
+   * NVARCHAR</code> and <code>LONGNVARCHAR</code> parameters.
+   *
+   * @param parameterName the name of the parameter
+   * @return a <code>java.io.Reader</code> object that contains the parameter value; if the value is
+   *     SQL <code>NULL</code>, the value returned is <code>null</code> 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 <code>CallableStatement</code>
+   * @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 <code>java.io.Reader</code> object in the
+   * Java programming language.
+   *
+   * @param parameterIndex the first parameter is 1, the second is 2, ...
+   * @return a <code>java.io.Reader</code> object that contains the parameter value; if the value is
+   *     SQL <code>NULL</code>, the value returned is <code>null</code> 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 <code>CallableStatement</code>
+   * @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 <code>java.io.Reader</code> object in the
+   * Java programming language.
+   *
+   * @param parameterName the name of the parameter
+   * @return a <code>java.io.Reader</code> object that contains the parameter value; if the value is
+   *     SQL <code>NULL</code>, the value returned is <code>null</code> 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 <code>CallableStatement</code>
+   * @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 <code>java.sql.Blob</code> object. The driver
+   * converts this to an SQL <code>BLOB</code> value when it sends it to the database.
+   *
+   * @param parameterName the name of the parameter
+   * @param x a <code>Blob</code> object that maps an SQL <code>BLOB</code> 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 <code>CallableStatement</code>
+   * @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 <code>java.sql.Clob</code> object. The driver
+   * converts this to an SQL <code>CLOB</code> value when it sends it to the database.
+   *
+   * @param parameterName the name of the parameter
+   * @param x a <code>Clob</code> object that maps an SQL <code>CLOB</code> 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 <code>CallableStatement</code>
+   * @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 <code>LONGVARCHAR</code> parameter, it
+   * may be more practical to send it via a <code>java.io.InputStream</code>. 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.
+   *
+   * <p><B>Note:</B> 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 <code>CallableStatement</code>
+   * @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 <code>LONGVARBINARY</code> parameter, it
+   * may be more practical to send it via a <code>java.io.InputStream</code> object. The data will
+   * be read from the stream as needed until end-of-file is reached.
+   *
+   * <p><B>Note:</B> 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 <code>CallableStatement</code>
+   * @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 <code>Reader</code> object, which is the given
+   * number of characters long. When a very large UNICODE value is input to a <code>LONGVARCHAR
+   * </code> parameter, it may be more practical to send it via a <code>java.io.Reader</code>
+   * 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.
+   *
+   * <p><B>Note:</B> 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 <code>java.io.Reader</code> 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 <code>CallableStatement</code>
+   * @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 <code>LONGVARCHAR</code> parameter, it may be more practical to send it via a <code>
+   * java.io.InputStream</code>. 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.
+   *
+   * <p><B>Note:</B> This stream object can either be a standard Java stream object or your own
+   * subclass that implements the standard interface.
+   *
+   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
+   * efficient to use a version of <code>setAsciiStream</code> 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 <code>CallableStatement</code>
+   * @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 <code>LONGVARBINARY</code> parameter, it may be more practical to send it via a
+   * <code>java.io.InputStream</code> object. The data will be read from the stream as needed until
+   * end-of-file is reached.
+   *
+   * <p><B>Note:</B> This stream object can either be a standard Java stream object or your own
+   * subclass that implements the standard interface.
+   *
+   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
+   * efficient to use a version of <code>setBinaryStream</code> 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 <code>CallableStatement</code>
+   * @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 <code>Reader</code> object. When a very large
+   * UNICODE value is input to a <code>LONGVARCHAR</code> parameter, it may be more practical to
+   * send it via a <code>java.io.Reader</code> 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.
+   *
+   * <p><B>Note:</B> This stream object can either be a standard Java stream object or your own
+   * subclass that implements the standard interface.
+   *
+   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
+   * efficient to use a version of <code>setCharacterStream</code> which takes a length parameter.
+   *
+   * @param parameterName the name of the parameter
+   * @param reader the <code>java.io.Reader</code> 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 <code>CallableStatement</code>
+   * @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 <code>Reader</code> object. The <code>Reader</code> 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.
+   *
+   * <p><B>Note:</B> This stream object can either be a standard Java stream object or your own
+   * subclass that implements the standard interface.
+   *
+   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
+   * efficient to use a version of <code>setNCharacterStream</code> 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
+   *     <code>CallableStatement</code>
+   * @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 <code>Reader</code> object. This method differs from the
+   * <code>setCharacterStream (int, Reader)</code> method because it informs the driver that the
+   * parameter value should be sent to the server as a <code>CLOB</code>. When the <code>
+   * setCharacterStream</code> 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 <code>LONGVARCHAR</code> or a
+   * <code>CLOB</code>
+   *
+   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
+   * efficient to use a version of <code>setClob</code> 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 <code>CallableStatement</code>
+   * @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
+   * <code>setBinaryStream (int, InputStream)</code> method because it informs the driver that the
+   * parameter value should be sent to the server as a <code>BLOB</code>. When the <code>
+   * setBinaryStream</code> 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 <code>LONGVARBINARY</code> or a
+   * <code>BLOB</code>
+   *
+   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
+   * efficient to use a version of <code>setBlob</code> 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 <code>CallableStatement</code>
+   * @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 <code>Reader</code> object. This method differs from the
+   * <code>setCharacterStream (int, Reader)</code> method because it informs the driver that the
+   * parameter value should be sent to the server as a <code>NCLOB</code>. When the <code>
+   * setCharacterStream</code> 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 <code>LONGNVARCHAR</code> or a
+   * <code>NCLOB</code>
+   *
+   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
+   * efficient to use a version of <code>setNClob</code> 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
+   *     <code>CallableStatement</code>
+   * @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 <code>
+   * SQLException</code> is thrown.
+   *
+   * <p>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> T getObject(int parameterIndex, Class<T> 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 <code>
+   * SQLException</code> is thrown.
+   *
+   * <p>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> T getObject(String parameterName, Class<T> type) throws SQLException {
+    return getObject(nameToIndex(parameterName), type);
+  }
+
+  /**
+   * Sets the value of the designated parameter with the given object.
+   *
+   * <p>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.
+   *
+   * <p>The given Java object will be converted to the given targetSqlType before being sent to the
+   * database.
+   *
+   * <p>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.
+   *
+   * <p>Note that this method may be used to pass database-specific abstract data types.
+   *
+   * <p>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.
+   *
+   * <p>This method is similar to {@link #setObject(String parameterName, Object x, SQLType
+   * targetSqlType, int scaleOrLength)}, except that it assumes a scale of zero.
+   *
+   * <p>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.
+   *
+   * <p>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.
+   *
+   * <p>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.
+   *
+   * <p>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 {
+    registerOutParameter(parameterIndex, 0);
+  }
+
+  /**
+   * 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.
+   *
+   * <p>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.
+   *
+   * <p>This version of {@code registerOutParameter} should be used when the parameter is of JDBC
+   * type {@code JDBCType.NUMERIC} or {@code JDBCType.DECIMAL}.
+   *
+   * <p>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.
+   *
+   * <p>All OUT parameters must be registered before a stored procedure is executed.
+   *
+   * <p>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.
+   *
+   * <p>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 <i>typeName</i> parameter is ignored.
+   *
+   * <p><B>Note:</B> 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.
+   *
+   * <p>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 <code>parameterName</code> to the JDBC type {@code sqlType}.
+   * All OUT parameters must be registered before a stored procedure is executed.
+   *
+   * <p>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.
+   *
+   * <p>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.
+   *
+   * <p>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 <code>parameterName</code> to be of JDBC type {@code sqlType}.
+   * All OUT parameters must be registered before a stored procedure is executed.
+   *
+   * <p>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.
+   *
+   * <p>This version of {@code registerOutParameter} should be used when the parameter is of JDBC
+   * type {@code JDBCType.NUMERIC} or {@code JDBCType.DECIMAL}.
+   *
+   * <p>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.
+   *
+   * <p>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.
+   *
+   * <p>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.
+   *
+   * <p><B>Note:</B> 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.
+   *
+   * <p>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 {
+    PreparedStatement prep =
+        new ClientPreparedStatement(
+            "SELECT * from information_schema.PARAMETERS "
+                + "WHERE SPECIFIC_NAME = ? "
+                + "AND SPECIFIC_SCHEMA = ? "
+                + "ORDER BY ORDINAL_POSITION",
+            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, isFunction());
+    return parameterMetaData;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/BasePreparedStatement.java b/src/main/java/org/mariadb/jdbc/BasePreparedStatement.java
new file mode 100644
index 0000000..3a7eab2
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/BasePreparedStatement.java
@@ -0,0 +1,1589 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+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.ParameterMetaData;
+import java.util.*;
+import java.util.concurrent.locks.ReentrantLock;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.util.Parameters;
+import org.mariadb.jdbc.codec.*;
+import org.mariadb.jdbc.export.ExceptionFactory;
+import org.mariadb.jdbc.export.Prepare;
+import org.mariadb.jdbc.plugin.Codec;
+import org.mariadb.jdbc.plugin.codec.*;
+import org.mariadb.jdbc.util.ParameterList;
+
+/** Common methods for prepare statement, for client and server prepare statement. */
+public abstract class BasePreparedStatement extends Statement implements PreparedStatement {
+
+  /** parameters */
+  protected Parameters parameters;
+
+  /** batching parameters */
+  protected List<Parameters> batchParameters;
+
+  /** prepare statement sql command */
+  protected final String sql;
+
+  /** PREPARE command result */
+  protected Prepare prepareResult = null;
+
+  /**
+   * Constructor
+   *
+   * @param sql sql command
+   * @param con connection
+   * @param lock thread safe lock
+   * @param canUseServerTimeout indicate if server can support server timeout
+   * @param canUseServerMaxRows indicate if server can support max rows
+   * @param autoGeneratedKeys indicate if automatif generated key retrival is required
+   * @param resultSetType resultset type
+   * @param resultSetConcurrency resultset concurrency
+   * @param defaultFetchSize default fetch size
+   */
+  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;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder("sql:'" + sql + "'");
+    sb.append(", parameters:[");
+    for (int i = 0; i < parameters.size(); i++) {
+      org.mariadb.jdbc.client.util.Parameter param = parameters.get(i);
+      if (param == null) {
+        sb.append("null");
+      } else {
+        sb.append(param.bestEffortStringValue(con.getContext()));
+      }
+      if (i != parameters.size() - 1) {
+        sb.append(",");
+      }
+    }
+    sb.append("]");
+    return sb.toString();
+  }
+
+  /**
+   * Set PREPARE result
+   *
+   * @param prepareResult prepare result
+   */
+  public void setPrepareResult(Prepare prepareResult) {
+    this.prepareResult = prepareResult;
+  }
+
+  /**
+   * Get cached metadata list
+   *
+   * @return metadata list
+   */
+  public ColumnDecoder[] getMeta() {
+    return this.prepareResult.getColumns();
+  }
+
+  /**
+   * update cached metadata list
+   *
+   * @param ci metadata columns
+   */
+  public void updateMeta(ColumnDecoder[] ci) {
+    this.prepareResult.setColumns(ci);
+  }
+
+  public abstract boolean execute() throws SQLException;
+
+  public abstract ResultSet executeQuery() throws SQLException;
+
+  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;
+
+  /**
+   * Set all parameters
+   *
+   * @param parameters parameters
+   */
+  public void setParameters(Parameters parameters) {
+    this.parameters = parameters;
+  }
+
+  /**
+   * Set parameter
+   *
+   * @param index parameter index
+   * @param param parameter
+   */
+  public void setParameter(int index, org.mariadb.jdbc.client.util.Parameter param) {
+    parameters.set(index, param);
+  }
+
+  @Override
+  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");
+  }
+
+  @Override
+  public boolean execute(String sql) throws SQLException {
+    throw exceptionFactory().create("execute(String sql) cannot be called on preparedStatement");
+  }
+
+  @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));
+    }
+  }
+
+  /**
+   * Sets the designated parameter to SQL <code>NULL</code>.
+   *
+   * <p><B>Note:</B> You must specify the parameter's SQL type.
+   *
+   * @param parameterIndex the first parameter is 1, the second is 2, ...
+   * @param sqlType the SQL type code defined in <code>java.sql.Types</code>
+   * @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 <code>
+   *     PreparedStatement</code>
+   * @throws SQLFeatureNotSupportedException if <code>sqlType</code> is a <code>ARRAY</code>, <code>
+   *     BLOB</code>, <code>CLOB</code>, <code>DATALINK</code>, <code>JAVA_OBJECT</code>, <code>
+   *     NCHAR</code>, <code>NCLOB</code>, <code>NVARCHAR</code>, <code>LONGNVARCHAR</code>, <code>
+   *     REF</code>, <code>ROWID</code>, <code>SQLXML</code> or <code>STRUCT</code> data type and
+   *     the JDBC driver does not support this data type
+   */
+  @Override
+  public void setNull(int parameterIndex, int sqlType) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, Parameter.NULL_PARAMETER);
+  }
+
+  /**
+   * Sets the designated parameter to the given Java <code>boolean</code> value. The driver converts
+   * this to an SQL <code>BIT</code> or <code>BOOLEAN</code> value when it sends it to the database.
+   *
+   * @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 or this method is called on a closed <code>
+   *     PreparedStatement</code>
+   */
+  @Override
+  public void setBoolean(int parameterIndex, boolean x) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new NonNullParameter<>(BooleanCodec.INSTANCE, x));
+  }
+
+  /**
+   * Sets the designated parameter to the given Java <code>byte</code> value. The driver converts
+   * this to an SQL <code>TINYINT</code> value when it sends it to the database.
+   *
+   * @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 or this method is called on a closed <code>
+   *     PreparedStatement</code>
+   */
+  @Override
+  public void setByte(int parameterIndex, byte x) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new NonNullParameter<>(ByteCodec.INSTANCE, x));
+  }
+
+  /**
+   * Sets the designated parameter to the given Java <code>short</code> value. The driver converts
+   * this to an SQL <code>SMALLINT</code> value when it sends it to the database.
+   *
+   * @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 or this method is called on a closed <code>
+   *     PreparedStatement</code>
+   */
+  @Override
+  public void setShort(int parameterIndex, short x) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new NonNullParameter<>(ShortCodec.INSTANCE, x));
+  }
+
+  /**
+   * Sets the designated parameter to the given Java <code>int</code> value. The driver converts
+   * this to an SQL <code>INTEGER</code> value when it sends it to the database.
+   *
+   * @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 or this method is called on a closed <code>
+   *     PreparedStatement</code>
+   */
+  @Override
+  public void setInt(int parameterIndex, int x) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new NonNullParameter<>(IntCodec.INSTANCE, x));
+  }
+
+  /**
+   * Sets the designated parameter to the given Java <code>long</code> value. The driver converts
+   * this to an SQL <code>BIGINT</code> value when it sends it to the database.
+   *
+   * @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 or this method is called on a closed <code>
+   *     PreparedStatement</code>
+   */
+  @Override
+  public void setLong(int parameterIndex, long x) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new NonNullParameter<>(LongCodec.INSTANCE, x));
+  }
+
+  /**
+   * Sets the designated parameter to the given Java <code>float</code> value. The driver converts
+   * this to an SQL <code>REAL</code> value when it sends it to the database.
+   *
+   * @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 or this method is called on a closed <code>
+   *     PreparedStatement</code>
+   */
+  @Override
+  public void setFloat(int parameterIndex, float x) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new NonNullParameter<>(FloatCodec.INSTANCE, x));
+  }
+
+  /**
+   * Sets the designated parameter to the given Java <code>double</code> value. The driver converts
+   * this to an SQL <code>DOUBLE</code> value when it sends it to the database.
+   *
+   * @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 or this method is called on a closed <code>
+   *     PreparedStatement</code>
+   */
+  @Override
+  public void setDouble(int parameterIndex, double x) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new NonNullParameter<>(DoubleCodec.INSTANCE, x));
+  }
+
+  /**
+   * Sets the designated parameter to the given <code>java.math.BigDecimal</code> value. The driver
+   * converts this to an SQL <code>NUMERIC</code> value when it sends it to the database.
+   *
+   * @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 or this method is called on a closed <code>
+   *     PreparedStatement</code>
+   */
+  @Override
+  public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(BigDecimalCodec.INSTANCE, x));
+  }
+
+  /**
+   * Sets the designated parameter to the given Java <code>String</code> value. The driver converts
+   * this to an SQL <code>VARCHAR</code> or <code>LONGVARCHAR</code> value (depending on the
+   * argument's size relative to the driver's limits on <code>VARCHAR</code> values) when it sends
+   * it to the database.
+   *
+   * @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 or this method is called on a closed <code>
+   *     PreparedStatement</code>
+   */
+  @Override
+  public void setString(int parameterIndex, String x) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(StringCodec.INSTANCE, x));
+  }
+
+  /**
+   * Sets the designated parameter to the given Java array of bytes. The driver converts this to an
+   * SQL <code>VARBINARY</code> or <code>LONGVARBINARY</code> (depending on the argument's size
+   * relative to the driver's limits on <code>VARBINARY</code> values) when it sends it to the
+   * database.
+   *
+   * @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 or this method is called on a closed <code>
+   *     PreparedStatement</code>
+   */
+  @Override
+  public void setBytes(int parameterIndex, byte[] x) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(ByteArrayCodec.INSTANCE, x));
+  }
+
+  /**
+   * Sets the designated parameter to the given <code>java.sql.Date</code> value using the default
+   * time zone of the virtual machine that is running the application. The driver converts this to
+   * an SQL <code>DATE</code> value when it sends it to the database.
+   *
+   * @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 or this method is called on a closed <code>
+   *     PreparedStatement</code>
+   */
+  @Override
+  public void setDate(int parameterIndex, Date x) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(DateCodec.INSTANCE, x));
+  }
+
+  /**
+   * Sets the designated parameter to the given <code>java.sql.Time</code> value. The driver
+   * converts this to an SQL <code>TIME</code> value when it sends it to the database.
+   *
+   * @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 or this method is called on a closed <code>
+   *     PreparedStatement</code>
+   */
+  @Override
+  public void setTime(int parameterIndex, Time x) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(TimeCodec.INSTANCE, x));
+  }
+
+  /**
+   * Sets the designated parameter to the given <code>java.sql.Timestamp</code> value. The driver
+   * converts this to an SQL <code>TIMESTAMP</code> value when it sends it to the database.
+   *
+   * @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 or this method is called on a closed <code>
+   *     PreparedStatement</code>
+   */
+  @Override
+  public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(TimestampCodec.INSTANCE, 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 <code>LONGVARCHAR</code> parameter, it
+   * may be more practical to send it via a <code>java.io.InputStream</code>. 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.
+   *
+   * <p><B>Note:</B> 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 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 <code>
+   *     PreparedStatement</code>
+   */
+  @Override
+  public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException {
+    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.
+   *
+   * <p>When a very large Unicode value is input to a <code>LONGVARCHAR</code> parameter, it may be
+   * more practical to send it via a <code>java.io.InputStream</code> 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.
+   *
+   * <p>The byte format of the Unicode stream must be a Java UTF-8, as defined in the Java Virtual
+   * Machine Specification.
+   *
+   * <p><B>Note:</B> 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 <code>java.io.InputStream</code> 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 <code>
+   *     PreparedStatement</code>
+   * @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 {
+    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 <code>LONGVARBINARY</code> parameter, it
+   * may be more practical to send it via a <code>java.io.InputStream</code> object. The data will
+   * be read from the stream as needed until end-of-file is reached.
+   *
+   * <p><B>Note:</B> 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 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 <code>
+   *     PreparedStatement</code>
+   */
+  @Override
+  public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, (long) length));
+  }
+
+  /**
+   * Clears the current parameter values immediately.
+   *
+   * <p>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 <code>clearParameters</code>.
+   *
+   * @throws SQLException if a database access error occurs or this method is called on a closed
+   *     <code>PreparedStatement</code>
+   */
+  @Override
+  public void clearParameters() throws SQLException {
+    checkNotClosed();
+    parameters = new ParameterList();
+  }
+
+  /**
+   * Sets the value of the designated parameter with the given object.
+   *
+   * <p>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, targetSqlType, null);
+  }
+
+  /**
+   * Sets the value of the designated parameter using the given object.
+   *
+   * <p>The JDBC specification specifies a standard mapping from Java <code>Object</code> types to
+   * SQL types. The given argument will be converted to the corresponding SQL type before being sent
+   * to the database.
+   *
+   * <p>Note that this method may be used to pass datatabase- specific abstract data types, by using
+   * a driver-specific Java type.
+   *
+   * <p>If the object is of a class implementing the interface <code>SQLData</code>, the JDBC driver
+   * should call the method <code>SQLData.writeSQL</code> to write it to the SQL data stream. If, on
+   * the other hand, the object is of a class implementing <code>Ref</code>, <code>Blob</code>,
+   * <code>Clob</code>, <code>NClob</code>, <code>Struct</code>, <code>java.net.URL</code>, <code>
+   * RowId</code>, <code>SQLXML</code> or <code>Array</code>, the driver should pass it to the
+   * database as a value of the corresponding SQL type.
+   *
+   * <p><b>Note:</b> Not all databases allow for a non-typed Null to be sent to the backend. For
+   * maximum portability, the <code>setNull</code> or the <code>
+   * setObject(int parameterIndex, Object x, int sqlType)</code> method should be used instead of
+   * <code>setObject(int parameterIndex, Object x)</code>.
+   *
+   * <p><b>Note:</b> 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 <code>
+   *     PreparedStatement</code> or the type of the given object is ambiguous
+   */
+  @Override
+  public void setObject(int parameterIndex, Object x) throws SQLException {
+    setInternalObject(parameterIndex, x, null, null);
+  }
+
+  /**
+   * Sets the designated parameter to the given <code>Reader</code> object, which is the given
+   * number of characters long. When a very large UNICODE value is input to a <code>LONGVARCHAR
+   * </code> parameter, it may be more practical to send it via a <code>java.io.Reader</code>
+   * 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.
+   *
+   * <p><B>Note:</B> 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 <code>java.io.Reader</code> 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 <code>
+   *     PreparedStatement</code>
+   * @since 1.2
+   */
+  @Override
+  public void setCharacterStream(int parameterIndex, Reader reader, int length)
+      throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(
+        parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader, (long) length));
+  }
+
+  /**
+   * Sets the designated parameter to the given <code>REF(&lt;structured-type&gt;)</code> value. The
+   * driver converts this to an SQL <code>REF</code> value when it sends it to the database.
+   *
+   * @param parameterIndex the first parameter is 1, the second is 2, ...
+   * @param x an SQL <code>REF</code> 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 <code>
+   *     PreparedStatement</code>
+   * @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 <code>java.sql.Blob</code> object. The driver
+   * converts this to an SQL <code>BLOB</code> value when it sends it to the database.
+   *
+   * @param parameterIndex the first parameter is 1, the second is 2, ...
+   * @param x a <code>Blob</code> object that maps an SQL <code>BLOB</code> 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 <code>
+   *     PreparedStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.2
+   */
+  @Override
+  public void setBlob(int parameterIndex, Blob x) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(BlobCodec.INSTANCE, x));
+  }
+
+  /**
+   * Sets the designated parameter to the given <code>java.sql.Clob</code> object. The driver
+   * converts this to an SQL <code>CLOB</code> value when it sends it to the database.
+   *
+   * @param parameterIndex the first parameter is 1, the second is 2, ...
+   * @param x a <code>Clob</code> object that maps an SQL <code>CLOB</code> 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 <code>
+   *     PreparedStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.2
+   */
+  @Override
+  public void setClob(int parameterIndex, Clob x) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(ClobCodec.INSTANCE, x));
+  }
+
+  /**
+   * Sets the designated parameter to the given <code>java.sql.Array</code> object. The driver
+   * converts this to an SQL <code>ARRAY</code> value when it sends it to the database.
+   *
+   * @param parameterIndex the first parameter is 1, the second is 2, ...
+   * @param x an <code>Array</code> object that maps an SQL <code>ARRAY</code> 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 <code>
+   *     PreparedStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.2
+   */
+  @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 <code>java.sql.Date</code> value, using the given
+   * <code>Calendar</code> object. The driver uses the <code>Calendar</code> object to construct an
+   * SQL <code>DATE</code> value, which the driver then sends to the database. With a <code>Calendar
+   * </code> object, the driver can calculate the date taking into account a custom timezone. If no
+   * <code>Calendar</code> 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 <code>Calendar</code> 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 <code>
+   *     PreparedStatement</code>
+   * @since 1.2
+   */
+  @Override
+  public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new ParameterWithCal<>(DateCodec.INSTANCE, x, cal));
+  }
+
+  /**
+   * Sets the designated parameter to the given <code>java.sql.Time</code> value, using the given
+   * <code>Calendar</code> object. The driver uses the <code>Calendar</code> object to construct an
+   * SQL <code>TIME</code> value, which the driver then sends to the database. With a <code>Calendar
+   * </code> object, the driver can calculate the time taking into account a custom timezone. If no
+   * <code>Calendar</code> 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 <code>Calendar</code> 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 <code>
+   *     PreparedStatement</code>
+   * @since 1.2
+   */
+  @Override
+  public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new ParameterWithCal<>(TimeCodec.INSTANCE, x, cal));
+  }
+
+  /**
+   * Sets the designated parameter to the given <code>java.sql.Timestamp</code> value, using the
+   * given <code>Calendar</code> object. The driver uses the <code>Calendar</code> object to
+   * construct an SQL <code>TIMESTAMP</code> value, which the driver then sends to the database.
+   * With a <code>Calendar</code> object, the driver can calculate the timestamp taking into account
+   * a custom timezone. If no <code>Calendar</code> 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 <code>Calendar</code> 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 <code>
+   *     PreparedStatement</code>
+   * @since 1.2
+   */
+  @Override
+  public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new ParameterWithCal<>(TimestampCodec.INSTANCE, x, cal));
+  }
+
+  /**
+   * Sets the designated parameter to SQL <code>NULL</code>. This version of the method <code>
+   * setNull</code> 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.
+   *
+   * <p><B>Note:</B> 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. If a JDBC driver does not need the type code
+   * or type name information, it may ignore it.
+   *
+   * <p>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 parameterIndex the first parameter is 1, the second is 2, ...
+   * @param sqlType a value from <code>java.sql.Types</code>
+   * @param typeName the fully-qualified name of an SQL user-defined type; ignored if the parameter
+   *     is not a user-defined type or REF
+   * @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 <code>
+   *     PreparedStatement</code>
+   * @throws SQLFeatureNotSupportedException if <code>sqlType</code> is a <code>ARRAY</code>, <code>
+   *     BLOB</code>, <code>CLOB</code>, <code>DATALINK</code>, <code>JAVA_OBJECT</code>, <code>
+   *     NCHAR</code>, <code>NCLOB</code>, <code>NVARCHAR</code>, <code>LONGNVARCHAR</code>, <code>
+   *     REF</code>, <code>ROWID</code>, <code>SQLXML</code> or <code>STRUCT</code> 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
+   */
+  @Override
+  public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, Parameter.NULL_PARAMETER);
+  }
+
+  /**
+   * Sets the designated parameter to the given <code>java.net.URL</code> value. The driver converts
+   * this to an SQL <code>DATALINK</code> value when it sends it to the database.
+   *
+   * @param parameterIndex the first parameter is 1, the second is 2, ...
+   * @param x the <code>java.net.URL</code> 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 <code>
+   *     PreparedStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.4
+   */
+  @Override
+  public void setURL(int parameterIndex, URL x) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(StringCodec.INSTANCE, x.toString()));
+  }
+
+  /**
+   * Sets the designated parameter to the given <code>java.sql.RowId</code> object. The driver
+   * converts this to an SQL <code>ROWID</code> value when it sends it to the database
+   *
+   * @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 or this method is called on a closed <code>
+   *     PreparedStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.6
+   */
+  @Override
+  public void setRowId(int parameterIndex, RowId x) throws SQLException {
+    throw exceptionFactory().notSupported("RowId parameter are not supported");
+  }
+
+  /**
+   * Sets the designated parameter to the given <code>String</code> object. The driver converts this
+   * to an SQL <code>NCHAR</code> or <code>NVARCHAR</code> or <code>LONGNVARCHAR</code> value
+   * (depending on the argument's size relative to the driver's limits on <code>NVARCHAR</code>
+   * values) when it sends it to the database.
+   *
+   * @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 <code>PreparedStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.6
+   */
+  @Override
+  public void setNString(int parameterIndex, String value) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(StringCodec.INSTANCE, value));
+  }
+
+  /**
+   * Sets the designated parameter to a <code>Reader</code> object. The <code>Reader</code> 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 of the first parameter is 1, the second is 2, ...
+   * @param value the parameter value
+   * @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 <code>PreparedStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.6
+   */
+  @Override
+  public void setNCharacterStream(int parameterIndex, Reader value, long length)
+      throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, value, length));
+  }
+
+  /**
+   * Sets the designated parameter to a <code>java.sql.NClob</code> object. The driver converts this
+   * to an SQL <code>NCLOB</code> value when it sends it to the database.
+   *
+   * @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 <code>PreparedStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.6
+   */
+  @Override
+  public void setNClob(int parameterIndex, NClob value) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(ClobCodec.INSTANCE, value));
+  }
+
+  /**
+   * Sets the designated parameter to a <code>Reader</code> object. The reader must contain the
+   * number of characters specified by length otherwise a <code>SQLException</code> will be
+   * generated when the <code>PreparedStatement</code> is executed. This method differs from the
+   * <code>setCharacterStream (int, Reader, int)</code> method because it informs the driver that
+   * the parameter value should be sent to the server as a <code>CLOB</code>. When the <code>
+   * setCharacterStream</code> 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 <code>LONGVARCHAR</code> or a
+   * <code>CLOB</code>
+   *
+   * @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 a database access error occurs; this method is called on a closed <code>
+   *     PreparedStatement</code> or if the length specified is less than zero.
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.6
+   */
+  @Override
+  public void setClob(int parameterIndex, Reader reader, long length) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader, length));
+  }
+
+  /**
+   * Sets the designated parameter to a <code>InputStream</code> object. The inputstream must
+   * contain the number of characters specified by length otherwise a <code>SQLException</code> will
+   * be generated when the <code>PreparedStatement</code> is executed. This method differs from the
+   * <code>setBinaryStream (int, InputStream, int)</code> method because it informs the driver that
+   * the parameter value should be sent to the server as a <code>BLOB</code>. When the <code>
+   * setBinaryStream</code> 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 <code>LONGVARBINARY</code> or a
+   * <code>BLOB</code>
+   *
+   * @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 <code>
+   *     PreparedStatement</code>; 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 {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, inputStream, length));
+  }
+
+  /**
+   * Sets the designated parameter to a <code>Reader</code> object. The reader must contain the
+   * number of characters specified by length otherwise a <code>SQLException</code> will be
+   * generated when the <code>PreparedStatement</code> is executed. This method differs from the
+   * <code>setCharacterStream (int, Reader, int)</code> method because it informs the driver that
+   * the parameter value should be sent to the server as a <code>NCLOB</code>. When the <code>
+   * setCharacterStream</code> 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 <code>LONGNVARCHAR</code> or a
+   * <code>NCLOB</code>
+   *
+   * @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 <code>
+   *     PreparedStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.6
+   */
+  @Override
+  public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader, length));
+  }
+
+  /**
+   * Sets the designated parameter to the given <code>java.sql.SQLXML</code> object. The driver
+   * converts this to an SQL <code>XML</code> value when it sends it to the database.
+   *
+   * @param parameterIndex index of the first parameter is 1, the second is 2, ...
+   * @param xmlObject a <code>SQLXML</code> object that maps an SQL <code>XML</code> 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 <code>
+   *     PreparedStatement</code> or the <code>java.xml.transform.Result</code>, <code>Writer</code>
+   *     or <code>OutputStream</code> has not been closed for the <code>SQLXML</code> object
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.6
+   */
+  @Override
+  public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException {
+    throw exceptionFactory().notSupported("SQLXML parameter are not supported");
+  }
+
+  private ExceptionFactory exceptionFactory() {
+    return con.getExceptionFactory().of(this);
+  }
+
+  /**
+   * Sets the value of the designated parameter with the given object.
+   *
+   * <p>If the second argument is an <code>InputStream</code> then the stream must contain the
+   * number of bytes specified by scaleOrLength. If the second argument is a <code>Reader</code>
+   * 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</code> when the prepared
+   * statement is executed.
+   *
+   * <p>The given Java object will be converted to the given targetSqlType before being sent to the
+   * database.
+   *
+   * <p>If the object has a custom mapping (is of a class implementing the interface <code>SQLData
+   * </code>), the JDBC driver should call the method <code>SQLData.writeSQL</code> to write it to
+   * the SQL data stream. If, on the other hand, the object is of a class implementing <code>Ref
+   * </code>, <code>Blob</code>, <code>Clob</code>, <code>NClob</code>, <code>Struct</code>, <code>
+   * java.net.URL</code>, or <code>Array</code>, the driver should pass it to the database as a
+   * value of the corresponding SQL type.
+   *
+   * <p>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 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 <code>java.sql.Types.DECIMAL</code> or <code>
+   *     java.sql.Types.NUMERIC types</code>, this is the number of digits after the decimal point.
+   *     For Java Object types <code>InputStream</code> and <code>Reader</code>, 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 <code>
+   *     PreparedStatement</code> 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
+   */
+  @Override
+  public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength)
+      throws SQLException {
+    setInternalObject(parameterIndex, x, targetSqlType, (long) scaleOrLength);
+  }
+
+  @SuppressWarnings({"unchecked", "rawtypes"})
+  private void setInternalObject(
+      int parameterIndex, Object obj, Integer targetSqlType, Long scaleOrLength)
+      throws SQLException {
+    checkIndex(parameterIndex);
+    if (obj == null) {
+      parameters.set(parameterIndex - 1, Parameter.NULL_PARAMETER);
+      return;
+    }
+
+    if (targetSqlType != null) {
+      // target type is defined.
+      // in case of not corresponding data type, converting
+      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;
+      }
+
+      if (obj instanceof String || obj instanceof Character) {
+        if (targetSqlType == Types.BLOB) {
+          throw exceptionFactory()
+              .create(
+                  String.format(
+                      "Cannot convert a %s to a Blob",
+                      obj instanceof String ? "string" : "character"));
+        }
+        String str = obj instanceof String ? (String) obj : ((Character) obj).toString();
+        try {
+          switch (targetSqlType) {
+            case Types.BIT:
+            case Types.BOOLEAN:
+              setBoolean(parameterIndex, !("false".equalsIgnoreCase(str) || "0".equals(str)));
+              return;
+            case Types.TINYINT:
+              setByte(parameterIndex, Byte.parseByte(str));
+              return;
+            case Types.SMALLINT:
+              setShort(parameterIndex, Short.parseShort(str));
+              return;
+            case Types.INTEGER:
+              setInt(parameterIndex, Integer.parseInt(str));
+              return;
+            case Types.DOUBLE:
+            case Types.FLOAT:
+              setDouble(parameterIndex, Double.valueOf(str));
+              return;
+            case Types.REAL:
+              setFloat(parameterIndex, Float.valueOf(str));
+              return;
+            case Types.BIGINT:
+              setLong(parameterIndex, Long.valueOf(str));
+              return;
+            case Types.DECIMAL:
+            case Types.NUMERIC:
+              setBigDecimal(parameterIndex, new BigDecimal(str));
+              return;
+            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);
+              return;
+            case Types.TIMESTAMP:
+              if (str.startsWith("0000-00-00")) {
+                setTimestamp(parameterIndex, null);
+              } else {
+                setTimestamp(parameterIndex, Timestamp.valueOf(str));
+              }
+              return;
+            case Types.TIME:
+              setTime(parameterIndex, Time.valueOf((String) obj));
+              return;
+            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 java.sql.Type %s", str, targetSqlType),
+                  "HY000",
+                  e);
+        }
+      } else if (obj instanceof Number) {
+        Number bd = (Number) obj;
+        switch (targetSqlType) {
+          case Types.TINYINT:
+            setByte(parameterIndex, bd.byteValue());
+            return;
+          case Types.SMALLINT:
+            setShort(parameterIndex, bd.shortValue());
+            return;
+          case Types.INTEGER:
+            setInt(parameterIndex, bd.intValue());
+            return;
+          case Types.BIGINT:
+            setLong(parameterIndex, bd.longValue());
+            return;
+          case Types.FLOAT:
+          case Types.DOUBLE:
+            setDouble(parameterIndex, bd.doubleValue());
+            return;
+          case Types.REAL:
+            setFloat(parameterIndex, bd.floatValue());
+            return;
+          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());
+            }
+            return;
+          case Types.BIT:
+            setBoolean(parameterIndex, bd.shortValue() != 0);
+            return;
+          case Types.CHAR:
+          case Types.VARCHAR:
+            setString(parameterIndex, bd.toString());
+            return;
+          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);
+          return;
+        } else if (targetSqlType == Types.BLOB) {
+          setBlob(parameterIndex, new MariaDbBlob((byte[]) obj));
+        } else {
+          throw exceptionFactory()
+              .create("Can only convert a byte[] to BINARY, VARBINARY, LONGVARBINARY or BLOB type");
+        }
+      }
+    }
+
+    // in case parameter still not set, defaulting to object type
+    for (Codec<?> codec : con.getContext().getConf().codecs()) {
+      if (codec.canEncode(obj)) {
+        Parameter p = new Parameter(codec, obj, scaleOrLength);
+        parameters.set(parameterIndex - 1, p);
+        return;
+      }
+    }
+
+    throw new SQLException(String.format("Type %s not supported type", obj.getClass().getName()));
+  }
+
+  /**
+   * 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 <code>LONGVARCHAR</code> parameter, it
+   * may be more practical to send it via a <code>java.io.InputStream</code>. 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.
+   *
+   * <p><B>Note:</B> 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 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 <code>
+   *     PreparedStatement</code>
+   * @since 1.6
+   */
+  @Override
+  public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException {
+    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 binary value is input to a <code>LONGVARBINARY</code> parameter, it
+   * may be more practical to send it via a <code>java.io.InputStream</code> object. The data will
+   * be read from the stream as needed until end-of-file is reached.
+   *
+   * <p><B>Note:</B> 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 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 <code>
+   *     PreparedStatement</code>
+   * @since 1.6
+   */
+  @Override
+  public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, length));
+  }
+
+  /**
+   * Sets the designated parameter to the given <code>Reader</code> object, which is the given
+   * number of characters long. When a very large UNICODE value is input to a <code>LONGVARCHAR
+   * </code> parameter, it may be more practical to send it via a <code>java.io.Reader</code>
+   * 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.
+   *
+   * <p><B>Note:</B> 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 <code>java.io.Reader</code> 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 <code>
+   *     PreparedStatement</code>
+   * @since 1.6
+   */
+  @Override
+  public void setCharacterStream(int parameterIndex, Reader reader, long length)
+      throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader, length));
+  }
+
+  /**
+   * Sets the designated parameter to the given input stream. When a very large ASCII value is input
+   * to a <code>LONGVARCHAR</code> parameter, it may be more practical to send it via a <code>
+   * java.io.InputStream</code>. 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.
+   *
+   * <p><B>Note:</B> This stream object can either be a standard Java stream object or your own
+   * subclass that implements the standard interface.
+   *
+   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
+   * efficient to use a version of <code>setAsciiStream</code> which takes a length parameter.
+   *
+   * @param parameterIndex the first parameter is 1, the second is 2, ...
+   * @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 <code>
+   *     PreparedStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.6
+   */
+  @Override
+  public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x));
+  }
+
+  /**
+   * Sets the designated parameter to the given input stream. When a very large binary value is
+   * input to a <code>LONGVARBINARY</code> parameter, it may be more practical to send it via a
+   * <code>java.io.InputStream</code> object. The data will be read from the stream as needed until
+   * end-of-file is reached.
+   *
+   * <p><B>Note:</B> This stream object can either be a standard Java stream object or your own
+   * subclass that implements the standard interface.
+   *
+   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
+   * efficient to use a version of <code>setBinaryStream</code> which takes a length parameter.
+   *
+   * @param parameterIndex the first parameter is 1, the second is 2, ...
+   * @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 <code>
+   *     PreparedStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.6
+   */
+  @Override
+  public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x));
+  }
+
+  /**
+   * Sets the designated parameter to the given <code>Reader</code> object. When a very large
+   * UNICODE value is input to a <code>LONGVARCHAR</code> parameter, it may be more practical to
+   * send it via a <code>java.io.Reader</code> 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.
+   *
+   * <p><B>Note:</B> This stream object can either be a standard Java stream object or your own
+   * subclass that implements the standard interface.
+   *
+   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
+   * efficient to use a version of <code>setCharacterStream</code> which takes a length parameter.
+   *
+   * @param parameterIndex the first parameter is 1, the second is 2, ...
+   * @param reader the <code>java.io.Reader</code> 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 <code>
+   *     PreparedStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.6
+   */
+  @Override
+  public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader));
+  }
+
+  /**
+   * Sets the designated parameter to a <code>Reader</code> object. The <code>Reader</code> 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.
+   *
+   * <p><B>Note:</B> This stream object can either be a standard Java stream object or your own
+   * subclass that implements the standard interface.
+   *
+   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
+   * efficient to use a version of <code>setNCharacterStream</code> 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 <code>PreparedStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.6
+   */
+  @Override
+  public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, value));
+  }
+
+  /**
+   * Sets the designated parameter to a <code>Reader</code> object. This method differs from the
+   * <code>setCharacterStream (int, Reader)</code> method because it informs the driver that the
+   * parameter value should be sent to the server as a <code>CLOB</code>. When the <code>
+   * setCharacterStream</code> 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 <code>LONGVARCHAR</code> or a
+   * <code>CLOB</code>
+   *
+   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
+   * efficient to use a version of <code>setClob</code> 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; this method is called on a closed <code>
+   *     PreparedStatement</code>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
+   */
+  @Override
+  public void setClob(int parameterIndex, Reader reader) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader));
+  }
+
+  /**
+   * Sets the designated parameter to a <code>InputStream</code> object. This method differs from
+   * the <code>setBinaryStream (int, InputStream)</code> method because it informs the driver that
+   * the parameter value should be sent to the server as a <code>BLOB</code>. When the <code>
+   * setBinaryStream</code> 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 <code>LONGVARBINARY</code> or a
+   * <code>BLOB</code>
+   *
+   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
+   * efficient to use a version of <code>setBlob</code> 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; this method is called on a closed <code>
+   *     PreparedStatement</code> 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
+   */
+  @Override
+  public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, inputStream));
+  }
+
+  /**
+   * Sets the designated parameter to a <code>Reader</code> object. This method differs from the
+   * <code>setCharacterStream (int, Reader)</code> method because it informs the driver that the
+   * parameter value should be sent to the server as a <code>NCLOB</code>. When the <code>
+   * setCharacterStream</code> 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 <code>LONGNVARCHAR</code> or a
+   * <code>NCLOB</code>
+   *
+   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
+   * efficient to use a version of <code>setNClob</code> 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 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 <code>PreparedStatement</code>
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
+   * @since 1.6
+   */
+  @Override
+  public void setNClob(int parameterIndex, Reader reader) throws SQLException {
+    checkIndex(parameterIndex);
+    parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader));
+  }
+
+  /**
+   * Sets the value of the designated parameter with the given object.
+   *
+   * <p>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.
+   *
+   * <p>The given Java object will be converted to the given targetSqlType before being sent to the
+   * database.
+   *
+   * <p>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.
+   *
+   * <p>Note that this method may be used to pass database-specific abstract data types.
+   *
+   * <p>The default implementation will throw {@code SQLFeatureNotSupportedException}
+   *
+   * @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 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 {@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
+   */
+  @Override
+  public void setObject(int parameterIndex, Object x, SQLType targetSqlType, int scaleOrLength)
+      throws SQLException {
+    setInternalObject(
+        parameterIndex,
+        x,
+        targetSqlType == null ? null : targetSqlType.getVendorTypeNumber(),
+        (long) scaleOrLength);
+  }
+
+  /**
+   * Sets the value of the designated parameter with the given object.
+   *
+   * <p>This method is similar to {@link #setObject(int parameterIndex, Object x, SQLType
+   * targetSqlType, int scaleOrLength)}, except that it assumes a scale of zero.
+   *
+   * <p>The default implementation will throw {@code SQLFeatureNotSupportedException}
+   *
+   * @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 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 {@code
+   *     PreparedStatement}
+   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support the specified
+   *     targetSqlType
+   * @see JDBCType
+   * @see SQLType
+   * @since 1.8
+   */
+  @Override
+  public void setObject(int parameterIndex, Object x, SQLType targetSqlType) throws SQLException {
+    setInternalObject(
+        parameterIndex,
+        x,
+        targetSqlType == null ? null : targetSqlType.getVendorTypeNumber(),
+        null);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/CallableParameterMetaData.java b/src/main/java/org/mariadb/jdbc/CallableParameterMetaData.java
new file mode 100644
index 0000000..f9a0f0c
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/CallableParameterMetaData.java
@@ -0,0 +1,357 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.math.BigDecimal;
+import java.sql.*;
+import java.util.BitSet;
+import java.util.Locale;
+
+/**
+ * Callable parameter metadata. Server doesn't give detailled information about parameter, so even
+ * if driver return those information, they are not completely accurate.
+ */
+public class CallableParameterMetaData implements java.sql.ParameterMetaData {
+  private final ResultSet rs;
+  private final int parameterCount;
+  private final boolean isFunction;
+
+  /**
+   * Constructor
+   *
+   * @param rs parameter result-set
+   * @param isFunction is command a function or a stored procedure
+   * @throws SQLException if any error occurs
+   */
+  public CallableParameterMetaData(ResultSet rs, boolean isFunction) throws SQLException {
+    this.rs = rs;
+    int count = 0;
+    while (rs.next()) count++;
+    this.parameterCount = count;
+    this.isFunction = isFunction;
+  }
+
+  /**
+   * Retrieves the number of parameters in the <code>PreparedStatement</code> object for which this
+   * <code>ParameterMetaData</code> object contains information.
+   *
+   * @return the number of parameters
+   * @since 1.4
+   */
+  @Override
+  public int getParameterCount() {
+    return parameterCount;
+  }
+
+  /**
+   * Retrieves whether null values are allowed in the designated parameter.
+   *
+   * @param index the first parameter is 1, the second is 2, ...
+   * @return the nullability status of the given parameter; one of <code>
+   *     ParameterMetaData.parameterNoNulls</code>, <code>ParameterMetaData.parameterNullable</code>
+   *     , or <code>ParameterMetaData.parameterNullableUnknown</code>
+   * @throws SQLException if a database access error occurs
+   * @since 1.4
+   */
+  @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);
+    }
+    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 <code>true</code> if so; <code>false</code> 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.
+   *
+   * <p>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
+   */
+  @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
+   */
+  @Override
+  public int getScale(int index) throws SQLException {
+    setIndex(index);
+    return rs.getInt("NUMERIC_SCALE");
+  }
+
+  /**
+   * Return the parameter name corresponding to index.
+   *
+   * @param index index
+   * @return parameter name
+   * @throws SQLException if wrong index
+   */
+  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 types from <code>java.sql.Types</code>
+   * @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").toUpperCase(Locale.ROOT);
+    switch (str) {
+      case "BIT":
+        return Types.BIT;
+      case "TINYINT":
+        return Types.TINYINT;
+      case "SMALLINT":
+      case "YEAR":
+        return Types.SMALLINT;
+      case "MEDIUMINT":
+      case "INT":
+      case "INT24":
+      case "INTEGER":
+        return Types.INTEGER;
+      case "LONG":
+      case "BIGINT":
+        return Types.BIGINT;
+      case "REAL":
+      case "DOUBLE":
+        return Types.DOUBLE;
+      case "FLOAT":
+        return Types.FLOAT;
+      case "DECIMAL":
+        return Types.DECIMAL;
+      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 "TIMESTAMP":
+      case "DATETIME":
+        return Types.TIMESTAMP;
+      case "BINARY":
+        return Types.BINARY;
+      case "VARBINARY":
+        return Types.VARBINARY;
+      case "TINYBLOB":
+      case "BLOB":
+      case "MEDIUMBLOB":
+      case "LONGBLOB":
+      case "GEOMETRY":
+        return Types.BLOB;
+      case "TEXT":
+      case "MEDIUMTEXT":
+      case "LONGTEXT":
+        return Types.CLOB;
+      default:
+        return Types.OTHER;
+    }
+  }
+
+  /**
+   * 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);
+  }
+
+  /**
+   * Retrieves the fully-qualified name of the Java class whose instances should be passed to the
+   * method <code>PreparedStatement.setObject</code>.
+   *
+   * @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 <code>PreparedStatement.setObject</code> 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").toUpperCase(Locale.ROOT);
+    switch (str) {
+      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":
+      case "SET":
+      case "GEOMETRY":
+      case "VARBINARY":
+      case "TINYBLOB":
+        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();
+
+      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();
+      default:
+        return Object.class.getName();
+    }
+  }
+
+  /**
+   * Retrieves the designated parameter's mode.
+   *
+   * @param index the first parameter is 1, the second is 2, ...
+   * @return mode of the parameter; one of <code>ParameterMetaData.parameterModeIn</code>, <code>
+   *     ParameterMetaData.parameterModeOut</code>, or <code>ParameterMetaData.parameterModeInOut
+   *     </code> <code>ParameterMetaData.parameterModeUnknown</code>.
+   * @throws SQLException if a database access error occurs
+   * @since 1.4
+   */
+  @Override
+  public int getParameterMode(int index) throws SQLException {
+    setIndex(index);
+    if (isFunction) return ParameterMetaData.parameterModeOut;
+    String str = rs.getString("PARAMETER_MODE");
+    switch (str) {
+      case "IN":
+        return ParameterMetaData.parameterModeIn;
+      case "OUT":
+        return ParameterMetaData.parameterModeOut;
+      case "INOUT":
+        return ParameterMetaData.parameterModeInOut;
+      default:
+        return ParameterMetaData.parameterModeUnknown;
+    }
+  }
+
+  /**
+   * Returns an object that implements the given interface to allow access to non-standard methods,
+   * or standard methods not exposed by the proxy.
+   *
+   * <p>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 result of
+   * calling <code>unwrap</code> 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 <code>SQLException
+   * </code> is thrown.
+   *
+   * @param iface A Class defining an interface that the result must implement.
+   * @return an object that implements the interface. Maybe a proxy for the actual implementing
+   *     object.
+   * @throws SQLException If no object found that implements the interface
+   * @since 1.6
+   */
+  @Override
+  public <T> T unwrap(Class<T> 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 <code>
+   * isWrapperFor</code> 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
+   * <code>unwrap</code> so that callers can use this method to avoid expensive <code>unwrap</code>
+   * calls that may fail. If this method returns true then calling <code>unwrap</code> 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
+   */
+  @Override
+  public boolean isWrapperFor(Class<?> iface) {
+    return iface.isInstance(this);
+  }
+}
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 0000000..683256a
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/ClientPreparedStatement.java
@@ -0,0 +1,527 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import static org.mariadb.jdbc.util.constants.Capabilities.*;
+
+import java.sql.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.locks.ReentrantLock;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.Completion;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.client.result.Result;
+import org.mariadb.jdbc.export.ExceptionFactory;
+import org.mariadb.jdbc.message.ClientMessage;
+import org.mariadb.jdbc.message.client.*;
+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.ParameterList;
+import org.mariadb.jdbc.util.constants.ServerStatus;
+
+/**
+ * Client side prepare statement. Question mark will be replaced by escaped parameters, client side
+ */
+public class ClientPreparedStatement extends BasePreparedStatement {
+  private final ClientParser parser;
+
+  /**
+   * Client prepare statement constructor
+   *
+   * @param sql command
+   * @param con connection
+   * @param lock thread safe lock
+   * @param canUseServerTimeout can server use timeout
+   * @param canUseServerMaxRows can server use max rows
+   * @param autoGeneratedKeys must command return automatically generated keys
+   * @param resultSetType resultset type
+   * @param resultSetConcurrency resultset concurrency
+   * @param defaultFetchSize default fetch size
+   */
+  public ClientPreparedStatement(
+      String sql,
+      Connection con,
+      ReentrantLock lock,
+      boolean canUseServerTimeout,
+      boolean canUseServerMaxRows,
+      int autoGeneratedKeys,
+      int resultSetType,
+      int resultSetConcurrency,
+      int defaultFetchSize) {
+    super(
+        sql,
+        con,
+        lock,
+        canUseServerTimeout,
+        canUseServerMaxRows,
+        autoGeneratedKeys,
+        resultSetType,
+        resultSetConcurrency,
+        defaultFetchSize);
+
+    boolean noBackslashEscapes =
+        (con.getContext().getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES) > 0;
+    parser = ClientParser.parameterParts(sql, noBackslashEscapes);
+    parameters = new ParameterList(parser.getParamCount());
+  }
+
+  /**
+   * use additional part for timeout if possible
+   *
+   * @return pre command for handling timeout
+   */
+  protected String preSqlCmd() {
+    if (queryTimeout != 0 && canUseServerTimeout) {
+      return "SET STATEMENT max_statement_time=" + queryTimeout + " FOR ";
+    }
+    return null;
+  }
+
+  private void executeInternal() throws SQLException {
+    checkNotClosed();
+    validParameters();
+    lock.lock();
+    try {
+      QueryWithParametersPacket query =
+          new QueryWithParametersPacket(preSqlCmd(), parser, parameters, localInfileInputStream);
+      results =
+          con.getClient()
+              .execute(
+                  query,
+                  this,
+                  fetchSize,
+                  maxRows,
+                  resultSetConcurrency,
+                  resultSetType,
+                  closeOnCompletion,
+                  false);
+    } finally {
+      localInfileInputStream = null;
+      lock.unlock();
+    }
+  }
+
+  private void executeInternalPreparedBatch() throws SQLException {
+    checkNotClosed();
+    if (autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS
+        && batchParameters.size() > 1
+        && con.getContext().hasClientCapability(STMT_BULK_OPERATIONS)) {
+      executeBatchBulk();
+    } else {
+      boolean possibleLoadLocal = con.getContext().hasClientCapability(LOCAL_FILES);
+      if (possibleLoadLocal) {
+        String sqlUpper = sql.toUpperCase(Locale.ROOT);
+        possibleLoadLocal =
+            sqlUpper.contains(" LOCAL ")
+                && sqlUpper.contains("LOAD")
+                && sqlUpper.contains(" INFILE");
+      }
+      if (possibleLoadLocal) {
+        executeBatchStd();
+      } else {
+        executeBatchPipeline();
+      }
+    }
+  }
+
+  /**
+   * Send COM_STMT_PREPARE + X * COM_STMT_BULK_EXECUTE, then read for the all answers
+   *
+   * @throws SQLException if IOException / Command error
+   */
+  private void executeBatchBulk() throws SQLException {
+    String cmd = escapeTimeout(sql);
+    try {
+      if (prepareResult == null) {
+        ClientMessage[] packets =
+            new ClientMessage[] {
+              new PreparePacket(cmd), new BulkExecutePacket(null, batchParameters, cmd, null)
+            };
+        List<Completion> res =
+            con.getClient()
+                .executePipeline(
+                    packets,
+                    this,
+                    0,
+                    maxRows,
+                    ResultSet.CONCUR_READ_ONLY,
+                    ResultSet.TYPE_FORWARD_ONLY,
+                    closeOnCompletion,
+                    false);
+        // in case of failover, prepare is done in failover, skipping prepare result
+        if (res.get(0) instanceof PrepareResultPacket) {
+          results = res.subList(1, res.size());
+        } else {
+          results = res;
+        }
+      } else {
+        results =
+            con.getClient()
+                .execute(
+                    new BulkExecutePacket(prepareResult, batchParameters, cmd, null),
+                    this,
+                    fetchSize,
+                    maxRows,
+                    resultSetConcurrency,
+                    resultSetType,
+                    closeOnCompletion,
+                    false);
+      }
+    } catch (SQLException bue) {
+      results = null;
+      throw exceptionFactory()
+          .createBatchUpdate(Collections.emptyList(), batchParameters.size(), bue);
+    }
+  }
+
+  /**
+   * Send n * COM_QUERY + n * read answer
+   *
+   * @throws SQLException if IOException / Command error
+   */
+  private void 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), null);
+    }
+    try {
+      results =
+          con.getClient()
+              .executePipeline(
+                  packets,
+                  this,
+                  0,
+                  maxRows,
+                  ResultSet.CONCUR_READ_ONLY,
+                  ResultSet.TYPE_FORWARD_ONLY,
+                  closeOnCompletion,
+                  false);
+    } catch (SQLException bue) {
+      results = null;
+      throw bue;
+    }
+  }
+
+  /**
+   * Send n * (COM_QUERY + read answer)
+   *
+   * @throws SQLException if IOException / Command error
+   */
+  private void executeBatchStd() throws SQLException {
+    int i = 0;
+    try {
+      results = new ArrayList<>();
+      for (; i < batchParameters.size(); i++) {
+        results.addAll(
+            con.getClient()
+                .execute(
+                    new QueryWithParametersPacket(
+                        preSqlCmd(), parser, batchParameters.get(i), localInfileInputStream),
+                    this,
+                    0,
+                    maxRows,
+                    ResultSet.CONCUR_READ_ONLY,
+                    ResultSet.TYPE_FORWARD_ONLY,
+                    closeOnCompletion,
+                    false));
+      }
+    } catch (SQLException bue) {
+      BatchUpdateException exception =
+          exceptionFactory().createBatchUpdate(results, batchParameters.size(), bue);
+      results = null;
+      localInfileInputStream = null;
+      throw exception;
+    }
+  }
+
+  /**
+   * Executes the SQL statement in this <code>PreparedStatement</code> object, which may be any kind
+   * of SQL statement. Some prepared statements return multiple results; the <code>execute</code>
+   * method handles these complex statements as well as the simpler form of statements handled by
+   * the methods <code>executeQuery</code> and <code>executeUpdate</code>.
+   *
+   * <p>The <code>execute</code> method returns a <code>boolean</code> to indicate the form of the
+   * first result. You must call either the method <code>getResultSet</code> or <code>getUpdateCount
+   * </code> to retrieve the result; you must call <code>getMoreResults</code> to move to any
+   * subsequent result(s).
+   *
+   * @return <code>true</code> if the first result is a <code>ResultSet</code> object; <code>false
+   *     </code> 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
+   *     <code>PreparedStatement</code> 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 <code>PreparedStatement</code> object and returns the <code>
+   * ResultSet</code> object generated by the query.
+   *
+   * @return a <code>ResultSet</code> object that contains the data produced by the query; never
+   *     <code>null</code>
+   * @throws SQLException if a database access error occurs; this method is called on a closed
+   *     <code>PreparedStatement</code> or the SQL statement does not return a <code>ResultSet
+   *     </code> 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();
+    currResult = results.remove(0);
+    if (currResult instanceof Result) {
+      return (Result) currResult;
+    }
+    return new CompleteResult(new ColumnDecoder[0], new byte[0][], con.getContext());
+  }
+
+  /**
+   * Executes the SQL statement in this <code>PreparedStatement</code> object, which must be an SQL
+   * Data Manipulation Language (DML) statement, such as <code>INSERT</code>, <code>UPDATE</code> or
+   * <code>DELETE</code>; 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
+   *     <code>PreparedStatement</code> or the SQL statement returns a <code>ResultSet</code> 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 <code>PreparedStatement</code> object, which must be an SQL
+   * Data Manipulation Language (DML) statement, such as <code>INSERT</code>, <code>UPDATE</code> or
+   * <code>DELETE</code>; or an SQL statement that returns nothing, such as a DDL statement.
+   *
+   * <p>This method should be used when the returned row count may exceed {@link Integer#MAX_VALUE}.
+   *
+   * <p>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
+   *     <code>PreparedStatement</code> or the SQL statement returns a <code>ResultSet</code> 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();
+  }
+
+  private ExceptionFactory exceptionFactory() {
+    return con.getExceptionFactory().of(this);
+  }
+
+  /**
+   * Adds a set of parameters to this <code>PreparedStatement</code> object's batch of commands.
+   *
+   * @throws SQLException if a database access error occurs or this method is called on a closed
+   *     <code>PreparedStatement</code>
+   * @see Statement#addBatch
+   * @since 1.2
+   */
+  @Override
+  public void addBatch() throws SQLException {
+    validParameters();
+    if (batchParameters == null) batchParameters = new ArrayList<>();
+    batchParameters.add(parameters);
+    parameters = parameters.clone();
+  }
+
+  /**
+   * Validate parameter number according to expected parameter.
+   *
+   * @throws SQLException if doesn't correspond
+   */
+  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");
+      }
+    }
+  }
+
+  @Override
+  public void setQueryTimeout(int seconds) throws SQLException {
+    super.setQueryTimeout(seconds);
+    if (canUseServerTimeout && prepareResult != null) {
+      prepareResult.close(con.getClient());
+      prepareResult = null;
+    }
+  }
+
+  @Override
+  public void setMaxRows(int max) throws SQLException {
+    super.setMaxRows(max);
+    if (canUseServerMaxRows && prepareResult != null) {
+      prepareResult.close(con.getClient());
+      prepareResult = null;
+    }
+  }
+
+  @Override
+  public void setLargeMaxRows(long max) throws SQLException {
+    super.setLargeMaxRows(max);
+    if (canUseServerMaxRows && prepareResult != null) {
+      prepareResult.close(con.getClient());
+      prepareResult = null;
+    }
+  }
+
+  /**
+   * Retrieves a <code>ResultSetMetaData</code> object that contains information about the columns
+   * of the <code>ResultSet</code> object that will be returned when this <code>PreparedStatement
+   * </code> object is executed.
+   *
+   * <p>Because a <code>PreparedStatement</code> object is precompiled, it is possible to know about
+   * the <code>ResultSet</code> object that it will return without having to execute it.
+   * Consequently, it is possible to invoke the method <code>getMetaData</code> on a <code>
+   * PreparedStatement</code> object rather than waiting to execute it and then invoking the <code>
+   * ResultSet.getMetaData</code> method on the <code>ResultSet</code> object that is returned.
+   *
+   * <p><B>NOTE:</B> Using this method may be expensive for some drivers due to the lack of
+   * underlying DBMS support.
+   *
+   * @return the description of a <code>ResultSet</code> object's columns or <code>null</code> if
+   *     the driver cannot return a <code>ResultSetMetaData</code> object
+   * @throws SQLException if a database access error occurs or this method is called on a closed
+   *     <code>PreparedStatement</code>
+   * @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)
+      con.getClient().execute(new PreparePacket(escapeTimeout(sql)), this, true);
+    return new org.mariadb.jdbc.client.result.ResultSetMetaData(
+        exceptionFactory(), prepareResult.getColumns(), con.getContext().getConf(), false);
+  }
+
+  /**
+   * Retrieves the number, types and properties of this <code>PreparedStatement</code> object's
+   * parameters.
+   *
+   * @return a <code>ParameterMetaData</code> object that contains information about the number,
+   *     types and properties for each parameter marker of this <code>PreparedStatement</code>
+   *     object
+   * @throws SQLException if a database access error occurs or this method is called on a closed
+   *     <code>PreparedStatement</code>
+   * @see ParameterMetaData
+   * @since 1.4
+   */
+  @Override
+  public java.sql.ParameterMetaData getParameterMetaData() throws SQLException {
+    // send COM_STMT_PREPARE
+    if (prepareResult == null) {
+      try {
+        con.getClient().execute(new PreparePacket(escapeTimeout(sql)), this, true);
+      } catch (SQLException e) {
+        return new SimpleParameterMetaData(exceptionFactory(), parser.getParamCount());
+      }
+    }
+    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 {
+      executeInternalPreparedBatch();
+      int[] updates = new int[batchParameters.size()];
+      if (results.size() != batchParameters.size()) {
+        for (int i = 0; i < batchParameters.size(); i++) {
+          updates[i] = Statement.SUCCESS_NO_INFO;
+        }
+      } else {
+        for (int i = 0; i < updates.length; i++) {
+          if (results.get(i) instanceof OkPacket) {
+            updates[i] = (int) ((OkPacket) results.get(i)).getAffectedRows();
+          } else {
+            updates[i] = org.mariadb.jdbc.Statement.SUCCESS_NO_INFO;
+          }
+        }
+      }
+      currResult = results.remove(0);
+      return updates;
+    } finally {
+      batchParameters.clear();
+      lock.unlock();
+    }
+  }
+
+  @Override
+  public long[] executeLargeBatch() throws SQLException {
+    checkNotClosed();
+    if (batchParameters == null || batchParameters.isEmpty()) return new long[0];
+    lock.lock();
+    try {
+      executeInternalPreparedBatch();
+      long[] updates = new long[results.size()];
+      for (int i = 0; i < results.size(); i++) {
+        updates[i] = ((OkPacket) results.get(i)).getAffectedRows();
+      }
+      currResult = results.remove(0);
+      return updates;
+
+    } finally {
+      batchParameters.clear();
+      lock.unlock();
+    }
+  }
+
+  @Override
+  public void close() throws SQLException {
+    if (prepareResult != null) {
+      lock.lock();
+      try {
+        prepareResult.close(this.con.getClient());
+      } finally {
+        lock.unlock();
+      }
+    }
+    con.fireStatementClosed(this);
+    super.close();
+  }
+
+  @Override
+  public String toString() {
+    return "ClientPreparedStatement{" + super.toString() + '}';
+  }
+}
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 0000000..8388b6c
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/Configuration.java
@@ -0,0 +1,2690 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.*;
+import org.mariadb.jdbc.export.HaMode;
+import org.mariadb.jdbc.export.SslMode;
+import org.mariadb.jdbc.plugin.Codec;
+import org.mariadb.jdbc.plugin.CredentialPlugin;
+import org.mariadb.jdbc.plugin.credential.CredentialPluginLoader;
+import org.mariadb.jdbc.util.log.Logger;
+import org.mariadb.jdbc.util.log.Loggers;
+import org.mariadb.jdbc.util.options.OptionAliases;
+
+/**
+ * parse and verification of URL.
+ *
+ * <p>basic syntax :<br>
+ * {@code
+ * jdbc:mariadb:[replication:|failover|loadbalance:|aurora:]//<hostDescription>[,<hostDescription>]/[database>]
+ * [?<key1>=<value1>[&<key2>=<value2>]] }
+ *
+ * <p>hostDescription:<br>
+ * - simple :<br>
+ * {@code <host>:<portnumber>}<br>
+ * (for example localhost:3306)<br>
+ * <br>
+ * - complex :<br>
+ * {@code address=[(type=(master|slave))][(port=<portnumber>)](host=<host>)}<br>
+ * <br>
+ * <br>
+ * type is by default master<br>
+ * port is by default 3306<br>
+ *
+ * <p>host can be dns name, ipv4 or ipv6.<br>
+ * in case of ipv6 and simple host description, the ip must be written inside bracket.<br>
+ * exemple : {@code jdbc:mariadb://[2001:0660:7401:0200:0000:0000:0edf:bdd7]:3306}<br>
+ *
+ * <p>Some examples :<br>
+ * {@code jdbc:mariadb://localhost:3306/database?user=greg&password=pass}<br>
+ * {@code
+ * jdbc:mariadb://address=(type=master)(host=master1),address=(port=3307)(type=slave)(host=slave1)/database?user=greg&password=pass}
+ * <br>
+ */
+public class Configuration {
+  private static final Logger logger = Loggers.getLogger(Configuration.class);
+
+  // standard options
+  private String user = null;
+  private String password = null;
+  private String database = null;
+  private List<HostAddress> addresses = null;
+  private HaMode haMode = HaMode.NONE;
+
+  private String initialUrl = null;
+  private Properties nonMappedOptions = null;
+
+  // various
+  private String timezone = null;
+  private Boolean autocommit = null;
+  private boolean useMysqlMetadata = false;
+  private boolean createDatabaseIfNotExist = false;
+  private TransactionIsolation transactionIsolation = null;
+  private int defaultFetchSize = 0;
+  private int maxQuerySizeToLog = 1024;
+  private Integer maxAllowedPacket = null;
+  private String geometryDefaultType = null;
+  private String restrictedAuth = null;
+  private String initSql = null;
+
+  // socket
+  private String socketFactory = null;
+  private int connectTimeout =
+      DriverManager.getLoginTimeout() > 0 ? DriverManager.getLoginTimeout() * 1000 : 30_000;
+  private String pipe = null;
+  private String localSocket = null;
+  private boolean uuidAsString = false;
+  private boolean tcpKeepAlive = true;
+  private int tcpKeepIdle = 0;
+  private int tcpKeepCount = 0;
+  private int tcpKeepInterval = 0;
+  private boolean tcpAbortiveClose = false;
+  private String localSocketAddress = null;
+  private int socketTimeout = 0;
+  private boolean useReadAheadInput = false;
+  private String tlsSocketType = null;
+
+  // SSL
+  private SslMode sslMode = SslMode.DISABLE;
+  private String serverSslCert = null;
+  private String keyStore = null;
+  private String keyStorePassword = null;
+  private String keyStoreType = null;
+  private String enabledSslCipherSuites = null;
+  private String enabledSslProtocolSuites = null;
+
+  // protocol
+  private boolean allowMultiQueries = false;
+  private boolean allowLocalInfile = true;
+  private boolean useCompression = false;
+  private boolean useAffectedRows = false;
+  private boolean useBulkStmts = true;
+  private boolean disablePipeline = false;
+  // prepare
+  private boolean cachePrepStmts = true;
+  private int prepStmtCacheSize = 250;
+  private boolean useServerPrepStmts = false;
+
+  // authentication
+  private CredentialPlugin credentialType = null;
+  private String sessionVariables = null;
+  private String connectionAttributes = null;
+  private String servicePrincipalName = null;
+
+  // meta
+  private boolean blankTableNameMeta = false;
+  private boolean tinyInt1isBit = true;
+  private boolean transformedBitIsBoolean = true;
+  private boolean yearIsDateType = true;
+  private boolean dumpQueriesOnException = false;
+  private boolean includeInnodbStatusInDeadlockExceptions = false;
+  private boolean includeThreadDumpInDeadlockExceptions = false;
+
+  // HA options
+  private int retriesAllDown = 120;
+  private String galeraAllowedState = null;
+  private boolean transactionReplay = false;
+  private int transactionReplaySize = 64;
+
+  // Pool options
+  private boolean pool = false;
+  private String poolName = null;
+  private int maxPoolSize = 8;
+  private int minPoolSize = 8;
+  private int maxIdleTime = 600_000;
+  private boolean registerJmxPool = true;
+  private int poolValidMinDelay = 1000;
+  private boolean useResetConnection = false;
+
+  // MySQL sha authentication
+  private String serverRsaPublicKeyFile = null;
+  private boolean allowPublicKeyRetrieval = false;
+
+  private Codec<?>[] codecs = null;
+
+  private Configuration() {}
+
+  private Configuration(
+      String user,
+      String password,
+      String database,
+      List<HostAddress> addresses,
+      HaMode haMode,
+      Properties nonMappedOptions,
+      String timezone,
+      Boolean autocommit,
+      boolean useMysqlMetadata,
+      boolean createDatabaseIfNotExist,
+      TransactionIsolation transactionIsolation,
+      int defaultFetchSize,
+      int maxQuerySizeToLog,
+      Integer maxAllowedPacket,
+      String geometryDefaultType,
+      String restrictedAuth,
+      String initSql,
+      String socketFactory,
+      int connectTimeout,
+      String pipe,
+      String localSocket,
+      boolean tcpKeepAlive,
+      boolean uuidAsString,
+      int tcpKeepIdle,
+      int tcpKeepCount,
+      int tcpKeepInterval,
+      boolean tcpAbortiveClose,
+      String localSocketAddress,
+      int socketTimeout,
+      boolean useReadAheadInput,
+      String tlsSocketType,
+      SslMode sslMode,
+      String serverSslCert,
+      String keyStore,
+      String keyStorePassword,
+      String keyStoreType,
+      String enabledSslCipherSuites,
+      String enabledSslProtocolSuites,
+      boolean allowMultiQueries,
+      boolean allowLocalInfile,
+      boolean useCompression,
+      boolean useAffectedRows,
+      boolean useBulkStmts,
+      boolean disablePipeline,
+      boolean cachePrepStmts,
+      int prepStmtCacheSize,
+      boolean useServerPrepStmts,
+      CredentialPlugin credentialType,
+      String sessionVariables,
+      String connectionAttributes,
+      String servicePrincipalName,
+      boolean blankTableNameMeta,
+      boolean tinyInt1isBit,
+      boolean transformedBitIsBoolean,
+      boolean yearIsDateType,
+      boolean dumpQueriesOnException,
+      boolean includeInnodbStatusInDeadlockExceptions,
+      boolean includeThreadDumpInDeadlockExceptions,
+      int retriesAllDown,
+      String galeraAllowedState,
+      boolean transactionReplay,
+      int transactionReplaySize,
+      boolean pool,
+      String poolName,
+      int maxPoolSize,
+      int minPoolSize,
+      int maxIdleTime,
+      boolean registerJmxPool,
+      int poolValidMinDelay,
+      boolean useResetConnection,
+      String serverRsaPublicKeyFile,
+      boolean allowPublicKeyRetrieval) {
+    this.user = user;
+    this.password = password;
+    this.database = database;
+    this.addresses = addresses;
+    this.haMode = haMode;
+    this.nonMappedOptions = nonMappedOptions;
+    this.timezone = timezone;
+    this.autocommit = autocommit;
+    this.useMysqlMetadata = useMysqlMetadata;
+    this.createDatabaseIfNotExist = createDatabaseIfNotExist;
+    this.transactionIsolation = transactionIsolation;
+    this.defaultFetchSize = defaultFetchSize;
+    this.maxQuerySizeToLog = maxQuerySizeToLog;
+    this.maxAllowedPacket = maxAllowedPacket;
+    this.geometryDefaultType = geometryDefaultType;
+    this.restrictedAuth = restrictedAuth;
+    this.initSql = initSql;
+    this.socketFactory = socketFactory;
+    this.connectTimeout = connectTimeout;
+    this.pipe = pipe;
+    this.localSocket = localSocket;
+    this.tcpKeepAlive = tcpKeepAlive;
+    this.uuidAsString = uuidAsString;
+    this.tcpKeepIdle = tcpKeepIdle;
+    this.tcpKeepCount = tcpKeepCount;
+    this.tcpKeepInterval = tcpKeepInterval;
+    this.tcpAbortiveClose = tcpAbortiveClose;
+    this.localSocketAddress = localSocketAddress;
+    this.socketTimeout = socketTimeout;
+    this.useReadAheadInput = useReadAheadInput;
+    this.tlsSocketType = tlsSocketType;
+    this.sslMode = sslMode;
+    this.serverSslCert = serverSslCert;
+    this.keyStore = keyStore;
+    this.keyStorePassword = keyStorePassword;
+    this.keyStoreType = keyStoreType;
+    this.enabledSslCipherSuites = enabledSslCipherSuites;
+    this.enabledSslProtocolSuites = enabledSslProtocolSuites;
+    this.allowMultiQueries = allowMultiQueries;
+    this.allowLocalInfile = allowLocalInfile;
+    this.useCompression = useCompression;
+    this.useAffectedRows = useAffectedRows;
+    this.useBulkStmts = useBulkStmts;
+    this.disablePipeline = disablePipeline;
+    this.cachePrepStmts = cachePrepStmts;
+    this.prepStmtCacheSize = prepStmtCacheSize;
+    this.useServerPrepStmts = useServerPrepStmts;
+    this.credentialType = credentialType;
+    this.sessionVariables = sessionVariables;
+    this.connectionAttributes = connectionAttributes;
+    this.servicePrincipalName = servicePrincipalName;
+    this.blankTableNameMeta = blankTableNameMeta;
+    this.tinyInt1isBit = tinyInt1isBit;
+    this.transformedBitIsBoolean = transformedBitIsBoolean;
+    this.yearIsDateType = yearIsDateType;
+    this.dumpQueriesOnException = dumpQueriesOnException;
+    this.includeInnodbStatusInDeadlockExceptions = includeInnodbStatusInDeadlockExceptions;
+    this.includeThreadDumpInDeadlockExceptions = includeThreadDumpInDeadlockExceptions;
+    this.retriesAllDown = retriesAllDown;
+    this.galeraAllowedState = galeraAllowedState;
+    this.transactionReplay = transactionReplay;
+    this.transactionReplaySize = transactionReplaySize;
+    this.pool = pool;
+    this.poolName = poolName;
+    this.maxPoolSize = maxPoolSize;
+    this.minPoolSize = minPoolSize;
+    this.maxIdleTime = maxIdleTime;
+    this.registerJmxPool = registerJmxPool;
+    this.poolValidMinDelay = poolValidMinDelay;
+    this.useResetConnection = useResetConnection;
+    this.serverRsaPublicKeyFile = serverRsaPublicKeyFile;
+    this.allowPublicKeyRetrieval = allowPublicKeyRetrieval;
+    this.initialUrl = buildUrl(this);
+  }
+
+  private Configuration(
+      String database,
+      List<HostAddress> addresses,
+      HaMode haMode,
+      String user,
+      String password,
+      String enabledSslProtocolSuites,
+      String socketFactory,
+      Integer connectTimeout,
+      String pipe,
+      String localSocket,
+      Boolean tcpKeepAlive,
+      Boolean uuidAsString,
+      Integer tcpKeepIdle,
+      Integer tcpKeepCount,
+      Integer tcpKeepInterval,
+      Boolean tcpAbortiveClose,
+      String localSocketAddress,
+      Integer socketTimeout,
+      Boolean allowMultiQueries,
+      Boolean allowLocalInfile,
+      Boolean useCompression,
+      Boolean blankTableNameMeta,
+      String credentialType,
+      String sslMode,
+      String transactionIsolation,
+      String enabledSslCipherSuites,
+      String sessionVariables,
+      Boolean tinyInt1isBit,
+      Boolean transformedBitIsBoolean,
+      Boolean yearIsDateType,
+      String timezone,
+      Boolean dumpQueriesOnException,
+      Integer prepStmtCacheSize,
+      Boolean useAffectedRows,
+      Boolean useServerPrepStmts,
+      String connectionAttributes,
+      Boolean useBulkStmts,
+      Boolean disablePipeline,
+      Boolean autocommit,
+      Boolean useMysqlMetadata,
+      Boolean createDatabaseIfNotExist,
+      Boolean includeInnodbStatusInDeadlockExceptions,
+      Boolean includeThreadDumpInDeadlockExceptions,
+      String servicePrincipalName,
+      Integer defaultFetchSize,
+      String tlsSocketType,
+      Integer maxQuerySizeToLog,
+      Integer maxAllowedPacket,
+      Integer retriesAllDown,
+      String galeraAllowedState,
+      Boolean pool,
+      String poolName,
+      Integer maxPoolSize,
+      Integer minPoolSize,
+      Integer maxIdleTime,
+      Boolean registerJmxPool,
+      Integer poolValidMinDelay,
+      Boolean useResetConnection,
+      String serverRsaPublicKeyFile,
+      Boolean allowPublicKeyRetrieval,
+      String serverSslCert,
+      String keyStore,
+      String keyStorePassword,
+      String keyStoreType,
+      Boolean useReadAheadInput,
+      Boolean cachePrepStmts,
+      Boolean transactionReplay,
+      Integer transactionReplaySize,
+      String geometryDefaultType,
+      String restrictedAuth,
+      String initSql,
+      Properties nonMappedOptions)
+      throws SQLException {
+    this.database = database;
+    this.addresses = addresses;
+    this.nonMappedOptions = nonMappedOptions;
+    if (haMode != null) this.haMode = haMode;
+    this.credentialType = CredentialPluginLoader.get(credentialType);
+    this.user = user;
+    this.password = password;
+    this.enabledSslProtocolSuites = enabledSslProtocolSuites;
+    this.socketFactory = socketFactory;
+    if (connectTimeout != null) this.connectTimeout = connectTimeout;
+    this.pipe = pipe;
+    this.localSocket = localSocket;
+    if (tcpKeepAlive != null) this.tcpKeepAlive = tcpKeepAlive;
+    if (uuidAsString != null) this.uuidAsString = uuidAsString;
+    if (tcpKeepIdle != null) this.tcpKeepIdle = tcpKeepIdle;
+    if (tcpKeepCount != null) this.tcpKeepCount = tcpKeepCount;
+    if (tcpKeepInterval != null) this.tcpKeepInterval = tcpKeepInterval;
+    if (tcpAbortiveClose != null) this.tcpAbortiveClose = tcpAbortiveClose;
+    this.localSocketAddress = localSocketAddress;
+    if (socketTimeout != null) this.socketTimeout = socketTimeout;
+    if (allowMultiQueries != null) this.allowMultiQueries = allowMultiQueries;
+    if (allowLocalInfile != null) this.allowLocalInfile = allowLocalInfile;
+    if (useCompression != null) this.useCompression = useCompression;
+    if (blankTableNameMeta != null) this.blankTableNameMeta = blankTableNameMeta;
+    if (this.credentialType != null
+        && this.credentialType.mustUseSsl()
+        && (sslMode == null || SslMode.from(sslMode) == SslMode.DISABLE)) {
+      this.sslMode = SslMode.VERIFY_FULL;
+    } else {
+      this.sslMode = sslMode != null ? SslMode.from(sslMode) : SslMode.DISABLE;
+    }
+    if (transactionIsolation != null)
+      this.transactionIsolation = TransactionIsolation.from(transactionIsolation);
+    this.enabledSslCipherSuites = enabledSslCipherSuites;
+    this.sessionVariables = sessionVariables;
+    if (tinyInt1isBit != null) this.tinyInt1isBit = tinyInt1isBit;
+    if (transformedBitIsBoolean != null) this.transformedBitIsBoolean = transformedBitIsBoolean;
+    if (yearIsDateType != null) this.yearIsDateType = yearIsDateType;
+    this.timezone = timezone;
+    if (dumpQueriesOnException != null) this.dumpQueriesOnException = dumpQueriesOnException;
+    if (prepStmtCacheSize != null) this.prepStmtCacheSize = prepStmtCacheSize;
+    if (useAffectedRows != null) this.useAffectedRows = useAffectedRows;
+    if (useServerPrepStmts != null) this.useServerPrepStmts = useServerPrepStmts;
+    this.connectionAttributes = connectionAttributes;
+    if (useBulkStmts != null) this.useBulkStmts = useBulkStmts;
+    if (disablePipeline != null) this.disablePipeline = disablePipeline;
+    if (autocommit != null) this.autocommit = autocommit;
+    if (useMysqlMetadata != null) this.useMysqlMetadata = useMysqlMetadata;
+    if (createDatabaseIfNotExist != null) this.createDatabaseIfNotExist = createDatabaseIfNotExist;
+    if (includeInnodbStatusInDeadlockExceptions != null)
+      this.includeInnodbStatusInDeadlockExceptions = includeInnodbStatusInDeadlockExceptions;
+    if (includeThreadDumpInDeadlockExceptions != null)
+      this.includeThreadDumpInDeadlockExceptions = includeThreadDumpInDeadlockExceptions;
+    if (servicePrincipalName != null) this.servicePrincipalName = servicePrincipalName;
+    if (defaultFetchSize != null) this.defaultFetchSize = defaultFetchSize;
+    if (tlsSocketType != null) this.tlsSocketType = tlsSocketType;
+    if (maxQuerySizeToLog != null) this.maxQuerySizeToLog = maxQuerySizeToLog;
+    if (maxAllowedPacket != null) this.maxAllowedPacket = maxAllowedPacket;
+    if (retriesAllDown != null) this.retriesAllDown = retriesAllDown;
+    if (galeraAllowedState != null) this.galeraAllowedState = galeraAllowedState;
+    if (pool != null) this.pool = pool;
+    if (poolName != null) this.poolName = poolName;
+    if (maxPoolSize != null) this.maxPoolSize = maxPoolSize;
+    // if min pool size default to maximum pool size if not set
+    if (minPoolSize != null) {
+      this.minPoolSize = minPoolSize;
+    } else {
+      this.minPoolSize = this.maxPoolSize;
+    }
+
+    if (maxIdleTime != null) this.maxIdleTime = maxIdleTime;
+    if (registerJmxPool != null) this.registerJmxPool = registerJmxPool;
+    if (poolValidMinDelay != null) this.poolValidMinDelay = poolValidMinDelay;
+    if (useResetConnection != null) this.useResetConnection = useResetConnection;
+    if (serverRsaPublicKeyFile != null) this.serverRsaPublicKeyFile = serverRsaPublicKeyFile;
+    if (allowPublicKeyRetrieval != null) this.allowPublicKeyRetrieval = allowPublicKeyRetrieval;
+    if (useReadAheadInput != null) this.useReadAheadInput = useReadAheadInput;
+    if (cachePrepStmts != null) this.cachePrepStmts = cachePrepStmts;
+    if (transactionReplay != null) this.transactionReplay = transactionReplay;
+    if (transactionReplaySize != null) this.transactionReplaySize = transactionReplaySize;
+    if (geometryDefaultType != null) this.geometryDefaultType = geometryDefaultType;
+    if (restrictedAuth != null) this.restrictedAuth = restrictedAuth;
+    if (initSql != null) this.initSql = initSql;
+    if (serverSslCert != null) this.serverSslCert = serverSslCert;
+    if (keyStore != null) this.keyStore = keyStore;
+    if (keyStorePassword != null) this.keyStorePassword = keyStorePassword;
+    if (keyStoreType != null) this.keyStoreType = keyStoreType;
+
+    // *************************************************************
+    // host primary check
+    // *************************************************************
+    boolean first = true;
+    for (HostAddress host : addresses) {
+      boolean primary = haMode != HaMode.REPLICATION || first;
+      if (host.primary == null) {
+        host.primary = primary;
+      }
+      first = false;
+    }
+
+    // *************************************************************
+    // option value verification
+    // *************************************************************
+
+    // int fields must all be positive
+    Field[] fields = Configuration.class.getDeclaredFields();
+    try {
+      for (Field field : fields) {
+        if (field.getType().equals(int.class)) {
+          int val = field.getInt(this);
+          if (val < 0) {
+            throw new SQLException(
+                String.format("Value for %s must be >= 1 (value is %s)", field.getName(), val));
+          }
+        }
+      }
+    } catch (IllegalArgumentException | IllegalAccessException ie) {
+      // eat
+    }
+  }
+
+  /**
+   * 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("permitMysqlScheme")));
+  }
+
+  /**
+   * parse connection string
+   *
+   * @param url connection string
+   * @return configuration resulting object
+   * @throws SQLException if not supported driver or wrong connection string format.
+   */
+  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 (acceptsUrl(url)) {
+      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 > paramIndex) {
+        hostAddressesString = urlSecondPart.substring(0, dbIndex);
+        additionalParameters = urlSecondPart.substring(dbIndex);
+      } else {
+        hostAddressesString = urlSecondPart;
+        additionalParameters = null;
+      }
+
+      if (additionalParameters != null) {
+        int optIndex = additionalParameters.indexOf("?");
+        String database;
+        if (optIndex < 0) {
+          database = (additionalParameters.length() > 1) ? additionalParameters.substring(1) : null;
+        } else {
+          if (optIndex == 0) {
+            database = null;
+          } else {
+            database = additionalParameters.substring(1, optIndex);
+            if (database.isEmpty()) database = null;
+          }
+          String urlParameters = additionalParameters.substring(optIndex + 1);
+          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));
+              }
+            }
+          }
+        }
+        builder.database(database);
+      } 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) {
+    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 (final Object keyObj : properties.keySet()) {
+        String realKey =
+            OptionAliases.OPTIONS_ALIASES.get(keyObj.toString().toLowerCase(Locale.ROOT));
+        if (realKey == null) realKey = keyObj.toString();
+        final Object propertyValue = properties.get(keyObj);
+        if (propertyValue != null && realKey != null) {
+          boolean used = false;
+          for (Field field : Builder.class.getDeclaredFields()) {
+            if (realKey.toLowerCase(Locale.ROOT).equals(field.getName().toLowerCase(Locale.ROOT))) {
+              field.setAccessible(true);
+              used = true;
+
+              if (field.getGenericType().equals(String.class)
+                  && !propertyValue.toString().isEmpty()) {
+                field.set(builder, propertyValue);
+              } else if (field.getGenericType().equals(Boolean.class)) {
+                switch (propertyValue.toString().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'",
+                            keyObj, propertyValue));
+                }
+              } else if (field.getGenericType().equals(Integer.class)) {
+                try {
+                  final Integer value = Integer.parseInt(propertyValue.toString());
+                  field.set(builder, value);
+                } catch (NumberFormatException n) {
+                  throw new IllegalArgumentException(
+                      String.format(
+                          "Optional parameter %s must be Integer, was '%s'",
+                          keyObj, propertyValue));
+                }
+              }
+            }
+          }
+          if (!used) nonMappedOptions.put(realKey, propertyValue);
+        }
+      }
+
+      // for compatibility with 2.x
+      if (isSet("useSsl", nonMappedOptions) || isSet("useSSL", nonMappedOptions)) {
+        Properties deprecatedDesc = new Properties();
+        try (InputStream inputStream =
+            Driver.class.getClassLoader().getResourceAsStream("deprecated.properties")) {
+          deprecatedDesc.load(inputStream);
+        } catch (IOException io) {
+          // eat
+        }
+        logger.warn(deprecatedDesc.getProperty("useSsl"));
+        if (isSet("trustServerCertificate", nonMappedOptions)) {
+          builder.sslMode("trust");
+          logger.warn(deprecatedDesc.getProperty("trustServerCertificate"));
+        } else if (isSet("disableSslHostnameVerification", nonMappedOptions)) {
+          logger.warn(deprecatedDesc.getProperty("disableSslHostnameVerification"));
+          builder.sslMode("verify-ca");
+        } else {
+          builder.sslMode("verify-full");
+        }
+      }
+    } catch (IllegalAccessException | SecurityException s) {
+      throw new IllegalArgumentException("Unexpected error", s);
+    }
+    builder._nonMappedOptions = nonMappedOptions;
+  }
+
+  private static boolean isSet(String key, Properties nonMappedOptions) {
+    String value = nonMappedOptions.getProperty(key);
+    return value != null && (value.equals("1") || value.equals("true") || value.isEmpty());
+  }
+
+  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);
+    }
+  }
+
+  /**
+   * Clone configuration with another user/password
+   *
+   * @param username new username
+   * @param password new password
+   * @return new cloned configuration object
+   */
+  public Configuration clone(String username, String password) {
+    return new Configuration(
+        username != null && username.isEmpty() ? null : username,
+        password != null && password.isEmpty() ? null : password,
+        this.database,
+        this.addresses,
+        this.haMode,
+        this.nonMappedOptions,
+        this.timezone,
+        this.autocommit,
+        this.useMysqlMetadata,
+        this.createDatabaseIfNotExist,
+        this.transactionIsolation,
+        this.defaultFetchSize,
+        this.maxQuerySizeToLog,
+        this.maxAllowedPacket,
+        this.geometryDefaultType,
+        this.restrictedAuth,
+        this.initSql,
+        this.socketFactory,
+        this.connectTimeout,
+        this.pipe,
+        this.localSocket,
+        this.tcpKeepAlive,
+        this.uuidAsString,
+        this.tcpKeepIdle,
+        this.tcpKeepCount,
+        this.tcpKeepInterval,
+        this.tcpAbortiveClose,
+        this.localSocketAddress,
+        this.socketTimeout,
+        this.useReadAheadInput,
+        this.tlsSocketType,
+        this.sslMode,
+        this.serverSslCert,
+        this.keyStore,
+        this.keyStorePassword,
+        this.keyStoreType,
+        this.enabledSslCipherSuites,
+        this.enabledSslProtocolSuites,
+        this.allowMultiQueries,
+        this.allowLocalInfile,
+        this.useCompression,
+        this.useAffectedRows,
+        this.useBulkStmts,
+        this.disablePipeline,
+        this.cachePrepStmts,
+        this.prepStmtCacheSize,
+        this.useServerPrepStmts,
+        this.credentialType,
+        this.sessionVariables,
+        this.connectionAttributes,
+        this.servicePrincipalName,
+        this.blankTableNameMeta,
+        this.tinyInt1isBit,
+        this.transformedBitIsBoolean,
+        this.yearIsDateType,
+        this.dumpQueriesOnException,
+        this.includeInnodbStatusInDeadlockExceptions,
+        this.includeThreadDumpInDeadlockExceptions,
+        this.retriesAllDown,
+        this.galeraAllowedState,
+        this.transactionReplay,
+        this.transactionReplaySize,
+        this.pool,
+        this.poolName,
+        this.maxPoolSize,
+        this.minPoolSize,
+        this.maxIdleTime,
+        this.registerJmxPool,
+        this.poolValidMinDelay,
+        this.useResetConnection,
+        this.serverRsaPublicKeyFile,
+        this.allowPublicKeyRetrieval);
+  }
+
+  /**
+   * Connection default database
+   *
+   * @return database
+   */
+  public String database() {
+    return database;
+  }
+
+  /**
+   * addresses
+   *
+   * @return addresses
+   */
+  public List<HostAddress> addresses() {
+    return addresses;
+  }
+
+  /**
+   * High availability mode
+   *
+   * @return configuration HA mode
+   */
+  public HaMode haMode() {
+    return haMode;
+  }
+
+  /**
+   * credential plugin to use
+   *
+   * @return credential plugin to use, null of none
+   */
+  public CredentialPlugin credentialPlugin() {
+    return credentialType;
+  }
+
+  /**
+   * configuration user
+   *
+   * @return user
+   */
+  public String user() {
+    return user;
+  }
+
+  /**
+   * configuration password
+   *
+   * @return password
+   */
+  public String password() {
+    return password;
+  }
+
+  /**
+   * Configuration generated URL depending on current configuration option. Password will be hidden
+   * by "***"
+   *
+   * @return generated url
+   */
+  public String initialUrl() {
+    return initialUrl;
+  }
+
+  /**
+   * server ssl certificate (file path / certificat content)
+   *
+   * @return server ssl certificate
+   */
+  public String serverSslCert() {
+    return serverSslCert;
+  }
+
+  /**
+   * key store
+   *
+   * @return key store
+   */
+  public String keyStore() {
+    return keyStore;
+  }
+
+  /**
+   * key store password
+   *
+   * @return key store password
+   */
+  public String keyStorePassword() {
+    return keyStorePassword;
+  }
+
+  /**
+   * key store type (to replace default javax.net.ssl.keyStoreType system property)
+   *
+   * @return key store type
+   */
+  public String keyStoreType() {
+    return keyStoreType;
+  }
+
+  /**
+   * permitted ssl protocol list (comma separated)
+   *
+   * @return enabled ssl protocol list
+   */
+  public String enabledSslProtocolSuites() {
+    return enabledSslProtocolSuites;
+  }
+
+  /**
+   * Socket factory class name
+   *
+   * @return socket factory
+   */
+  public String socketFactory() {
+    return socketFactory;
+  }
+
+  /**
+   * socket connect timeout
+   *
+   * @return connect timeout
+   */
+  public int connectTimeout() {
+    return connectTimeout;
+  }
+
+  /**
+   * Set connect timeout
+   *
+   * @param connectTimeout timeout value
+   * @return current configuration
+   */
+  public Configuration connectTimeout(int connectTimeout) {
+    this.connectTimeout = connectTimeout;
+    return this;
+  }
+
+  /**
+   * Pipe path
+   *
+   * @return pipe value
+   */
+  public String pipe() {
+    return pipe;
+  }
+
+  /**
+   * local socket configuration
+   *
+   * @return local socket path
+   */
+  public String localSocket() {
+    return localSocket;
+  }
+
+  /**
+   * socket tcp keep alive
+   *
+   * @return socket tcp keep alive value
+   */
+  public boolean tcpKeepAlive() {
+    return tcpKeepAlive;
+  }
+
+  /**
+   * must uuid fields return as String and not java.util.UUID
+   *
+   * @return must UUID return as String and not uuid
+   */
+  public boolean uuidAsString() {
+    return uuidAsString;
+  }
+
+  /**
+   * socket tcp keep idle (java 11+ only)
+   *
+   * @return socket tcp keep idle
+   */
+  public int tcpKeepIdle() {
+    return tcpKeepIdle;
+  }
+
+  /**
+   * socket tcp keep count (java 11+ only)
+   *
+   * @return socket tcp keep count
+   */
+  public int tcpKeepCount() {
+    return tcpKeepCount;
+  }
+
+  /**
+   * socket tcp keep interval (java 11+ only)
+   *
+   * @return socket tcp keep interval
+   */
+  public int tcpKeepInterval() {
+    return tcpKeepInterval;
+  }
+
+  /**
+   * close using TCP abortive close (RST TCP packet, in place or FIN packet)
+   *
+   * @return close using TCP abortive close
+   */
+  public boolean tcpAbortiveClose() {
+    return tcpAbortiveClose;
+  }
+
+  /**
+   * local socket address path
+   *
+   * @return local socket address
+   */
+  public String localSocketAddress() {
+    return localSocketAddress;
+  }
+
+  /**
+   * socket timeout
+   *
+   * @return socket timeout
+   */
+  public int socketTimeout() {
+    return socketTimeout;
+  }
+
+  /**
+   * permit using multi queries command
+   *
+   * @return permit using multi queries command
+   */
+  public boolean allowMultiQueries() {
+    return allowMultiQueries;
+  }
+
+  /**
+   * permits LOAD LOCAL INFILE commands
+   *
+   * @return allow LOAD LOCAL INFILE
+   */
+  public boolean allowLocalInfile() {
+    return allowLocalInfile;
+  }
+
+  /**
+   * Enable compression if server has compression capability
+   *
+   * @return use compression
+   */
+  public boolean useCompression() {
+    return useCompression;
+  }
+
+  /**
+   * force returning blank table metadata (for old oracle compatibility)
+   *
+   * @return metadata table return blank
+   */
+  public boolean blankTableNameMeta() {
+    return blankTableNameMeta;
+  }
+
+  /**
+   * SSl mode
+   *
+   * @return ssl mode
+   */
+  public SslMode sslMode() {
+    return sslMode;
+  }
+
+  /**
+   * Default transaction isolation
+   *
+   * @return default transaction isolation.
+   */
+  public TransactionIsolation transactionIsolation() {
+    return transactionIsolation;
+  }
+
+  /**
+   * autorized cipher list.
+   *
+   * @return list of permitted ciphers
+   */
+  public String enabledSslCipherSuites() {
+    return enabledSslCipherSuites;
+  }
+
+  /**
+   * coma separated Session variable list
+   *
+   * @return session variable
+   */
+  public String sessionVariables() {
+    return sessionVariables;
+  }
+
+  /**
+   * Must tinyint(1) be considered as Bit
+   *
+   * @return true if tinyint(1) must be considered as Bit
+   */
+  public boolean tinyInt1isBit() {
+    return tinyInt1isBit;
+  }
+
+  /**
+   * Must tinyint(1) be considered as Boolean or Bit
+   *
+   * @return true if tinyint(1) must be considered as Boolean
+   */
+  public boolean transformedBitIsBoolean() {
+    return transformedBitIsBoolean;
+  }
+
+  /**
+   * Must year be return by default as Date in result-set
+   *
+   * @return year is Date type
+   */
+  public boolean yearIsDateType() {
+    return yearIsDateType;
+  }
+
+  /**
+   * Set timezone
+   *
+   * @return timezone
+   */
+  public String timezone() {
+    return timezone;
+  }
+
+  /**
+   * Must query by logged on exception.
+   *
+   * @return dump queries on exception
+   */
+  public boolean dumpQueriesOnException() {
+    return dumpQueriesOnException;
+  }
+
+  /**
+   * Prepare statement cache size.
+   *
+   * @return Prepare statement cache size
+   */
+  public int prepStmtCacheSize() {
+    return prepStmtCacheSize;
+  }
+
+  /**
+   * Use affected row
+   *
+   * @return use affected rows
+   */
+  public boolean useAffectedRows() {
+    return useAffectedRows;
+  }
+
+  /**
+   * Use server prepared statement. IF false, using client prepared statement.
+   *
+   * @return use server prepared statement
+   */
+  public boolean useServerPrepStmts() {
+    return useServerPrepStmts;
+  }
+
+  /**
+   * Connections attributes
+   *
+   * @return connection meta informations
+   */
+  public String connectionAttributes() {
+    return connectionAttributes;
+  }
+
+  /**
+   * Use server COM_STMT_BULK for batching.
+   *
+   * @return use server bulk command.
+   */
+  public boolean useBulkStmts() {
+    return useBulkStmts;
+  }
+
+  /**
+   * Disable pipeline.
+   *
+   * @return is pipeline disabled.
+   */
+  public boolean disablePipeline() {
+    return disablePipeline;
+  }
+
+  /**
+   * Force session autocommit on connection creation
+   *
+   * @return autocommit forced value
+   */
+  public Boolean autocommit() {
+    return autocommit;
+  }
+
+  /**
+   * Force returning MySQL metadata information
+   *
+   * @return force returning MySQL in metadata
+   */
+  public boolean useMysqlMetadata() {
+    return useMysqlMetadata;
+  }
+
+  /**
+   * create database if not exist
+   *
+   * @return create database if not exist
+   */
+  public boolean createDatabaseIfNotExist() {
+    return createDatabaseIfNotExist;
+  }
+
+  /**
+   * On deadlock exception, must driver execute additional commands to show innodb status in error
+   * description.
+   *
+   * @return includeInnodbStatusInDeadlockExceptions
+   */
+  public boolean includeInnodbStatusInDeadlockExceptions() {
+    return includeInnodbStatusInDeadlockExceptions;
+  }
+
+  /**
+   * On deadlock exception, must driver display threads information on error description.
+   *
+   * @return include Thread Dump In Deadlock Exceptions
+   */
+  public boolean includeThreadDumpInDeadlockExceptions() {
+    return includeThreadDumpInDeadlockExceptions;
+  }
+
+  /**
+   * Service principal name (GSSAPI option)
+   *
+   * @return service principal name
+   */
+  public String servicePrincipalName() {
+    return servicePrincipalName;
+  }
+
+  /**
+   * result-set streaming default fetch size
+   *
+   * @return Default fetch size.
+   */
+  public int defaultFetchSize() {
+    return defaultFetchSize;
+  }
+
+  /**
+   * non standard options
+   *
+   * @return non standard options
+   */
+  public Properties nonMappedOptions() {
+    return nonMappedOptions;
+  }
+
+  /**
+   * TLS socket type
+   *
+   * @return TLS socket type
+   */
+  public String tlsSocketType() {
+    return tlsSocketType;
+  }
+
+  /**
+   * query maximum size to log (query will be truncated of more than this limit)
+   *
+   * @return max query log size
+   */
+  public int maxQuerySizeToLog() {
+    return maxQuerySizeToLog;
+  }
+
+  /**
+   * max_allowed_packet value to avoid sending packet with non supported size, droping the
+   * connection without reason.
+   *
+   * @return max_allowed_packet value
+   */
+  public Integer maxAllowedPacket() {
+    return maxAllowedPacket;
+  }
+
+  /**
+   * retry the maximum retry number of attempts to reconnect after a failover.
+   *
+   * @return the maximum retry number of attempts to reconnect after a failover.
+   */
+  public int retriesAllDown() {
+    return retriesAllDown;
+  }
+
+  /**
+   * Galera comma separated allowed state
+   *
+   * @return galera allowed state
+   */
+  public String galeraAllowedState() {
+    return galeraAllowedState;
+  }
+
+  /**
+   * Create pool
+   *
+   * @return create pool if don't exists
+   */
+  public boolean pool() {
+    return pool;
+  }
+
+  /**
+   * pool name
+   *
+   * @return pool name.
+   */
+  public String poolName() {
+    return poolName;
+  }
+
+  /**
+   * max pool size
+   *
+   * @return maximum pool size
+   */
+  public int maxPoolSize() {
+    return maxPoolSize;
+  }
+
+  /**
+   * Minimum pool size
+   *
+   * @return minimum pool size
+   */
+  public int minPoolSize() {
+    return minPoolSize;
+  }
+
+  /**
+   * Max idle time
+   *
+   * @return pool max idle time.
+   */
+  public int maxIdleTime() {
+    return maxIdleTime;
+  }
+
+  /**
+   * register pool information to JMX
+   *
+   * @return register pool to JMX
+   */
+  public boolean registerJmxPool() {
+    return registerJmxPool;
+  }
+
+  /**
+   * Pool mininum validation delay.
+   *
+   * @return pool validation delay
+   */
+  public int poolValidMinDelay() {
+    return poolValidMinDelay;
+  }
+
+  /**
+   * Must connection returned to pool be RESET
+   *
+   * @return use RESET on connection
+   */
+  public boolean useResetConnection() {
+    return useResetConnection;
+  }
+
+  /**
+   * Server RSA public key file for caching_sha2_password authentication
+   *
+   * @return server key file
+   */
+  public String serverRsaPublicKeyFile() {
+    return serverRsaPublicKeyFile;
+  }
+
+  /**
+   * permit mysql authentication to retrieve server certificate
+   *
+   * @return is driver allowed to retrieve server certificate from server
+   */
+  public boolean allowPublicKeyRetrieval() {
+    return allowPublicKeyRetrieval;
+  }
+
+  /**
+   * Read all data from socket in advance
+   *
+   * @return use read ahead buffer implementation
+   */
+  public boolean useReadAheadInput() {
+    return useReadAheadInput;
+  }
+
+  /**
+   * Cache prepared statement result.
+   *
+   * @return cache prepare results
+   */
+  public boolean cachePrepStmts() {
+    return cachePrepStmts;
+  }
+
+  /**
+   * implements transaction replay failover
+   *
+   * @return true if transaction must be replayed on failover.
+   */
+  public boolean transactionReplay() {
+    return transactionReplay;
+  }
+
+  /**
+   * transaction replay maximum number of saved command.
+   *
+   * @return transaction replay buffer size.
+   */
+  public int transactionReplaySize() {
+    return transactionReplaySize;
+  }
+
+  /**
+   * geometry default decoding implementation
+   *
+   * @return geometry default type
+   */
+  public String geometryDefaultType() {
+    return geometryDefaultType;
+  }
+
+  /**
+   * Restrict authentication plugin to comma separated plugin list
+   *
+   * @return authorized authentication list
+   */
+  public String restrictedAuth() {
+    return restrictedAuth;
+  }
+
+  /**
+   * Execute initial command when connection is established
+   *
+   * @return initial SQL command
+   */
+  public String initSql() {
+    return initSql;
+  }
+
+  /**
+   * datatype Encoder/decoder list
+   *
+   * @return codec list
+   */
+  public Codec<?>[] codecs() {
+    return codecs;
+  }
+
+  /**
+   * 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;
+
+    if (password == null) {
+      return initialUrl.equals(that.initialUrl) && that.password == null;
+    }
+    return initialUrl.equals(that.initialUrl) && password.equals(that.password);
+  }
+
+  /**
+   * Generate initialURL property
+   *
+   * @param conf current configuration
+   * @return initialUrl value.
+   */
+  protected static String buildUrl(Configuration conf) {
+    Configuration defaultConf = new Configuration();
+    StringBuilder sb = new StringBuilder();
+    sb.append("jdbc:mariadb:");
+    if (conf.haMode != HaMode.NONE) {
+      sb.append(conf.haMode.toString().toLowerCase(Locale.ROOT)).append(":");
+    }
+    sb.append("//");
+    for (int i = 0; i < conf.addresses.size(); i++) {
+      HostAddress hostAddress = conf.addresses.get(i);
+      if (i > 0) {
+        sb.append(",");
+      }
+      if ((conf.haMode == HaMode.NONE && hostAddress.primary)
+          || (conf.haMode == HaMode.REPLICATION
+              && ((i == 0 && hostAddress.primary) || (i != 0 && !hostAddress.primary)))) {
+        sb.append(hostAddress.host);
+        if (hostAddress.port != 3306) sb.append(":").append(hostAddress.port);
+      } else {
+        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 (conf.database != null) {
+      sb.append(conf.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 = Configuration.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(conf);
+
+        if (obj != null && (!(obj instanceof Properties) || ((Properties) obj).size() > 0)) {
+
+          if ("password".equals(field.getName())) {
+            sb.append(first ? '?' : '&');
+            first = false;
+            sb.append(field.getName()).append('=');
+            sb.append("***");
+            continue;
+          }
+
+          if (field.getType().equals(String.class)) {
+            sb.append(first ? '?' : '&');
+            first = false;
+            sb.append(field.getName()).append('=');
+            sb.append((String) obj);
+          } else if (field.getType().equals(boolean.class)) {
+            boolean defaultValue = field.getBoolean(defaultConf);
+            if (!obj.equals(defaultValue)) {
+              sb.append(first ? '?' : '&');
+              first = false;
+              sb.append(field.getName()).append('=');
+              sb.append(obj);
+            }
+          } else if (field.getType().equals(int.class)) {
+            try {
+              int defaultValue = field.getInt(defaultConf);
+              if (!obj.equals(defaultValue)) {
+                sb.append(first ? '?' : '&');
+                sb.append(field.getName()).append('=').append(obj);
+                first = false;
+              }
+            } catch (IllegalAccessException n) {
+              // eat
+            }
+          } else if (field.getType().equals(Properties.class)) {
+            sb.append(first ? '?' : '&');
+            first = false;
+            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));
+            }
+          } else if (field.getType().equals(CredentialPlugin.class)) {
+            Object defaultValue = field.get(defaultConf);
+            if (!obj.equals(defaultValue)) {
+              sb.append(first ? '?' : '&');
+              first = false;
+              sb.append(field.getName()).append('=');
+              sb.append(((CredentialPlugin) obj).type());
+            }
+          } else {
+            Object defaultValue = field.get(defaultConf);
+            if (!obj.equals(defaultValue)) {
+              sb.append(first ? '?' : '&');
+              first = false;
+              sb.append(field.getName()).append('=');
+              sb.append(obj);
+            }
+          }
+        }
+      }
+
+    } catch (IllegalAccessException n) {
+      n.printStackTrace();
+    } catch (SecurityException s) {
+      // only for jws, so never thrown
+      throw new IllegalArgumentException("Security too restrictive : " + s.getMessage());
+    }
+    conf.loadCodecs();
+    return sb.toString();
+  }
+
+  @SuppressWarnings("rawtypes")
+  private void loadCodecs() {
+    ServiceLoader<Codec> loader =
+        ServiceLoader.load(Codec.class, Configuration.class.getClassLoader());
+    List<Codec<?>> result = new ArrayList<>();
+    loader.iterator().forEachRemaining(result::add);
+    codecs = result.toArray(new Codec<?>[0]);
+  }
+
+  @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<HostAddress> _addresses = new ArrayList<>();
+
+    // standard options
+    private String user;
+    private String password;
+    private String database;
+
+    // various
+    private String timezone;
+    private Boolean autocommit;
+    private Boolean useMysqlMetadata;
+    private Boolean createDatabaseIfNotExist;
+    private Integer defaultFetchSize;
+    private Integer maxQuerySizeToLog;
+    private Integer maxAllowedPacket;
+    private String geometryDefaultType;
+    private String restrictedAuth;
+    private String initSql;
+    private String transactionIsolation;
+
+    // socket
+    private String socketFactory;
+    private Integer connectTimeout;
+    private String pipe;
+    private String localSocket;
+    private Boolean tcpKeepAlive;
+    private Boolean uuidAsString;
+    private Integer tcpKeepIdle;
+    private Integer tcpKeepCount;
+    private Integer tcpKeepInterval;
+    private Boolean tcpAbortiveClose;
+    private String localSocketAddress;
+    private Integer socketTimeout;
+    private Boolean useReadAheadInput;
+    private String tlsSocketType;
+
+    // SSL
+    private String sslMode;
+    private String serverSslCert;
+    private String keyStore;
+    private String keyStorePassword;
+    private String keyStoreType;
+    private String enabledSslCipherSuites;
+    private String enabledSslProtocolSuites;
+
+    // protocol
+    private Boolean allowMultiQueries;
+    private Boolean allowLocalInfile;
+    private Boolean useCompression;
+    private Boolean useAffectedRows;
+    private Boolean useBulkStmts;
+    private Boolean disablePipeline;
+    // prepare
+    private Boolean cachePrepStmts;
+    private Integer prepStmtCacheSize;
+    private Boolean useServerPrepStmts;
+
+    // authentication
+    private String credentialType;
+    private String sessionVariables;
+    private String connectionAttributes;
+    private String servicePrincipalName;
+
+    // meta
+    private Boolean blankTableNameMeta;
+    private Boolean tinyInt1isBit;
+
+    private Boolean transformedBitIsBoolean;
+    private Boolean yearIsDateType;
+    private Boolean dumpQueriesOnException;
+    private Boolean includeInnodbStatusInDeadlockExceptions;
+    private Boolean includeThreadDumpInDeadlockExceptions;
+
+    // HA options
+    private Integer retriesAllDown;
+    private String galeraAllowedState;
+    private Boolean transactionReplay;
+    private Integer transactionReplaySize;
+
+    // Pool options
+    private Boolean pool;
+    private String poolName;
+    private Integer maxPoolSize;
+    private Integer minPoolSize;
+    private Integer maxIdleTime;
+    private Boolean registerJmxPool;
+    private Integer poolValidMinDelay;
+    private Boolean useResetConnection;
+
+    // MySQL sha authentication
+    private String serverRsaPublicKeyFile;
+    private Boolean allowPublicKeyRetrieval;
+
+    /**
+     * set user to authenticate to server
+     *
+     * @param user user
+     * @return this {@link Builder}
+     */
+    public Builder user(String user) {
+      this.user = nullOrEmpty(user);
+      return this;
+    }
+
+    /**
+     * Server SSL certificate (path or file content)
+     *
+     * @param serverSslCert set Server SSL certificate (path or file content)
+     * @return this {@link Builder}
+     */
+    public Builder serverSslCert(String serverSslCert) {
+      this.serverSslCert = nullOrEmpty(serverSslCert);
+      return this;
+    }
+
+    /**
+     * 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)
+     *
+     * @param keyStore client store certificates
+     * @return this {@link Builder}
+     */
+    public Builder keyStore(String keyStore) {
+      this.keyStore = nullOrEmpty(keyStore);
+      return this;
+    }
+
+    /**
+     * Client keystore password
+     *
+     * @param keyStorePassword client store password
+     * @return this {@link Builder}
+     */
+    public Builder keyStorePassword(String keyStorePassword) {
+      this.keyStorePassword = nullOrEmpty(keyStorePassword);
+      return this;
+    }
+
+    /**
+     * Key store type
+     *
+     * @param keyStoreType key store type
+     * @return this {@link Builder}
+     */
+    public Builder keyStoreType(String keyStoreType) {
+      this.keyStoreType = nullOrEmpty(keyStoreType);
+      return this;
+    }
+
+    /**
+     * User password
+     *
+     * @param password password
+     * @return this {@link Builder}
+     */
+    public Builder password(String password) {
+      this.password = nullOrEmpty(password);
+      return this;
+    }
+
+    /**
+     * Set ssl protocol list to user (comma separated)
+     *
+     * @param enabledSslProtocolSuites set possible SSL(TLS) protocol to use
+     * @return this {@link Builder}
+     */
+    public Builder enabledSslProtocolSuites(String enabledSslProtocolSuites) {
+      this.enabledSslProtocolSuites = nullOrEmpty(enabledSslProtocolSuites);
+      return this;
+    }
+
+    /**
+     * Set default database
+     *
+     * @param database database
+     * @return this {@link Builder}
+     */
+    public Builder database(String database) {
+      this.database = database;
+      return this;
+    }
+
+    /**
+     * Set failover High-availability mode
+     *
+     * @param haMode High-availability mode
+     * @return this {@link Builder}
+     */
+    public Builder haMode(HaMode haMode) {
+      this._haMode = haMode;
+      return this;
+    }
+
+    /**
+     * Add Host to possible addresses to connect
+     *
+     * @param host hostname or IP
+     * @param port port
+     * @return this {@link Builder}
+     */
+    public Builder addHost(String host, int port) {
+      this._addresses.add(HostAddress.from(nullOrEmpty(host), port));
+      return this;
+    }
+
+    /**
+     * Add Host to possible addresses to connect
+     *
+     * @param host hostname or IP
+     * @param port port
+     * @param master is master or replica
+     * @return this {@link Builder}
+     */
+    public Builder addHost(String host, int port, boolean master) {
+      this._addresses.add(HostAddress.from(nullOrEmpty(host), port, master));
+      return this;
+    }
+
+    /**
+     * add host addresses
+     *
+     * @param hostAddress host addresses
+     * @return this {@link Builder}
+     */
+    public Builder addresses(HostAddress... hostAddress) {
+      this._addresses = new ArrayList<>();
+      this._addresses.addAll(Arrays.asList(hostAddress));
+      return this;
+    }
+
+    /**
+     * Socket factory
+     *
+     * @param socketFactory socket factory
+     * @return this {@link Builder}
+     */
+    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 = nullOrEmpty(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 = nullOrEmpty(localSocket);
+      return this;
+    }
+
+    /**
+     * Indicate if TCP keep-alive must be enabled.
+     *
+     * @param tcpKeepAlive value
+     * @return this {@link Builder}
+     */
+    public Builder tcpKeepAlive(Boolean tcpKeepAlive) {
+      this.tcpKeepAlive = tcpKeepAlive;
+      return this;
+    }
+
+    /**
+     * Indicate if UUID fields must returns as String
+     *
+     * @param uuidAsString value
+     * @return this {@link Builder}
+     */
+    public Builder uuidAsString(Boolean uuidAsString) {
+      this.uuidAsString = uuidAsString;
+      return this;
+    }
+
+    /**
+     * Indicate TCP keep-idle value (for java 11+ only).
+     *
+     * @param tcpKeepIdle value
+     * @return this {@link Builder}
+     */
+    public Builder tcpKeepIdle(Integer tcpKeepIdle) {
+      this.tcpKeepIdle = tcpKeepIdle;
+      return this;
+    }
+
+    /**
+     * Indicate TCP keep-count value (for java 11+ only).
+     *
+     * @param tcpKeepCount value
+     * @return this {@link Builder}
+     */
+    public Builder tcpKeepCount(Integer tcpKeepCount) {
+      this.tcpKeepCount = tcpKeepCount;
+      return this;
+    }
+
+    /**
+     * Indicate TCP keep-interval value (for java 11+ only).
+     *
+     * @param tcpKeepInterval value
+     * @return this {@link Builder}
+     */
+    public Builder tcpKeepInterval(Integer tcpKeepInterval) {
+      this.tcpKeepInterval = tcpKeepInterval;
+      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 what default Object type Geometry a resultset.getObject must return. possibility :
+     *
+     * <ul>
+     *   <li>null or empty is WKB byte array
+     *   <li>'default' will return org.mariadb.mariadb.jdbc.type Object
+     * </ul>
+     *
+     * In the future JTS might be implemented
+     *
+     * @param geometryDefault value
+     * @return this {@link Builder}
+     */
+    public Builder geometryDefaultType(String geometryDefault) {
+      this.geometryDefaultType = nullOrEmpty(geometryDefault);
+      return this;
+    }
+
+    /**
+     * restrict authentication method to secure list. Default "default".
+     *
+     * @param restrictedAuth use authentication plugin list
+     * @return this {@link Builder}
+     */
+    public Builder restrictedAuth(String restrictedAuth) {
+      this.restrictedAuth = restrictedAuth;
+      return this;
+    }
+
+    /**
+     * permit to execute an SQL command on connection creation
+     *
+     * @param initSql initial SQL command
+     * @return this {@link Builder}
+     */
+    public Builder initSql(String initSql) {
+      this.initSql = initSql;
+      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 = nullOrEmpty(localSocketAddress);
+      return this;
+    }
+
+    /**
+     * Indicate the network socket timeout (SO_TIMEOUT) in milliseconds. Value of 0 disables this
+     * timeout.
+     *
+     * <p>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)".
+     *
+     * <p>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;
+    }
+
+    /**
+     * Indicate if LOAD DATA LOCAL INFILE are permitted. This will disable all pipelining
+     * implementation.
+     *
+     * @param allowLocalInfile indicate if permit LOAD DATA LOCAL INFILE commands
+     * @return this {@link Builder}
+     */
+    public Builder allowLocalInfile(Boolean allowLocalInfile) {
+      this.allowLocalInfile = allowLocalInfile;
+      return this;
+    }
+
+    /**
+     * Indicate to compress 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;
+    }
+
+    /**
+     * Set blank table name for metadata (old oracle compatibility)
+     *
+     * @param blankTableNameMeta use blank table name
+     * @return this {@link Builder}
+     */
+    public Builder blankTableNameMeta(Boolean blankTableNameMeta) {
+      this.blankTableNameMeta = blankTableNameMeta;
+      return this;
+    }
+
+    /**
+     * set credential plugin type
+     *
+     * @param credentialType credential plugin type
+     * @return this {@link Builder}
+     */
+    public Builder credentialType(String credentialType) {
+      this.credentialType = nullOrEmpty(credentialType);
+      return this;
+    }
+
+    /**
+     * Set ssl model
+     *
+     * @param sslMode ssl requirement
+     * @return this {@link Builder}
+     */
+    public Builder sslMode(String sslMode) {
+      this.sslMode = sslMode;
+      return this;
+    }
+
+    /**
+     * force default transaction isolation, not using server default
+     *
+     * @param transactionIsolation indicate default transaction isolation
+     * @return this {@link Builder}
+     */
+    public Builder transactionIsolation(String transactionIsolation) {
+      this.transactionIsolation = nullOrEmpty(transactionIsolation);
+      return this;
+    }
+
+    /**
+     * set possible cipher list (comma separated), not using default java cipher list
+     *
+     * @param enabledSslCipherSuites ssl cipher list
+     * @return this {@link Builder}
+     */
+    public Builder enabledSslCipherSuites(String enabledSslCipherSuites) {
+      this.enabledSslCipherSuites = nullOrEmpty(enabledSslCipherSuites);
+      return this;
+    }
+
+    /**
+     * set connection session variables (comma separated)
+     *
+     * @param sessionVariables session variable list
+     * @return this {@link Builder}
+     */
+    public Builder sessionVariables(String sessionVariables) {
+      this.sessionVariables = nullOrEmpty(sessionVariables);
+      return this;
+    }
+
+    /**
+     * TinyInt(1) to be considered as bit
+     *
+     * @param tinyInt1isBit Indicate if Tinyint(1) to be considered as bit
+     * @return this {@link Builder}
+     */
+    public Builder tinyInt1isBit(Boolean tinyInt1isBit) {
+      this.tinyInt1isBit = tinyInt1isBit;
+      return this;
+    }
+
+    /**
+     * TinyInt(1) to be considered as boolean
+     *
+     * @param transformedBitIsBoolean Indicate if Tinyint(1) to be considered as boolean
+     * @return this {@link Builder}
+     */
+    public Builder transformedBitIsBoolean(Boolean transformedBitIsBoolean) {
+      this.transformedBitIsBoolean = transformedBitIsBoolean;
+      return this;
+    }
+
+    /**
+     * Year datatype to be considered as date
+     *
+     * @param yearIsDateType indicate if Year must be considered as Date
+     * @return this {@link Builder}
+     */
+    public Builder yearIsDateType(Boolean yearIsDateType) {
+      this.yearIsDateType = yearIsDateType;
+      return this;
+    }
+
+    /**
+     * Force timezone to use this timezone, not default java one
+     *
+     * @param timezone default timezone
+     * @return this {@link Builder}
+     */
+    public Builder timezone(String timezone) {
+      this.timezone = nullOrEmpty(timezone);
+      return this;
+    }
+
+    /**
+     * Must queries be dump on exception stracktrace.
+     *
+     * @param dumpQueriesOnException must queries be dump on exception
+     * @return this {@link Builder}
+     */
+    public Builder dumpQueriesOnException(Boolean dumpQueriesOnException) {
+      this.dumpQueriesOnException = dumpQueriesOnException;
+      return this;
+    }
+
+    /**
+     * If using server prepared statement, set LRU prepare cache size
+     *
+     * @param prepStmtCacheSize prepare cache size
+     * @return this {@link Builder}
+     */
+    public Builder prepStmtCacheSize(Integer prepStmtCacheSize) {
+      this.prepStmtCacheSize = prepStmtCacheSize;
+      return this;
+    }
+
+    /**
+     * Indicate server to return affected rows in place of found rows. This impact the return number
+     * of rows affected by update
+     *
+     * @param useAffectedRows Indicate to user affected rows in place of found rows
+     * @return this {@link Builder}
+     */
+    public Builder useAffectedRows(Boolean useAffectedRows) {
+      this.useAffectedRows = useAffectedRows;
+      return this;
+    }
+
+    /**
+     * Indicate to use Client or Server prepared statement
+     *
+     * @param useServerPrepStmts use Server prepared statement
+     * @return this {@link Builder}
+     */
+    public Builder useServerPrepStmts(Boolean useServerPrepStmts) {
+      this.useServerPrepStmts = useServerPrepStmts;
+      return this;
+    }
+
+    /**
+     * Additional connection attributes to identify connection
+     *
+     * @param connectionAttributes additional connection attributes
+     * @return this {@link Builder}
+     */
+    public Builder connectionAttributes(String connectionAttributes) {
+      this.connectionAttributes = nullOrEmpty(connectionAttributes);
+      return this;
+    }
+
+    /**
+     * Use server dedicated bulk batch command
+     *
+     * @param useBulkStmts use server bulk batch command.
+     * @return this {@link Builder}
+     */
+    public Builder useBulkStmts(Boolean useBulkStmts) {
+      this.useBulkStmts = useBulkStmts;
+      return this;
+    }
+
+    /**
+     * Disable pipeline
+     *
+     * @param disablePipeline disable pipeline.
+     * @return this {@link Builder}
+     */
+    public Builder disablePipeline(Boolean disablePipeline) {
+      this.disablePipeline = disablePipeline;
+      return this;
+    }
+
+    /**
+     * Permit to force autocommit connection value
+     *
+     * @param autocommit autocommit value
+     * @return this {@link Builder}
+     */
+    public Builder autocommit(Boolean autocommit) {
+      this.autocommit = autocommit;
+      return this;
+    }
+
+    /**
+     * Permit indicating to force DatabaseMetadata.getDatabaseProductName() to return `MySQL` as
+     * database type, not real database type
+     *
+     * @param useMysqlMetadata force DatabaseMetadata.getDatabaseProductName() to return `MySQL`
+     * @return this {@link Builder}
+     */
+    public Builder useMysqlMetadata(Boolean useMysqlMetadata) {
+      this.useMysqlMetadata = useMysqlMetadata;
+      return this;
+    }
+
+    /**
+     * Create database if not exist. This is mainly for test, since does require an additional query
+     * after connection
+     *
+     * @param createDatabaseIfNotExist must driver create database if doesn't exist
+     * @return this {@link Builder}
+     */
+    public Builder createDatabaseIfNotExist(Boolean createDatabaseIfNotExist) {
+      this.createDatabaseIfNotExist = createDatabaseIfNotExist;
+      return this;
+    }
+
+    /**
+     * On dead-lock exception must add innodb status in exception error message. If enabled, an
+     * additional command will be done to retrieve innodb status when dead-lock occurs.
+     *
+     * @param includeInnodbStatusInDeadlockExceptions Must dead-lock exception must add innodb
+     *     status in exception error message
+     * @return this {@link Builder}
+     */
+    public Builder includeInnodbStatusInDeadlockExceptions(
+        Boolean includeInnodbStatusInDeadlockExceptions) {
+      this.includeInnodbStatusInDeadlockExceptions = includeInnodbStatusInDeadlockExceptions;
+      return this;
+    }
+
+    /**
+     * Dead-lock error will contain threads information
+     *
+     * @param includeThreadDumpInDeadlockExceptions must dead-lock error contain treads informations
+     * @return this {@link Builder}
+     */
+    public Builder includeThreadDumpInDeadlockExceptions(
+        Boolean includeThreadDumpInDeadlockExceptions) {
+      this.includeThreadDumpInDeadlockExceptions = includeThreadDumpInDeadlockExceptions;
+      return this;
+    }
+
+    /**
+     * set service principal name (GSSAPI)
+     *
+     * @param servicePrincipalName service principal name (GSSAPI)
+     * @return this {@link Builder}
+     */
+    public Builder servicePrincipalName(String servicePrincipalName) {
+      this.servicePrincipalName = nullOrEmpty(servicePrincipalName);
+      return this;
+    }
+
+    /**
+     * Set default fetch size
+     *
+     * @param defaultFetchSize default fetch size
+     * @return this {@link Builder}
+     */
+    public Builder defaultFetchSize(Integer defaultFetchSize) {
+      this.defaultFetchSize = defaultFetchSize;
+      return this;
+    }
+
+    /**
+     * Permit to defined default tls plugin type
+     *
+     * @param tlsSocketType default tls socket plugin to use
+     * @return this {@link Builder}
+     */
+    public Builder tlsSocketType(String tlsSocketType) {
+      this.tlsSocketType = nullOrEmpty(tlsSocketType);
+      return this;
+    }
+
+    /**
+     * Set the log size limit for query
+     *
+     * @param maxQuerySizeToLog set query size limit
+     * @return this {@link Builder}
+     */
+    public Builder maxQuerySizeToLog(Integer maxQuerySizeToLog) {
+      this.maxQuerySizeToLog = maxQuerySizeToLog;
+      return this;
+    }
+
+    /**
+     * Indicate to driver server max_allowed_packet. This permit to driver to avoid sending commands
+     * too big, that would have make server to drop connection
+     *
+     * @param maxAllowedPacket indicate server max_allowed_packet value
+     * @return this {@link Builder}
+     */
+    public Builder maxAllowedPacket(Integer maxAllowedPacket) {
+      this.maxAllowedPacket = maxAllowedPacket;
+      return this;
+    }
+
+    /**
+     * When failover occurs, how many connection attempt before throwing error when reconnecting
+     *
+     * @param retriesAllDown number of attemps to reconnect
+     * @return this {@link Builder}
+     */
+    public Builder retriesAllDown(Integer retriesAllDown) {
+      this.retriesAllDown = retriesAllDown;
+      return this;
+    }
+
+    /**
+     * Indicate galera allowed state (comma separated), permitting to validate if galera node is
+     * synchronized
+     *
+     * @param galeraAllowedState galera allowed state
+     * @return this {@link Builder}
+     */
+    public Builder galeraAllowedState(String galeraAllowedState) {
+      this.galeraAllowedState = nullOrEmpty(galeraAllowedState);
+      return this;
+    }
+
+    /**
+     * Create pool if not existing, or get a connection for the pool associate with this connection
+     * string if existing.
+     *
+     * @param pool use pool
+     * @return this {@link Builder}
+     */
+    public Builder pool(Boolean pool) {
+      this.pool = pool;
+      return this;
+    }
+
+    /**
+     * set pool name
+     *
+     * @param poolName pool name
+     * @return this {@link Builder}
+     */
+    public Builder poolName(String poolName) {
+      this.poolName = nullOrEmpty(poolName);
+      return this;
+    }
+
+    /**
+     * Set the limit number of connection in pool.
+     *
+     * @param maxPoolSize maximum connection size in pool.
+     * @return this {@link Builder}
+     */
+    public Builder maxPoolSize(Integer maxPoolSize) {
+      this.maxPoolSize = maxPoolSize;
+      return this;
+    }
+
+    /**
+     * Minimum pool size.
+     *
+     * @param minPoolSize minimum pool size
+     * @return this {@link Builder}
+     */
+    public Builder minPoolSize(Integer minPoolSize) {
+      this.minPoolSize = minPoolSize;
+      return this;
+    }
+
+    /**
+     * Set the maximum idle time of a connection indicating that connection must be released
+     *
+     * @param maxIdleTime maximum idle time of a connection in pool
+     * @return this {@link Builder}
+     */
+    public Builder maxIdleTime(Integer maxIdleTime) {
+      this.maxIdleTime = maxIdleTime;
+      return this;
+    }
+
+    /**
+     * Must pool register JMX information
+     *
+     * @param registerJmxPool register pool to JMX
+     * @return this {@link Builder}
+     */
+    public Builder registerJmxPool(Boolean registerJmxPool) {
+      this.registerJmxPool = registerJmxPool;
+      return this;
+    }
+
+    /**
+     * Pool will validate connection before giving it. This amount of time indicate that recently
+     * use connection can skip validation 0 means connection will be validated each time (even is
+     * just used)
+     *
+     * @param poolValidMinDelay time limit indicating that connection in pool must be validated
+     * @return this {@link Builder}
+     */
+    public Builder poolValidMinDelay(Integer poolValidMinDelay) {
+      this.poolValidMinDelay = poolValidMinDelay;
+      return this;
+    }
+
+    /**
+     * Indicate that connection returned to pool must be RESETed like having proper connection
+     * state.
+     *
+     * @param useResetConnection use reset connection when returning connection to pool.
+     * @return this {@link Builder}
+     */
+    public Builder useResetConnection(Boolean useResetConnection) {
+      this.useResetConnection = useResetConnection;
+      return this;
+    }
+
+    /**
+     * MySQL Authentication RSA server file, for mysql authentication
+     *
+     * @param serverRsaPublicKeyFile server RSA public key file
+     * @return this {@link Builder}
+     */
+    public Builder serverRsaPublicKeyFile(String serverRsaPublicKeyFile) {
+      this.serverRsaPublicKeyFile = nullOrEmpty(serverRsaPublicKeyFile);
+      return this;
+    }
+
+    /**
+     * Allow RSA server file retrieval from MySQL server
+     *
+     * @param allowPublicKeyRetrieval Allow RSA server file retrieval from MySQL server
+     * @return this {@link Builder}
+     */
+    public Builder allowPublicKeyRetrieval(Boolean allowPublicKeyRetrieval) {
+      this.allowPublicKeyRetrieval = allowPublicKeyRetrieval;
+      return this;
+    }
+
+    /**
+     * Cache all socket available information.
+     *
+     * @param useReadAheadInput cache available socket data when reading socket.
+     * @return this {@link Builder}
+     */
+    public Builder useReadAheadInput(Boolean useReadAheadInput) {
+      this.useReadAheadInput = useReadAheadInput;
+      return this;
+    }
+
+    /**
+     * Cache server prepare result
+     *
+     * @param cachePrepStmts cache server prepared result
+     * @return this {@link Builder}
+     */
+    public Builder cachePrepStmts(Boolean cachePrepStmts) {
+      this.cachePrepStmts = cachePrepStmts;
+      return this;
+    }
+
+    /**
+     * Must cache commands in transaction and replay transaction on failover.
+     *
+     * @param transactionReplay cache transaction and replay on failover
+     * @return this {@link Builder}
+     */
+    public Builder transactionReplay(Boolean transactionReplay) {
+      this.transactionReplay = transactionReplay;
+      return this;
+    }
+
+    /**
+     * Transaction replay cache size
+     *
+     * @param transactionReplaySize transaction replay cache size
+     * @return this {@link Builder}
+     */
+    public Builder transactionReplaySize(Integer transactionReplaySize) {
+      this.transactionReplaySize = transactionReplaySize;
+      return this;
+    }
+
+    /**
+     * Build a configuration
+     *
+     * @return a Configuration object
+     * @throws SQLException if option data type doesn't correspond
+     */
+    public Configuration build() throws SQLException {
+      Configuration conf =
+          new Configuration(
+              this.database,
+              this._addresses,
+              this._haMode,
+              this.user,
+              this.password,
+              this.enabledSslProtocolSuites,
+              this.socketFactory,
+              this.connectTimeout,
+              this.pipe,
+              this.localSocket,
+              this.tcpKeepAlive,
+              this.uuidAsString,
+              this.tcpKeepIdle,
+              this.tcpKeepCount,
+              this.tcpKeepInterval,
+              this.tcpAbortiveClose,
+              this.localSocketAddress,
+              this.socketTimeout,
+              this.allowMultiQueries,
+              this.allowLocalInfile,
+              this.useCompression,
+              this.blankTableNameMeta,
+              this.credentialType,
+              this.sslMode,
+              this.transactionIsolation,
+              this.enabledSslCipherSuites,
+              this.sessionVariables,
+              this.tinyInt1isBit,
+              this.transformedBitIsBoolean,
+              this.yearIsDateType,
+              this.timezone,
+              this.dumpQueriesOnException,
+              this.prepStmtCacheSize,
+              this.useAffectedRows,
+              this.useServerPrepStmts,
+              this.connectionAttributes,
+              this.useBulkStmts,
+              this.disablePipeline,
+              this.autocommit,
+              this.useMysqlMetadata,
+              this.createDatabaseIfNotExist,
+              this.includeInnodbStatusInDeadlockExceptions,
+              this.includeThreadDumpInDeadlockExceptions,
+              this.servicePrincipalName,
+              this.defaultFetchSize,
+              this.tlsSocketType,
+              this.maxQuerySizeToLog,
+              this.maxAllowedPacket,
+              this.retriesAllDown,
+              this.galeraAllowedState,
+              this.pool,
+              this.poolName,
+              this.maxPoolSize,
+              this.minPoolSize,
+              this.maxIdleTime,
+              this.registerJmxPool,
+              this.poolValidMinDelay,
+              this.useResetConnection,
+              this.serverRsaPublicKeyFile,
+              this.allowPublicKeyRetrieval,
+              this.serverSslCert,
+              this.keyStore,
+              this.keyStorePassword,
+              this.keyStoreType,
+              this.useReadAheadInput,
+              this.cachePrepStmts,
+              this.transactionReplay,
+              this.transactionReplaySize,
+              this.geometryDefaultType,
+              this.restrictedAuth,
+              this.initSql,
+              this._nonMappedOptions);
+      conf.initialUrl = buildUrl(conf);
+      return conf;
+    }
+  }
+
+  private static String nullOrEmpty(String val) {
+    return (val == null || val.isEmpty()) ? null : val;
+  }
+}
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 0000000..e870cce
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/Connection.java
@@ -0,0 +1,933 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+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.ConnectionEvent;
+import org.mariadb.jdbc.client.Client;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.impl.StandardClient;
+import org.mariadb.jdbc.export.ExceptionFactory;
+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.message.client.ResetPacket;
+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;
+
+/** Public Connection class */
+public class Connection implements java.sql.Connection {
+
+  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 Configuration conf;
+  private ExceptionFactory exceptionFactory;
+  private final Client client;
+  private final Properties clientInfo = new Properties();
+  private int lowercaseTableNames = -1;
+  private final AtomicInteger savepointId = new AtomicInteger();
+  private boolean readOnly;
+  private final boolean canUseServerTimeout;
+  private final boolean canCachePrepStmts;
+  private final boolean canUseServerMaxRows;
+  private final int defaultFetchSize;
+  private final boolean forceTransactionEnd;
+  private MariaDbPoolConnection poolConnection;
+
+  /**
+   * Connection construction.
+   *
+   * @param conf configuration
+   * @param lock thread safe locker
+   * @param client client object
+   */
+  public Connection(Configuration conf, ReentrantLock lock, Client client) {
+    this.conf = conf;
+    this.forceTransactionEnd =
+        Boolean.parseBoolean(conf.nonMappedOptions().getProperty("forceTransactionEnd", "false"));
+    this.lock = lock;
+    this.exceptionFactory = client.getExceptionFactory().setConnection(this);
+    this.client = client;
+    Context context = this.client.getContext();
+    this.canUseServerTimeout =
+        context.getVersion().isMariaDBServer()
+            && context.getVersion().versionGreaterOrEqual(10, 1, 2);
+    this.canUseServerMaxRows =
+        context.getVersion().isMariaDBServer()
+            && context.getVersion().versionGreaterOrEqual(10, 3, 0);
+    this.canCachePrepStmts = context.getConf().cachePrepStmts();
+    this.defaultFetchSize = context.getConf().defaultFetchSize();
+  }
+
+  /**
+   * Internal method. Indicate that connection is created from internal pool
+   *
+   * @param poolConnection PoolConnection
+   */
+  public void setPoolConnection(MariaDbPoolConnection poolConnection) {
+    this.poolConnection = poolConnection;
+    this.exceptionFactory = exceptionFactory.setPoolConnection(poolConnection);
+  }
+
+  /**
+   * 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 StandardClient(conf, client.getHostAddress(), new ReentrantLock(), true)) {
+      cli.execute(new QueryPacket("KILL QUERY " + client.getContext().getThreadId()), false);
+    }
+  }
+
+  @Override
+  public Statement createStatement() {
+    return new Statement(
+        this,
+        lock,
+        canUseServerTimeout,
+        canUseServerMaxRows,
+        Statement.RETURN_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());
+  }
+
+  /**
+   * Prepare statement creation
+   *
+   * @param sql sql
+   * @param autoGeneratedKeys auto generated key required
+   * @param resultSetType result-set type
+   * @param resultSetConcurrency concurrency
+   * @param useBinary use server prepare statement
+   * @return prepared statement
+   * @throws SQLException if Prepare fails
+   */
+  public PreparedStatement prepareInternal(
+      String sql,
+      int autoGeneratedKeys,
+      int resultSetType,
+      int resultSetConcurrency,
+      boolean useBinary)
+      throws SQLException {
+    checkNotClosed();
+    if (useBinary) {
+      try {
+        return new ServerPreparedStatement(
+            NativeSql.parse(sql, client.getContext()),
+            this,
+            lock,
+            canUseServerTimeout,
+            canUseServerMaxRows,
+            canCachePrepStmts,
+            autoGeneratedKeys,
+            resultSetType,
+            resultSetConcurrency,
+            defaultFetchSize);
+      } catch (SQLException e) {
+        // failover to client
+      }
+    }
+    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() {
+    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(((autoCommit) ? "set autocommit=1" : "set autocommit=0")), true);
+    } finally {
+      lock.unlock();
+    }
+  }
+
+  @Override
+  public void commit() throws SQLException {
+    lock.lock();
+    try {
+      if (forceTransactionEnd
+          || (client.getContext().getServerStatus() & ServerStatus.IN_TRANSACTION) > 0) {
+        client.execute(new QueryPacket("COMMIT"), false);
+      }
+    } finally {
+      lock.unlock();
+    }
+  }
+
+  @Override
+  public void rollback() throws SQLException {
+    lock.lock();
+    try {
+      if (forceTransactionEnd
+          || (client.getContext().getServerStatus() & ServerStatus.IN_TRANSACTION) > 0) {
+        client.execute(new QueryPacket("ROLLBACK"), true);
+      }
+    } finally {
+      lock.unlock();
+    }
+  }
+
+  @Override
+  public void close() throws SQLException {
+    if (poolConnection != null) {
+      poolConnection.fireConnectionClosed(new ConnectionEvent(poolConnection));
+      return;
+    }
+    client.close();
+  }
+
+  @Override
+  public boolean isClosed() {
+    return client.isClosed();
+  }
+
+  /**
+   * Connection context.
+   *
+   * @return connection context.
+   */
+  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 (this.readOnly != readOnly) {
+        client.setReadOnly(readOnly);
+      }
+      this.readOnly = readOnly;
+      getContext().addStateFlag(ConnectionState.STATE_READ_ONLY);
+    } finally {
+      lock.unlock();
+    }
+  }
+
+  @Override
+  public String getCatalog() throws SQLException {
+
+    if (client.getContext().hasClientCapability(Capabilities.CLIENT_SESSION_TRACK)) {
+      return client.getContext().getDatabase();
+    }
+
+    Statement stmt = createStatement();
+    ResultSet rs = stmt.executeQuery("select database()");
+    rs.next();
+    client.getContext().setDatabase(rs.getString(1));
+    return client.getContext().getDatabase();
+  }
+
+  @Override
+  public void setCatalog(String catalog) throws SQLException {
+    // null catalog means keep current.
+    // there is no possibility to set no database when one is selected
+    if (catalog == null
+        || (client.getContext().hasClientCapability(Capabilities.CLIENT_SESSION_TRACK)
+            && catalog.equals(client.getContext().getDatabase()))) {
+      return;
+    }
+    lock.lock();
+    try {
+      getContext().addStateFlag(ConnectionState.STATE_DATABASE);
+      client.execute(new ChangeDbPacket(catalog), true);
+      client.getContext().setDatabase(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 {
+      checkNotClosed();
+      getContext().addStateFlag(ConnectionState.STATE_TRANSACTION_ISOLATION);
+      client.getContext().setTransactionIsolationLevel(level);
+      client.execute(new QueryPacket(query), true);
+    } 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;
+          } else {
+            last.setNextWarning(warning);
+          }
+          last = warning;
+        }
+      }
+    }
+    return first;
+  }
+
+  @Override
+  public void clearWarnings() {
+    client.getContext().setWarning(0);
+  }
+
+  @Override
+  public Statement createStatement(int resultSetType, int resultSetConcurrency)
+      throws SQLException {
+    checkNotClosed();
+    return new Statement(
+        this,
+        lock,
+        canUseServerTimeout,
+        canUseServerMaxRows,
+        Statement.RETURN_GENERATED_KEYS,
+        resultSetType,
+        resultSetConcurrency,
+        defaultFetchSize);
+  }
+
+  @Override
+  public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency)
+      throws SQLException {
+    return prepareInternal(
+        sql,
+        Statement.RETURN_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 <procedure/function name>[(?,?, ...)]}\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,
+          canCachePrepStmts,
+          resultSetType,
+          resultSetConcurrency);
+    } else {
+      return new ProcedureStatement(
+          this,
+          query,
+          database,
+          procedureName,
+          lock,
+          canUseServerTimeout,
+          canUseServerMaxRows,
+          canCachePrepStmts,
+          resultSetType,
+          resultSetConcurrency);
+    }
+  }
+
+  @Override
+  public Map<String, Class<?>> getTypeMap() {
+    return new HashMap<>();
+  }
+
+  @Override
+  public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
+    throw exceptionFactory.notSupported("TypeMap are not supported");
+  }
+
+  @Override
+  public int getHoldability() {
+    return ResultSet.HOLD_CURSORS_OVER_COMMIT;
+  }
+
+  @Override
+  public void setHoldability(int holdability) {
+    // not supported
+  }
+
+  @Override
+  public Savepoint setSavepoint() throws SQLException {
+    MariaDbSavepoint savepoint = new MariaDbSavepoint(savepointId.incrementAndGet());
+    client.execute(new QueryPacket("SAVEPOINT `" + savepoint.rawValue() + "`"), true);
+    return savepoint;
+  }
+
+  @Override
+  public Savepoint setSavepoint(String name) throws SQLException {
+    MariaDbSavepoint savepoint = new MariaDbSavepoint(name.replace("`", "``"));
+    client.execute(new QueryPacket("SAVEPOINT `" + savepoint.rawValue() + "`"), true);
+    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.MariaDbSavepoint) {
+          client.execute(
+              new QueryPacket(
+                  "ROLLBACK TO SAVEPOINT `"
+                      + ((Connection.MariaDbSavepoint) savepoint).rawValue()
+                      + "`"),
+              true);
+        } 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.MariaDbSavepoint) {
+          client.execute(
+              new QueryPacket(
+                  "RELEASE SAVEPOINT `"
+                      + ((Connection.MariaDbSavepoint) savepoint).rawValue()
+                      + "`"),
+              true);
+        } 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 {
+    return prepareCall(sql, resultSetType, resultSetConcurrency);
+  }
+
+  @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, Statement.RETURN_GENERATED_KEYS);
+  }
+
+  @Override
+  public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
+    return prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
+  }
+
+  @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, true);
+      return true;
+    } catch (SQLException sqle) {
+      if (poolConnection != null) {
+        MariaDbPoolConnection poolConnection = this.poolConnection;
+        poolConnection.fireConnectionErrorOccurred(sqle);
+        poolConnection.close();
+      }
+      return false;
+    } finally {
+      lock.unlock();
+    }
+  }
+
+  @Override
+  public void setClientInfo(String name, String value) {
+    clientInfo.put(name, value);
+  }
+
+  @Override
+  public String getClientInfo(String name) {
+    return (String) clientInfo.get(name);
+  }
+
+  @Override
+  public Properties getClientInfo() {
+    return clientInfo;
+  }
+
+  @Override
+  public void setClientInfo(Properties properties) {
+    clientInfo.putAll(properties);
+  }
+
+  @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() {
+    // We support only catalog
+    return null;
+  }
+
+  @Override
+  public void setSchema(String schema) {
+    // 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 {
+    if (poolConnection != null) {
+      MariaDbPoolConnection poolConnection = this.poolConnection;
+      poolConnection.close();
+      return;
+    }
+    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");
+    }
+    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> T unwrap(Class<T> 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) {
+    return iface.isInstance(this);
+  }
+
+  /**
+   * Associate connection client
+   *
+   * @return connection client
+   */
+  public Client getClient() {
+    return client;
+  }
+
+  /** Internal Savepoint implementation */
+  class MariaDbSavepoint implements java.sql.Savepoint {
+
+    private final String name;
+    private final Integer id;
+
+    public MariaDbSavepoint(final String name) {
+      this.name = name;
+      this.id = null;
+    }
+
+    public MariaDbSavepoint(final int savepointId) {
+      this.id = savepointId;
+      this.name = null;
+    }
+
+    /**
+     * Retrieves the generated ID for the savepoint that this <code>Savepoint</code> 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 <code>Savepoint</code> 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;
+    }
+  }
+
+  /**
+   * Reset connection set has it was after creating a "fresh" new connection.
+   * defaultTransactionIsolation must have been initialized.
+   *
+   * <p>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 =
+        conf.useResetConnection()
+            && getContext().getVersion().isMariaDBServer()
+            && (getContext().getVersion().versionGreaterOrEqual(10, 3, 13)
+                || (getContext().getVersion().getMajorVersion() == 10
+                    && getContext().getVersion().getMinorVersion() == 2
+                    && getContext().getVersion().versionGreaterOrEqual(10, 2, 22)));
+
+    if (useComReset) {
+      client.execute(ResetPacket.INSTANCE, true);
+    }
+
+    // in transaction => rollback
+    if (forceTransactionEnd
+        || (client.getContext().getServerStatus() & ServerStatus.IN_TRANSACTION) > 0) {
+      client.execute(new QueryPacket("ROLLBACK"), true);
+    }
+
+    int stateFlag = getContext().getStateFlag();
+    if (stateFlag != 0) {
+      try {
+        if ((stateFlag & ConnectionState.STATE_NETWORK_TIMEOUT) != 0) {
+          setNetworkTimeout(null, conf.socketTimeout());
+        }
+        if ((stateFlag & ConnectionState.STATE_AUTOCOMMIT) != 0) {
+          setAutoCommit(conf.autocommit() == null ? true : conf.autocommit());
+        }
+        if ((stateFlag & ConnectionState.STATE_DATABASE) != 0) {
+          setCatalog(conf.database());
+        }
+        if ((stateFlag & ConnectionState.STATE_READ_ONLY) != 0) {
+          setReadOnly(false); // default to master connection
+        }
+        if (!useComReset && (stateFlag & ConnectionState.STATE_TRANSACTION_ISOLATION) != 0) {
+          setTransactionIsolation(
+              conf.transactionIsolation() == null
+                  ? java.sql.Connection.TRANSACTION_REPEATABLE_READ
+                  : conf.transactionIsolation().getLevel());
+        }
+      } catch (SQLException sqle) {
+        throw exceptionFactory.create("error resetting connection");
+      }
+    }
+
+    client.reset();
+
+    clearWarnings();
+  }
+
+  /**
+   * Current server thread id.
+   *
+   * @return current server thread id
+   */
+  public long getThreadId() {
+    return client.getContext().getThreadId();
+  }
+
+  /**
+   * Fire event to indicate to StatementEventListeners registered on the connection that a
+   * PreparedStatement is closed.
+   *
+   * @param prep prepare statement closing
+   */
+  public void fireStatementClosed(PreparedStatement prep) {
+    if (poolConnection != null) {
+      poolConnection.fireStatementClosed(prep);
+    }
+  }
+
+  /**
+   * Get connection exception factory
+   *
+   * @return connection exception factory
+   */
+  protected ExceptionFactory getExceptionFactory() {
+    return exceptionFactory;
+  }
+
+  /**
+   * for _TEST_ only
+   *
+   * @return current host
+   */
+  public String __test_host() {
+    return this.client.getHostAddress().toString();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/DatabaseMetaData.java b/src/main/java/org/mariadb/jdbc/DatabaseMetaData.java
new file mode 100644
index 0000000..a4bf275
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/DatabaseMetaData.java
@@ -0,0 +1,3885 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.sql.*;
+import java.sql.Statement;
+import java.text.ParseException;
+import java.util.*;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ServerVersion;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.client.result.Result;
+import org.mariadb.jdbc.util.VersionFactory;
+import org.mariadb.jdbc.util.constants.ColumnFlags;
+import org.mariadb.jdbc.util.constants.ServerStatus;
+
+/** Mariadb Database metadata */
+public class DatabaseMetaData implements java.sql.DatabaseMetaData {
+
+  /** MariaDB driver name */
+  public static final String DRIVER_NAME = "MariaDB Connector/J";
+
+  private final org.mariadb.jdbc.Connection connection;
+  private final Configuration conf;
+
+  /**
+   * Constructor.
+   *
+   * @param connection connection
+   * @param conf configuration
+   */
+  public DatabaseMetaData(org.mariadb.jdbc.Connection connection, Configuration conf) {
+    this.connection = connection;
+    this.conf = conf;
+  }
+
+  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 (conf.tinyInt1isBit()) {
+      upperCaseWithoutSize =
+          " IF(COLUMN_TYPE like 'tinyint(1)%', '"
+              + (conf.transformedBitIsBoolean() ? "BOOLEAN" : "BIT")
+              + "', "
+              + upperCaseWithoutSize
+              + ")";
+    }
+
+    if (!conf.yearIsDateType()) {
+      return " IF(COLUMN_TYPE IN ('year(2)', 'year(4)'), 'SMALLINT', " + upperCaseWithoutSize + ")";
+    }
+
+    return upperCaseWithoutSize;
+  }
+
+  // Extract identifier quoted string from input String.
+  // Return new position, or -1 on error
+  private static int skipWhiteSpace(char[] part, int startPos) {
+    for (int i = startPos; i < part.length; i++) {
+      if (!Character.isWhitespace(part[i])) {
+        return i;
+      }
+    }
+    return part.length;
+  }
+
+  private static int parseIdentifier(char[] part, int startPos, Identifier identifier)
+      throws ParseException {
+    int pos = skipWhiteSpace(part, startPos);
+    if (part[pos] != '`') {
+      throw new ParseException(new String(part), pos);
+    }
+    pos++;
+    StringBuilder sb = new StringBuilder();
+    int quotes = 0;
+    for (; pos < part.length; pos++) {
+      char ch = part[pos];
+      if (ch == '`') {
+        quotes++;
+      } else {
+        for (int j = 0; j < quotes / 2; j++) {
+          sb.append('`');
+        }
+        if (quotes % 2 == 1) {
+          if (ch == '.') {
+            if (identifier.schema != null) {
+              throw new ParseException(new String(part), pos);
+            }
+            identifier.schema = sb.toString();
+            return parseIdentifier(part, pos + 1, identifier);
+          }
+          identifier.name = sb.toString();
+          return pos;
+        }
+        quotes = 0;
+        sb.append(ch);
+      }
+    }
+    throw new ParseException(new String(part), startPos);
+  }
+
+  private static int skipKeyword(char[] part, int startPos, String keyword) throws ParseException {
+    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);
+      }
+    }
+    return pos;
+  }
+
+  private static int getImportedKeyAction(String actionKey) {
+    if (actionKey == null) {
+      return java.sql.DatabaseMetaData.importedKeyRestrict;
+    }
+    switch (actionKey) {
+      case "NO ACTION":
+        return java.sql.DatabaseMetaData.importedKeyNoAction;
+
+      case "CASCADE":
+        return java.sql.DatabaseMetaData.importedKeyCascade;
+
+      case "SET NULL":
+        return java.sql.DatabaseMetaData.importedKeySetNull;
+
+      case "SET DEFAULT":
+        return java.sql.DatabaseMetaData.importedKeySetDefault;
+
+      case "RESTRICT":
+        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 (noBackslashEscapes) {
+      return value.replace("'", "''");
+    }
+    return value
+        .replace("\\", "\\\\")
+        .replace("'", "\\'")
+        .replace("\0", "\\0")
+        .replace("\"", "\\\"");
+  }
+
+  private int parseIdentifierList(char[] part, int startPos, List<Identifier> 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.
+   *
+   * @param tableDef table definition
+   * @param tableName table name
+   * @param catalog catalog
+   * @param connection connection
+   * @return resultset resultset
+   * @throws ParseException exception
+   */
+  private ResultSet getImportedKeys(
+      String tableDef, String tableName, String catalog, org.mariadb.jdbc.Connection connection)
+      throws Exception, SQLException {
+    boolean importedKeysWithConstraintNames =
+        Boolean.parseBoolean(
+            conf.nonMappedOptions().getProperty("importedKeysWithConstraintNames", "true"));
+    String[] columnNames = {
+      "PKTABLE_CAT", "PKTABLE_SCHEM", "PKTABLE_NAME",
+      "PKCOLUMN_NAME", "FKTABLE_CAT", "FKTABLE_SCHEM",
+      "FKTABLE_NAME", "FKCOLUMN_NAME", "KEY_SEQ",
+      "UPDATE_RULE", "DELETE_RULE", "FK_NAME",
+      "PK_NAME", "DEFERRABILITY"
+    };
+    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");
+
+    List<String[]> data = new ArrayList<>();
+
+    for (String part : parts) {
+      part = part.trim();
+      if (!part.toUpperCase(Locale.ROOT).startsWith("CONSTRAINT")
+          && !part.toUpperCase(Locale.ROOT).contains("FOREIGN KEY")) {
+        continue;
+      }
+      char[] partChar = part.toCharArray();
+
+      Identifier constraintName = new Identifier();
+
+      int pos = skipKeyword(partChar, 0, "CONSTRAINT");
+      pos = parseIdentifier(partChar, pos, constraintName);
+      pos = skipKeyword(partChar, pos, "FOREIGN KEY");
+      List<Identifier> foreignKeyCols = new ArrayList<>();
+      pos = parseIdentifierList(partChar, pos, foreignKeyCols);
+      pos = skipKeyword(partChar, pos, "REFERENCES");
+      Identifier pkTable = new Identifier();
+      pos = parseIdentifier(partChar, pos, pkTable);
+      List<Identifier> primaryKeyCols = new ArrayList<>();
+      parseIdentifierList(partChar, pos, primaryKeyCols);
+
+      int onUpdateReferenceAction = java.sql.DatabaseMetaData.importedKeyRestrict;
+      int onDeleteReferenceAction = java.sql.DatabaseMetaData.importedKeyRestrict;
+
+      for (String referenceAction :
+          new String[] {"RESTRICT", "CASCADE", "SET NULL", "NO ACTION", "SET DEFAULT"}) {
+        if (part.toUpperCase(Locale.ROOT).contains("ON UPDATE " + referenceAction)) {
+          onUpdateReferenceAction = getImportedKeyAction(referenceAction);
+        }
+        if (part.toUpperCase(Locale.ROOT).contains("ON DELETE " + referenceAction)) {
+          onDeleteReferenceAction = getImportedKeyAction(referenceAction);
+        }
+      }
+      Map<String, Map<String[], String>> externalInfos = new HashMap<>();
+      for (int i = 0; i < primaryKeyCols.size(); i++) {
+
+        String[] row = new String[columnNames.length];
+        row[0] = pkTable.schema; // PKTABLE_CAT
+        if (row[0] == null) {
+          row[0] = catalog;
+        }
+        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
+        if (importedKeysWithConstraintNames) {
+          String ext =
+              (pkTable.schema == null ? "" : quoteIdentifier(pkTable.schema) + ".")
+                  + quoteIdentifier(pkTable.name);
+          if (!externalInfos.containsKey(ext)) {
+            externalInfos.put(ext, getExtImportedKeys(ext, connection));
+          }
+          row[12] = null; // PK_NAME
+          Map<String[], String> externalInfo = externalInfos.get(ext);
+          if (externalInfo != null) {
+            for (Map.Entry<String[], String> entry : externalInfo.entrySet()) {
+              boolean foundAll = true;
+
+              for (String keyPart : entry.getKey()) {
+                boolean foundKey = false;
+                for (Identifier keyCol : primaryKeyCols) {
+                  if (keyCol.name.equals(keyPart)) {
+                    foundKey = true;
+                    break;
+                  }
+                }
+                if (!foundKey) foundAll = false;
+              }
+              if (foundAll) {
+                row[12] = entry.getValue();
+              }
+            }
+          }
+        } else {
+          row[12] = null; // PK_NAME
+        }
+        row[13] = Integer.toString(DatabaseMetaData.importedKeyNotDeferrable); // DEFERRABILITY
+        data.add(row);
+      }
+    }
+    String[][] arr = data.toArray(new String[0][]);
+
+    /* Sort array by PKTABLE_CAT, PKTABLE_NAME, and KEY_SEQ.*/
+    Arrays.sort(
+        arr,
+        (row1, row2) -> {
+          int result = row1[0].compareTo(row2[0]); // PKTABLE_CAT
+          if (result == 0) {
+            result = row1[2].compareTo(row2[2]); // PKTABLE_NAME
+            if (result == 0) {
+              result = row1[8].length() - row2[8].length(); // KEY_SEQ
+              if (result == 0) {
+                result = row1[8].compareTo(row2[8]);
+              }
+            }
+          }
+          return result;
+        });
+    return CompleteResult.createResultSet(
+        columnNames, dataTypes, arr, connection.getContext(), ColumnFlags.PRIMARY_KEY);
+  }
+
+  private Map<String[], String> getExtImportedKeys(
+      String tableName, org.mariadb.jdbc.Connection connection) throws SQLException {
+    ResultSet rs = connection.createStatement().executeQuery("SHOW CREATE TABLE " + tableName);
+    rs.next();
+    String refTableDef = rs.getString(2);
+    Map<String[], String> res = new HashMap<>();
+    String[] parts = refTableDef.split("\n");
+    for (int i = 1; i < parts.length - 1; i++) {
+      String part = parts[i].trim();
+      if (part.startsWith("`")) {
+        // field
+        continue;
+      }
+      if (part.startsWith("PRIMARY KEY") || part.startsWith("UNIQUE KEY")) {
+        String name = "PRIMARY";
+        if (part.indexOf("`") < part.indexOf("(")) {
+          int offset = part.indexOf("`");
+          name = part.substring(offset + 1, part.indexOf("`", offset + 1));
+        }
+
+        String subPart = part.substring(part.indexOf("(") + 1, part.lastIndexOf(")"));
+        List<String> cols = new ArrayList<>();
+        int pos = 0;
+        while (pos < subPart.length()) {
+          pos = subPart.indexOf("`", pos);
+          int endpos = subPart.indexOf("`", pos + 1);
+          cols.add(subPart.substring(pos + 1, endpos));
+          pos = endpos + 1;
+        }
+        res.put(cols.toArray(new String[0]), name);
+      }
+    }
+    return res;
+  }
+
+  /**
+   * Retrieves a description of the primary key columns that are referenced by the given table's
+   * foreign key columns (the primary keys imported by a table). They are ordered by PKTABLE_CAT,
+   * PKTABLE_SCHEM, PKTABLE_NAME, and KEY_SEQ.
+   *
+   * <p>Each primary key column description has the following columns:
+   *
+   * <OL>
+   *   <LI><B>PKTABLE_CAT</B> String {@code =>} primary key table catalog being imported (may be
+   *       <code>null</code>)
+   *   <LI><B>PKTABLE_SCHEM</B> String {@code =>} primary key table schema being imported (may be
+   *       <code>null</code>)
+   *   <LI><B>PKTABLE_NAME</B> String {@code =>} primary key table name being imported
+   *   <LI><B>PKCOLUMN_NAME</B> String {@code =>} primary key column name being imported
+   *   <LI><B>FKTABLE_CAT</B> String {@code =>} foreign key table catalog (may be <code>null</code>)
+   *   <LI><B>FKTABLE_SCHEM</B> String {@code =>} foreign key table schema (may be <code>null</code>
+   *       )
+   *   <LI><B>FKTABLE_NAME</B> String {@code =>} foreign key table name
+   *   <LI><B>FKCOLUMN_NAME</B> String {@code =>} foreign key column name
+   *   <LI><B>KEY_SEQ</B> short {@code =>} sequence number within a foreign key( a value of 1
+   *       represents the first column of the foreign key, a value of 2 would represent the second
+   *       column within the foreign key).
+   *   <LI><B>UPDATE_RULE</B> short {@code =>} What happens to a foreign key when the primary key is
+   *       updated:
+   *       <UL>
+   *         <LI>importedNoAction - do not allow update of primary key if it has been imported
+   *         <LI>importedKeyCascade - change imported key to agree with primary key update
+   *         <LI>importedKeySetNull - change imported key to <code>NULL</code> if its primary key
+   *             has been updated
+   *         <LI>importedKeySetDefault - change imported key to default values if its primary key
+   *             has been updated
+   *         <LI>importedKeyRestrict - same as importedKeyNoAction (for ODBC 2.x compatibility)
+   *       </UL>
+   *   <LI><B>DELETE_RULE</B> short {@code =>} What happens to the foreign key when primary is
+   *       deleted.
+   *       <UL>
+   *         <LI>importedKeyNoAction - do not allow delete of primary key if it has been imported
+   *         <LI>importedKeyCascade - delete rows that import a deleted key
+   *         <LI>importedKeySetNull - change imported key to NULL if its primary key has been
+   *             deleted
+   *         <LI>importedKeyRestrict - same as importedKeyNoAction (for ODBC 2.x compatibility)
+   *         <LI>importedKeySetDefault - change imported key to default if its primary key has been
+   *             deleted
+   *       </UL>
+   *   <LI><B>FK_NAME</B> String {@code =>} foreign key name (may be <code>null</code>)
+   *   <LI><B>PK_NAME</B> String {@code =>} primary key name (may be <code>null</code>)
+   *   <LI><B>DEFERRABILITY</B> short {@code =>} can the evaluation of foreign key constraints be
+   *       deferred until commit
+   *       <UL>
+   *         <LI>importedKeyInitiallyDeferred - see SQL92 for definition
+   *         <LI>importedKeyInitiallyImmediate - see SQL92 for definition
+   *         <LI>importedKeyNotDeferrable - see SQL92 for definition
+   *       </UL>
+   * </OL>
+   *
+   * @param catalog a catalog name; must match the catalog name as it is stored in the database; ""
+   *     retrieves those without a catalog; <code>null</code> means that the catalog name should not
+   *     be used to narrow the search
+   * @param schema a schema name; must match the schema name as it is stored in the database; ""
+   *     retrieves those without a schema; <code>null</code> means that the schema name should not
+   *     be used to narrow the search
+   * @param table a table name; must match the table name as it is stored in the database
+   * @return <code>ResultSet</code> - each row is a primary key column description
+   * @throws SQLException if a database access error occurs
+   * @see #getExportedKeys
+   */
+  public ResultSet getImportedKeys(String catalog, String schema, String table)
+      throws SQLException {
+
+    // 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 (catalog == null || catalog.isEmpty()) {
+      return getImportedKeysUsingInformationSchema(catalog, table);
+    }
+
+    try {
+      return getImportedKeysUsingShowCreateTable(catalog, table);
+    } catch (Exception e) {
+      // Likely, parsing failed, try out I_S query.
+      return getImportedKeysUsingInformationSchema(catalog, table);
+    }
+  }
+
+  private String dataTypeClause(String fullTypeColumnName) {
+    return " CASE data_type"
+        + " WHEN 'bit' THEN "
+        + Types.BIT
+        + " WHEN 'tinyblob' THEN "
+        + Types.VARBINARY
+        + " WHEN 'mediumblob' THEN "
+        + Types.LONGVARBINARY
+        + " WHEN 'longblob' THEN "
+        + Types.LONGVARBINARY
+        + " WHEN 'blob' THEN "
+        + Types.LONGVARBINARY
+        + " WHEN 'tinytext' THEN "
+        + Types.VARCHAR
+        + " WHEN 'mediumtext' THEN "
+        + Types.LONGVARCHAR
+        + " WHEN 'longtext' THEN "
+        + Types.LONGVARCHAR
+        + " WHEN 'text' THEN "
+        + Types.LONGVARCHAR
+        + " WHEN 'date' THEN "
+        + Types.DATE
+        + " WHEN 'datetime' THEN "
+        + Types.TIMESTAMP
+        + " WHEN 'decimal' THEN "
+        + Types.DECIMAL
+        + " WHEN 'double' THEN "
+        + Types.DOUBLE
+        + " WHEN 'enum' THEN "
+        + Types.VARCHAR
+        + " WHEN 'float' THEN "
+        + Types.REAL
+        + " WHEN 'int' THEN IF( "
+        + fullTypeColumnName
+        + " like '%unsigned%', "
+        + Types.INTEGER
+        + ","
+        + Types.INTEGER
+        + ")"
+        + " WHEN 'bigint' THEN "
+        + Types.BIGINT
+        + " WHEN 'mediumint' THEN "
+        + Types.INTEGER
+        + " WHEN 'null' THEN "
+        + Types.NULL
+        + " WHEN 'set' THEN "
+        + Types.VARCHAR
+        + " WHEN 'smallint' THEN IF( "
+        + fullTypeColumnName
+        + " like '%unsigned%', "
+        + Types.SMALLINT
+        + ","
+        + Types.SMALLINT
+        + ")"
+        + " WHEN 'varchar' THEN "
+        + Types.VARCHAR
+        + " WHEN 'varbinary' THEN "
+        + Types.VARBINARY
+        + " WHEN 'char' THEN "
+        + Types.CHAR
+        + " WHEN 'binary' THEN "
+        + Types.BINARY
+        + " WHEN 'time' THEN "
+        + Types.TIME
+        + " WHEN 'timestamp' THEN "
+        + Types.TIMESTAMP
+        + " WHEN 'tinyint' THEN "
+        + (conf.tinyInt1isBit()
+            ? "IF("
+                + fullTypeColumnName
+                + " like 'tinyint(1)%',"
+                + (conf.transformedBitIsBoolean() ? Types.BOOLEAN : Types.BIT)
+                + ","
+                + Types.TINYINT
+                + ") "
+            : Types.TINYINT)
+        + " WHEN 'year' THEN "
+        + (conf.yearIsDateType() ? Types.DATE : Types.SMALLINT)
+        + " ELSE "
+        + Types.OTHER
+        + " END ";
+  }
+
+  private ResultSet executeQuery(String sql) throws SQLException {
+    Statement stmt = connection.createStatement();
+    Result rs = (Result) stmt.executeQuery(sql);
+    rs.setStatement(null); // bypass Hibernate statement tracking (CONJ-49)
+    rs.useAliasAsName();
+    return rs;
+  }
+
+  private String escapeQuote(String value) {
+    return value == null
+        ? "null"
+        : "'"
+            + escapeString(
+                value,
+                (connection.getContext().getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES) > 0)
+            + "'";
+  }
+
+  /**
+   * Generate part of the information schema query that restricts catalog names In the driver,
+   * catalogs is the equivalent to MariaDB schemas.
+   *
+   * @param columnName - column name in the information schema table
+   * @param catalog - catalog name. This driver does not (always) follow JDBC standard for following
+   *     special values, due to ConnectorJ compatibility 1. empty string ("") - matches current
+   *     catalog (i.e. database). JDBC standard says only tables without catalog should be returned
+   *     - such tables do not exist in MariaDB. If there is no current catalog, then empty string
+   *     matches any catalog. 2. null - if nullCatalogMeansCurrent=true (which is the default), then
+   *     the handling is the same as for "" . i.e. return current catalog.JDBC-conforming way would
+   *     be to match any catalog with null parameter. This can be switched with
+   *     nullCatalogMeansCurrent=false in the connection URL.
+   * @return part of SQL query ,that restricts search for the catalog.
+   */
+  private boolean catalogCond(
+      boolean firstCondition, StringBuilder sb, String columnName, String catalog) {
+    // null catalog => searching without any catalog restriction
+    if (catalog == null) return firstCondition;
+
+    // empty catalog => search restricting to current catalog
+    if (catalog.isEmpty()) {
+      sb.append(firstCondition ? " WHERE " : " AND ").append(columnName).append(" = database()");
+      return false;
+    }
+
+    // search with specified catalog
+    sb.append(firstCondition ? " WHERE " : " AND ")
+        .append(columnName)
+        .append("=")
+        .append(escapeQuote(catalog));
+    return false;
+  }
+
+  // Helper to generate  information schema queries with "like" or "equals" condition (typically  on
+  // table name)
+  private boolean patternCond(
+      boolean firstCondition, StringBuilder sb, String columnName, String tableName) {
+    if (tableName == null || "%".equals(tableName)) {
+      return firstCondition;
+    }
+    sb.append(firstCondition ? " WHERE " : " AND ")
+        .append(columnName)
+        .append((tableName.indexOf('%') == -1 && tableName.indexOf('_') == -1) ? "=" : " LIKE ")
+        .append("'")
+        .append(
+            escapeString(
+                tableName,
+                (connection.getContext().getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES)
+                    != 0))
+        .append("'");
+    return false;
+  }
+
+  /**
+   * Retrieves a description of the given table's primary key columns. They are ordered by
+   * COLUMN_NAME.
+   *
+   * <p>Each primary key column description has the following columns:
+   *
+   * <OL>
+   *   <li><B>TABLE_CAT</B> String {@code =>} table catalog
+   *   <li><B>TABLE_SCHEM</B> String {@code =>} table schema (may be <code>null</code>)
+   *   <li><B>TABLE_NAME</B> String {@code =>} table name
+   *   <li><B>COLUMN_NAME</B> String {@code =>} column name
+   *   <li><B>KEY_SEQ</B> short {@code =>} sequence number within primary key( a value of 1
+   *       represents the first column of the primary key, a value of 2 would represent the second
+   *       column within the primary key).
+   *   <li><B>PK_NAME</B> String {@code =>} primary key name
+   * </OL>
+   *
+   * @param catalog a catalog name; must match the catalog name as it is stored in the database; ""
+   *     retrieves those without a catalog; <code>null</code> means that the catalog name should not
+   *     be used to narrow the search
+   * @param schema a schema name; must match the schema name as it is stored in the database; ""
+   *     retrieves those without a schema; <code>null</code> means that the schema name should not
+   *     be used to narrow the search
+   * @param table a table name; must match the table name as it is stored in the database
+   * @return <code>ResultSet</code> - each row is a primary key column description
+   * @throws SQLException if a database access error occurs
+   */
+  public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException {
+    // MySQL 8 now use 'PRI' in place of 'pri'
+    StringBuilder sb =
+        new StringBuilder(
+            "SELECT A.TABLE_SCHEMA TABLE_CAT, NULL TABLE_SCHEM, A.TABLE_NAME, A.COLUMN_NAME,"
+                + " B.SEQ_IN_INDEX KEY_SEQ, B.INDEX_NAME PK_NAME  FROM INFORMATION_SCHEMA.COLUMNS"
+                + " A, INFORMATION_SCHEMA.STATISTICS B WHERE A.COLUMN_KEY in ('PRI','pri') AND"
+                + " B.INDEX_NAME='PRIMARY'");
+
+    catalogCond(false, sb, "A.TABLE_SCHEMA", catalog);
+    catalogCond(false, sb, "B.TABLE_SCHEMA", catalog);
+    patternCond(false, sb, "A.TABLE_NAME", table);
+    patternCond(false, sb, "B.TABLE_NAME", table);
+    sb.append(
+        " AND A.TABLE_SCHEMA = B.TABLE_SCHEMA AND A.TABLE_NAME = B.TABLE_NAME AND A.COLUMN_NAME ="
+            + " B.COLUMN_NAME ORDER BY A.COLUMN_NAME");
+
+    return executeQuery(sb.toString());
+  }
+
+  /**
+   * Retrieves a description of the tables available in the given catalog. Only table descriptions
+   * matching the catalog, schema, table name and type criteria are returned. They are ordered by
+   * <code>TABLE_TYPE</code>, <code>TABLE_CAT</code>, <code>TABLE_SCHEM</code> and <code>TABLE_NAME
+   * </code>. Each table description has the following columns:
+   *
+   * <OL>
+   *   <LI><B>TABLE_CAT</B> String {@code =>} table catalog (may be <code>null</code>)
+   *   <LI><B>TABLE_SCHEM</B> String {@code =>} table schema (may be <code>null</code>)
+   *   <LI><B>TABLE_NAME</B> String {@code =>} table name
+   *   <LI><B>TABLE_TYPE</B> String {@code =>} table type. Typical types are "TABLE", "VIEW",
+   *       "SYSTEM TABLE", "GLOBAL TEMPORARY", "LOCAL TEMPORARY", "ALIAS", "SYNONYM".
+   *   <LI><B>REMARKS</B> String {@code =>} explanatory comment on the table
+   *   <LI><B>TYPE_CAT</B> String {@code =>} the types catalog (may be <code>null</code>)
+   *   <LI><B>TYPE_SCHEM</B> String {@code =>} the types schema (may be <code>null</code>)
+   *   <LI><B>TYPE_NAME</B> String {@code =>} type name (may be <code>null</code>)
+   *   <LI><B>SELF_REFERENCING_COL_NAME</B> String {@code =>} name of the designated "identifier"
+   *       column of a typed table (may be <code>null</code>)
+   *   <LI><B>REF_GENERATION</B> String {@code =>} specifies how values in SELF_REFERENCING_COL_NAME
+   *       are created. Values are "SYSTEM", "USER", "DERIVED". (may be <code>null</code>)
+   * </OL>
+   *
+   * <p><B>Note:</B> Some databases may not return information for all tables.
+   *
+   * @param catalog a catalog name; must match the catalog name as it is stored in the database; ""
+   *     retrieves those without a catalog; <code>null</code> means that the catalog name should not
+   *     be used to narrow the search
+   * @param schemaPattern a schema name pattern; must match the schema name as it is stored in the
+   *     database; "" retrieves those without a schema; <code>null</code> means that the schema name
+   *     should not be used to narrow the search
+   * @param tableNamePattern a table name pattern; must match the table name as it is stored in the
+   *     database
+   * @param types a list of table types, which must be from the list of table types returned from
+   *     {@link #getTableTypes},to include; <code>null</code> returns all types
+   * @return <code>ResultSet</code> - each row is a table description
+   * @throws SQLException if a database access error occurs
+   * @see #getSearchStringEscape
+   */
+  public ResultSet getTables(
+      String catalog, String schemaPattern, String tableNamePattern, String[] types)
+      throws SQLException {
+
+    StringBuilder sb =
+        new StringBuilder(
+            "SELECT TABLE_SCHEMA TABLE_CAT, NULL  TABLE_SCHEM,  TABLE_NAME, IF(TABLE_TYPE='BASE"
+                + " TABLE' or TABLE_TYPE='SYSTEM VERSIONED', 'TABLE', TABLE_TYPE) as TABLE_TYPE,"
+                + " TABLE_COMMENT REMARKS, NULL TYPE_CAT, NULL TYPE_SCHEM, NULL TYPE_NAME, NULL"
+                + " SELF_REFERENCING_COL_NAME,  NULL REF_GENERATION FROM"
+                + " INFORMATION_SCHEMA.TABLES");
+    boolean firstCondition = true;
+    firstCondition = catalogCond(true, sb, "TABLE_SCHEMA", catalog);
+    firstCondition = patternCond(firstCondition, sb, "TABLE_NAME", tableNamePattern);
+
+    if (types != null && types.length > 0) {
+      boolean mustAddType = false;
+
+      StringBuilder sqlType =
+          new StringBuilder(((firstCondition) ? " WHERE " : " AND ") + " TABLE_TYPE IN (");
+      for (String s : types) {
+        if (mustAddType) sqlType.append(",");
+        mustAddType = true;
+        if (s == null) {
+          mustAddType = false;
+          continue;
+        }
+        String type = "TABLE".equals(s) ? "'BASE TABLE','SYSTEM VERSIONED'" : escapeQuote(s);
+        sqlType.append(type);
+      }
+      sqlType.append(")");
+      if (mustAddType) sb.append(sqlType);
+    }
+
+    sb.append(" ORDER BY TABLE_TYPE, TABLE_SCHEMA, TABLE_NAME");
+
+    return executeQuery(sb.toString());
+  }
+
+  /**
+   * Retrieves a description of table columns available in the specified catalog.
+   *
+   * <p>Only column descriptions matching the catalog, schema, table and column name criteria are
+   * returned. They are ordered by <code>TABLE_CAT</code>,<code>TABLE_SCHEM</code>, <code>TABLE_NAME
+   * </code>, and <code>ORDINAL_POSITION</code>.
+   *
+   * <p>Each column description has the following columns:
+   *
+   * <OL>
+   *   <LI><B>TABLE_CAT</B> String {@code =>} table catalog (may be <code>null</code>)
+   *   <LI><B>TABLE_SCHEM</B> String {@code =>} table schema (may be <code>null</code>)
+   *   <LI><B>TABLE_NAME</B> String {@code =>} table name
+   *   <LI><B>COLUMN_NAME</B> String {@code =>} column name
+   *   <LI><B>DATA_TYPE</B> int {@code =>} SQL type from java.sql.Types
+   *   <LI><B>TYPE_NAME</B> String {@code =>} Data source dependent type name, for a UDT the type
+   *       name is fully qualified
+   *   <LI><B>COLUMN_SIZE</B> int {@code =>} column size.
+   *   <LI><B>BUFFER_LENGTH</B> is not used.
+   *   <LI><B>DECIMAL_DIGITS</B> int {@code =>} the number of fractional digits. Null is returned
+   *       for data types where DECIMAL_DIGITS is not applicable.
+   *   <LI><B>NUM_PREC_RADIX</B> int {@code =>} Radix (typically either 10 or 2)
+   *   <LI><B>NULLABLE</B> int {@code =>} is NULL allowed.
+   *       <UL>
+   *         <LI>columnNoNulls - might not allow <code>NULL</code> values
+   *         <LI>columnNullable - definitely allows <code>NULL</code> values
+   *         <LI>columnNullableUnknown - nullability unknown
+   *       </UL>
+   *   <LI><B>REMARKS</B> String {@code =>} comment describing column (may be <code>null</code>)
+   *   <LI><B>COLUMN_DEF</B> String {@code =>} default value for the column, which should be
+   *       interpreted as a string when the value is enclosed in single quotes (may be <code>null
+   *       </code>)
+   *   <LI><B>SQL_DATA_TYPE</B> int {@code =>} unused
+   *   <LI><B>SQL_DATETIME_SUB</B> int {@code =>} unused
+   *   <LI><B>CHAR_OCTET_LENGTH</B> int {@code =>} for char types the maximum number of bytes in the
+   *       column
+   *   <LI><B>ORDINAL_POSITION</B> int {@code =>} index of column in table (starting at 1)
+   *   <LI><B>IS_NULLABLE</B> String {@code =>} ISO rules are used to determine the nullability for
+   *       a column.
+   *       <UL>
+   *         <LI>YES --- if the column can include NULLs
+   *         <LI>NO --- if the column cannot include NULLs
+   *         <LI>empty string --- if the nullability for the column is unknown
+   *       </UL>
+   *   <LI><B>SCOPE_CATALOG</B> String {@code =>} catalog of table that is the scope of a reference
+   *       attribute (<code>null</code> if DATA_TYPE isn't REF)
+   *   <LI><B>SCOPE_SCHEMA</B> String {@code =>} schema of table that is the scope of a reference
+   *       attribute (<code>null</code> if the DATA_TYPE isn't REF)
+   *   <LI><B>SCOPE_TABLE</B> String {@code =>} table name that this the scope of a reference
+   *       attribute (<code>null</code> if the DATA_TYPE isn't REF)
+   *   <LI><B>SOURCE_DATA_TYPE</B> short {@code =>} source type of a distinct type or user-generated
+   *       Ref type, SQL type from java.sql.Types (<code>null</code> if DATA_TYPE isn't DISTINCT or
+   *       user-generated REF)
+   *   <LI><B>IS_AUTOINCREMENT</B> String {@code =>} Indicates whether this column is auto
+   *       incremented
+   *       <UL>
+   *         <LI>YES --- if the column is auto incremented
+   *         <LI>NO --- if the column is not auto incremented
+   *         <LI>empty string --- if it cannot be determined whether the column is auto incremented
+   *       </UL>
+   *   <LI><B>IS_GENERATEDCOLUMN</B> String {@code =>} Indicates whether this is a generated column
+   *       <UL>
+   *         <LI>YES --- if this a generated column
+   *         <LI>NO --- if this not a generated column
+   *         <LI>empty string --- if it cannot be determined whether this is a generated column
+   *       </UL>
+   * </OL>
+   *
+   * <p>The COLUMN_SIZE column specifies the column size for the given column. 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. Null is returned for data
+   * types where the column size is not applicable.
+   *
+   * @param catalog a catalog name; must match the catalog name as it is stored in the database; ""
+   *     retrieves those without a catalog; <code>null</code> means that the catalog name should not
+   *     be used to narrow the search
+   * @param schemaPattern a schema name pattern; must match the schema name as it is stored in the
+   *     database; "" retrieves those without a schema; <code>null</code> means that the schema name
+   *     should not be used to narrow the search
+   * @param tableNamePattern a table name pattern; must match the table name as it is stored in the
+   *     database
+   * @param columnNamePattern a column name pattern; must match the column name as it is stored in
+   *     the database
+   * @return <code>ResultSet</code> - each row is a column description
+   * @throws SQLException if a database access error occurs
+   * @see #getSearchStringEscape
+   */
+  public ResultSet getColumns(
+      String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern)
+      throws SQLException {
+
+    ServerVersion version = connection.getContext().getVersion();
+    boolean supportsFractionalSeconds =
+        version.isMariaDBServer()
+            /* "In MariaDB 5.3 and later, the TIME, DATETIME, and TIMESTAMP types, along with the temporal
+            functions, CAST and dynamic columns, now support microseconds."
+            https://web.archive.org/web/20130928042640/https://mariadb.com/kb/en/microseconds-in-mariadb/
+            */
+            ? version.versionGreaterOrEqual(5, 3, 0)
+            // See https://dev.mysql.com/doc/relnotes/mysql/5.6/en/news-5-6-4.html
+            : version.versionGreaterOrEqual(5, 6, 4);
+    StringBuilder sb = new StringBuilder();
+    sb.append(
+        "SELECT TABLE_SCHEMA TABLE_CAT, NULL TABLE_SCHEM, TABLE_NAME, COLUMN_NAME,"
+            + dataTypeClause("COLUMN_TYPE")
+            + " DATA_TYPE,"
+            + DataTypeClause(conf)
+            + " TYPE_NAME, "
+            + " CASE DATA_TYPE"
+            + "  WHEN 'date' THEN 10");
+    if (supportsFractionalSeconds) {
+      sb.append(
+          "  WHEN 'time' THEN "
+              + "IF(DATETIME_PRECISION = 0, 10, CAST(11 + DATETIME_PRECISION as signed integer))"
+              + "  WHEN 'datetime' THEN "
+              + "IF(DATETIME_PRECISION = 0, 19, CAST(20 + DATETIME_PRECISION as signed integer))"
+              + "  WHEN 'timestamp' THEN "
+              + "IF(DATETIME_PRECISION = 0, 19, CAST(20 + DATETIME_PRECISION as signed integer))");
+    } else {
+      // Older versions do not include the DATETIME_PRECISION column in INFORMATION_SCHEMA.COLUMNS.
+      sb.append(" WHEN 'time' THEN 10 WHEN 'datetime' THEN 19 WHEN 'timestamp' THEN 19");
+    }
+    sb.append(
+        (conf.yearIsDateType() ? "" : " WHEN 'year' THEN 5")
+            + "  ELSE "
+            + "  IF(NUMERIC_PRECISION IS NULL, LEAST(CHARACTER_MAXIMUM_LENGTH,"
+            + Integer.MAX_VALUE
+            + "), NUMERIC_PRECISION) "
+            + " END"
+            + " COLUMN_SIZE, 65535 BUFFER_LENGTH, "
+            + " CONVERT (CASE DATA_TYPE"
+            + " WHEN 'year' THEN "
+            + (conf.yearIsDateType() ? "NUMERIC_SCALE" : "0")
+            + " WHEN 'tinyint' THEN "
+            + (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,  "
+            + " LEAST(CHARACTER_OCTET_LENGTH,"
+            + Integer.MAX_VALUE
+            + ") CHAR_OCTET_LENGTH, ORDINAL_POSITION, IS_NULLABLE, NULL SCOPE_CATALOG, NULL"
+            + " SCOPE_SCHEMA, NULL SCOPE_TABLE, NULL SOURCE_DATA_TYPE, IF(EXTRA ="
+            + " 'auto_increment','YES','NO') IS_AUTOINCREMENT,  IF(EXTRA in ('VIRTUAL',"
+            + " 'PERSISTENT', 'VIRTUAL GENERATED', 'STORED GENERATED') ,'YES','NO')"
+            + " IS_GENERATEDCOLUMN  FROM INFORMATION_SCHEMA.COLUMNS");
+    boolean firstCondition = catalogCond(true, sb, "TABLE_SCHEMA", catalog);
+    firstCondition = patternCond(firstCondition, sb, "TABLE_NAME", tableNamePattern);
+    firstCondition = patternCond(firstCondition, sb, "COLUMN_NAME", columnNamePattern);
+    sb.append(" ORDER BY TABLE_CAT, TABLE_SCHEM, TABLE_NAME, ORDINAL_POSITION");
+    return executeQuery(sb.toString());
+  }
+
+  /**
+   * Retrieves a description of the foreign key columns that reference the given table's primary key
+   * columns (the foreign keys exported by a table). They are ordered by FKTABLE_CAT, FKTABLE_SCHEM,
+   * FKTABLE_NAME, and KEY_SEQ.
+   *
+   * <p>Each foreign key column description has the following columns:
+   *
+   * <OL>
+   *   <LI><B>PKTABLE_CAT</B> String {@code =>} primary key table catalog (may be <code>null</code>)
+   *   <LI><B>PKTABLE_SCHEM</B> String {@code =>} primary key table schema (may be <code>null</code>
+   *       )
+   *   <LI><B>PKTABLE_NAME</B> String {@code =>} primary key table name
+   *   <LI><B>PKCOLUMN_NAME</B> String {@code =>} primary key column name
+   *   <LI><B>FKTABLE_CAT</B> String {@code =>} foreign key table catalog (may be <code>null</code>)
+   *       being exported (may be <code>null</code>)
+   *   <LI><B>FKTABLE_SCHEM</B> String {@code =>} foreign key table schema (may be <code>null</code>
+   *       ) being exported (may be <code>null</code>)
+   *   <LI><B>FKTABLE_NAME</B> String {@code =>} foreign key table name being exported
+   *   <LI><B>FKCOLUMN_NAME</B> String {@code =>} foreign key column name being exported
+   *   <LI><B>KEY_SEQ</B> short {@code =>} sequence number within foreign key( a value of 1
+   *       represents the first column of the foreign key, a value of 2 would represent the second
+   *       column within the foreign key).
+   *   <LI><B>UPDATE_RULE</B> short {@code =>} What happens to foreign key when primary is updated:
+   *       <UL>
+   *         <LI>importedNoAction - do not allow update of primary key if it has been imported
+   *         <LI>importedKeyCascade - change imported key to agree with primary key update
+   *         <LI>importedKeySetNull - change imported key to <code>NULL</code> if its primary key
+   *             has been updated
+   *         <LI>importedKeySetDefault - change imported key to default values if its primary key
+   *             has been updated
+   *         <LI>importedKeyRestrict - same as importedKeyNoAction (for ODBC 2.x compatibility)
+   *       </UL>
+   *   <LI><B>DELETE_RULE</B> short {@code =>} What happens to the foreign key when primary is
+   *       deleted.
+   *       <UL>
+   *         <LI>importedKeyNoAction - do not allow delete of primary key if it has been imported
+   *         <LI>importedKeyCascade - delete rows that import a deleted key
+   *         <LI>importedKeySetNull - change imported key to <code>NULL</code> if its primary key
+   *             has been deleted
+   *         <LI>importedKeyRestrict - same as importedKeyNoAction (for ODBC 2.x compatibility)
+   *         <LI>importedKeySetDefault - change imported key to default if its primary key has been
+   *             deleted
+   *       </UL>
+   *   <LI><B>FK_NAME</B> String {@code =>} foreign key name (may be <code>null</code>)
+   *   <LI><B>PK_NAME</B> String {@code =>} primary key name (may be <code>null</code>)
+   *   <LI><B>DEFERRABILITY</B> short {@code =>} can the evaluation of foreign key constraints be
+   *       deferred until commit
+   *       <UL>
+   *         <LI>importedKeyInitiallyDeferred - see SQL92 for definition
+   *         <LI>importedKeyInitiallyImmediate - see SQL92 for definition
+   *         <LI>importedKeyNotDeferrable - see SQL92 for definition
+   *       </UL>
+   * </OL>
+   *
+   * @param catalog a catalog name; must match the catalog name as it is stored in this database; ""
+   *     retrieves those without a catalog; <code>null</code> means that the catalog name should not
+   *     be used to narrow the search
+   * @param schema a schema name; must match the schema name as it is stored in the database; ""
+   *     retrieves those without a schema; <code>null</code> means that the schema name should not
+   *     be used to narrow the search
+   * @param table a table name; must match the table name as it is stored in this database
+   * @return a <code>ResultSet</code> object in which each row is a foreign key column description
+   * @throws SQLException if a database access error occurs
+   * @see #getImportedKeys
+   */
+  public ResultSet getExportedKeys(String catalog, String schema, String table)
+      throws SQLException {
+    StringBuilder sb =
+        new StringBuilder(
+            "SELECT KCU.REFERENCED_TABLE_SCHEMA PKTABLE_CAT, NULL PKTABLE_SCHEM, "
+                + " KCU.REFERENCED_TABLE_NAME PKTABLE_NAME, KCU.REFERENCED_COLUMN_NAME"
+                + " PKCOLUMN_NAME, KCU.TABLE_SCHEMA FKTABLE_CAT, NULL FKTABLE_SCHEM, "
+                + " KCU.TABLE_NAME FKTABLE_NAME, KCU.COLUMN_NAME FKCOLUMN_NAME,"
+                + " KCU.POSITION_IN_UNIQUE_CONSTRAINT KEY_SEQ, CASE update_rule    WHEN 'RESTRICT'"
+                + " THEN 1   WHEN 'NO ACTION' THEN 3   WHEN 'CASCADE' THEN 0   WHEN 'SET NULL' THEN"
+                + " 2   WHEN 'SET DEFAULT' THEN 4 END UPDATE_RULE, CASE DELETE_RULE  WHEN"
+                + " 'RESTRICT' THEN 1  WHEN 'NO ACTION' THEN 3  WHEN 'CASCADE' THEN 0  WHEN 'SET"
+                + " NULL' THEN 2  WHEN 'SET DEFAULT' THEN 4 END DELETE_RULE, RC.CONSTRAINT_NAME"
+                + " FK_NAME, RC.UNIQUE_CONSTRAINT_NAME PK_NAME,"
+                + importedKeyNotDeferrable
+                + " 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");
+    boolean firstCondition = true;
+    firstCondition = catalogCond(firstCondition, sb, "KCU.REFERENCED_TABLE_SCHEMA", catalog);
+    firstCondition = patternCond(firstCondition, sb, "KCU.REFERENCED_TABLE_NAME", table);
+    sb.append(" ORDER BY FKTABLE_CAT, FKTABLE_SCHEM, FKTABLE_NAME, KEY_SEQ");
+
+    return executeQuery(sb.toString());
+  }
+
+  /**
+   * GetImportedKeysUsingInformationSchema.
+   *
+   * @param catalog catalog
+   * @param table table
+   * @return resultset
+   * @throws SQLException exception
+   */
+  public ResultSet getImportedKeysUsingInformationSchema(final String catalog, String table)
+      throws SQLException {
+    if (table == null) {
+      throw new SQLException("'table' parameter in getImportedKeys cannot be null");
+    }
+    StringBuilder sb =
+        new StringBuilder(
+            "SELECT KCU.REFERENCED_TABLE_SCHEMA PKTABLE_CAT, NULL PKTABLE_SCHEM, "
+                + " KCU.REFERENCED_TABLE_NAME PKTABLE_NAME, KCU.REFERENCED_COLUMN_NAME"
+                + " PKCOLUMN_NAME, KCU.TABLE_SCHEMA FKTABLE_CAT, NULL FKTABLE_SCHEM, "
+                + " KCU.TABLE_NAME FKTABLE_NAME, KCU.COLUMN_NAME FKCOLUMN_NAME,"
+                + " KCU.POSITION_IN_UNIQUE_CONSTRAINT KEY_SEQ, CASE update_rule    WHEN 'RESTRICT'"
+                + " THEN 1   WHEN 'NO ACTION' THEN 3   WHEN 'CASCADE' THEN 0   WHEN 'SET NULL' THEN"
+                + " 2   WHEN 'SET DEFAULT' THEN 4 END UPDATE_RULE, CASE DELETE_RULE  WHEN"
+                + " 'RESTRICT' THEN 1  WHEN 'NO ACTION' THEN 3  WHEN 'CASCADE' THEN 0  WHEN 'SET"
+                + " NULL' THEN 2  WHEN 'SET DEFAULT' THEN 4 END DELETE_RULE, RC.CONSTRAINT_NAME"
+                + " FK_NAME, RC.UNIQUE_CONSTRAINT_NAME PK_NAME,"
+                + importedKeyNotDeferrable
+                + " 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 ");
+
+    boolean firstCondition = catalogCond(true, sb, "KCU.TABLE_SCHEMA", catalog);
+    sb.append(firstCondition ? " WHERE " : " AND ")
+        .append("KCU.TABLE_NAME = ")
+        .append(escapeQuote(table));
+
+    sb.append(" ORDER BY PKTABLE_CAT, PKTABLE_SCHEM, PKTABLE_NAME, KEY_SEQ");
+
+    return executeQuery(sb.toString());
+  }
+
+  /**
+   * GetImportedKeysUsingShowCreateTable.
+   *
+   * @param catalog catalog
+   * @param table table
+   * @return resultset
+   * @throws Exception exception
+   */
+  public ResultSet getImportedKeysUsingShowCreateTable(final String catalog, String table)
+      throws Exception {
+    ResultSet rs =
+        connection
+            .createStatement()
+            .executeQuery(
+                "SHOW CREATE TABLE " + quoteIdentifier(catalog) + "." + quoteIdentifier(table));
+    rs.next();
+    String tableDef = rs.getString(2);
+    return getImportedKeys(tableDef, table, catalog, connection);
+  }
+
+  /**
+   * Retrieves a description of a table's optimal set of columns that uniquely identifies a row.
+   * They are ordered by SCOPE.
+   *
+   * <p>Each column description has the following columns:
+   *
+   * <OL>
+   *   <LI><B>SCOPE</B> short {@code =>} actual scope of result
+   *       <UL>
+   *         <LI>bestRowTemporary - very temporary, while using row
+   *         <LI>bestRowTransaction - valid for remainder of current transaction
+   *         <LI>bestRowSession - valid for remainder of current session
+   *       </UL>
+   *   <LI><B>COLUMN_NAME</B> String {@code =>} column name
+   *   <LI><B>DATA_TYPE</B> int {@code =>} SQL data type from java.sql.Types
+   *   <LI><B>TYPE_NAME</B> String {@code =>} Data source dependent type name, for a UDT the type
+   *       name is fully qualified
+   *   <LI><B>COLUMN_SIZE</B> int {@code =>} precision
+   *   <LI><B>BUFFER_LENGTH</B> int {@code =>} not used
+   *   <LI><B>DECIMAL_DIGITS</B> short {@code =>} scale - Null is returned for data types where
+   *       DECIMAL_DIGITS is not applicable.
+   *   <LI><B>PSEUDO_COLUMN</B> short {@code =>} is this a pseudo column like an Oracle ROWID
+   *       <UL>
+   *         <LI>bestRowUnknown - may or may not be pseudo column
+   *         <LI>bestRowNotPseudo - is NOT a pseudo column
+   *         <LI>bestRowPseudo - is a pseudo column
+   *       </UL>
+   * </OL>
+   *
+   * <p>The COLUMN_SIZE column represents the specified column size for the given column. 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. Null is returned for data types where the column size is not applicable.
+   *
+   * @param catalog a catalog name; must match the catalog name as it is stored in the database; ""
+   *     retrieves those without a catalog; <code>null</code> means that the catalog name should not
+   *     be used to narrow the search
+   * @param schema a schema name; must match the schema name as it is stored in the database; ""
+   *     retrieves those without a schema; <code>null</code> means that the schema name should not
+   *     be used to narrow the search
+   * @param table a table name; must match the table name as it is stored in the database
+   * @param scope the scope of interest; use same values as SCOPE
+   * @param nullable include columns that are nullable.
+   * @return <code>ResultSet</code> - each row is a column description
+   * @throws SQLException if a database access error occurs
+   */
+  public ResultSet getBestRowIdentifier(
+      String catalog, String schema, String table, int scope, final boolean nullable)
+      throws SQLException {
+
+    if (table == null) {
+      throw new SQLException("'table' parameter cannot be null in getBestRowIdentifier()");
+    }
+    boolean hasIsGeneratedCol =
+        (connection.getContext().getVersion().isMariaDBServer()
+            && connection.getContext().getVersion().versionGreaterOrEqual(10, 2, 0));
+
+    StringBuilder sbInner =
+        new StringBuilder("SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE COLUMN_KEY = 'PRI'");
+    catalogCond(false, sbInner, "TABLE_SCHEMA", catalog);
+    sbInner.append(" AND TABLE_NAME = ").append(escapeQuote(table));
+
+    StringBuilder sb =
+        new StringBuilder(
+            "SELECT "
+                + 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,"
+                + (hasIsGeneratedCol
+                    ? ("IF(IS_GENERATED='NEVER'," + bestRowNotPseudo + "," + bestRowPseudo + ")")
+                    : bestRowNotPseudo)
+                + " PSEUDO_COLUMN"
+                + " FROM INFORMATION_SCHEMA.COLUMNS"
+                + " WHERE (COLUMN_KEY  = 'PRI'"
+                + " OR (COLUMN_KEY = 'UNI' AND NOT EXISTS ("
+                + sbInner
+                + " )))");
+    catalogCond(false, sb, "TABLE_SCHEMA", catalog);
+    sb.append(" AND TABLE_NAME = ").append(escapeQuote(table));
+    if (!nullable) sb.append(" AND IS_NULLABLE = 'NO'");
+
+    return executeQuery(sb.toString());
+  }
+
+  public boolean generatedKeyAlwaysReturned() {
+    return true;
+  }
+
+  /**
+   * Retrieves a description of the pseudo or hidden columns available in a given table within the
+   * specified catalog and schema. Pseudo or hidden columns may not always be stored within a table
+   * and are not visible in a ResultSet unless they are specified in the query's outermost SELECT
+   * list. Pseudo or hidden columns may not necessarily be able to be modified. If there are no
+   * pseudo or hidden columns, an empty ResultSet is returned.
+   *
+   * <p>Only column descriptions matching the catalog, schema, table and column name criteria are
+   * returned. They are ordered by <code>TABLE_CAT</code>,<code>TABLE_SCHEM</code>, <code>TABLE_NAME
+   * </code> and <code>COLUMN_NAME</code>.
+   *
+   * <p>Each column description has the following columns:
+   *
+   * <OL>
+   *   <LI><B>TABLE_CAT</B> String {@code =>} table catalog (may be <code>null</code>)
+   *   <LI><B>TABLE_SCHEM</B> String {@code =>} table schema (may be <code>null</code>)
+   *   <LI><B>TABLE_NAME</B> String {@code =>} table name
+   *   <LI><B>COLUMN_NAME</B> String {@code =>} column name
+   *   <LI><B>DATA_TYPE</B> int {@code =>} SQL type from java.sql.Types
+   *   <LI><B>COLUMN_SIZE</B> int {@code =>} column size.
+   *   <LI><B>DECIMAL_DIGITS</B> int {@code =>} the number of fractional digits. Null is returned
+   *       for data types where DECIMAL_DIGITS is not applicable.
+   *   <LI><B>NUM_PREC_RADIX</B> int {@code =>} Radix (typically either 10 or 2)
+   *   <LI><B>COLUMN_USAGE</B> String {@code =>} The allowed usage for the column. The value
+   *       returned will correspond to the enum name returned by PseudoColumnUsage.name()
+   *   <LI><B>REMARKS</B> String {@code =>} comment describing column (may be <code>null</code>)
+   *   <LI><B>CHAR_OCTET_LENGTH</B> int {@code =>} for char types the maximum number of bytes in the
+   *       column
+   *   <LI><B>IS_NULLABLE</B> String {@code =>} ISO rules are used to determine the nullability for
+   *       a column.
+   *       <UL>
+   *         <LI>YES --- if the column can include NULLs
+   *         <LI>NO --- if the column cannot include NULLs
+   *         <LI>empty string --- if the nullability for the column is unknown
+   *       </UL>
+   * </OL>
+   *
+   * <p>The COLUMN_SIZE column specifies the column size for the given column. 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. Null is returned for data
+   * types where the column size is not applicable.
+   *
+   * @param catalog a catalog name; must match the catalog name as it is stored in the database; ""
+   *     retrieves those without a catalog; <code>null</code> means that the catalog name should not
+   *     be used to narrow the search
+   * @param schemaPattern a schema name pattern; must match the schema name as it is stored in the
+   *     database; "" retrieves those without a schema; <code>null</code> means that the schema name
+   *     should not be used to narrow the search
+   * @param tableNamePattern a table name pattern; must match the table name as it is stored in the
+   *     database
+   * @param columnNamePattern a column name pattern; must match the column name as it is stored in
+   *     the database
+   * @return <code>ResultSet</code> - each row is a column description
+   * @throws SQLException if a database access error occurs
+   * @see PseudoColumnUsage
+   * @since 1.7
+   */
+  public ResultSet getPseudoColumns(
+      String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern)
+      throws SQLException {
+    return connection
+        .createStatement()
+        .executeQuery(
+            "SELECT ' ' TABLE_CAT, ' ' TABLE_SCHEM, ' ' TABLE_NAME, ' ' COLUMN_NAME, 0 DATA_TYPE, 0"
+                + " COLUMN_SIZE, 0 DECIMAL_DIGITS, 10 NUM_PREC_RADIX, ' ' COLUMN_USAGE,  ' '"
+                + " REMARKS, 0 CHAR_OCTET_LENGTH, 'YES' IS_NULLABLE FROM DUAL WHERE 1=0");
+  }
+
+  public boolean allProceduresAreCallable() {
+    return true;
+  }
+
+  public boolean allTablesAreSelectable() {
+    return true;
+  }
+
+  @Override
+  public String getURL() {
+    return conf.initialUrl();
+  }
+
+  public String getUserName() {
+    return conf.user();
+  }
+
+  public boolean isReadOnly() throws SQLException {
+    java.sql.Statement st = connection.createStatement();
+    ResultSet rs = st.executeQuery("SELECT @@READ_ONLY");
+    rs.next();
+    return rs.getInt(1) == 1;
+  }
+
+  public boolean nullsAreSortedHigh() {
+    return false;
+  }
+
+  public boolean nullsAreSortedLow() {
+    return true;
+  }
+
+  public boolean nullsAreSortedAtStart() {
+    return false;
+  }
+
+  public boolean nullsAreSortedAtEnd() {
+    return true;
+  }
+
+  /**
+   * Return Server type. MySQL or MariaDB. MySQL can be forced for compatibility with option
+   * "useMysqlMetadata"
+   *
+   * @return server type
+   */
+  public String getDatabaseProductName() {
+    if (!conf.useMysqlMetadata() && connection.getContext().getVersion().isMariaDBServer()) {
+      return "MariaDB";
+    }
+    return "MySQL";
+  }
+
+  public String getDatabaseProductVersion() {
+    return connection.getContext().getVersion().getVersion();
+  }
+
+  public String getDriverName() {
+    return DRIVER_NAME;
+  }
+
+  public String getDriverVersion() {
+    return VersionFactory.getInstance().getVersion();
+  }
+
+  public int getDriverMajorVersion() {
+    return VersionFactory.getInstance().getMajorVersion();
+  }
+
+  public int getDriverMinorVersion() {
+    return VersionFactory.getInstance().getMinorVersion();
+  }
+
+  public boolean usesLocalFiles() {
+    return false;
+  }
+
+  public boolean usesLocalFilePerTable() {
+    return false;
+  }
+
+  public boolean supportsMixedCaseIdentifiers() throws SQLException {
+    return (connection.getLowercaseTableNames() == 0);
+  }
+
+  public boolean storesUpperCaseIdentifiers() {
+    return false;
+  }
+
+  public boolean storesLowerCaseIdentifiers() throws SQLException {
+    return (connection.getLowercaseTableNames() == 1);
+  }
+
+  public boolean storesMixedCaseIdentifiers() throws SQLException {
+    return (connection.getLowercaseTableNames() == 2);
+  }
+
+  public boolean supportsMixedCaseQuotedIdentifiers() throws SQLException {
+    return supportsMixedCaseIdentifiers();
+  }
+
+  public boolean storesUpperCaseQuotedIdentifiers() {
+    return storesUpperCaseIdentifiers();
+  }
+
+  public boolean storesLowerCaseQuotedIdentifiers() throws SQLException {
+    return storesLowerCaseIdentifiers();
+  }
+
+  public boolean storesMixedCaseQuotedIdentifiers() throws SQLException {
+    return storesMixedCaseIdentifiers();
+  }
+
+  public String getIdentifierQuoteString() {
+    return "`";
+  }
+
+  /**
+   * Retrieves a comma-separated list of all of this database's SQL keywords that are NOT also
+   * SQL:2003 keywords.
+   *
+   * @return the list of this database's keywords that are not also SQL:2003 keywords
+   */
+  @Override
+  public String getSQLKeywords() {
+    return "ACCESSIBLE,ANALYZE,ASENSITIVE,BEFORE,BIGINT,BINARY,BLOB,CALL,CHANGE,CONDITION,DATABASE,DATABASES,"
+               + "DAY_HOUR,DAY_MICROSECOND,DAY_MINUTE,DAY_SECOND,DELAYED,DETERMINISTIC,DISTINCTROW,DIV,DUAL,EACH,"
+               + "ELSEIF,ENCLOSED,ESCAPED,EXIT,EXPLAIN,FLOAT4,FLOAT8,FORCE,FULLTEXT,GENERAL,HIGH_PRIORITY,"
+               + "HOUR_MICROSECOND,HOUR_MINUTE,HOUR_SECOND,IF,IGNORE,IGNORE_SERVER_IDS,INDEX,INFILE,INOUT,INT1,INT2,"
+               + "INT3,INT4,INT8,ITERATE,KEY,KEYS,KILL,LEAVE,LIMIT,LINEAR,LINES,LOAD,LOCALTIME,LOCALTIMESTAMP,LOCK,"
+               + "LONG,LONGBLOB,LONGTEXT,LOOP,LOW_PRIORITY,MASTER_HEARTBEAT_PERIOD,MASTER_SSL_VERIFY_SERVER_CERT,"
+               + "MAXVALUE,MEDIUMBLOB,MEDIUMINT,MEDIUMTEXT,MIDDLEINT,MINUTE_MICROSECOND,MINUTE_SECOND,MOD,MODIFIES,"
+               + "NO_WRITE_TO_BINLOG,OPTIMIZE,OPTIONALLY,OUT,OUTFILE,PURGE,RANGE,READ_WRITE,READS,REGEXP,RELEASE,"
+               + "RENAME,REPEAT,REPLACE,REQUIRE,RESIGNAL,RESTRICT,RETURN,RLIKE,SCHEMAS,SECOND_MICROSECOND,SENSITIVE,"
+               + "SEPARATOR,SHOW,SIGNAL,SLOW,SPATIAL,SPECIFIC,SQL_BIG_RESULT,SQL_CALC_FOUND_ROWS,SQL_SMALL_RESULT,"
+               + "SQLEXCEPTION,SSL,STARTING,STRAIGHT_JOIN,TERMINATED,TINYBLOB,TINYINT,TINYTEXT,TRIGGER,UNDO,UNLOCK,"
+               + "UNSIGNED,USE,UTC_DATE,UTC_TIME,UTC_TIMESTAMP,VARBINARY,VARCHARACTER,WHILE,XOR,YEAR_MONTH,ZEROFILL";
+  }
+
+  /**
+   * List of numeric functions.
+   *
+   * @return List of numeric functions.
+   */
+  @Override
+  public String getNumericFunctions() {
+    return "DIV,ABS,ACOS,ASIN,ATAN,ATAN2,CEIL,CEILING,CONV,COS,COT,CRC32,DEGREES,EXP,FLOOR,GREATEST,LEAST,LN,LOG,"
+               + "LOG10,LOG2,MOD,OCT,PI,POW,POWER,RADIANS,RAND,ROUND,SIGN,SIN,SQRT,TAN,TRUNCATE";
+  }
+
+  /**
+   * List of string functions.
+   *
+   * @return List of string functions.
+   */
+  @Override
+  public String getStringFunctions() {
+    return "ASCII,BIN,BIT_LENGTH,CAST,CHARACTER_LENGTH,CHAR_LENGTH,CONCAT,CONCAT_WS,CONVERT,ELT,EXPORT_SET,"
+               + "EXTRACTVALUE,FIELD,FIND_IN_SET,FORMAT,FROM_BASE64,HEX,INSTR,LCASE,LEFT,LENGTH,LIKE,LOAD_FILE,LOCATE,LOWER,LPAD,LTRIM,MAKE_SET,MATCH"
+               + " AGAINST,MID,NOT LIKE,NOT"
+               + " REGEXP,OCTET_LENGTH,ORD,POSITION,QUOTE,REPEAT,REPLACE,REVERSE,RIGHT,RPAD,RTRIM,SOUNDEX,SOUNDS"
+               + " LIKE,SPACE,STRCMP,SUBSTR,SUBSTRING,"
+               + "SUBSTRING_INDEX,TO_BASE64,TRIM,UCASE,UNHEX,UPDATEXML,UPPER,WEIGHT_STRING";
+  }
+
+  /**
+   * List of system functions.
+   *
+   * @return List of system functions.
+   */
+  @Override
+  public String getSystemFunctions() {
+    return "DATABASE,USER,SYSTEM_USER,SESSION_USER,LAST_INSERT_ID,VERSION";
+  }
+
+  /**
+   * List of time and date functions.
+   *
+   * @return List of time and date functions.
+   */
+  @Override
+  public String getTimeDateFunctions() {
+    return "ADDDATE,ADDTIME,CONVERT_TZ,CURDATE,CURRENT_DATE,CURRENT_TIME,CURRENT_TIMESTAMP,CURTIME,DATEDIFF,"
+               + "DATE_ADD,DATE_FORMAT,DATE_SUB,DAY,DAYNAME,DAYOFMONTH,DAYOFWEEK,DAYOFYEAR,EXTRACT,FROM_DAYS,"
+               + "FROM_UNIXTIME,GET_FORMAT,HOUR,LAST_DAY,LOCALTIME,LOCALTIMESTAMP,MAKEDATE,MAKETIME,MICROSECOND,"
+               + "MINUTE,MONTH,MONTHNAME,NOW,PERIOD_ADD,PERIOD_DIFF,QUARTER,SECOND,SEC_TO_TIME,STR_TO_DATE,SUBDATE,"
+               + "SUBTIME,SYSDATE,TIMEDIFF,TIMESTAMPADD,TIMESTAMPDIFF,TIME_FORMAT,TIME_TO_SEC,TO_DAYS,TO_SECONDS,"
+               + "UNIX_TIMESTAMP,UTC_DATE,UTC_TIME,UTC_TIMESTAMP,WEEK,WEEKDAY,WEEKOFYEAR,YEAR,YEARWEEK";
+  }
+
+  public String getSearchStringEscape() {
+    return "\\";
+  }
+
+  public String getExtraNameCharacters() {
+    return "#@";
+  }
+
+  public boolean supportsAlterTableWithAddColumn() {
+    return true;
+  }
+
+  public boolean supportsAlterTableWithDropColumn() {
+    return true;
+  }
+
+  public boolean supportsColumnAliasing() {
+    return true;
+  }
+
+  public boolean nullPlusNonNullIsNull() {
+    return true;
+  }
+
+  public boolean supportsConvert() {
+    return true;
+  }
+
+  /**
+   * Retrieves whether this database supports the JDBC scalar function CONVERT for conversions
+   * between the JDBC types fromType and toType. The JDBC types are the generic SQL data types
+   * defined in java.sql.Types.
+   *
+   * @param fromType the type to convert from; one of the type codes from the class java.sql.Types
+   * @param toType the type to convert to; one of the type codes from the class java.sql.Types
+   * @return true if so; false otherwise
+   */
+  public boolean supportsConvert(int fromType, int toType) {
+    switch (fromType) {
+      case Types.TINYINT:
+      case Types.SMALLINT:
+      case Types.INTEGER:
+      case Types.BIGINT:
+      case Types.REAL:
+      case Types.FLOAT:
+      case Types.DECIMAL:
+      case Types.NUMERIC:
+      case Types.DOUBLE:
+      case Types.BIT:
+      case Types.BOOLEAN:
+        switch (toType) {
+          case Types.TINYINT:
+          case Types.SMALLINT:
+          case Types.INTEGER:
+          case Types.BIGINT:
+          case Types.REAL:
+          case Types.FLOAT:
+          case Types.DECIMAL:
+          case Types.NUMERIC:
+          case Types.DOUBLE:
+          case Types.BIT:
+          case Types.BOOLEAN:
+          case Types.CHAR:
+          case Types.VARCHAR:
+          case Types.LONGVARCHAR:
+          case Types.BINARY:
+          case Types.VARBINARY:
+          case Types.LONGVARBINARY:
+            return true;
+          default:
+            return false;
+        }
+
+      case Types.BLOB:
+        switch (toType) {
+          case Types.BINARY:
+          case Types.VARBINARY:
+          case Types.LONGVARBINARY:
+          case Types.CHAR:
+          case Types.VARCHAR:
+          case Types.LONGVARCHAR:
+          case Types.TINYINT:
+          case Types.SMALLINT:
+          case Types.INTEGER:
+          case Types.BIGINT:
+          case Types.REAL:
+          case Types.FLOAT:
+          case Types.DECIMAL:
+          case Types.NUMERIC:
+          case Types.DOUBLE:
+          case Types.BIT:
+          case Types.BOOLEAN:
+            return true;
+          default:
+            return false;
+        }
+
+      case Types.CHAR:
+      case Types.CLOB:
+      case Types.VARCHAR:
+      case Types.LONGVARCHAR:
+      case Types.BINARY:
+      case Types.VARBINARY:
+      case Types.LONGVARBINARY:
+        switch (toType) {
+          case Types.BIT:
+          case Types.TINYINT:
+          case Types.SMALLINT:
+          case Types.INTEGER:
+          case Types.BIGINT:
+          case Types.FLOAT:
+          case Types.REAL:
+          case Types.DOUBLE:
+          case Types.NUMERIC:
+          case Types.DECIMAL:
+          case Types.CHAR:
+          case Types.VARCHAR:
+          case Types.LONGVARCHAR:
+          case Types.BINARY:
+          case Types.VARBINARY:
+          case Types.LONGVARBINARY:
+          case Types.DATE:
+          case Types.TIME:
+          case Types.TIMESTAMP:
+          case Types.BLOB:
+          case Types.CLOB:
+          case Types.BOOLEAN:
+          case Types.NCHAR:
+          case Types.LONGNVARCHAR:
+          case Types.NCLOB:
+            return true;
+          default:
+            return false;
+        }
+
+      case Types.DATE:
+        switch (toType) {
+          case Types.DATE:
+          case Types.CHAR:
+          case Types.VARCHAR:
+          case Types.LONGVARCHAR:
+          case Types.BINARY:
+          case Types.VARBINARY:
+          case Types.LONGVARBINARY:
+            return true;
+
+          default:
+            return false;
+        }
+
+      case Types.TIME:
+        switch (toType) {
+          case Types.TIME:
+          case Types.CHAR:
+          case Types.VARCHAR:
+          case Types.LONGVARCHAR:
+          case Types.BINARY:
+          case Types.VARBINARY:
+          case Types.LONGVARBINARY:
+            return true;
+          default:
+            return false;
+        }
+
+      case Types.TIMESTAMP:
+        switch (toType) {
+          case Types.TIMESTAMP:
+          case Types.CHAR:
+          case Types.VARCHAR:
+          case Types.LONGVARCHAR:
+          case Types.BINARY:
+          case Types.VARBINARY:
+          case Types.LONGVARBINARY:
+          case Types.TIME:
+          case Types.DATE:
+            return true;
+          default:
+            return false;
+        }
+      default:
+        return false;
+    }
+  }
+
+  public boolean supportsTableCorrelationNames() {
+    return true;
+  }
+
+  public boolean supportsDifferentTableCorrelationNames() {
+    return true;
+  }
+
+  public boolean supportsExpressionsInOrderBy() {
+    return true;
+  }
+
+  public boolean supportsOrderByUnrelated() {
+    return true;
+  }
+
+  public boolean supportsGroupBy() {
+    return true;
+  }
+
+  public boolean supportsGroupByUnrelated() {
+    return true;
+  }
+
+  public boolean supportsGroupByBeyondSelect() {
+    return true;
+  }
+
+  public boolean supportsLikeEscapeClause() {
+    return true;
+  }
+
+  public boolean supportsMultipleResultSets() {
+    return true;
+  }
+
+  public boolean supportsMultipleTransactions() {
+    return true;
+  }
+
+  public boolean supportsNonNullableColumns() {
+    return true;
+  }
+
+  @Override
+  public boolean supportsMinimumSQLGrammar() {
+    return true;
+  }
+
+  @Override
+  public boolean supportsCoreSQLGrammar() {
+    return true;
+  }
+
+  @Override
+  public boolean supportsExtendedSQLGrammar() {
+    return true;
+  }
+
+  @Override
+  public boolean supportsANSI92EntryLevelSQL() {
+    return true;
+  }
+
+  @Override
+  public boolean supportsANSI92IntermediateSQL() {
+    return true;
+  }
+
+  @Override
+  public boolean supportsANSI92FullSQL() {
+    return true;
+  }
+
+  public boolean supportsIntegrityEnhancementFacility() {
+    return true;
+  }
+
+  public boolean supportsOuterJoins() {
+    return true;
+  }
+
+  public boolean supportsFullOuterJoins() {
+    return true;
+  }
+
+  public boolean supportsLimitedOuterJoins() {
+    return true;
+  }
+
+  public String getSchemaTerm() {
+    return "schema";
+  }
+
+  public String getProcedureTerm() {
+    return "procedure";
+  }
+
+  public String getCatalogTerm() {
+    return "database";
+  }
+
+  public boolean isCatalogAtStart() {
+    return true;
+  }
+
+  public String getCatalogSeparator() {
+    return ".";
+  }
+
+  public boolean supportsSchemasInDataManipulation() {
+    return false;
+  }
+
+  public boolean supportsSchemasInProcedureCalls() {
+    return false;
+  }
+
+  public boolean supportsSchemasInTableDefinitions() {
+    return false;
+  }
+
+  public boolean supportsSchemasInIndexDefinitions() {
+    return false;
+  }
+
+  public boolean supportsSchemasInPrivilegeDefinitions() {
+    return false;
+  }
+
+  public boolean supportsCatalogsInDataManipulation() {
+    return true;
+  }
+
+  public boolean supportsCatalogsInProcedureCalls() {
+    return true;
+  }
+
+  public boolean supportsCatalogsInTableDefinitions() {
+    return true;
+  }
+
+  public boolean supportsCatalogsInIndexDefinitions() {
+    return true;
+  }
+
+  public boolean supportsCatalogsInPrivilegeDefinitions() {
+    return true;
+  }
+
+  public boolean supportsPositionedDelete() {
+    return false;
+  }
+
+  public boolean supportsPositionedUpdate() {
+    return false;
+  }
+
+  public boolean supportsSelectForUpdate() {
+    return true;
+  }
+
+  public boolean supportsStoredProcedures() {
+    return true;
+  }
+
+  public boolean supportsSubqueriesInComparisons() {
+    return true;
+  }
+
+  public boolean supportsSubqueriesInExists() {
+    return true;
+  }
+
+  public boolean supportsSubqueriesInIns() {
+    return true;
+  }
+
+  public boolean supportsSubqueriesInQuantifieds() {
+    return true;
+  }
+
+  public boolean supportsCorrelatedSubqueries() {
+    return true;
+  }
+
+  public boolean supportsUnion() {
+    return true;
+  }
+
+  public boolean supportsUnionAll() {
+    return true;
+  }
+
+  public boolean supportsOpenCursorsAcrossCommit() {
+    return true;
+  }
+
+  public boolean supportsOpenCursorsAcrossRollback() {
+    return true;
+  }
+
+  public boolean supportsOpenStatementsAcrossCommit() {
+    return true;
+  }
+
+  public boolean supportsOpenStatementsAcrossRollback() {
+    return true;
+  }
+
+  public int getMaxBinaryLiteralLength() {
+    return Integer.MAX_VALUE;
+  }
+
+  public int getMaxCharLiteralLength() {
+    return Integer.MAX_VALUE;
+  }
+
+  public int getMaxColumnNameLength() {
+    return 64;
+  }
+
+  public int getMaxColumnsInGroupBy() {
+    return 64;
+  }
+
+  public int getMaxColumnsInIndex() {
+    return 16;
+  }
+
+  public int getMaxColumnsInOrderBy() {
+    return 64;
+  }
+
+  public int getMaxColumnsInSelect() {
+    return Short.MAX_VALUE;
+  }
+
+  public int getMaxColumnsInTable() {
+    return 0;
+  }
+
+  public int getMaxConnections() {
+    return 0;
+  }
+
+  public int getMaxCursorNameLength() {
+    return 0;
+  }
+
+  public int getMaxIndexLength() {
+    return 256;
+  }
+
+  public int getMaxSchemaNameLength() {
+    return 0;
+  }
+
+  public int getMaxProcedureNameLength() {
+    return 64;
+  }
+
+  public int getMaxCatalogNameLength() {
+    return 0;
+  }
+
+  public int getMaxRowSize() {
+    return 0;
+  }
+
+  public boolean doesMaxRowSizeIncludeBlobs() {
+    return false;
+  }
+
+  public int getMaxStatementLength() {
+    return 0;
+  }
+
+  public int getMaxStatements() {
+    return 0;
+  }
+
+  public int getMaxTableNameLength() {
+    return 64;
+  }
+
+  public int getMaxTablesInSelect() {
+    return 256;
+  }
+
+  public int getMaxUserNameLength() {
+    return 0;
+  }
+
+  public int getDefaultTransactionIsolation() {
+    return java.sql.Connection.TRANSACTION_REPEATABLE_READ;
+  }
+
+  /**
+   * Retrieves whether this database supports transactions. If not, invoking the method <code>commit
+   * </code> is a noop, and the isolation level is <code>TRANSACTION_NONE</code>.
+   *
+   * @return <code>true</code> if transactions are supported; <code>false</code> otherwise
+   */
+  public boolean supportsTransactions() {
+    return true;
+  }
+
+  /* Helper to generate  information schema with "equality" condition (typically on catalog name)
+   */
+
+  /**
+   * Retrieves whether this database supports the given transaction isolation level.
+   *
+   * @param level one of the transaction isolation levels defined in <code>java.sql.Connection
+   *     </code>
+   * @return <code>true</code> if so; <code>false</code> otherwise
+   * @see java.sql.Connection
+   */
+  public boolean supportsTransactionIsolationLevel(int level) {
+    switch (level) {
+      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;
+    }
+  }
+
+  public boolean supportsDataDefinitionAndDataManipulationTransactions() {
+    return true;
+  }
+
+  public boolean supportsDataManipulationTransactionsOnly() {
+    return false;
+  }
+
+  public boolean dataDefinitionCausesTransactionCommit() {
+    return true;
+  }
+
+  public boolean dataDefinitionIgnoredInTransactions() {
+    return false;
+  }
+
+  /**
+   * Retrieves a description of the stored procedures available in the given catalog. Only procedure
+   * descriptions matching the schema and procedure name criteria are returned. They are ordered by
+   * <code>PROCEDURE_CAT</code>, <code>PROCEDURE_SCHEM</code>, <code>PROCEDURE_NAME</code> and
+   * <code>SPECIFIC_ NAME</code>.
+   *
+   * <p>Each procedure description has the following columns:
+   *
+   * <OL>
+   *   <LI><B>PROCEDURE_CAT</B> String {@code =>} procedure catalog (may be <code>null</code>)
+   *   <LI><B>PROCEDURE_SCHEM</B> String {@code =>} procedure schema (may be <code>null</code>)
+   *   <LI><B>PROCEDURE_NAME</B> String {@code =>} procedure name
+   *   <LI>reserved for future use
+   *   <LI>reserved for future use
+   *   <LI>reserved for future use
+   *   <LI><B>REMARKS</B> String {@code =>} explanatory comment on the procedure
+   *   <LI><B>PROCEDURE_TYPE</B> short {@code =>} kind of procedure:
+   *       <UL>
+   *         <LI>procedureResultUnknown - Cannot determine if a return value will be returned
+   *         <LI>procedureNoResult - Does not return a return value
+   *         <LI>procedureReturnsResult - Returns a return value
+   *       </UL>
+   *   <LI><B>SPECIFIC_NAME</B> String {@code =>} The name which uniquely identifies this procedure
+   *       within its schema.
+   * </OL>
+   *
+   * <p>A user may not have permissions to execute any of the procedures that are returned by <code>
+   * getProcedures</code>
+   *
+   * @param catalog a catalog name; must match the catalog name as it is stored in the database; ""
+   *     retrieves those without a catalog; <code>null</code> means that the catalog name should not
+   *     be used to narrow the search
+   * @param schemaPattern a schema name pattern; must match the schema name as it is stored in the
+   *     database; "" retrieves those without a schema; <code>null</code> means that the schema name
+   *     should not be used to narrow the search
+   * @param procedureNamePattern a procedure name pattern; must match the procedure name as it is
+   *     stored in the database
+   * @return <code>ResultSet</code> - each row is a procedure description
+   * @throws SQLException if a database access error occurs
+   * @see #getSearchStringEscape
+   */
+  public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern)
+      throws SQLException {
+
+    StringBuilder sb =
+        new StringBuilder(
+            "SELECT ROUTINE_SCHEMA PROCEDURE_CAT,"
+                + "NULL PROCEDURE_SCHEM, "
+                + "ROUTINE_NAME PROCEDURE_NAME,"
+                + " NULL RESERVED1,"
+                + " NULL RESERVED2,"
+                + " NULL RESERVED3,"
+                + " ROUTINE_COMMENT REMARKS,"
+                + " CASE ROUTINE_TYPE "
+                + "  WHEN 'FUNCTION' THEN "
+                + procedureReturnsResult
+                + "  WHEN 'PROCEDURE' THEN "
+                + procedureNoResult
+                + "  ELSE "
+                + procedureResultUnknown
+                + " END PROCEDURE_TYPE,"
+                + " SPECIFIC_NAME "
+                + " FROM INFORMATION_SCHEMA.ROUTINES ");
+
+    boolean firstCondition = catalogCond(true, sb, "ROUTINE_SCHEMA", catalog);
+    firstCondition = patternCond(firstCondition, sb, "ROUTINE_NAME", procedureNamePattern);
+
+    return executeQuery(sb.toString());
+  }
+
+  /**
+   * Retrieves a description of the given catalog's stored procedure parameter and result columns.
+   *
+   * <p>Only descriptions matching the schema, procedure and parameter name criteria are returned.
+   * They are ordered by PROCEDURE_CAT, PROCEDURE_SCHEM, PROCEDURE_NAME and SPECIFIC_NAME. Within
+   * this, the return value, if any, is first. Next are the parameter descriptions in call order.
+   * The column descriptions follow in column number order.
+   *
+   * <p>Each row in the <code>ResultSet</code> is a parameter description or column description with
+   * the following fields:
+   *
+   * <OL>
+   *   <LI><B>PROCEDURE_CAT</B> String {@code =>} procedure catalog (may be <code>null</code>)
+   *   <LI><B>PROCEDURE_SCHEM</B> String {@code =>} procedure schema (may be <code>null</code>)
+   *   <LI><B>PROCEDURE_NAME</B> String {@code =>} procedure name
+   *   <LI><B>COLUMN_NAME</B> String {@code =>} column/parameter name
+   *   <LI><B>COLUMN_TYPE</B> Short {@code =>} kind of column/parameter:
+   *       <UL>
+   *         <LI>procedureColumnUnknown - nobody knows
+   *         <LI>procedureColumnIn - IN parameter
+   *         <LI>procedureColumnInOut - INOUT parameter
+   *         <LI>procedureColumnOut - OUT parameter
+   *         <LI>procedureColumnReturn - procedure return value
+   *         <LI>procedureColumnResult - result column in <code>ResultSet</code>
+   *       </UL>
+   *   <LI><B>DATA_TYPE</B> int {@code =>} SQL type from java.sql.Types
+   *   <LI><B>TYPE_NAME</B> String {@code =>} SQL type name, for a UDT type the type name is fully
+   *       qualified
+   *   <LI><B>PRECISION</B> int {@code =>} precision
+   *   <LI><B>LENGTH</B> int {@code =>} length in bytes of data
+   *   <LI><B>SCALE</B> short {@code =>} scale - null is returned for data types where SCALE is not
+   *       applicable.
+   *   <LI><B>RADIX</B> short {@code =>} radix
+   *   <LI><B>NULLABLE</B> short {@code =>} can it contain NULL.
+   *       <UL>
+   *         <LI>procedureNoNulls - does not allow NULL values
+   *         <LI>procedureNullable - allows NULL values
+   *         <LI>procedureNullableUnknown - nullability unknown
+   *       </UL>
+   *   <LI><B>REMARKS</B> String {@code =>} comment describing parameter/column
+   *   <LI><B>COLUMN_DEF</B> String {@code =>} default value for the column, which should be
+   *       interpreted as a string when the value is enclosed in single quotes (may be <code>null
+   *       </code>)
+   *       <UL>
+   *         <LI>The string NULL (not enclosed in quotes) - if NULL was specified as the default
+   *             value
+   *         <LI>TRUNCATE (not enclosed in quotes) - if the specified default value cannot be
+   *             represented without truncation
+   *         <LI>NULL - if a default value was not specified
+   *       </UL>
+   *   <LI><B>SQL_DATA_TYPE</B> int {@code =>} reserved for future use
+   *   <LI><B>SQL_DATETIME_SUB</B> int {@code =>} reserved for future use
+   *   <LI><B>CHAR_OCTET_LENGTH</B> int {@code =>} the maximum length of binary and character based
+   *       columns. For any other datatype the returned value is a NULL
+   *   <LI><B>ORDINAL_POSITION</B> int {@code =>} the ordinal position, starting from 1, for the
+   *       input and output parameters for a procedure. A value of 0 is returned if this row
+   *       describes the procedure's return value. For result set columns, it is the ordinal
+   *       position of the column in the result set starting from 1. If there are multiple result
+   *       sets, the column ordinal positions are implementation defined.
+   *   <LI><B>IS_NULLABLE</B> String {@code =>} ISO rules are used to determine the nullability for
+   *       a column.
+   *       <UL>
+   *         <LI>YES --- if the column can include NULLs
+   *         <LI>NO --- if the column cannot include NULLs
+   *         <LI>empty string --- if the nullability for the column is unknown
+   *       </UL>
+   *   <LI><B>SPECIFIC_NAME</B> String {@code =>} the name which uniquely identifies this procedure
+   *       within its schema.
+   * </OL>
+   *
+   * <p><B>Note:</B> Some databases may not return the column descriptions for a procedure.
+   *
+   * <p>The PRECISION column represents the specified column size for the given column. 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. Null is returned for data
+   * types where the column size is not applicable.
+   *
+   * @param catalog a catalog name; must match the catalog name as it is stored in the database; ""
+   *     retrieves those without a catalog; <code>null</code> means that the catalog name should not
+   *     be used to narrow the search
+   * @param schemaPattern a schema name pattern; must match the schema name as it is stored in the
+   *     database; "" retrieves those without a schema; <code>null</code> means that the schema name
+   *     should not be used to narrow the search
+   * @param procedureNamePattern a procedure name pattern; must match the procedure name as it is
+   *     stored in the database
+   * @param columnNamePattern a column name pattern; must match the column name as it is stored in
+   *     the database
+   * @return <code>ResultSet</code> - each row describes a stored procedure parameter or column
+   * @throws SQLException if a database access error occurs
+   * @see #getSearchStringEscape
+   */
+  public ResultSet getProcedureColumns(
+      String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern)
+      throws SQLException {
+    /*
+     *  Get info from information_schema.parameters
+     */
+    StringBuilder sb =
+        new StringBuilder(
+            "SELECT SPECIFIC_SCHEMA PROCEDURE_CAT, NULL PROCEDURE_SCHEM, SPECIFIC_NAME"
+                + " PROCEDURE_NAME, PARAMETER_NAME COLUMN_NAME,  CASE PARAMETER_MODE   WHEN 'IN'"
+                + " THEN "
+                + procedureColumnIn
+                + "  WHEN 'OUT' THEN "
+                + procedureColumnOut
+                + "  WHEN 'INOUT' THEN "
+                + procedureColumnInOut
+                + "  ELSE IF(PARAMETER_MODE IS NULL,"
+                + procedureColumnReturn
+                + ","
+                + procedureColumnUnknown
+                + ")"
+                + " END COLUMN_TYPE,"
+                + dataTypeClause("DTD_IDENTIFIER")
+                + " DATA_TYPE,"
+                + "DATA_TYPE TYPE_NAME,"
+                + " CASE DATA_TYPE"
+                + "  WHEN 'time' THEN "
+                + "IF(DATETIME_PRECISION = 0, 10, CAST(11 + DATETIME_PRECISION as signed integer))"
+                + "  WHEN 'date' THEN 10"
+                + "  WHEN 'datetime' THEN "
+                + "IF(DATETIME_PRECISION = 0, 19, CAST(20 + DATETIME_PRECISION as signed integer))"
+                + "  WHEN 'timestamp' THEN "
+                + "IF(DATETIME_PRECISION = 0, 19, CAST(20 + DATETIME_PRECISION as signed integer))"
+                + "  ELSE "
+                + "  IF(NUMERIC_PRECISION IS NULL, LEAST(CHARACTER_MAXIMUM_LENGTH,"
+                + Integer.MAX_VALUE
+                + "), NUMERIC_PRECISION) "
+                + " END `PRECISION`,"
+                + " CASE DATA_TYPE"
+                + "  WHEN 'time' THEN "
+                + "IF(DATETIME_PRECISION = 0, 10, CAST(11 + DATETIME_PRECISION as signed integer))"
+                + "  WHEN 'date' THEN 10"
+                + "  WHEN 'datetime' THEN "
+                + "IF(DATETIME_PRECISION = 0, 19, CAST(20 + DATETIME_PRECISION as signed integer))"
+                + "  WHEN 'timestamp' THEN "
+                + "IF(DATETIME_PRECISION = 0, 19, CAST(20 + DATETIME_PRECISION as signed integer))"
+                + "  ELSE "
+                + "  IF(NUMERIC_PRECISION IS NULL, LEAST(CHARACTER_MAXIMUM_LENGTH,"
+                + Integer.MAX_VALUE
+                + "), NUMERIC_PRECISION) "
+                + " END `LENGTH`,"
+                + " CASE DATA_TYPE"
+                + "  WHEN 'time' THEN CAST(DATETIME_PRECISION as signed integer)"
+                + "  WHEN 'datetime' THEN CAST(DATETIME_PRECISION as signed integer)"
+                + "  WHEN 'timestamp' THEN CAST(DATETIME_PRECISION as signed integer)"
+                + "  ELSE NUMERIC_SCALE "
+                + " END `SCALE`,"
+                + "10 RADIX,"
+                + procedureNullableUnknown
+                + " NULLABLE,NULL REMARKS,NULL COLUMN_DEF,0 SQL_DATA_TYPE,0"
+                + " SQL_DATETIME_SUB,CHARACTER_OCTET_LENGTH CHAR_OCTET_LENGTH ,ORDINAL_POSITION, ''"
+                + " IS_NULLABLE, SPECIFIC_NAME  FROM INFORMATION_SCHEMA.PARAMETERS");
+
+    boolean firstCondition = catalogCond(true, sb, "SPECIFIC_SCHEMA", catalog);
+    firstCondition = patternCond(firstCondition, sb, "SPECIFIC_NAME", procedureNamePattern);
+    firstCondition = patternCond(firstCondition, sb, "PARAMETER_NAME", columnNamePattern);
+    sb.append(" ORDER BY SPECIFIC_SCHEMA, SPECIFIC_NAME, ORDINAL_POSITION");
+
+    return executeQuery(sb.toString());
+  }
+
+  /**
+   * Retrieves a description of the given catalog's system or user function parameters and return
+   * type.
+   *
+   * <p>Only descriptions matching the schema, function and parameter name criteria are returned.
+   * They are ordered by <code>FUNCTION_CAT</code>, <code>FUNCTION_SCHEM</code>, <code>FUNCTION_NAME
+   * </code> and <code>SPECIFIC_ NAME</code>. Within this, the return value, if any, is first. Next
+   * are the parameter descriptions in call order. The column descriptions follow in column number
+   * order.
+   *
+   * <p>Each row in the <code>ResultSet</code> is a parameter description, column description or
+   * return type description with the following fields:
+   *
+   * <OL>
+   *   <LI><B>FUNCTION_CAT</B> String {@code =>} function catalog (may be <code>null</code>)
+   *   <LI><B>FUNCTION_SCHEM</B> String {@code =>} function schema (may be <code>null</code>)
+   *   <LI><B>FUNCTION_NAME</B> String {@code =>} function name. This is the name used to invoke the
+   *       function
+   *   <LI><B>COLUMN_NAME</B> String {@code =>} column/parameter name
+   *   <LI><B>COLUMN_TYPE</B> Short {@code =>} kind of column/parameter:
+   *       <UL>
+   *         <LI>functionColumnUnknown - nobody knows
+   *         <LI>functionColumnIn - IN parameter
+   *         <LI>functionColumnInOut - INOUT parameter
+   *         <LI>functionColumnOut - OUT parameter
+   *         <LI>functionColumnReturn - function return value
+   *         <LI>functionColumnResult - Indicates that the parameter or column is a column in the
+   *             <code>ResultSet</code>
+   *       </UL>
+   *   <LI><B>DATA_TYPE</B> int {@code =>} SQL type from java.sql.Types
+   *   <LI><B>TYPE_NAME</B> String {@code =>} SQL type name, for a UDT type the type name is fully
+   *       qualified
+   *   <LI><B>PRECISION</B> int {@code =>} precision
+   *   <LI><B>LENGTH</B> int {@code =>} length in bytes of data
+   *   <LI><B>SCALE</B> short {@code =>} scale - null is returned for data types where SCALE is not
+   *       applicable.
+   *   <LI><B>RADIX</B> short {@code =>} radix
+   *   <LI><B>NULLABLE</B> short {@code =>} can it contain NULL.
+   *       <UL>
+   *         <LI>functionNoNulls - does not allow NULL values
+   *         <LI>functionNullable - allows NULL values
+   *         <LI>functionNullableUnknown - nullability unknown
+   *       </UL>
+   *   <LI><B>REMARKS</B> String {@code =>} comment describing column/parameter
+   *   <LI><B>CHAR_OCTET_LENGTH</B> int {@code =>} the maximum length of binary and character based
+   *       parameters or columns. For any other datatype the returned value is a NULL
+   *   <LI><B>ORDINAL_POSITION</B> int {@code =>} the ordinal position, starting from 1, for the
+   *       input and output parameters. A value of 0 is returned if this row describes the
+   *       function's return value. For result set columns, it is the ordinal position of the column
+   *       in the result set starting from 1.
+   *   <LI><B>IS_NULLABLE</B> String {@code =>} ISO rules are used to determine the nullability for
+   *       a parameter or column.
+   *       <UL>
+   *         <LI>YES --- if the parameter or column can include NULLs
+   *         <LI>NO --- if the parameter or column cannot include NULLs
+   *         <LI>empty string --- if the nullability for the parameter or column is unknown
+   *       </UL>
+   *   <LI><B>SPECIFIC_NAME</B> String {@code =>} the name which uniquely identifies this function
+   *       within its schema. This is a user specified, or DBMS generated, name that may be
+   *       different then the <code>FUNCTION_NAME</code> for example with overload functions
+   * </OL>
+   *
+   * <p>The PRECISION column represents the specified column size for the given parameter or column.
+   * 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. Null is returned for data types where the column size is not applicable.
+   *
+   * @param catalog a catalog name; must match the catalog name as it is stored in the database; ""
+   *     retrieves those without a catalog; <code>null</code> means that the catalog name should not
+   *     be used to narrow the search
+   * @param schemaPattern a schema name pattern; must match the schema name as it is stored in the
+   *     database; "" retrieves those without a schema; <code>null</code> means that the schema name
+   *     should not be used to narrow the search
+   * @param functionNamePattern a procedure name pattern; must match the function name as it is
+   *     stored in the database
+   * @param columnNamePattern a parameter name pattern; must match the parameter or column name as
+   *     it is stored in the database
+   * @return <code>ResultSet</code> - each row describes a user function parameter, column or return
+   *     type
+   * @throws SQLException if a database access error occurs
+   * @see #getSearchStringEscape
+   * @since 1.6
+   */
+  public ResultSet getFunctionColumns(
+      String catalog, String schemaPattern, String functionNamePattern, String columnNamePattern)
+      throws SQLException {
+
+    StringBuilder sb =
+        new StringBuilder(
+            "SELECT SPECIFIC_SCHEMA `FUNCTION_CAT`, NULL `FUNCTION_SCHEM`, SPECIFIC_NAME"
+                + " FUNCTION_NAME, PARAMETER_NAME COLUMN_NAME,  CASE PARAMETER_MODE   WHEN 'IN'"
+                + " THEN "
+                + functionColumnIn
+                + "  WHEN 'OUT' THEN "
+                + functionColumnOut
+                + "  WHEN 'INOUT' THEN "
+                + functionColumnInOut
+                + "  ELSE "
+                + functionReturn
+                + " END COLUMN_TYPE,"
+                + dataTypeClause("DTD_IDENTIFIER")
+                + " DATA_TYPE,DATA_TYPE TYPE_NAME,NUMERIC_PRECISION"
+                + " `PRECISION`,CHARACTER_MAXIMUM_LENGTH LENGTH,NUMERIC_SCALE SCALE,10 RADIX,"
+                + procedureNullableUnknown
+                + " NULLABLE,NULL REMARKS,CHARACTER_OCTET_LENGTH CHAR_OCTET_LENGTH"
+                + " ,ORDINAL_POSITION, '' IS_NULLABLE, SPECIFIC_NAME  FROM"
+                + " INFORMATION_SCHEMA.PARAMETERS");
+
+    boolean firstCondition = catalogCond(true, sb, "SPECIFIC_SCHEMA", catalog);
+    firstCondition = patternCond(firstCondition, sb, "SPECIFIC_NAME", functionNamePattern);
+    firstCondition = patternCond(firstCondition, sb, "PARAMETER_NAME", columnNamePattern);
+    sb.append(firstCondition ? " WHERE " : " AND ")
+        .append(" ROUTINE_TYPE='FUNCTION' ORDER BY FUNCTION_CAT, SPECIFIC_NAME, ORDINAL_POSITION");
+
+    return executeQuery(sb.toString());
+  }
+
+  public ResultSet getSchemas() throws SQLException {
+    return executeQuery("SELECT '' TABLE_SCHEM, '' TABLE_catalog  FROM DUAL WHERE 1=0");
+  }
+
+  public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException {
+    return executeQuery("SELECT  ' ' table_schem, ' ' table_catalog FROM DUAL WHERE 1=0");
+  }
+
+  public ResultSet getCatalogs() throws SQLException {
+    return executeQuery("SELECT SCHEMA_NAME TABLE_CAT FROM INFORMATION_SCHEMA.SCHEMATA ORDER BY 1");
+  }
+
+  public ResultSet getTableTypes() throws SQLException {
+    return executeQuery(
+        "SELECT 'TABLE' TABLE_TYPE UNION SELECT 'SYSTEM VIEW' TABLE_TYPE UNION SELECT 'VIEW'"
+            + " TABLE_TYPE");
+  }
+
+  /**
+   * Retrieves a description of the access rights for a table's columns.
+   *
+   * <p>Only privileges matching the column name criteria are returned. They are ordered by
+   * COLUMN_NAME and PRIVILEGE.
+   *
+   * <p>Each privilege description has the following columns:
+   *
+   * <OL>
+   *   <LI><B>TABLE_CAT</B> String {@code =>} table catalog (may be <code>null</code>)
+   *   <LI><B>TABLE_SCHEM</B> String {@code =>} table schema (may be <code>null</code>)
+   *   <LI><B>TABLE_NAME</B> String {@code =>} table name
+   *   <LI><B>COLUMN_NAME</B> String {@code =>} column name
+   *   <LI><B>GRANTOR</B> String {@code =>} grantor of access (may be <code>null</code>)
+   *   <LI><B>GRANTEE</B> String {@code =>} grantee of access
+   *   <LI><B>PRIVILEGE</B> String {@code =>} name of access (SELECT, INSERT, UPDATE, REFRENCES,
+   *       ...)
+   *   <LI><B>IS_GRANTABLE</B> String {@code =>} "YES" if grantee is permitted to grant to others;
+   *       "NO" if not; <code>null</code> if unknown
+   * </OL>
+   *
+   * @param catalog a catalog name; must match the catalog name as it is stored in the database; ""
+   *     retrieves those without a catalog; <code>null</code> means that the catalog name should not
+   *     be used to narrow the search
+   * @param schema a schema name; must match the schema name as it is stored in the database; ""
+   *     retrieves those without a schema; <code>null</code> means that the schema name should not
+   *     be used to narrow the search
+   * @param table a table name; must match the table name as it is stored in the database
+   * @param columnNamePattern a column name pattern; must match the column name as it is stored in
+   *     the database
+   * @return <code>ResultSet</code> - each row is a column privilege description
+   * @throws SQLException if a database access error occurs
+   * @see #getSearchStringEscape
+   */
+  public ResultSet getColumnPrivileges(
+      String catalog, String schema, String table, String columnNamePattern) throws SQLException {
+
+    if (table == null) {
+      throw new SQLException("'table' parameter must not be null");
+    }
+    StringBuilder sb =
+        new StringBuilder(
+            "SELECT TABLE_SCHEMA TABLE_CAT, "
+                + "NULL TABLE_SCHEM, "
+                + "TABLE_NAME, "
+                + "COLUMN_NAME, "
+                + "NULL AS GRANTOR, "
+                + "GRANTEE, "
+                + "PRIVILEGE_TYPE AS PRIVILEGE, "
+                + "IS_GRANTABLE "
+                + "FROM INFORMATION_SCHEMA.COLUMN_PRIVILEGES");
+    boolean firstCondition = catalogCond(true, sb, "TABLE_SCHEMA", catalog);
+    sb.append(firstCondition ? " WHERE " : " AND ")
+        .append(" TABLE_NAME = ")
+        .append(escapeQuote(table));
+    patternCond(false, sb, "COLUMN_NAME", columnNamePattern);
+    sb.append(" ORDER BY COLUMN_NAME, PRIVILEGE_TYPE");
+
+    return executeQuery(sb.toString());
+  }
+
+  /**
+   * Retrieves a description of the access rights for each table available in a catalog. Note that a
+   * table privilege applies to one or more columns in the table. It would be wrong to assume that
+   * this privilege applies to all columns (this may be true for some systems but is not true for
+   * all.)
+   *
+   * <p>Only privileges matching the schema and table name criteria are returned. They are ordered
+   * by <code>TABLE_CAT</code>, <code>TABLE_SCHEM</code>, <code>TABLE_NAME</code>, and <code>
+   * PRIVILEGE</code>.
+   *
+   * <p>Each privilege description has the following columns:
+   *
+   * <OL>
+   *   <LI><B>TABLE_CAT</B> String {@code =>} table catalog (may be <code>null</code>)
+   *   <LI><B>TABLE_SCHEM</B> String {@code =>} table schema (may be <code>null</code>)
+   *   <LI><B>TABLE_NAME</B> String {@code =>} table name
+   *   <LI><B>GRANTOR</B> String {@code =>} grantor of access (may be <code>null</code>)
+   *   <LI><B>GRANTEE</B> String {@code =>} grantee of access
+   *   <LI><B>PRIVILEGE</B> String {@code =>} name of access (SELECT, INSERT, UPDATE, REFRENCES,
+   *       ...)
+   *   <LI><B>IS_GRANTABLE</B> String {@code =>} "YES" if grantee is permitted to grant to others;
+   *       "NO" if not; <code>null</code> if unknown
+   * </OL>
+   *
+   * @param catalog a catalog name; must match the catalog name as it is stored in the database; ""
+   *     retrieves those without a catalog; <code>null</code> means that the catalog name should not
+   *     be used to narrow the search
+   * @param schemaPattern a schema name pattern; must match the schema name as it is stored in the
+   *     database; "" retrieves those without a schema; <code>null</code> means that the schema name
+   *     should not be used to narrow the search
+   * @param tableNamePattern a table name pattern; must match the table name as it is stored in the
+   *     database
+   * @return <code>ResultSet</code> - each row is a table privilege description
+   * @throws SQLException if a database access error occurs
+   * @see #getSearchStringEscape
+   */
+  public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern)
+      throws SQLException {
+    StringBuilder sb =
+        new StringBuilder(
+            "SELECT TABLE_SCHEMA TABLE_CAT, "
+                + "NULL TABLE_SCHEM, "
+                + "TABLE_NAME, "
+                + "NULL GRANTOR,"
+                + "GRANTEE, "
+                + "PRIVILEGE_TYPE PRIVILEGE, "
+                + "IS_GRANTABLE "
+                + "FROM INFORMATION_SCHEMA.TABLE_PRIVILEGES");
+    boolean firstCondition = catalogCond(true, sb, "TABLE_SCHEMA", catalog);
+    firstCondition = patternCond(firstCondition, sb, "TABLE_NAME", tableNamePattern);
+    sb.append(" ORDER BY TABLE_SCHEMA, TABLE_NAME,  PRIVILEGE_TYPE ");
+
+    return executeQuery(sb.toString());
+  }
+
+  /**
+   * Retrieves a description of a table's columns that are automatically updated when any value in a
+   * row is updated. They are unordered.
+   *
+   * <p>Each column description has the following columns:
+   *
+   * <OL>
+   *   <LI><B>SCOPE</B> short {@code =>} is not used
+   *   <LI><B>COLUMN_NAME</B> String {@code =>} column name
+   *   <LI><B>DATA_TYPE</B> int {@code =>} SQL data type from <code>java.sql.Types</code>
+   *   <LI><B>TYPE_NAME</B> String {@code =>} Data source-dependent type name
+   *   <LI><B>COLUMN_SIZE</B> int {@code =>} precision
+   *   <LI><B>BUFFER_LENGTH</B> int {@code =>} length of column value in bytes
+   *   <LI><B>DECIMAL_DIGITS</B> short {@code =>} scale - Null is returned for data types where
+   *       DECIMAL_DIGITS is not applicable.
+   *   <LI><B>PSEUDO_COLUMN</B> short {@code =>} whether this is pseudo column like an Oracle ROWID
+   *       <UL>
+   *         <LI>versionColumnUnknown - may or may not be pseudo column
+   *         <LI>versionColumnNotPseudo - is NOT a pseudo column
+   *         <LI>versionColumnPseudo - is a pseudo column
+   *       </UL>
+   * </OL>
+   *
+   * <p>The COLUMN_SIZE column represents the specified column size for the given column. 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. Null is returned for data types where the column size is not applicable.
+   *
+   * @param catalog a catalog name; must match the catalog name as it is stored in the database; ""
+   *     retrieves those without a catalog;<code>null</code> means that the catalog name should not
+   *     be used to narrow the search
+   * @param schema a schema name; must match the schema name as it is stored in the database; ""
+   *     retrieves those without a schema; <code>null</code> means that the schema name should not
+   *     be used to narrow the search
+   * @param table a table name; must match the table name as it is stored in the database
+   * @return a <code>ResultSet</code> object in which each row is a column description
+   * @throws SQLException if a database access error occurs
+   */
+  public ResultSet getVersionColumns(String catalog, String schema, String table)
+      throws SQLException {
+    String sql =
+        "SELECT 0 SCOPE, ' ' COLUMN_NAME, 0 DATA_TYPE,"
+            + " ' ' TYPE_NAME, 0 COLUMN_SIZE, 0 BUFFER_LENGTH,"
+            + " 0 DECIMAL_DIGITS, 0 PSEUDO_COLUMN "
+            + " FROM DUAL WHERE 1 = 0";
+    return executeQuery(sql);
+  }
+
+  /**
+   * Retrieves a description of the foreign key columns in the given foreign key table that
+   * reference the primary key or the columns representing a unique constraint of the parent table
+   * (could be the same or a different table). The number of columns returned from the parent table
+   * must match the number of columns that make up the foreign key. They are ordered by FKTABLE_CAT,
+   * FKTABLE_SCHEM, FKTABLE_NAME, and KEY_SEQ.
+   *
+   * <p>Each foreign key column description has the following columns:
+   *
+   * <OL>
+   *   <LI><B>PKTABLE_CAT</B> String {@code =>} parent key table catalog (may be <code>null</code>)
+   *   <LI><B>PKTABLE_SCHEM</B> String {@code =>} parent key table schema (may be <code>null</code>)
+   *   <LI><B>PKTABLE_NAME</B> String {@code =>} parent key table name
+   *   <LI><B>PKCOLUMN_NAME</B> String {@code =>} parent key column name
+   *   <LI><B>FKTABLE_CAT</B> String {@code =>} foreign key table catalog (may be <code>null</code>)
+   *       being exported (may be <code>null</code>)
+   *   <LI><B>FKTABLE_SCHEM</B> String {@code =>} foreign key table schema (may be <code>null</code>
+   *       ) being exported (may be <code>null</code>)
+   *   <LI><B>FKTABLE_NAME</B> String {@code =>} foreign key table name being exported
+   *   <LI><B>FKCOLUMN_NAME</B> String {@code =>} foreign key column name being exported
+   *   <LI><B>KEY_SEQ</B> short {@code =>} sequence number within foreign key( a value of 1
+   *       represents the first column of the foreign key, a value of 2 would represent the second
+   *       column within the foreign key).
+   *   <LI><B>UPDATE_RULE</B> short {@code =>} What happens to foreign key when parent key is
+   *       updated:
+   *       <UL>
+   *         <LI>importedNoAction - do not allow update of parent key if it has been imported
+   *         <LI>importedKeyCascade - change imported key to agree with parent key update
+   *         <LI>importedKeySetNull - change imported key to <code>NULL</code> if its parent key has
+   *             been updated
+   *         <LI>importedKeySetDefault - change imported key to default values if its parent key has
+   *             been updated
+   *         <LI>importedKeyRestrict - same as importedKeyNoAction (for ODBC 2.x compatibility)
+   *       </UL>
+   *   <LI><B>DELETE_RULE</B> short {@code =>} What happens to the foreign key when parent key is
+   *       deleted.
+   *       <UL>
+   *         <LI>importedKeyNoAction - do not allow delete of parent key if it has been imported
+   *         <LI>importedKeyCascade - delete rows that import a deleted key
+   *         <LI>importedKeySetNull - change imported key to <code>NULL</code> if its primary key
+   *             has been deleted
+   *         <LI>importedKeyRestrict - same as importedKeyNoAction (for ODBC 2.x compatibility)
+   *         <LI>importedKeySetDefault - change imported key to default if its parent key has been
+   *             deleted
+   *       </UL>
+   *   <LI><B>FK_NAME</B> String {@code =>} foreign key name (may be <code>null</code>)
+   *   <LI><B>PK_NAME</B> String {@code =>} parent key name (may be <code>null</code>)
+   *   <LI><B>DEFERRABILITY</B> short {@code =>} can the evaluation of foreign key constraints be
+   *       deferred until commit
+   *       <UL>
+   *         <LI>importedKeyInitiallyDeferred - see SQL92 for definition
+   *         <LI>importedKeyInitiallyImmediate - see SQL92 for definition
+   *         <LI>importedKeyNotDeferrable - see SQL92 for definition
+   *       </UL>
+   * </OL>
+   *
+   * @param parentCatalog a catalog name; must match the catalog name as it is stored in the
+   *     database; "" retrieves those without a catalog; <code>null</code> means drop catalog name
+   *     from the selection criteria
+   * @param parentSchema a schema name; must match the schema name as it is stored in the database;
+   *     "" retrieves those without a schema; <code>null</code> means drop schema name from the
+   *     selection criteria
+   * @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; <code>null</code> 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; <code>null</code> means drop schema name from the
+   *     selection criteria
+   * @param foreignTable the name of the table that imports the key; pattern, or null (means any
+   *     table) value is stored in the database
+   * @return <code>ResultSet</code> - each row is a foreign key column description
+   * @throws SQLException if a database access error occurs
+   * @see #getImportedKeys
+   */
+  public ResultSet getCrossReference(
+      String parentCatalog,
+      String parentSchema,
+      String parentTable,
+      String foreignCatalog,
+      String foreignSchema,
+      String foreignTable)
+      throws SQLException {
+
+    StringBuilder sb =
+        new StringBuilder(
+            "SELECT KCU.REFERENCED_TABLE_SCHEMA PKTABLE_CAT, NULL PKTABLE_SCHEM, "
+                + " KCU.REFERENCED_TABLE_NAME PKTABLE_NAME, KCU.REFERENCED_COLUMN_NAME"
+                + " PKCOLUMN_NAME, KCU.TABLE_SCHEMA FKTABLE_CAT, NULL FKTABLE_SCHEM, "
+                + " KCU.TABLE_NAME FKTABLE_NAME, KCU.COLUMN_NAME FKCOLUMN_NAME,"
+                + " KCU.POSITION_IN_UNIQUE_CONSTRAINT KEY_SEQ, CASE update_rule    WHEN 'RESTRICT'"
+                + " THEN 1   WHEN 'NO ACTION' THEN 3   WHEN 'CASCADE' THEN 0   WHEN 'SET NULL' THEN"
+                + " 2   WHEN 'SET DEFAULT' THEN 4 END UPDATE_RULE, CASE DELETE_RULE  WHEN"
+                + " 'RESTRICT' THEN 1  WHEN 'NO ACTION' THEN 3  WHEN 'CASCADE' THEN 0  WHEN 'SET"
+                + " NULL' THEN 2  WHEN 'SET DEFAULT' THEN 4 END DELETE_RULE, RC.CONSTRAINT_NAME"
+                + " FK_NAME, RC.UNIQUE_CONSTRAINT_NAME PK_NAME,"
+                + importedKeyNotDeferrable
+                + " 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 ");
+
+    boolean firstCondition = catalogCond(true, sb, "KCU.REFERENCED_TABLE_SCHEMA", parentCatalog);
+    firstCondition = catalogCond(firstCondition, sb, "KCU.TABLE_SCHEMA", foreignCatalog);
+    firstCondition = patternCond(firstCondition, sb, "KCU.REFERENCED_TABLE_NAME", parentTable);
+    firstCondition = patternCond(firstCondition, sb, "KCU.TABLE_NAME", foreignTable);
+    sb.append("ORDER BY FKTABLE_CAT, FKTABLE_SCHEM, FKTABLE_NAME, KEY_SEQ");
+
+    return executeQuery(sb.toString());
+  }
+
+  /**
+   * Retrieves a description of all the data types supported by this database. They are ordered by
+   * DATA_TYPE and then by how closely the data type maps to the corresponding JDBC SQL type.
+   *
+   * <p>If the database supports SQL distinct types, then getTypeInfo() will return a single row
+   * with a TYPE_NAME of DISTINCT and a DATA_TYPE of Types.DISTINCT. If the database supports SQL
+   * structured types, then getTypeInfo() will return a single row with a TYPE_NAME of STRUCT and a
+   * DATA_TYPE of Types.STRUCT.
+   *
+   * <p>If SQL distinct or structured types are supported, then information on the individual types
+   * may be obtained from the getUDTs() method.
+   *
+   * <p>Each type description has the following columns:
+   *
+   * <OL>
+   *   <LI><B>TYPE_NAME</B> String {@code =>} Type name
+   *   <LI><B>DATA_TYPE</B> int {@code =>} SQL data type from java.sql.Types
+   *   <LI><B>PRECISION</B> int {@code =>} maximum precision
+   *   <LI><B>LITERAL_PREFIX</B> String {@code =>} prefix used to quote a literal (may be <code>null
+   *       </code>)
+   *   <LI><B>LITERAL_SUFFIX</B> String {@code =>} suffix used to quote a literal (may be <code>null
+   *       </code>)
+   *   <LI><B>CREATE_PARAMS</B> String {@code =>} parameters used in creating the type (may be
+   *       <code>null</code>)
+   *   <LI><B>NULLABLE</B> short {@code =>} can you use NULL for this type.
+   *       <UL>
+   *         <LI>typeNoNulls - does not allow NULL values
+   *         <LI>typeNullable - allows NULL values
+   *         <LI>typeNullableUnknown - nullability unknown
+   *       </UL>
+   *   <LI><B>CASE_SENSITIVE</B> boolean{@code =>} is it case sensitive.
+   *   <LI><B>SEARCHABLE</B> short {@code =>} can you use "WHERE" based on this type:
+   *       <UL>
+   *         <LI>typePredNone - No support
+   *         <LI>typePredChar - Only supported with WHERE .. LIKE
+   *         <LI>typePredBasic - Supported except for WHERE .. LIKE
+   *         <LI>typeSearchable - Supported for all WHERE ..
+   *       </UL>
+   *   <LI><B>UNSIGNED_ATTRIBUTE</B> boolean {@code =>} is it unsigned.
+   *   <LI><B>FIXED_PREC_SCALE</B> boolean {@code =>} can it be a money value.
+   *   <LI><B>AUTO_INCREMENT</B> boolean {@code =>} can it be used for an auto-increment value.
+   *   <LI><B>LOCAL_TYPE_NAME</B> String {@code =>} localized version of type name (may be <code>
+   *       null</code>)
+   *   <LI><B>MINIMUM_SCALE</B> short {@code =>} minimum scale supported
+   *   <LI><B>MAXIMUM_SCALE</B> short {@code =>} maximum scale supported
+   *   <LI><B>SQL_DATA_TYPE</B> int {@code =>} unused
+   *   <LI><B>SQL_DATETIME_SUB</B> int {@code =>} unused
+   *   <LI><B>NUM_PREC_RADIX</B> int {@code =>} usually 2 or 10
+   * </OL>
+   *
+   * <p>The PRECISION column represents the maximum column size that the server supports for the
+   * given datatype. 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. Null is returned for data types where the column size is not applicable.
+   *
+   * @return a <code>ResultSet</code> object in which each row is an SQL type description
+   */
+  public ResultSet getTypeInfo() {
+    String[] columnNames = {
+      "TYPE_NAME", "DATA_TYPE", "PRECISION", "LITERAL_PREFIX", "LITERAL_SUFFIX",
+      "CREATE_PARAMS", "NULLABLE", "CASE_SENSITIVE", "SEARCHABLE", "UNSIGNED_ATTRIBUTE",
+      "FIXED_PREC_SCALE", "AUTO_INCREMENT", "LOCAL_TYPE_NAME", "MINIMUM_SCALE", "MAXIMUM_SCALE",
+      "SQL_DATA_TYPE", "SQL_DATETIME_SUB", "NUM_PREC_RADIX"
+    };
+    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 = {
+      {"BIT", "-7", "1", "", "", "", "1", "1", "3", "0", "0", "0", "BIT", "0", "0", "0", "0", "10"},
+      {
+        "BOOL", "-7", "1", "", "", "", "1", "1", "3", "0", "0", "0", "BOOL", "0", "0", "0", "0",
+        "10"
+      },
+      {
+        "TINYINT",
+        "-6",
+        "3",
+        "",
+        "",
+        "[(M)] [UNSIGNED] [ZEROFILL]",
+        "1",
+        "0",
+        "3",
+        "1",
+        "0",
+        "1",
+        "TINYINT",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "TINYINT UNSIGNED",
+        "-6",
+        "3",
+        "",
+        "",
+        "[(M)] [UNSIGNED] [ZEROFILL]",
+        "1",
+        "0",
+        "3",
+        "1",
+        "0",
+        "1",
+        "TINYINT UNSIGNED",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "BIGINT",
+        "-5",
+        "19",
+        "",
+        "",
+        "[(M)] [UNSIGNED] [ZEROFILL]",
+        "1",
+        "0",
+        "3",
+        "1",
+        "0",
+        "1",
+        "BIGINT",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "BIGINT UNSIGNED",
+        "-5",
+        "20",
+        "",
+        "",
+        "[(M)] [ZEROFILL]",
+        "1",
+        "0",
+        "3",
+        "1",
+        "0",
+        "1",
+        "BIGINT UNSIGNED",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "LONG VARBINARY",
+        "-4",
+        "16777215",
+        "'",
+        "'",
+        "",
+        "1",
+        "1",
+        "3",
+        "0",
+        "0",
+        "0",
+        "LONG VARBINARY",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "MEDIUMBLOB",
+        "-4",
+        "16777215",
+        "'",
+        "'",
+        "",
+        "1",
+        "1",
+        "3",
+        "0",
+        "0",
+        "0",
+        "MEDIUMBLOB",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "LONGBLOB",
+        "-4",
+        "2147483647",
+        "'",
+        "'",
+        "",
+        "1",
+        "1",
+        "3",
+        "0",
+        "0",
+        "0",
+        "LONGBLOB",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "BLOB", "-4", "65535", "'", "'", "", "1", "1", "3", "0", "0", "0", "BLOB", "0", "0", "0",
+        "0", "10"
+      },
+      {
+        "TINYBLOB",
+        "-4",
+        "255",
+        "'",
+        "'",
+        "",
+        "1",
+        "1",
+        "3",
+        "0",
+        "0",
+        "0",
+        "TINYBLOB",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "VARBINARY",
+        "-3",
+        "255",
+        "'",
+        "'",
+        "(M)",
+        "1",
+        "1",
+        "3",
+        "0",
+        "0",
+        "0",
+        "VARBINARY",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "BINARY", "-2", "255", "'", "'", "(M)", "1", "1", "3", "0", "0", "0", "BINARY", "0", "0",
+        "0", "0", "10"
+      },
+      {
+        "LONG VARCHAR",
+        "-1",
+        "16777215",
+        "'",
+        "'",
+        "",
+        "1",
+        "0",
+        "3",
+        "0",
+        "0",
+        "0",
+        "LONG VARCHAR",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "MEDIUMTEXT",
+        "-1",
+        "16777215",
+        "'",
+        "'",
+        "",
+        "1",
+        "0",
+        "3",
+        "0",
+        "0",
+        "0",
+        "MEDIUMTEXT",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "LONGTEXT",
+        "-1",
+        "2147483647",
+        "'",
+        "'",
+        "",
+        "1",
+        "0",
+        "3",
+        "0",
+        "0",
+        "0",
+        "LONGTEXT",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "TEXT", "-1", "65535", "'", "'", "", "1", "0", "3", "0", "0", "0", "TEXT", "0", "0", "0",
+        "0", "10"
+      },
+      {
+        "TINYTEXT",
+        "-1",
+        "255",
+        "'",
+        "'",
+        "",
+        "1",
+        "0",
+        "3",
+        "0",
+        "0",
+        "0",
+        "TINYTEXT",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "CHAR", "1", "255", "'", "'", "(M)", "1", "0", "3", "0", "0", "0", "CHAR", "0", "0", "0",
+        "0", "10"
+      },
+      {
+        "NUMERIC",
+        "2",
+        "65",
+        "",
+        "",
+        "[(M,D])] [ZEROFILL]",
+        "1",
+        "0",
+        "3",
+        "0",
+        "0",
+        "1",
+        "NUMERIC",
+        "-308",
+        "308",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "DECIMAL",
+        "3",
+        "65",
+        "",
+        "",
+        "[(M,D])] [ZEROFILL]",
+        "1",
+        "0",
+        "3",
+        "0",
+        "0",
+        "1",
+        "DECIMAL",
+        "-308",
+        "308",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "INTEGER",
+        "4",
+        "10",
+        "",
+        "",
+        "[(M)] [UNSIGNED] [ZEROFILL]",
+        "1",
+        "0",
+        "3",
+        "1",
+        "0",
+        "1",
+        "INTEGER",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "INTEGER UNSIGNED",
+        "4",
+        "10",
+        "",
+        "",
+        "[(M)] [ZEROFILL]",
+        "1",
+        "0",
+        "3",
+        "1",
+        "0",
+        "1",
+        "INTEGER UNSIGNED",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "INT",
+        "4",
+        "10",
+        "",
+        "",
+        "[(M)] [UNSIGNED] [ZEROFILL]",
+        "1",
+        "0",
+        "3",
+        "1",
+        "0",
+        "1",
+        "INT",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "INT UNSIGNED",
+        "4",
+        "10",
+        "",
+        "",
+        "[(M)] [ZEROFILL]",
+        "1",
+        "0",
+        "3",
+        "1",
+        "0",
+        "1",
+        "INT UNSIGNED",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "MEDIUMINT",
+        "4",
+        "7",
+        "",
+        "",
+        "[(M)] [UNSIGNED] [ZEROFILL]",
+        "1",
+        "0",
+        "3",
+        "1",
+        "0",
+        "1",
+        "MEDIUMINT",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "MEDIUMINT UNSIGNED",
+        "4",
+        "8",
+        "",
+        "",
+        "[(M)] [ZEROFILL]",
+        "1",
+        "0",
+        "3",
+        "1",
+        "0",
+        "1",
+        "MEDIUMINT UNSIGNED",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "SMALLINT",
+        "5",
+        "5",
+        "",
+        "",
+        "[(M)] [UNSIGNED] [ZEROFILL]",
+        "1",
+        "0",
+        "3",
+        "1",
+        "0",
+        "1",
+        "SMALLINT",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "SMALLINT UNSIGNED",
+        "5",
+        "5",
+        "",
+        "",
+        "[(M)] [ZEROFILL]",
+        "1",
+        "0",
+        "3",
+        "1",
+        "0",
+        "1",
+        "SMALLINT UNSIGNED",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "FLOAT",
+        "7",
+        "10",
+        "",
+        "",
+        "[(M|D)] [ZEROFILL]",
+        "1",
+        "0",
+        "3",
+        "0",
+        "0",
+        "1",
+        "FLOAT",
+        "-38",
+        "38",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "DOUBLE",
+        "8",
+        "17",
+        "",
+        "",
+        "[(M|D)] [ZEROFILL]",
+        "1",
+        "0",
+        "3",
+        "0",
+        "0",
+        "1",
+        "DOUBLE",
+        "-308",
+        "308",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "DOUBLE PRECISION",
+        "8",
+        "17",
+        "",
+        "",
+        "[(M,D)] [ZEROFILL]",
+        "1",
+        "0",
+        "3",
+        "0",
+        "0",
+        "1",
+        "DOUBLE PRECISION",
+        "-308",
+        "308",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "REAL",
+        "8",
+        "17",
+        "",
+        "",
+        "[(M,D)] [ZEROFILL]",
+        "1",
+        "0",
+        "3",
+        "0",
+        "0",
+        "1",
+        "REAL",
+        "-308",
+        "308",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "VARCHAR", "12", "255", "'", "'", "(M)", "1", "0", "3", "0", "0", "0", "VARCHAR", "0", "0",
+        "0", "0", "10"
+      },
+      {
+        "ENUM", "12", "65535", "'", "'", "", "1", "0", "3", "0", "0", "0", "ENUM", "0", "0", "0",
+        "0", "10"
+      },
+      {
+        "SET", "12", "64", "'", "'", "", "1", "0", "3", "0", "0", "0", "SET", "0", "0", "0", "0",
+        "10"
+      },
+      {
+        "DATE", "91", "10", "'", "'", "", "1", "0", "3", "0", "0", "0", "DATE", "0", "0", "0", "0",
+        "10"
+      },
+      {
+        "TIME", "92", "18", "'", "'", "[(M)]", "1", "0", "3", "0", "0", "0", "TIME", "0", "0", "0",
+        "0", "10"
+      },
+      {
+        "DATETIME",
+        "93",
+        "27",
+        "'",
+        "'",
+        "[(M)]",
+        "1",
+        "0",
+        "3",
+        "0",
+        "0",
+        "0",
+        "DATETIME",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      },
+      {
+        "TIMESTAMP",
+        "93",
+        "27",
+        "'",
+        "'",
+        "[(M)]",
+        "1",
+        "0",
+        "3",
+        "0",
+        "0",
+        "0",
+        "TIMESTAMP",
+        "0",
+        "0",
+        "0",
+        "0",
+        "10"
+      }
+    };
+
+    return CompleteResult.createResultSet(columnNames, dataTypes, data, connection.getContext(), 0);
+  }
+
+  /**
+   * Retrieves a description of the given table's indices and statistics. They are ordered by
+   * NON_UNIQUE, TYPE, INDEX_NAME, and ORDINAL_POSITION.
+   *
+   * <p>Each index column description has the following columns:
+   *
+   * <ol>
+   *   <li><B>TABLE_CAT</B> String {@code =>} table catalog (may be <code>null</code>)
+   *   <li><B>TABLE_SCHEM</B> String {@code =>} table schema (may be <code>null</code>)
+   *   <li><B>TABLE_NAME</B> String {@code =>} table name
+   *   <li><B>NON_UNIQUE</B> boolean {@code =>} Can index values be non-unique. false when TYPE is
+   *       tableIndexStatistic
+   *   <li><B>INDEX_QUALIFIER</B> String {@code =>} index catalog (may be <code>null</code>); <code>
+   *       null</code> when TYPE is tableIndexStatistic
+   *   <li><B>INDEX_NAME</B> String {@code =>} index name; <code>null</code> when TYPE is
+   *       tableIndexStatistic
+   *   <li><B>TYPE</B> short {@code =>} index type:
+   *       <ul>
+   *         <li>tableIndexStatistic - this identifies table statistics that are returned in
+   *             conjuction with a table's index descriptions
+   *         <li>tableIndexClustered - this is a clustered index
+   *         <li>tableIndexHashed - this is a hashed index
+   *         <li>tableIndexOther - this is some other style of index
+   *       </ul>
+   *   <li><B>ORDINAL_POSITION</B> short {@code =>} column sequence number within index; zero when
+   *       TYPE is tableIndexStatistic
+   *   <li><B>COLUMN_NAME</B> String {@code =>} column name; <code>null</code> when TYPE is
+   *       tableIndexStatistic
+   *   <li><B>ASC_OR_DESC</B> String {@code =>} column sort sequence, "A" {@code =>} ascending, "D"
+   *       {@code =>} descending, may be <code>null</code> if sort sequence is not supported; <code>
+   *       null</code> when TYPE is tableIndexStatistic
+   *   <li><B>CARDINALITY</B> long {@code =>} When TYPE is tableIndexStatistic, then this is the
+   *       number of rows in the table; otherwise, it is the number of unique values in the index.
+   *   <li><B>PAGES</B> long {@code =>} When TYPE is tableIndexStatisic then this is the number of
+   *       pages used for the table, otherwise it is the number of pages used for the current index.
+   *   <li><B>FILTER_CONDITION</B> String {@code =>} Filter condition, if any. (may be <code>null
+   *       </code>)
+   * </ol>
+   *
+   * @param catalog a catalog name; must match the catalog name as it is stored in this database; ""
+   *     retrieves those without a catalog; <code>null</code> means that the catalog name should not
+   *     be used to narrow the search
+   * @param schema a schema name; must match the schema name as it is stored in this database; ""
+   *     retrieves those without a schema; <code>null</code> means that the schema name should not
+   *     be used to narrow the search
+   * @param table a table name; must match the table name as it is stored in this database
+   * @param unique when true, return only indices for unique values; when false, return indices
+   *     regardless of whether unique or not
+   * @param approximate when true, result is allowed to reflect approximate or out of data values;
+   *     when false, results are requested to be accurate
+   * @return <code>ResultSet</code> - each row is an index column description
+   * @throws SQLException if a database access error occurs
+   */
+  public ResultSet getIndexInfo(
+      String catalog, String schema, String table, boolean unique, boolean approximate)
+      throws SQLException {
+    if (table == null) {
+      throw new SQLException("'table' parameter must not be null");
+    }
+    StringBuilder sb =
+        new StringBuilder(
+            "SELECT TABLE_SCHEMA TABLE_CAT, "
+                + "NULL TABLE_SCHEM, "
+                + "TABLE_NAME, "
+                + "NON_UNIQUE, "
+                + "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");
+
+    boolean firstCondition = catalogCond(true, sb, "TABLE_SCHEMA", catalog);
+    sb.append(firstCondition ? " WHERE " : " AND ")
+        .append("TABLE_NAME = ")
+        .append(escapeQuote(table));
+    if (unique) sb.append(" AND NON_UNIQUE = 0");
+    sb.append(" ORDER BY NON_UNIQUE, TYPE, INDEX_NAME, ORDINAL_POSITION");
+
+    return executeQuery(sb.toString());
+  }
+
+  /**
+   * Retrieves whether this database supports the given result set type. ResultSet.TYPE_FORWARD_ONLY
+   * and ResultSet.TYPE_SCROLL_INSENSITIVE are supported.
+   *
+   * @param type one of the following <code>ResultSet</code> constants:
+   *     <ul>
+   *       <li><code>ResultSet.TYPE_FORWARD_ONLY</code>
+   *       <li><code>ResultSet.TYPE_SCROLL_INSENSITIVE</code>
+   *       <li><code>ResultSet.TYPE_SCROLL_SENSITIVE</code>
+   *     </ul>
+   *
+   * @return true if supported
+   */
+  public boolean supportsResultSetType(int type) {
+    return (type == ResultSet.TYPE_SCROLL_INSENSITIVE || type == ResultSet.TYPE_FORWARD_ONLY);
+  }
+
+  /**
+   * Retrieves whether this database supports the given concurrency type in combination with the
+   * given result set type. All are supported, but combination that use
+   * ResultSet.TYPE_SCROLL_INSENSITIVE.
+   *
+   * @param type one of the following <code>ResultSet</code> constants:
+   *     <ul>
+   *       <li><code>ResultSet.TYPE_FORWARD_ONLY</code>
+   *       <li><code>ResultSet.TYPE_SCROLL_INSENSITIVE</code>
+   *       <li><code>ResultSet.TYPE_SCROLL_SENSITIVE</code>
+   *     </ul>
+   *
+   * @param concurrency one of the following <code>ResultSet</code> constants:
+   *     <ul>
+   *       <li><code>ResultSet.CONCUR_READ_ONLY</code>
+   *       <li><code>ResultSet.CONCUR_UPDATABLE</code>
+   *     </ul>
+   *
+   * @return true if supported
+   */
+  public boolean supportsResultSetConcurrency(int type, int concurrency) {
+    // Support all concurrency (ResultSet.CONCUR_READ_ONLY and ResultSet.CONCUR_UPDATABLE)
+    // so just return scroll type
+    return type == ResultSet.TYPE_SCROLL_INSENSITIVE || type == ResultSet.TYPE_FORWARD_ONLY;
+  }
+
+  public boolean ownUpdatesAreVisible(int type) {
+    return supportsResultSetType(type);
+  }
+
+  public boolean ownDeletesAreVisible(int type) {
+    return supportsResultSetType(type);
+  }
+
+  public boolean ownInsertsAreVisible(int type) {
+    return supportsResultSetType(type);
+  }
+
+  public boolean othersUpdatesAreVisible(int type) {
+    return false;
+  }
+
+  public boolean othersDeletesAreVisible(int type) {
+    return false;
+  }
+
+  public boolean othersInsertsAreVisible(int type) {
+    return false;
+  }
+
+  public boolean updatesAreDetected(int type) {
+    return false;
+  }
+
+  public boolean deletesAreDetected(int type) {
+    return false;
+  }
+
+  public boolean insertsAreDetected(int type) {
+    return false;
+  }
+
+  public boolean supportsBatchUpdates() {
+    return true;
+  }
+
+  /**
+   * Retrieves a description of the user-defined types (UDTs) defined in a particular schema.
+   * Schema-specific UDTs may have type <code>JAVA_OBJECT</code>, <code>STRUCT</code>, or <code>
+   * DISTINCT</code>.
+   *
+   * <p>Only types matching the catalog, schema, type name and type criteria are returned. They are
+   * ordered by <code>DATA_TYPE</code>, <code>TYPE_CAT</code>, <code>TYPE_SCHEM</code> and <code>
+   * TYPE_NAME</code>. The type name parameter may be a fully-qualified name. In this case, the
+   * catalog and schemaPattern parameters are ignored.
+   *
+   * <p>Each type description has the following columns:
+   *
+   * <ol>
+   *   <li><B>TYPE_CAT</B> String {@code =>} the type's catalog (may be <code>null</code>)
+   *   <li><B>TYPE_SCHEM</B> String {@code =>} type's schema (may be <code>null</code>)
+   *   <li><B>TYPE_NAME</B> String {@code =>} type name
+   *   <li><B>CLASS_NAME</B> String {@code =>} Java class name
+   *   <li><B>DATA_TYPE</B> int {@code =>} type value defined in java.sql.Types. One of JAVA_OBJECT,
+   *       STRUCT, or DISTINCT
+   *   <li><B>REMARKS</B> String {@code =>} explanatory comment on the type
+   *   <li><B>BASE_TYPE</B> short {@code =>} type code of the source type of a DISTINCT type or the
+   *       type that implements the user-generated reference type of the SELF_REFERENCING_COLUMN of
+   *       a structured type as defined in java.sql.Types (<code>null</code> if DATA_TYPE is not
+   *       DISTINCT or not STRUCT with REFERENCE_GENERATION = USER_DEFINED)
+   * </ol>
+   *
+   * <p><B>Note:</B> If the driver does not support UDTs, an empty result set is returned.
+   *
+   * @param catalog a catalog name; must match the catalog name as it is stored in the database; ""
+   *     retrieves those without a catalog; <code>null</code> means that the catalog name should not
+   *     be used to narrow the search
+   * @param schemaPattern a schema pattern name; must match the schema name as it is stored in the
+   *     database; "" retrieves those without a schema; <code>null</code> means that the schema name
+   *     should not be used to narrow the search
+   * @param typeNamePattern a type name pattern; must match the type name as it is stored in the
+   *     database; may be a fully qualified name
+   * @param types a list of user-defined types (JAVA_OBJECT, STRUCT, or DISTINCT) to include; <code>
+   *     null</code> returns all types
+   * @return <code>ResultSet</code> object in which each row describes a UDT
+   * @throws SQLException if a database access error occurs
+   * @see #getSearchStringEscape
+   * @since 1.2
+   */
+  @Override
+  public ResultSet getUDTs(
+      String catalog, String schemaPattern, String typeNamePattern, int[] types)
+      throws SQLException {
+    String sql =
+        "SELECT ' ' TYPE_CAT, NULL TYPE_SCHEM, ' ' TYPE_NAME, ' ' CLASS_NAME, 0 DATA_TYPE, ' '"
+            + " REMARKS, 0 BASE_TYPE FROM DUAL WHERE 1=0";
+
+    return executeQuery(sql);
+  }
+
+  public org.mariadb.jdbc.Connection getConnection() {
+    return connection;
+  }
+
+  public boolean supportsSavepoints() {
+    return true;
+  }
+
+  public boolean supportsNamedParameters() {
+    return false;
+  }
+
+  public boolean supportsMultipleOpenResults() {
+    return false;
+  }
+
+  public boolean supportsGetGeneratedKeys() {
+    return true;
+  }
+
+  /**
+   * Retrieves a description of the user-defined type (UDT) hierarchies defined in a particular
+   * schema in this database. Only the immediate super type/ subtype relationship is modeled. Only
+   * supertype information for UDTs matching the catalog, schema, and type name is returned. The
+   * type name parameter may be a fully-qualified name. When the UDT name supplied is a
+   * fully-qualified name, the catalog and schemaPattern parameters are ignored. If a UDT does not
+   * have a direct super type, it is not listed here. A row of the <code>ResultSet</code> object
+   * returned by this method describes the designated UDT and a direct supertype. A row has the
+   * following columns:
+   *
+   * <OL>
+   *   <li><B>TYPE_CAT</B> String {@code =>} the UDT's catalog (may be <code>null</code>)
+   *   <li><B>TYPE_SCHEM</B> String {@code =>} UDT's schema (may be <code>null</code>)
+   *   <li><B>TYPE_NAME</B> String {@code =>} type name of the UDT
+   *   <li><B>SUPERTYPE_CAT</B> String {@code =>} the direct super type's catalog (may be <code>null
+   *       </code>)
+   *   <li><B>SUPERTYPE_SCHEM</B> String {@code =>} the direct super type's schema (may be <code>
+   *       null</code>)
+   *   <li><B>SUPERTYPE_NAME</B> String {@code =>} the direct super type's name
+   * </OL>
+   *
+   * <p><B>Note:</B> If the driver does not support type hierarchies, an empty result set is
+   * returned.
+   *
+   * @param catalog a catalog name; "" retrieves those without a catalog; <code>null</code> means
+   *     drop catalog name from the selection criteria
+   * @param schemaPattern a schema name pattern; "" retrieves those without a schema
+   * @param typeNamePattern a UDT name pattern; may be a fully-qualified name
+   * @return a <code>ResultSet</code> object in which a row gives information about the designated
+   *     UDT
+   * @throws SQLException if a database access error occurs
+   * @see #getSearchStringEscape
+   * @since 1.4
+   */
+  public ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern)
+      throws SQLException {
+    String sql =
+        "SELECT  ' ' TYPE_CAT, NULL TYPE_SCHEM, ' ' TYPE_NAME, ' ' SUPERTYPE_CAT, ' '"
+            + " SUPERTYPE_SCHEM, ' '  SUPERTYPE_NAME FROM DUAL WHERE 1=0";
+
+    return executeQuery(sql);
+  }
+
+  /**
+   * Retrieves a description of the table hierarchies defined in a particular schema in this
+   * database.
+   *
+   * <p>Only supertable information for tables matching the catalog, schema and table name are
+   * returned. The table name parameter may be a fully-qualified name, in which case, the catalog
+   * and schemaPattern parameters are ignored. If a table does not have a super table, it is not
+   * listed here. Supertables have to be defined in the same catalog and schema as the sub tables.
+   * Therefore, the type description does not need to include this information for the supertable.
+   *
+   * <p>Each type description has the following columns:
+   *
+   * <OL>
+   *   <li><B>TABLE_CAT</B> String {@code =>} the type's catalog (may be <code>null</code>)
+   *   <li><B>TABLE_SCHEM</B> String {@code =>} type's schema (may be <code>null</code>)
+   *   <li><B>TABLE_NAME</B> String {@code =>} type name
+   *   <li><B>SUPERTABLE_NAME</B> String {@code =>} the direct super type's name
+   * </OL>
+   *
+   * <p><B>Note:</B> If the driver does not support type hierarchies, an empty result set is
+   * returned.
+   *
+   * @param catalog a catalog name; "" retrieves those without a catalog; <code>null</code> means
+   *     drop catalog name from the selection criteria
+   * @param schemaPattern a schema name pattern; "" retrieves those without a schema
+   * @param tableNamePattern a table name pattern; may be a fully-qualified name
+   * @return a <code>ResultSet</code> object in which each row is a type description
+   * @throws SQLException if a database access error occurs
+   * @see #getSearchStringEscape
+   * @since 1.4
+   */
+  public ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern)
+      throws SQLException {
+    String sql =
+        "SELECT  ' ' TABLE_CAT, ' ' TABLE_SCHEM, ' ' TABLE_NAME, ' ' SUPERTABLE_NAME FROM DUAL"
+            + " WHERE 1=0";
+    return executeQuery(sql);
+  }
+
+  /**
+   * Retrieves a description of the given attribute of the given type for a user-defined type (UDT)
+   * that is available in the given schema and catalog. Descriptions are returned only for
+   * attributes of UDTs matching the catalog, schema, type, and attribute name criteria. They are
+   * ordered by <code>TYPE_CAT</code>, <code>TYPE_SCHEM</code>, <code>TYPE_NAME</code> and <code>
+   * ORDINAL_POSITION</code>. This description does not contain inherited attributes. The <code>
+   * ResultSet</code> object that is returned has the following columns:
+   *
+   * <OL>
+   *   <li><B>TYPE_CAT</B> String {@code =>} type catalog (may be <code>null</code>)
+   *   <li><B>TYPE_SCHEM</B> String {@code =>} type schema (may be <code>null</code>)
+   *   <li><B>TYPE_NAME</B> String {@code =>} type name
+   *   <li><B>ATTR_NAME</B> String {@code =>} attribute name
+   *   <li><B>DATA_TYPE</B> int {@code =>} attribute type SQL type from java.sql.Types
+   *   <li><B>ATTR_TYPE_NAME</B> String {@code =>} Data source dependent type name. For a UDT, the
+   *       type name is fully qualified. For a REF, the type name is fully qualified and represents
+   *       the target type of the reference type.
+   *   <li><B>ATTR_SIZE</B> int {@code =>} column size. For char or date types this is the maximum
+   *       number of characters; for numeric or decimal types this is precision.
+   *   <li><B>DECIMAL_DIGITS</B> int {@code =>} the number of fractional digits. Null is returned
+   *       for data types where DECIMAL_DIGITS is not applicable.
+   *   <li><B>NUM_PREC_RADIX</B> int {@code =>} Radix (typically either 10 or 2)
+   *   <li><B>NULLABLE</B> int {@code =>} whether NULL is allowed
+   *       <UL>
+   *         <li>attributeNoNulls - might not allow NULL values
+   *         <li>attributeNullable - definitely allows NULL values
+   *         <li>attributeNullableUnknown - nullability unknown
+   *       </UL>
+   *   <li><B>REMARKS</B> String {@code =>} comment describing column (may be <code>null</code>)
+   *   <li><B>ATTR_DEF</B> String {@code =>} default value (may be<code>null</code>)
+   *   <li><B>SQL_DATA_TYPE</B> int {@code =>} unused
+   *   <li><B>SQL_DATETIME_SUB</B> int {@code =>} unused
+   *   <li><B>CHAR_OCTET_LENGTH</B> int {@code =>} for char types the maximum number of bytes in the
+   *       column
+   *   <li><B>ORDINAL_POSITION</B> int {@code =>} index of the attribute in the UDT (starting at 1)
+   *   <li><B>IS_NULLABLE</B> String {@code =>} ISO rules are used to determine the nullability for
+   *       a attribute.
+   *       <UL>
+   *         <li>YES --- if the attribute can include NULLs
+   *         <li>NO --- if the attribute cannot include NULLs
+   *         <li>empty string --- if the nullability for the attribute is unknown
+   *       </UL>
+   *   <li><B>SCOPE_CATALOG</B> String {@code =>} catalog of table that is the scope of a reference
+   *       attribute (<code>null</code> if DATA_TYPE isn't REF)
+   *   <li><B>SCOPE_SCHEMA</B> String {@code =>} schema of table that is the scope of a reference
+   *       attribute (<code>null</code> if DATA_TYPE isn't REF)
+   *   <li><B>SCOPE_TABLE</B> String {@code =>} table name that is the scope of a reference
+   *       attribute (<code>null</code> if the DATA_TYPE isn't REF)
+   *   <li><B>SOURCE_DATA_TYPE</B> short {@code =>} source type of a distinct type or user-generated
+   *       Ref type,SQL type from java.sql.Types (<code>null</code> if DATA_TYPE isn't DISTINCT or
+   *       user-generated REF)
+   * </OL>
+   *
+   * @param catalog a catalog name; must match the catalog name as it is stored in the database; ""
+   *     retrieves those without a catalog; <code>null</code> means that the catalog name should not
+   *     be used to narrow the search
+   * @param schemaPattern a schema name pattern; must match the schema name as it is stored in the
+   *     database; "" retrieves those without a schema; <code>null</code> means that the schema name
+   *     should not be used to narrow the search
+   * @param typeNamePattern a type name pattern; must match the type name as it is stored in the
+   *     database
+   * @param attributeNamePattern an attribute name pattern; must match the attribute name as it is
+   *     declared in the database
+   * @return a <code>ResultSet</code> object in which each row is an attribute description
+   * @throws SQLException if a database access error occurs
+   * @see #getSearchStringEscape
+   * @since 1.4
+   */
+  public ResultSet getAttributes(
+      String catalog, String schemaPattern, String typeNamePattern, String attributeNamePattern)
+      throws SQLException {
+
+    String sql =
+        "SELECT ' ' TYPE_CAT, ' ' TYPE_SCHEM, ' ' TYPE_NAME, ' ' ATTR_NAME, 0 DATA_TYPE, ' '"
+            + " ATTR_TYPE_NAME, 0 ATTR_SIZE, 0 DECIMAL_DIGITS, 0 NUM_PREC_RADIX, 0 NULLABLE, ' '"
+            + " REMARKS, ' ' ATTR_DEF,  0 SQL_DATA_TYPE, 0 SQL_DATETIME_SUB, 0 CHAR_OCTET_LENGTH, 0"
+            + " ORDINAL_POSITION, ' ' IS_NULLABLE, ' ' SCOPE_CATALOG, ' ' SCOPE_SCHEMA, ' '"
+            + " SCOPE_TABLE, 0 SOURCE_DATA_TYPE FROM DUAL  WHERE 1=0";
+
+    return executeQuery(sql);
+  }
+
+  public boolean supportsResultSetHoldability(int holdability) {
+    return holdability == ResultSet.HOLD_CURSORS_OVER_COMMIT;
+  }
+
+  public int getResultSetHoldability() {
+    return ResultSet.HOLD_CURSORS_OVER_COMMIT;
+  }
+
+  public int getDatabaseMajorVersion() {
+    return connection.getContext().getVersion().getMajorVersion();
+  }
+
+  public int getDatabaseMinorVersion() {
+    return connection.getContext().getVersion().getMinorVersion();
+  }
+
+  @Override
+  public int getJDBCMajorVersion() {
+    return 4;
+  }
+
+  @Override
+  public int getJDBCMinorVersion() {
+    return 2;
+  }
+
+  @Override
+  public int getSQLStateType() {
+    return sqlStateSQL99;
+  }
+
+  public boolean locatorsUpdateCopy() {
+    return false;
+  }
+
+  public boolean supportsStatementPooling() {
+    return false;
+  }
+
+  public RowIdLifetime getRowIdLifetime() {
+    return RowIdLifetime.ROWID_UNSUPPORTED;
+  }
+
+  public boolean supportsStoredFunctionsUsingCallSyntax() {
+    return true;
+  }
+
+  public boolean autoCommitFailureClosesAllResultSets() {
+    return false;
+  }
+
+  /**
+   * Retrieves a list of the client info properties that the driver supports. The result set
+   * contains the following columns
+   *
+   * <ol>
+   *   <li>NAME String : The name of the client info property
+   *   <li>MAX_LEN int : The maximum length of the value for the property
+   *   <li>DEFAULT_VALUE String : The default value of the property
+   *   <li>DESCRIPTION String : A description of the property. This will typically contain
+   *       information as to where this property is stored in the database.
+   * </ol>
+   *
+   * <p>The ResultSet is sorted by the NAME column
+   *
+   * @return A ResultSet object; each row is a supported client info property
+   */
+  public ResultSet getClientInfoProperties() {
+    String[] columnNames = new String[] {"NAME", "MAX_LEN", "DEFAULT_VALUE", "DESCRIPTION"};
+
+    DataType[] types =
+        new DataType[] {
+          DataType.VARSTRING, DataType.INTEGER, DataType.VARSTRING, DataType.VARSTRING
+        };
+    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(), 0);
+  }
+
+  /**
+   * Retrieves a description of the system and user functions available in the given catalog. Only
+   * system and user function descriptions matching the schema and function name criteria are
+   * returned. They are ordered by <code>FUNCTION_CAT</code>, <code>FUNCTION_SCHEM</code>, <code>
+   * FUNCTION_NAME</code> and <code>SPECIFIC_ NAME</code>.
+   *
+   * <p>Each function description has the following columns:
+   *
+   * <OL>
+   *   <li><B>FUNCTION_CAT</B> String {@code =>} function catalog (may be <code>null</code>)
+   *   <li><B>FUNCTION_SCHEM</B> String {@code =>} function schema (may be <code>null</code>)
+   *   <li><B>FUNCTION_NAME</B> String {@code =>} function name. This is the name used to invoke the
+   *       function
+   *   <li><B>REMARKS</B> String {@code =>} explanatory comment on the function
+   *   <li><B>FUNCTION_TYPE</B> short {@code =>} kind of function:
+   *       <UL>
+   *         <li>functionResultUnknown - Cannot determine if a return value or table will be
+   *             returned
+   *         <li>functionNoTable- Does not return a table
+   *         <li>functionReturnsTable - Returns a table
+   *       </UL>
+   *   <li><B>SPECIFIC_NAME</B> String {@code =>} the name which uniquely identifies this function
+   *       within its schema. This is a user specified, or DBMS generated, name that may be
+   *       different then the <code>FUNCTION_NAME</code> for example with overload functions
+   * </OL>
+   *
+   * <p>A user may not have permission to execute any of the functions that are returned by <code>
+   * getFunctions</code>
+   *
+   * @param catalog a catalog name; must match the catalog name as it is stored in the database; ""
+   *     retrieves those without a catalog; <code>null</code> means that the catalog name should not
+   *     be used to narrow the search
+   * @param schemaPattern a schema name pattern; must match the schema name as it is stored in the
+   *     database; "" retrieves those without a schema; <code>null</code> means that the schema name
+   *     should not be used to narrow the search
+   * @param functionNamePattern a function name pattern; must match the function name as it is
+   *     stored in the database
+   * @return <code>ResultSet</code> - each row is a function description
+   * @throws SQLException if a database access error occurs
+   * @see #getSearchStringEscape
+   * @since 1.6
+   */
+  public ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern)
+      throws SQLException {
+    StringBuilder sb =
+        new StringBuilder(
+            "SELECT ROUTINE_SCHEMA FUNCTION_CAT,"
+                + "NULL FUNCTION_SCHEM, "
+                + "ROUTINE_NAME FUNCTION_NAME, "
+                + "ROUTINE_COMMENT REMARKS, "
+                + functionNoTable
+                + " FUNCTION_TYPE, "
+                + "SPECIFIC_NAME "
+                + " FROM INFORMATION_SCHEMA.ROUTINES");
+    boolean firstCondition = catalogCond(true, sb, "ROUTINE_SCHEMA", catalog);
+    firstCondition = patternCond(firstCondition, sb, "ROUTINE_NAME", functionNamePattern);
+    sb.append(firstCondition ? " WHERE " : " AND ").append(" ROUTINE_TYPE='FUNCTION'");
+
+    return executeQuery(sb.toString());
+  }
+
+  @Override
+  public <T> T unwrap(Class<T> 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) {
+    return iface.isInstance(this);
+  }
+
+  @Override
+  public long getMaxLogicalLobSize() {
+    return 4294967295L;
+  }
+
+  private static class Identifier {
+    public String schema;
+    public String name;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/Driver.java b/src/main/java/org/mariadb/jdbc/Driver.java
new file mode 100644
index 0000000..db5da27
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/Driver.java
@@ -0,0 +1,189 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+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 java.util.concurrent.locks.ReentrantLock;
+import org.mariadb.jdbc.client.Client;
+import org.mariadb.jdbc.client.impl.MultiPrimaryClient;
+import org.mariadb.jdbc.client.impl.MultiPrimaryReplicaClient;
+import org.mariadb.jdbc.client.impl.ReplayClient;
+import org.mariadb.jdbc.client.impl.StandardClient;
+import org.mariadb.jdbc.pool.Pools;
+import org.mariadb.jdbc.util.VersionFactory;
+
+/** MariaDB Driver */
+public final class Driver implements java.sql.Driver {
+
+  static {
+    try {
+      DriverManager.registerDriver(new Driver());
+    } catch (SQLException e) {
+      // eat
+    }
+  }
+
+  /**
+   * Connect according to configuration
+   *
+   * @param configuration configuration
+   * @return a Connection
+   * @throws SQLException if connect fails
+   */
+  public static Connection connect(Configuration configuration) throws SQLException {
+    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:
+        ClientInstance<Configuration, HostAddress, ReentrantLock, Boolean, Client> clientInstance =
+            (configuration.transactionReplay()) ? ReplayClient::new : StandardClient::new;
+
+        if (configuration.addresses().isEmpty()) {
+          // unix socket / windows pipe
+          client = clientInstance.apply(configuration, null, lock, false);
+        } else {
+          // loop until finding
+          SQLException lastException = null;
+          for (HostAddress host : configuration.addresses()) {
+            try {
+              client = clientInstance.apply(configuration, host, lock, false);
+              return new Connection(configuration, lock, client);
+            } catch (SQLException e) {
+              lastException = e;
+            }
+          }
+          throw lastException;
+        }
+        break;
+    }
+    return new Connection(configuration, lock, client);
+  }
+
+  @FunctionalInterface
+  private interface ClientInstance<T, U, V, W, R> {
+    R apply(T t, U u, V v, W w) throws SQLException;
+  }
+
+  /**
+   * Connect to the given connection string.
+   *
+   * @param url the url to connect to
+   * @return a connection
+   * @throws SQLException if it is not possible to connect
+   */
+  public Connection connect(final String url, final Properties props) throws SQLException {
+    Configuration configuration = Configuration.parse(url, props);
+    if (configuration != null) {
+      if (configuration.pool()) {
+        return Pools.retrievePool(configuration).getPoolConnection().getConnection();
+      }
+      return connect(configuration);
+    }
+    return null;
+  }
+
+  /**
+   * returns true if the driver can accept the url.
+   *
+   * @param url the url to test
+   * @return true if the url is valid for this driver
+   */
+  @Override
+  public boolean acceptsURL(String url) {
+    return Configuration.acceptsUrl(url);
+  }
+
+  /**
+   * Get the property info.
+   *
+   * @param url the url to get properties for
+   * @param info the info props
+   * @return all possible connector options
+   * @throws SQLException if there is a problem getting the property info
+   */
+  public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException {
+    Configuration conf = Configuration.parse(url, info);
+    if (conf == null) {
+      return new DriverPropertyInfo[0];
+    }
+
+    Properties propDesc = new Properties();
+    try (InputStream inputStream =
+        Driver.class.getClassLoader().getResourceAsStream("driver.properties")) {
+      propDesc.load(inputStream);
+    } catch (IOException io) {
+      // eat
+    }
+
+    List<DriverPropertyInfo> 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[0]);
+  }
+
+  /**
+   * gets the major version of the driver.
+   *
+   * @return the major versions
+   */
+  public int getMajorVersion() {
+    return VersionFactory.getInstance().getMajorVersion();
+  }
+
+  /**
+   * gets the minor version of the driver.
+   *
+   * @return the minor version
+   */
+  public int getMinorVersion() {
+    return VersionFactory.getInstance().getMinorVersion();
+  }
+
+  /**
+   * checks if the driver is jdbc compliant.
+   *
+   * @return true since the driver is not compliant
+   */
+  public boolean jdbcCompliant() {
+    return true;
+  }
+
+  public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException {
+    throw new SQLFeatureNotSupportedException("Use logging parameters for enabling logging.");
+  }
+}
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 0000000..c40df49
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/FunctionStatement.java
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.sql.*;
+import java.util.concurrent.locks.ReentrantLock;
+import org.mariadb.jdbc.client.util.Parameters;
+import org.mariadb.jdbc.util.ParameterList;
+
+/** Function callable statement implementation */
+public class FunctionStatement extends BaseCallableStatement implements CallableStatement {
+  /**
+   * Constructor of function callable statement
+   *
+   * @param con current connection
+   * @param databaseName database
+   * @param procedureName procedure
+   * @param arguments arguments
+   * @param lock thread lock object
+   * @param canUseServerTimeout can use server timeout
+   * @param canUseServerMaxRows can use server max rows
+   * @param canCachePrepStmts can cache server prepared result
+   * @param resultSetType result set type
+   * @param resultSetConcurrency concurrency type
+   * @throws SQLException if any error occurs
+   */
+  public FunctionStatement(
+      Connection con,
+      String databaseName,
+      String procedureName,
+      String arguments,
+      ReentrantLock lock,
+      boolean canUseServerTimeout,
+      boolean canUseServerMaxRows,
+      boolean canCachePrepStmts,
+      int resultSetType,
+      int resultSetConcurrency)
+      throws SQLException {
+    super(
+        "SELECT " + procedureName + arguments,
+        con,
+        lock,
+        databaseName,
+        procedureName,
+        canUseServerTimeout,
+        canUseServerMaxRows,
+        canCachePrepStmts,
+        resultSetType,
+        resultSetConcurrency,
+        0);
+    registerOutParameter(1, null);
+  }
+
+  @Override
+  public boolean isFunction() {
+    return true;
+  }
+
+  @Override
+  protected void handleParameterOutput() throws SQLException {
+    this.outputResultFromRes(1);
+  }
+
+  @Override
+  public void registerOutParameter(int index, int sqlType) throws SQLException {
+    if (index != 1) {
+      throw con.getExceptionFactory()
+          .of(this)
+          .create(String.format("wrong parameter index %s", index));
+    }
+    super.registerOutParameter(index, sqlType);
+  }
+
+  @Override
+  protected void executeInternal() throws SQLException {
+    preValidParameters();
+    super.executeInternal();
+  }
+
+  /**
+   * Ensures that returning value is not taken as a parameter.
+   *
+   * @throws SQLException if any exception
+   */
+  protected void preValidParameters() throws SQLException {
+    // remove first parameter, as it's an output param only
+    Parameters 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();
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder("FunctionStatement{sql:'" + sql + "'");
+    sb.append(", parameters:[");
+    for (int i = 0; i < parameters.size(); i++) {
+      org.mariadb.jdbc.client.util.Parameter param = parameters.get(i);
+      if (outputParameters.contains(i + 1)) sb.append("<OUT>");
+      if (param == null) {
+        sb.append("null");
+      } else {
+        sb.append(param.bestEffortStringValue(con.getContext()));
+      }
+      if (i != parameters.size() - 1) {
+        sb.append(",");
+      }
+    }
+    sb.append("]}");
+    return sb.toString();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/HostAddress.java b/src/main/java/org/mariadb/jdbc/HostAddress.java
new file mode 100644
index 0000000..f489b56
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/HostAddress.java
@@ -0,0 +1,193 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import org.mariadb.jdbc.export.HaMode;
+
+/** Host entry */
+public class HostAddress {
+
+  /** host address */
+  public final String host;
+
+  /** port */
+  public int port;
+
+  /** primary node */
+  public Boolean primary;
+
+  /**
+   * Constructor.
+   *
+   * @param host host
+   * @param port port
+   * @param primary is primary
+   */
+  private HostAddress(String host, int port, Boolean primary) {
+    this.host = host;
+    this.port = port;
+    this.primary = primary;
+  }
+
+  /**
+   * Create a Host without knowledge of primary/replica goal
+   *
+   * @param host host (DNS/IP)
+   * @param port port
+   * @return host
+   */
+  public static HostAddress from(String host, int port) {
+    return new HostAddress(host, port, null);
+  }
+
+  /**
+   * Create a Host
+   *
+   * @param host host (DNS/IP)
+   * @param port port
+   * @param primary is primary
+   * @return host
+   */
+  public static HostAddress from(String host, int port, boolean primary) {
+    return new HostAddress(host, port, primary);
+  }
+
+  /**
+   * parse - parse server addresses from the URL fragment.
+   *
+   * @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<HostAddress> parse(String spec, HaMode haMode) throws SQLException {
+    if ("".equals(spec)) {
+      return new ArrayList<>(0);
+    }
+    String[] tokens = spec.trim().split(",");
+    int size = tokens.length;
+    List<HostAddress> arr = new ArrayList<>(size);
+
+    for (int i = 0; i < tokens.length; i++) {
+      String token = tokens[i];
+      if (token.startsWith("address=")) {
+        arr.add(parseParameterHostAddress(token, haMode, i == 0));
+      } else {
+        arr.add(parseSimpleHostAddress(token, haMode, i == 0));
+      }
+    }
+
+    return arr;
+  }
+
+  private static HostAddress parseSimpleHostAddress(String str, HaMode haMode, boolean first)
+      throws SQLException {
+    String host;
+    int port = 3306;
+
+    if (str.charAt(0) == '[') {
+      /* IPv6 addresses in URLs are enclosed in square brackets */
+      int ind = str.indexOf(']');
+      host = str.substring(1, ind);
+      if (ind != (str.length() - 1) && str.charAt(ind + 1) == ':') {
+        port = getPort(str.substring(ind + 2));
+      }
+    } else if (str.contains(":")) {
+      /* Parse host:port */
+      String[] hostPort = str.split(":");
+      host = hostPort[0];
+      port = getPort(hostPort[1]);
+    } else {
+      /* Just host name is given */
+      host = str;
+    }
+
+    boolean primary = haMode != HaMode.REPLICATION || first;
+
+    return new HostAddress(host, port, primary);
+  }
+
+  private static int getPort(String portString) throws SQLException {
+    try {
+      return Integer.parseInt(portString);
+    } catch (NumberFormatException nfe) {
+      throw new SQLException("Incorrect port value : " + portString);
+    }
+  }
+
+  private static HostAddress parseParameterHostAddress(String str, HaMode haMode, boolean first)
+      throws SQLException {
+    String host = null;
+    int port = 3306;
+    Boolean primary = null;
+
+    String[] array = str.replace(" ", "").split("(?=\\()|(?<=\\))");
+    for (int i = 1; i < array.length; i++) {
+      String[] token = array[i].replace("(", "").replace(")", "").trim().split("=");
+      if (token.length != 2) {
+        throw new IllegalArgumentException(
+            "Invalid connection URL, expected key=value pairs, found " + array[i]);
+      }
+      String key = token[0].toLowerCase();
+      String value = token[1].toLowerCase();
+
+      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) {
+      if (haMode == HaMode.REPLICATION) {
+        primary = first;
+      } else {
+        primary = true;
+      }
+    }
+
+    return new HostAddress(host, port, primary);
+  }
+
+  @Override
+  public String toString() {
+    return String.format(
+        "address=(host=%s)(port=%s)%s",
+        host, port, ((primary != null) ? ("(type=" + (primary ? "primary)" : "replica)")) : ""));
+  }
+
+  @Override
+  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
+        && Objects.equals(host, that.host)
+        && Objects.equals(primary, that.primary);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(host, port, primary);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/MariaDbBlob.java b/src/main/java/org/mariadb/jdbc/MariaDbBlob.java
new file mode 100644
index 0000000..c0ff330
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/MariaDbBlob.java
@@ -0,0 +1,433 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.io.*;
+import java.sql.Blob;
+import java.sql.SQLException;
+import java.util.Arrays;
+
+/** MariaDB Blob implementation */
+public class MariaDbBlob implements Blob, Serializable {
+
+  private static final long serialVersionUID = -4736603161284649490L;
+
+  /** content */
+  protected byte[] data;
+
+  /** data offset */
+  protected transient int offset;
+
+  /** data length */
+  protected transient int length;
+
+  /** Creates an empty blob. */
+  public MariaDbBlob() {
+    data = new byte[0];
+    offset = 0;
+    length = 0;
+  }
+
+  /**
+   * Creates a blob with content.
+   *
+   * @param bytes the content for the blob.
+   */
+  public MariaDbBlob(byte[] bytes) {
+    if (bytes == null) {
+      throw new IllegalArgumentException("byte array is null");
+    }
+    data = bytes;
+    offset = 0;
+    length = bytes.length;
+  }
+
+  /**
+   * Creates a blob with content.
+   *
+   * @param bytes the content for the blob.
+   * @param offset offset
+   * @param length length
+   */
+  public MariaDbBlob(byte[] bytes, int offset, int length) {
+    if (bytes == null) {
+      throw new IllegalArgumentException("byte array is null");
+    }
+    data = bytes;
+    this.offset = offset;
+    this.length = Math.min(bytes.length - offset, length);
+  }
+
+  private MariaDbBlob(int offset, int length, byte[] bytes) {
+    this.data = bytes;
+    this.offset = offset;
+    this.length = length;
+  }
+
+  /**
+   * Return a new Blob from blob data
+   *
+   * @param bytes data
+   * @param offset data offset
+   * @param length data length
+   * @return new Blob
+   */
+  public static MariaDbBlob safeMariaDbBlob(byte[] bytes, int offset, int length) {
+    return new MariaDbBlob(offset, length, bytes);
+  }
+
+  /**
+   * Returns the number of bytes in the <code>BLOB</code> value designated by this <code>Blob</code>
+   * object.
+   *
+   * @return length of the <code>BLOB</code> in bytes
+   */
+  public long length() {
+    return length;
+  }
+
+  /**
+   * Retrieves all or part of the <code>BLOB</code> value that this <code>Blob</code> object
+   * represents, as an array of bytes. This <code>byte</code> array contains up to <code>length
+   * </code> consecutive bytes starting at position <code>pos</code>.
+   *
+   * @param pos the ordinal position of the first byte in the <code>BLOB</code> value to be
+   *     extracted; the first byte is at position 1
+   * @param length the number of consecutive bytes to be copied; the value for length must be 0 or
+   *     greater
+   * @return a byte array containing up to <code>length</code> consecutive bytes from the <code>BLOB
+   *     </code> value designated by this <code>Blob</code> object, starting with the byte at
+   *     position <code>pos</code>
+   * @throws SQLException if there is an error accessing the <code>BLOB</code> value; if pos is less
+   *     than 1 or length is less than 0
+   * @see #setBytes
+   * @since 1.2
+   */
+  public byte[] getBytes(final long pos, final int length) throws SQLException {
+    if (pos < 1) {
+      throw new SQLException(
+          String.format("Out of range (position should be > 0, but is %s)", pos));
+    }
+    final int offset = this.offset + (int) (pos - 1);
+    byte[] result = new byte[length];
+    System.arraycopy(data, offset, result, 0, Math.min(this.length - (int) (pos - 1), length));
+    return result;
+  }
+
+  /**
+   * Retrieves the <code>BLOB</code> value designated by this <code>Blob</code> instance as a
+   * stream.
+   *
+   * @return a stream containing the <code>BLOB</code> data
+   * @throws SQLException if something went wrong
+   * @see #setBinaryStream
+   */
+  public InputStream getBinaryStream() throws SQLException {
+    return getBinaryStream(1, length);
+  }
+
+  /**
+   * Returns an <code>InputStream</code> object that contains a partial <code>Blob</code> value,
+   * starting with the byte specified by pos, which is length bytes in length.
+   *
+   * @param pos the offset to the first byte of the partial value to be retrieved. The first byte in
+   *     the <code>Blob</code> is at position 1
+   * @param length the length in bytes of the partial value to be retrieved
+   * @return <code>InputStream</code> through which the partial <code>Blob</code> value can be read.
+   * @throws SQLException if pos is less than 1 or if pos is greater than the number of bytes in the
+   *     <code>Blob</code> or if pos + length is greater than the number of bytes in the <code>Blob
+   *     </code>
+   */
+  public InputStream getBinaryStream(final long pos, final long length) throws SQLException {
+    if (pos < 1) {
+      throw new SQLException("Out of range (position should be > 0)");
+    }
+    if (pos - 1 > this.length) {
+      throw new SQLException("Out of range (position > stream size)");
+    }
+    if (pos + length - 1 > this.length) {
+      throw new SQLException("Out of range (position + length - 1 > streamSize)");
+    }
+
+    return new ByteArrayInputStream(data, this.offset + (int) pos - 1, (int) length);
+  }
+
+  /**
+   * Retrieves the byte position at which the specified byte array <code>pattern</code> begins
+   * within the <code>BLOB</code> value that this <code>Blob</code> object represents. The search
+   * for <code>pattern</code> begins at position <code>start</code>.
+   *
+   * @param pattern the byte array for which to search
+   * @param start the position at which to begin searching; the first position is 1
+   * @return the position at which the pattern appears, else -1
+   */
+  public long position(final byte[] pattern, final long start) throws SQLException {
+    if (pattern.length == 0) {
+      return 0;
+    }
+    if (start < 1) {
+      throw new SQLException(
+          String.format("Out of range (position should be > 0, but is %s)", start));
+    }
+    if (start > this.length) {
+      throw new SQLException("Out of range (start > stream size)");
+    }
+
+    outer:
+    for (int i = (int) (offset + start - 1); i <= offset + this.length - pattern.length; i++) {
+      for (int j = 0; j < pattern.length; j++) {
+        if (data[i + j] != pattern[j]) {
+          continue outer;
+        }
+      }
+      return i + 1 - offset;
+    }
+    return -1;
+  }
+
+  /**
+   * Retrieves the byte position in the <code>BLOB</code> value designated by this <code>Blob</code>
+   * object at which <code>pattern</code> begins. The search begins at position <code>start</code>.
+   *
+   * @param pattern the <code>Blob</code> object designating the <code>BLOB</code> value for which
+   *     to search
+   * @param start the position in the <code>BLOB</code> value at which to begin searching; the first
+   *     position is 1
+   * @return the position at which the pattern begins, else -1
+   */
+  public long position(final Blob pattern, final long start) throws SQLException {
+    byte[] blobBytes = pattern.getBytes(1, (int) pattern.length());
+    return position(blobBytes, start);
+  }
+
+  /**
+   * Writes the given array of bytes to the <code>BLOB</code> value that this <code>Blob</code>
+   * object represents, starting at position <code>pos</code>, and returns the number of bytes
+   * written. The array of bytes will overwrite the existing bytes in the <code>Blob</code> object
+   * starting at the position <code>pos</code>. If the end of the <code>Blob</code> value is reached
+   * while writing the array of bytes, then the length of the <code>Blob</code> value will be
+   * increased to accommodate the extra bytes.
+   *
+   * @param pos the position in the <code>BLOB</code> object at which to start writing; the first
+   *     position is 1
+   * @param bytes the array of bytes to be written to the <code>BLOB</code> value that this <code>
+   *     Blob</code> object represents
+   * @return the number of bytes written
+   * @see #getBytes
+   */
+  public int setBytes(final long pos, final byte[] bytes) throws SQLException {
+    if (pos < 1) {
+      throw new SQLException("pos should be > 0, first position is 1.");
+    }
+
+    final int arrayPos = (int) pos - 1;
+
+    if (length > arrayPos + bytes.length) {
+
+      System.arraycopy(bytes, 0, data, offset + arrayPos, bytes.length);
+
+    } else {
+
+      byte[] newContent = new byte[arrayPos + bytes.length];
+      if (Math.min(arrayPos, length) > 0) {
+        System.arraycopy(data, this.offset, newContent, 0, Math.min(arrayPos, length));
+      }
+      System.arraycopy(bytes, 0, newContent, arrayPos, bytes.length);
+      data = newContent;
+      length = arrayPos + bytes.length;
+      offset = 0;
+    }
+    return bytes.length;
+  }
+
+  /**
+   * Writes all or part of the given <code>byte</code> array to the <code>BLOB</code> value that
+   * this <code>Blob</code> object represents and returns the number of bytes written. Writing
+   * starts at position <code>pos</code> in the <code>BLOB</code> value; <code>len</code> bytes from
+   * the given byte array are written. The array of bytes will overwrite the existing bytes in the
+   * <code>Blob</code> object starting at the position <code>pos</code>. If the end of the <code>
+   * Blob</code> value is reached while writing the array of bytes, then the length of the <code>
+   * Blob</code> value will be increased to accommodate the extra bytes.
+   *
+   * <p><b>Note:</b> If the value specified for <code>pos</code> is greater than the length+1 of the
+   * <code>BLOB</code> value then the behavior is undefined. Some JDBC drivers may throw a <code>
+   * SQLException</code> while other drivers may support this operation.
+   *
+   * @param pos the position in the <code>BLOB</code> object at which to start writing; the first
+   *     position is 1
+   * @param bytes the array of bytes to be written to this <code>BLOB</code> object
+   * @param offset the offset into the array <code>bytes</code> at which to start reading the bytes
+   *     to be set
+   * @param len the number of bytes to be written to the <code>BLOB</code> value from the array of
+   *     bytes <code>bytes</code>
+   * @return the number of bytes written
+   * @throws SQLException if there is an error accessing the <code>BLOB</code> value or if pos is
+   *     less than 1
+   * @see #getBytes
+   */
+  public int setBytes(final long pos, final byte[] bytes, final int offset, final int len)
+      throws SQLException {
+
+    if (pos < 1) {
+      throw new SQLException("pos should be > 0, first position is 1.");
+    }
+
+    final int arrayPos = (int) pos - 1;
+    final int byteToWrite = Math.min(bytes.length - offset, len);
+
+    if (length > arrayPos + byteToWrite) {
+
+      System.arraycopy(bytes, offset, data, this.offset + arrayPos, byteToWrite);
+
+    } else {
+
+      byte[] newContent = new byte[arrayPos + byteToWrite];
+      if (Math.min(arrayPos, length) > 0) {
+        System.arraycopy(data, this.offset, newContent, 0, Math.min(arrayPos, length));
+      }
+      System.arraycopy(bytes, offset, newContent, arrayPos, byteToWrite);
+      data = newContent;
+      length = arrayPos + byteToWrite;
+      this.offset = 0;
+    }
+
+    return byteToWrite;
+  }
+
+  /**
+   * Retrieves a stream that can be used to write to the <code>BLOB</code> value that this <code>
+   * Blob</code> object represents. The stream begins at position <code>pos</code>. The bytes
+   * written to the stream will overwrite the existing bytes in the <code>Blob</code> object
+   * starting at the position <code>pos</code>. If the end of the <code>Blob</code> value is reached
+   * while writing to the stream, then the length of the <code>Blob</code> value will be increased
+   * to accommodate the extra bytes.
+   *
+   * <p><b>Note:</b> If the value specified for <code>pos</code> is greater than the length+1 of the
+   * <code>BLOB</code> value then the behavior is undefined. Some JDBC drivers may throw a <code>
+   * SQLException</code> while other drivers may support this operation.
+   *
+   * @param pos the position in the <code>BLOB</code> value at which to start writing; the first
+   *     position is 1
+   * @return a <code>java.io.OutputStream</code> object to which data can be written
+   * @throws SQLException if there is an error accessing the <code>BLOB</code> value or if pos is
+   *     less than 1
+   * @see #getBinaryStream
+   * @since 1.4
+   */
+  public OutputStream setBinaryStream(final long pos) throws SQLException {
+    if (pos < 1) {
+      throw new SQLException("Invalid position in blob");
+    }
+    if (offset > 0) {
+      byte[] tmp = new byte[length];
+      System.arraycopy(data, offset, tmp, 0, length);
+      data = tmp;
+      offset = 0;
+    }
+    return new BlobOutputStream(this, (int) (pos - 1) + offset);
+  }
+
+  /**
+   * Truncates the <code>BLOB</code> value that this <code>Blob</code> object represents to be
+   * <code>len</code> bytes in length.
+   *
+   * @param len the length, in bytes, to which the <code>BLOB</code> value that this <code>Blob
+   *     </code> object represents should be truncated
+   */
+  public void truncate(final long len) {
+    if (len >= 0 && len < this.length) {
+      this.length = (int) len;
+    }
+  }
+
+  /**
+   * This method frees the <code>Blob</code> object and releases the resources that it holds. The
+   * object is invalid once the <code>free</code> method is called.
+   *
+   * <p>After <code>free</code> has been called, any attempt to invoke a method other than <code>
+   * free</code> will result in a <code>SQLException</code> being thrown. If <code>free</code> is
+   * called multiple times, the subsequent calls to <code>free</code> are treated as a no-op.
+   */
+  public void free() {
+    this.data = new byte[0];
+    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;
+  }
+
+  static 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) {
+
+      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
new file mode 100644
index 0000000..a06cd20
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/MariaDbClob.java
@@ -0,0 +1,298 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.io.*;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.StandardCharsets;
+import java.sql.Clob;
+import java.sql.NClob;
+import java.sql.SQLException;
+
+/** MariaDB Clob implementation */
+public class MariaDbClob extends MariaDbBlob implements Clob, NClob, Serializable {
+
+  private static final long serialVersionUID = -3066501059817815286L;
+
+  /**
+   * Creates a Clob with content.
+   *
+   * @param bytes the content for the Clob.
+   */
+  public MariaDbClob(byte[] bytes) {
+    super(bytes);
+  }
+
+  /**
+   * Creates a Clob with content.
+   *
+   * @param bytes the content for the Clob.
+   * @param offset offset
+   * @param length length
+   */
+  public MariaDbClob(byte[] bytes, int offset, int length) {
+    super(bytes, offset, length);
+  }
+
+  /** Creates an empty Clob. */
+  public MariaDbClob() {
+    super();
+  }
+
+  /**
+   * ToString implementation.
+   *
+   * @return string value of blob content.
+   */
+  public String toString() {
+    return new String(data, offset, length, StandardCharsets.UTF_8);
+  }
+
+  /**
+   * Get sub string.
+   *
+   * @param pos position
+   * @param length length of sub string
+   * @return substring
+   * @throws SQLException if pos is less than 1 or length is less than 0
+   */
+  public String getSubString(long pos, int length) throws SQLException {
+
+    if (pos < 1) {
+      throw new SQLException("position must be >= 1");
+    }
+
+    if (length < 0) {
+      throw new SQLException("length must be > 0");
+    }
+
+    String val = toString();
+    return val.substring((int) pos - 1, Math.min((int) pos - 1 + length, val.length()));
+  }
+
+  public Reader getCharacterStream() {
+    return new StringReader(toString());
+  }
+
+  /**
+   * Returns a Reader object that contains a partial Clob value, starting with the character
+   * specified by pos, which is length characters in length.
+   *
+   * @param pos the offset to the first character of the partial value to be retrieved. The first
+   *     character in the Clob is at position 1.
+   * @param length the length in characters of the partial value to be retrieved.
+   * @return Reader through which the partial Clob value can be read.
+   * @throws SQLException if pos is less than 1 or if pos is greater than the number of characters
+   *     in the Clob or if pos + length is greater than the number of characters in the Clob
+   */
+  public Reader getCharacterStream(long pos, long length) throws SQLException {
+    String val = toString();
+    if (val.length() < (int) pos - 1 + length) {
+      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);
+  }
+
+  /**
+   * Set character stream.
+   *
+   * @param pos position
+   * @return writer
+   * @throws SQLException if position is invalid
+   */
+  public Writer setCharacterStream(long pos) throws SQLException {
+    int bytePosition = utf8Position((int) pos - 1);
+    OutputStream stream = setBinaryStream(bytePosition + 1);
+    return new OutputStreamWriter(stream, StandardCharsets.UTF_8);
+  }
+
+  public InputStream getAsciiStream() throws SQLException {
+    return getBinaryStream();
+  }
+
+  public long position(String searchStr, long start) {
+    return toString().indexOf(searchStr, (int) start - 1) + 1;
+  }
+
+  public long position(Clob searchStr, long start) {
+    return position(searchStr.toString(), start);
+  }
+
+  /**
+   * Convert character position into byte position in UTF8 byte array.
+   *
+   * @param charPosition charPosition
+   * @return byte position
+   */
+  private int utf8Position(int charPosition) {
+    int pos = offset;
+    for (int i = 0; i < charPosition; i++) {
+      int byteValue = data[pos] & 0xff;
+      if (byteValue < 0x80) {
+        pos += 1;
+      } else if (byteValue < 0xE0) {
+        pos += 2;
+      } else if (byteValue < 0xF0) {
+        pos += 3;
+      } else {
+        pos += 4;
+      }
+    }
+    return pos;
+  }
+
+  /**
+   * Set String.
+   *
+   * @param pos position
+   * @param str string
+   * @return string length
+   * @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 {
+    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 {
+    return setBinaryStream(utf8Position((int) pos - 1) + 1);
+  }
+
+  /** Return character length of the Clob. Assume UTF8 encoding. */
+  @Override
+  public long length() {
+    // The length of a character string is the number of UTF-16 units (not the number of characters)
+    long len = 0;
+    int pos = offset;
+
+    // set ASCII (<= 127 chars)
+    while (len < length && data[pos] > 0) {
+      len++;
+      pos++;
+    }
+
+    // multi-bytes UTF-8
+    while (pos < offset + length) {
+      byte firstByte = data[pos++];
+      if (firstByte < 0) {
+        if (firstByte >> 5 != -2 || (firstByte & 30) == 0) {
+          if (firstByte >> 4 == -2) {
+            if (pos + 1 < offset + length) {
+              pos += 2;
+              len++;
+            } else {
+              throw new UncheckedIOException("invalid UTF8", new CharacterCodingException());
+            }
+          } else if (firstByte >> 3 != -2) {
+            throw new UncheckedIOException("invalid UTF8", new CharacterCodingException());
+          } else if (pos + 2 < offset + length) {
+            pos += 3;
+            len += 2;
+          } else {
+            // bad truncated UTF8
+            pos += offset + length;
+            len += 1;
+          }
+        } else {
+          pos++;
+          len++;
+        }
+      } else {
+        len++;
+      }
+    }
+    return len;
+  }
+
+  @Override
+  public void truncate(final long truncateLen) {
+
+    // truncate the number of UTF-16 characters
+    // this can result in a bad UTF-8 string if string finish with a
+    // character represented in 2 UTF-16
+    long len = 0;
+    int pos = offset;
+
+    // set ASCII (<= 127 chars)
+    while (len < length && len < truncateLen && data[pos] >= 0) {
+      len++;
+      pos++;
+    }
+
+    // multi-bytes UTF-8
+    while (pos < offset + length && len < truncateLen) {
+      byte firstByte = data[pos++];
+      if (firstByte < 0) {
+        if (firstByte >> 5 != -2 || (firstByte & 30) == 0) {
+          if (firstByte >> 4 == -2) {
+            if (pos + 1 < offset + length) {
+              pos += 2;
+              len++;
+            } else {
+              throw new UncheckedIOException("invalid UTF8", new CharacterCodingException());
+            }
+          } else if (firstByte >> 3 != -2) {
+            throw new UncheckedIOException("invalid UTF8", new CharacterCodingException());
+          } else if (pos + 2 < offset + length) {
+            if (len + 2 <= truncateLen) {
+              pos += 3;
+              len += 2;
+            } else {
+              // truncation will result in bad UTF-8 String
+              pos += 1;
+              len = truncateLen;
+            }
+          } else {
+            throw new UncheckedIOException("invalid UTF8", new CharacterCodingException());
+          }
+        } else {
+          pos++;
+          len++;
+        }
+      } else {
+        len++;
+      }
+    }
+    length = pos - offset;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+
+    MariaDbClob that = (MariaDbClob) 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;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/MariaDbDataSource.java b/src/main/java/org/mariadb/jdbc/MariaDbDataSource.java
new file mode 100644
index 0000000..f1060be
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/MariaDbDataSource.java
@@ -0,0 +1,290 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.io.PrintWriter;
+import java.sql.*;
+import java.sql.Connection;
+import java.util.logging.Logger;
+import javax.sql.*;
+
+/** MariaDB basic datasource */
+public class MariaDbDataSource implements DataSource, ConnectionPoolDataSource, XADataSource {
+
+  /** configuration */
+  private Configuration conf = null;
+
+  /** url permitting creating configuration */
+  private String url = null;
+
+  /** username */
+  private String user = null;
+
+  /** password */
+  private String password = null;
+
+  /** connect timeout */
+  private Integer loginTimeout = null;
+
+  /** Basic constructor */
+  public MariaDbDataSource() {}
+
+  /**
+   * Constructor with URL
+   *
+   * @param url connection string
+   * @throws SQLException if url is not supported
+   */
+  public MariaDbDataSource(String url) throws SQLException {
+    if (Configuration.acceptsUrl(url)) {
+      this.url = url;
+    } else {
+      throw new SQLException(String.format("Wrong mariaDB url: %s", url));
+    }
+  }
+
+  /**
+   * Create configuration from url/user/password/loginTimeout
+   *
+   * @throws SQLException if not supported
+   */
+  private void config() throws SQLException {
+    if (url == null) throw new SQLException("url not set");
+    conf = Configuration.parse(url);
+    if (loginTimeout != null) conf.connectTimeout(loginTimeout * 1000);
+    if (user != null || password != null) {
+      conf = conf.clone(user, password);
+    }
+    if (user != null) {
+      user = conf.user();
+    }
+    if (password != null) {
+      password = conf.password();
+    }
+  }
+
+  /**
+   * 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 {
+    if (conf == null) config();
+    return Driver.connect(conf);
+  }
+
+  /**
+   * 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
+   */
+  @Override
+  public Connection getConnection(String username, String password) throws SQLException {
+    if (conf == null) config();
+    Configuration conf = this.conf.clone(username, password);
+    return Driver.connect(conf);
+  }
+
+  /**
+   * Returns an object that implements the given interface to allow access to non-standard methods,
+   * or standard methods not exposed by the proxy.
+   *
+   * <p>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 result of
+   * calling <code>unwrap</code> 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 <code>SQLException
+   * </code> is thrown.
+   *
+   * @param iface A Class defining an interface that the result must implement.
+   * @return an object that implements the interface. Maybe a proxy for the actual implementing
+   *     object.
+   * @throws SQLException If no object found that implements the interface
+   */
+  @Override
+  public <T> T unwrap(Class<T> iface) throws SQLException {
+    if (isWrapperFor(iface)) {
+      return iface.cast(this);
+    }
+    throw new SQLException("Datasource 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 <code>
+   * isWrapperFor</code> 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
+   * <code>unwrap</code> so that callers can use this method to avoid expensive <code>unwrap</code>
+   * calls that may fail. If this method returns true then calling <code>unwrap</code> 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);
+  }
+
+  /**
+   * Implementation doesn't use logwriter
+   *
+   * @return the log writer for this data source or null if logging is disabled
+   * @see #setLogWriter
+   */
+  @Override
+  public PrintWriter getLogWriter() {
+    return null;
+  }
+
+  /**
+   * Implementation doesn't use logwriter
+   *
+   * @param out the new log writer; to disable logging, set to null
+   * @see #getLogWriter
+   */
+  @Override
+  public void setLogWriter(PrintWriter out) {}
+
+  /**
+   * 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 <code>DataSource</code> object is
+   * created, the login timeout is initially to 30s.
+   *
+   * @return the data source login time limit
+   * @see #setLoginTimeout
+   */
+  @Override
+  public int getLoginTimeout() {
+    if (loginTimeout != null) return loginTimeout;
+    if (conf != null) return conf.connectTimeout() / 1000;
+    return DriverManager.getLoginTimeout() > 0 ? DriverManager.getLoginTimeout() : 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 <code>DataSource</code> object
+   * is created, the login timeout is initially 30s.
+   *
+   * @param seconds the data source login time limit
+   * @throws SQLException if wrong configuration set
+   * @see #getLoginTimeout
+   */
+  @Override
+  public void setLoginTimeout(int seconds) throws SQLException {
+    loginTimeout = seconds;
+    if (conf != null) config();
+  }
+
+  /**
+   * Not implemented
+   *
+   * @return the parent Logger for this data source
+   */
+  @Override
+  public Logger getParentLogger() {
+    return null;
+  }
+
+  @Override
+  public PooledConnection getPooledConnection() throws SQLException {
+    if (conf == null) config();
+    return new MariaDbPoolConnection(Driver.connect(conf));
+  }
+
+  @Override
+  public PooledConnection getPooledConnection(String username, String password)
+      throws SQLException {
+    if (conf == null) config();
+    Configuration conf = this.conf.clone(username, password);
+    return new MariaDbPoolConnection(Driver.connect(conf));
+  }
+
+  @Override
+  public XAConnection getXAConnection() throws SQLException {
+    if (conf == null) config();
+    return new MariaDbPoolConnection(Driver.connect(conf));
+  }
+
+  @Override
+  public XAConnection getXAConnection(String username, String password) throws SQLException {
+    if (conf == null) config();
+    Configuration conf = this.conf.clone(username, password);
+    return new MariaDbPoolConnection(Driver.connect(conf));
+  }
+
+  /**
+   * Sets the URL for this datasource
+   *
+   * @param url connection string
+   * @throws SQLException if url is not accepted
+   */
+  public void setUrl(String url) throws SQLException {
+    if (Configuration.acceptsUrl(url)) {
+      this.url = url;
+      config();
+    } else {
+      throw new SQLException(String.format("Wrong mariaDB url: %s", url));
+    }
+  }
+
+  /**
+   * Returns the URL for this datasource
+   *
+   * @return the URL for this datasource
+   */
+  public String getUrl() {
+    if (conf == null) return url;
+    return conf.initialUrl();
+  }
+
+  /**
+   * get User
+   *
+   * @return user
+   */
+  public String getUser() {
+    return user;
+  }
+
+  /**
+   * Set user
+   *
+   * @param user user
+   * @throws SQLException if wrong resulting connection string
+   */
+  public void setUser(String user) throws SQLException {
+    this.user = user;
+    if (conf != null) config();
+  }
+
+  /**
+   * Set password
+   *
+   * @param password password
+   * @throws SQLException if wrong configuration
+   */
+  public void setPassword(String password) throws SQLException {
+    this.password = password;
+    if (conf != null) config();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/MariaDbPoolConnection.java b/src/main/java/org/mariadb/jdbc/MariaDbPoolConnection.java
new file mode 100644
index 0000000..340e68c
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/MariaDbPoolConnection.java
@@ -0,0 +1,300 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import javax.sql.*;
+import javax.transaction.xa.XAException;
+import javax.transaction.xa.XAResource;
+import javax.transaction.xa.Xid;
+import org.mariadb.jdbc.util.StringUtils;
+
+/** MariaDB pool connection implementation */
+public class MariaDbPoolConnection implements PooledConnection, XAConnection {
+
+  private final Connection connection;
+  private final List<ConnectionEventListener> connectionEventListeners;
+  private final List<StatementEventListener> statementEventListeners;
+
+  /**
+   * Constructor.
+   *
+   * @param connection connection to retrieve connection options
+   */
+  public MariaDbPoolConnection(Connection connection) {
+    this.connection = connection;
+    this.connection.setPoolConnection(this);
+    statementEventListeners = new CopyOnWriteArrayList<>();
+    connectionEventListeners = new CopyOnWriteArrayList<>();
+  }
+
+  @Override
+  public Connection getConnection() {
+    return connection;
+  }
+
+  @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);
+  }
+
+  /**
+   * Fire statement close event to registered listeners.
+   *
+   * @param statement closing statement
+   */
+  public void fireStatementClosed(PreparedStatement statement) {
+    StatementEvent event = new StatementEvent(this, statement);
+    for (StatementEventListener listener : statementEventListeners) {
+      listener.statementClosed(event);
+    }
+  }
+
+  /**
+   * Fire statement error event to registered listeners.
+   *
+   * @param statement closing statement
+   * @param returnEx exception
+   */
+  public void fireStatementErrorOccurred(PreparedStatement statement, SQLException returnEx) {
+    StatementEvent event = new StatementEvent(this, statement, returnEx);
+    for (StatementEventListener listener : statementEventListeners) {
+      listener.statementErrorOccurred(event);
+    }
+  }
+
+  /**
+   * Fire connection close event to registered listeners.
+   *
+   * @param event close connection event
+   */
+  public void fireConnectionClosed(ConnectionEvent event) {
+    for (ConnectionEventListener listener : connectionEventListeners) {
+      listener.connectionClosed(event);
+    }
+  }
+
+  /**
+   * Fire connection error event to registered listeners.
+   *
+   * @param returnEx exception
+   */
+  public void fireConnectionErrorOccurred(SQLException returnEx) {
+    ConnectionEvent event = new ConnectionEvent(this, returnEx);
+    for (ConnectionEventListener listener : connectionEventListeners) {
+      listener.connectionErrorOccurred(event);
+    }
+  }
+
+  /**
+   * Close underlying connection
+   *
+   * @throws SQLException if close fails
+   */
+  @Override
+  public void close() throws SQLException {
+    fireConnectionClosed(new ConnectionEvent(this));
+    connection.setPoolConnection(null);
+    connection.close();
+  }
+
+  /**
+   * Create XID string
+   *
+   * @param xid xid value
+   * @return XID string
+   */
+  public static String xidToString(Xid xid) {
+    return "0x"
+        + StringUtils.byteArrayToHexString(xid.getGlobalTransactionId())
+        + ",0x"
+        + StringUtils.byteArrayToHexString(xid.getBranchQualifier())
+        + ",0x"
+        + Integer.toHexString(xid.getFormatId());
+  }
+
+  @Override
+  public XAResource getXAResource() {
+    return new MariaDbXAResource();
+  }
+
+  private class MariaDbXAResource implements XAResource {
+
+    private 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;
+    }
+
+    private void execute(String command) throws XAException {
+      try {
+        connection.createStatement().execute(command);
+      } catch (SQLException sqle) {
+        throw mapXaException(sqle);
+      }
+    }
+
+    @Override
+    public void commit(Xid xid, boolean onePhase) throws XAException {
+      execute("XA COMMIT " + xidToString(xid) + ((onePhase) ? " ONE PHASE" : ""));
+    }
+
+    @Override
+    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));
+    }
+
+    @Override
+    public void forget(Xid xid) {
+      // Not implemented by the server
+    }
+
+    @Override
+    public int getTransactionTimeout() {
+      // not implemented
+      return 0;
+    }
+
+    public Configuration getConf() {
+      return connection.getContext().getConf();
+    }
+
+    @Override
+    public boolean isSameRM(XAResource xaResource) {
+      if (xaResource instanceof MariaDbXAResource) {
+        MariaDbXAResource other = (MariaDbXAResource) xaResource;
+        return other.getConf().equals(this.getConf());
+      }
+      return false;
+    }
+
+    @Override
+    public int prepare(Xid xid) throws XAException {
+      execute("XA PREPARE " + xidToString(xid));
+      return XA_OK;
+    }
+
+    @Override
+    public Xid[] recover(int flags) throws XAException {
+      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<MariaDbXid> 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);
+      }
+    }
+
+    @Override
+    public void rollback(Xid xid) throws XAException {
+      execute("XA ROLLBACK " + xidToString(xid));
+    }
+
+    @Override
+    public boolean setTransactionTimeout(int i) {
+      return false;
+    }
+
+    @Override
+    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));
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/MariaDbPoolDataSource.java b/src/main/java/org/mariadb/jdbc/MariaDbPoolDataSource.java
new file mode 100644
index 0000000..326a82f
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/MariaDbPoolDataSource.java
@@ -0,0 +1,308 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.io.Closeable;
+import java.io.PrintWriter;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.SQLTimeoutException;
+import java.util.List;
+import java.util.logging.Logger;
+import javax.sql.*;
+import org.mariadb.jdbc.pool.Pool;
+import org.mariadb.jdbc.pool.Pools;
+
+/** MariaDB pool datasource. This use mariadb internal pool. */
+public class MariaDbPoolDataSource
+    implements DataSource, ConnectionPoolDataSource, XADataSource, Closeable, AutoCloseable {
+
+  private Pool pool;
+  private Configuration conf = null;
+  private String url = null;
+  private String user = null;
+  private String password = null;
+  private Integer loginTimeout = null;
+
+  /** Constructor */
+  public MariaDbPoolDataSource() {}
+
+  /**
+   * Constructor with url
+   *
+   * @param url connection string
+   * @throws SQLException if configuration fails
+   */
+  public MariaDbPoolDataSource(String url) throws SQLException {
+    if (Configuration.acceptsUrl(url)) {
+      this.url = url;
+      conf = Configuration.parse(url);
+      pool = Pools.retrievePool(conf);
+    } else {
+      throw new SQLException(String.format("Wrong mariaDB url: %s", url));
+    }
+  }
+
+  private void config() throws SQLException {
+    if (url == null) throw new SQLException("url not set");
+    conf = Configuration.parse(url);
+    if (loginTimeout != null) conf.connectTimeout(loginTimeout * 1000);
+    if (user != null || password != null) {
+      conf = conf.clone(user, password);
+    }
+    if (user != null) {
+      user = conf.user();
+    }
+    if (password != null) {
+      password = conf.password();
+    }
+
+    pool = Pools.retrievePool(conf);
+  }
+
+  /**
+   * 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 {
+    if (conf == null) config();
+    return pool.getPoolConnection().getConnection();
+  }
+
+  /**
+   * 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
+   */
+  @Override
+  public Connection getConnection(String username, String password) throws SQLException {
+    if (conf == null) config();
+    return pool.getPoolConnection(username, password).getConnection();
+  }
+
+  /**
+   * Returns an object that implements the given interface to allow access to non-standard methods,
+   * or standard methods not exposed by the proxy.
+   *
+   * <p>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 result of
+   * calling <code>unwrap</code> 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 <code>SQLException
+   * </code> is thrown.
+   *
+   * @param iface A Class defining an interface that the result must implement.
+   * @return an object that implements the interface. Maybe a proxy for the actual implementing
+   *     object.
+   * @throws SQLException If no object found that implements the interface
+   */
+  @Override
+  public <T> T unwrap(Class<T> iface) throws SQLException {
+    if (isWrapperFor(iface)) {
+      return iface.cast(this);
+    }
+    throw new SQLException("Datasource 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 <code>
+   * isWrapperFor</code> 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
+   * <code>unwrap</code> so that callers can use this method to avoid expensive <code>unwrap</code>
+   * calls that may fail. If this method returns true then calling <code>unwrap</code> 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);
+  }
+
+  /**
+   * Implementation doesn't use logwriter
+   *
+   * @return the log writer for this data source or null if logging is disabled
+   * @see #setLogWriter
+   */
+  @Override
+  public PrintWriter getLogWriter() {
+    return null;
+  }
+
+  /**
+   * Implementation doesn't use logwriter
+   *
+   * @param out the new log writer; to disable logging, set to null
+   * @see #getLogWriter
+   */
+  @Override
+  public void setLogWriter(PrintWriter out) {}
+
+  /**
+   * 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 <code>DataSource</code> object is
+   * created, the login timeout is initially to 30s.
+   *
+   * @return the data source login time limit
+   * @see #setLoginTimeout
+   */
+  @Override
+  public int getLoginTimeout() {
+    if (loginTimeout != null) return loginTimeout;
+    if (conf != null) return conf.connectTimeout() / 1000;
+    return DriverManager.getLoginTimeout() > 0 ? DriverManager.getLoginTimeout() : 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 <code>DataSource</code> object
+   * is created, the login timeout is initially 30s.
+   *
+   * @param seconds the data source login time limit
+   * @throws SQLException if wrong configuration set
+   * @see #getLoginTimeout
+   */
+  @Override
+  public void setLoginTimeout(int seconds) throws SQLException {
+    loginTimeout = seconds;
+    if (conf != null) config();
+  }
+
+  /**
+   * Not implemented
+   *
+   * @return the parent Logger for this data source
+   */
+  @Override
+  public Logger getParentLogger() {
+    return null;
+  }
+
+  @Override
+  public PooledConnection getPooledConnection() throws SQLException {
+    if (conf == null) config();
+    return pool.getPoolConnection();
+  }
+
+  @Override
+  public PooledConnection getPooledConnection(String username, String password)
+      throws SQLException {
+    if (conf == null) config();
+    return pool.getPoolConnection(username, password);
+  }
+
+  @Override
+  public XAConnection getXAConnection() throws SQLException {
+    if (conf == null) config();
+    return pool.getPoolConnection();
+  }
+
+  @Override
+  public XAConnection getXAConnection(String username, String password) throws SQLException {
+    if (conf == null) config();
+    return pool.getPoolConnection(username, password);
+  }
+
+  /**
+   * Sets the URL for this datasource
+   *
+   * @param url connection string
+   * @throws SQLException if url is not accepted
+   */
+  public void setUrl(String url) throws SQLException {
+    if (Configuration.acceptsUrl(url)) {
+      this.url = url;
+      config();
+    } else {
+      throw new SQLException(String.format("Wrong mariaDB url: %s", url));
+    }
+  }
+
+  /**
+   * Returns the URL for this datasource
+   *
+   * @return the URL for this datasource
+   */
+  public String getUrl() {
+    if (conf == null) return url;
+    return conf.initialUrl();
+  }
+
+  /**
+   * return user
+   *
+   * @return user
+   */
+  public String getUser() {
+    return user;
+  }
+
+  /**
+   * Set user
+   *
+   * @param user user
+   * @throws SQLException if configuration fails
+   */
+  public void setUser(String user) throws SQLException {
+    this.user = user;
+    if (conf != null) config();
+  }
+
+  /**
+   * set password
+   *
+   * @param password password
+   * @throws SQLException if configuration fails
+   */
+  public void setPassword(String password) throws SQLException {
+    this.password = password;
+    if (conf != null) config();
+  }
+
+  /** Close datasource. */
+  public void close() {
+    pool.close();
+  }
+
+  /**
+   * get pool name
+   *
+   * @return pool name
+   */
+  public String getPoolName() {
+    return (pool != null) ? pool.getPoolTag() : null;
+  }
+
+  /**
+   * Get current idle threads. !! For testing purpose only !!
+   *
+   * @return current thread id's
+   */
+  public List<Long> testGetConnectionIdleThreadIds() {
+    return (pool != null) ? pool.testGetConnectionIdleThreadIds() : null;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/MariaDbXid.java b/src/main/java/org/mariadb/jdbc/MariaDbXid.java
new file mode 100644
index 0000000..3ba44e5
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/MariaDbXid.java
@@ -0,0 +1,77 @@
+package org.mariadb.jdbc;
+
+import java.util.Arrays;
+import java.util.Objects;
+import javax.transaction.xa.Xid;
+
+/** MariaDB XID implementation */
+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;
+  }
+
+  @Override
+  public int hashCode() {
+    int result = Objects.hash(formatId);
+    result = 31 * result + Arrays.hashCode(globalTransactionId);
+    result = 31 * result + Arrays.hashCode(branchQualifier);
+    return result;
+  }
+
+  /**
+   * Get format id from XID
+   *
+   * @return format id
+   */
+  public int getFormatId() {
+    return formatId;
+  }
+
+  /**
+   * Get global transaction id from XID
+   *
+   * @return global transaction id
+   */
+  public byte[] getGlobalTransactionId() {
+    return globalTransactionId;
+  }
+
+  /**
+   * Get branch qualifier from XID
+   *
+   * @return branch qualifier
+   */
+  public byte[] getBranchQualifier() {
+    return branchQualifier;
+  }
+}
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 0000000..019e672
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/ParameterMetaData.java
@@ -0,0 +1,213 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.sql.SQLException;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.export.ExceptionFactory;
+
+/** Parameter metadata */
+public class ParameterMetaData implements java.sql.ParameterMetaData {
+
+  private final ColumnDecoder[] params;
+  private final ExceptionFactory exceptionFactory;
+
+  /**
+   * Constructor
+   *
+   * @param exceptionFactory exception factory
+   * @param params columns metadata
+   */
+  protected ParameterMetaData(ExceptionFactory exceptionFactory, ColumnDecoder[] params) {
+    this.params = params;
+    this.exceptionFactory = exceptionFactory;
+  }
+
+  /**
+   * Retrieves the number of parameters in the <code>PreparedStatement</code> object for which this
+   * <code>ParameterMetaData</code> 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 <code>
+   *     ParameterMetaData.parameterNoNulls</code>, <code>ParameterMetaData.parameterNullable</code>
+   * @throws SQLException if wrong index
+   */
+  @Override
+  public int isNullable(int idx) throws SQLException {
+    checkIndex(idx);
+    return java.sql.ParameterMetaData.parameterNullable;
+  }
+
+  /**
+   * Retrieves whether values for the designated parameter can be signed numbers.
+   *
+   * @param idx the first parameter is 1, the second is 2, ...
+   * @return <code>true</code> if so; <code>false</code> 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.
+   *
+   * <p>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 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 idx the first parameter is 1, the second is 2, ...
+   * @return SQL types from <code>java.sql.Types</code>
+   * @throws SQLException because not supported
+   */
+  @Override
+  public int getParameterType(int idx) throws SQLException {
+    checkIndex(idx);
+    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);
+    // https://jira.mariadb.org/browse/XPT-279 Xpand can return wrong datatype for parameters
+    DataType type = params[idx - 1].getType();
+    return type == null ? null : type.name();
+  }
+
+  /**
+   * Retrieves the fully-qualified name of the Java class whose instances should be passed to the
+   * method <code>PreparedStatement.setObject</code>.
+   *
+   * @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 <code>PreparedStatement.setObject</code> 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 <code>ParameterMetaData.parameterModeIn</code>, <code>
+   *     ParameterMetaData.parameterModeOut</code>, or <code>ParameterMetaData.parameterModeInOut
+   *     </code> <code>ParameterMetaData.parameterModeUnknown</code>.
+   */
+  @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.
+   *
+   * <p>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 result of
+   * calling <code>unwrap</code> 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 <code>SQLException
+   * </code> is thrown.
+   *
+   * @param iface A Class defining an interface that the result must implement.
+   * @return an object that implements the interface. Maybe a proxy for the actual implementing
+   *     object.
+   * @throws SQLException If no object found that implements the interface
+   */
+  @Override
+  public <T> T unwrap(Class<T> 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 <code>
+   * isWrapperFor</code> 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
+   * <code>unwrap</code> so that callers can use this method to avoid expensive <code>unwrap</code>
+   * calls that may fail. If this method returns true then calling <code>unwrap</code> 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 0000000..b917ceb
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/ProcedureStatement.java
@@ -0,0 +1,93 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.sql.CallableStatement;
+import java.sql.SQLException;
+import java.util.concurrent.locks.ReentrantLock;
+import org.mariadb.jdbc.client.Completion;
+import org.mariadb.jdbc.client.result.Result;
+
+/** Procedure callable statement */
+public class ProcedureStatement extends BaseCallableStatement implements CallableStatement {
+
+  /**
+   * Constructor
+   *
+   * @param con connection
+   * @param sql sql
+   * @param databaseName database
+   * @param procedureName procedure
+   * @param lock thread locker
+   * @param canUseServerTimeout can use server timeout
+   * @param canUseServerMaxRows can use server max rows
+   * @param canCachePrepStmts can cache server prepared result
+   * @param resultSetType result-set type
+   * @param resultSetConcurrency concurrency
+   * @throws SQLException if any exception occurs
+   */
+  public ProcedureStatement(
+      Connection con,
+      String sql,
+      String databaseName,
+      String procedureName,
+      ReentrantLock lock,
+      boolean canUseServerTimeout,
+      boolean canUseServerMaxRows,
+      boolean canCachePrepStmts,
+      int resultSetType,
+      int resultSetConcurrency)
+      throws SQLException {
+    super(
+        sql,
+        con,
+        lock,
+        databaseName,
+        procedureName,
+        canUseServerTimeout,
+        canUseServerMaxRows,
+        canCachePrepStmts,
+        resultSetType,
+        resultSetConcurrency,
+        0);
+  }
+
+  @Override
+  public boolean isFunction() {
+    return false;
+  }
+
+  @Override
+  protected void handleParameterOutput() throws SQLException {
+    // output result-set is the last result-set
+    // or in case finishing with an OK_PACKET, just the one before
+    for (int i = 1; i <= Math.min(this.results.size(), 2); i++) {
+      Completion compl = this.results.get(this.results.size() - i);
+      if (compl instanceof Result && (((Result) compl).isOutputParameter())) {
+        outputResultFromRes(i);
+      }
+    }
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder("ProcedureStatement{sql:'" + sql + "'");
+    sb.append(", parameters:[");
+    for (int i = 0; i < parameters.size(); i++) {
+      org.mariadb.jdbc.client.util.Parameter param = parameters.get(i);
+      if (outputParameters.contains(i + 1)) sb.append("<OUT>");
+      if (param == null) {
+        sb.append("null");
+      } else {
+        sb.append(param.bestEffortStringValue(con.getContext()));
+      }
+      if (i != parameters.size() - 1) {
+        sb.append(",");
+      }
+    }
+    sb.append("]}");
+    return sb.toString();
+  }
+}
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 0000000..7dc982d
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/ServerPreparedStatement.java
@@ -0,0 +1,695 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import static org.mariadb.jdbc.util.constants.Capabilities.*;
+
+import java.sql.*;
+import java.util.*;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.regex.Pattern;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.Completion;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.client.result.Result;
+import org.mariadb.jdbc.client.util.Parameters;
+import org.mariadb.jdbc.export.ExceptionFactory;
+import org.mariadb.jdbc.message.ClientMessage;
+import org.mariadb.jdbc.message.client.BulkExecutePacket;
+import org.mariadb.jdbc.message.client.ExecutePacket;
+import org.mariadb.jdbc.message.client.PrepareExecutePacket;
+import org.mariadb.jdbc.message.client.PreparePacket;
+import org.mariadb.jdbc.message.server.OkPacket;
+import org.mariadb.jdbc.message.server.PrepareResultPacket;
+import org.mariadb.jdbc.util.ParameterList;
+
+/**
+ * Server prepare statement. command will generate COM_STMT_PREPARE + COM_STMT_EXECUTE (+
+ * COM_STMT_CLOSE)
+ */
+public class ServerPreparedStatement extends BasePreparedStatement {
+  private static final Pattern PREPARABLE_STATEMENT_PATTERN =
+      Pattern.compile("^(SELECT|UPDATE|INSERT|DELETE|REPLACE|DO|CALL)", Pattern.CASE_INSENSITIVE);
+  private final boolean canCachePrepStmts;
+  /**
+   * Server prepare statement constructor
+   *
+   * @param sql command
+   * @param con connection
+   * @param lock thread safe lock
+   * @param canUseServerTimeout can server use timeout
+   * @param canUseServerMaxRows can server use max rows
+   * @param canCachePrepStmts can server cache prepared statement
+   * @param autoGeneratedKeys must command return automatically generated keys
+   * @param resultSetType resultset type
+   * @param resultSetConcurrency resultset concurrency
+   * @param defaultFetchSize default fetch size
+   * @throws SQLException if prepare fails
+   */
+  public ServerPreparedStatement(
+      String sql,
+      Connection con,
+      ReentrantLock lock,
+      boolean canUseServerTimeout,
+      boolean canUseServerMaxRows,
+      boolean canCachePrepStmts,
+      int autoGeneratedKeys,
+      int resultSetType,
+      int resultSetConcurrency,
+      int defaultFetchSize)
+      throws SQLException {
+    super(
+        sql,
+        con,
+        lock,
+        canUseServerTimeout,
+        canUseServerMaxRows,
+        autoGeneratedKeys,
+        resultSetType,
+        resultSetConcurrency,
+        defaultFetchSize);
+    this.canCachePrepStmts = canCachePrepStmts;
+    prepareResult = canCachePrepStmts ? con.getContext().getPrepareCache().get(sql, this) : null;
+    if (prepareResult == null && !PREPARABLE_STATEMENT_PATTERN.matcher(sql).find()) {
+      con.getClient().execute(new PreparePacket(sql), this, true);
+    }
+    parameters = new ParameterList();
+  }
+
+  /**
+   * Execute command with parameters
+   *
+   * @throws SQLException if any error occurs
+   */
+  protected void executeInternal() throws SQLException {
+    checkNotClosed();
+    validParameters();
+    lock.lock();
+    String cmd = escapeTimeout(sql);
+    if (prepareResult == null)
+      if (canCachePrepStmts) prepareResult = con.getContext().getPrepareCache().get(cmd, this);
+    try {
+      if (prepareResult == null && con.getContext().permitPipeline()) {
+        executePipeline(cmd);
+      } else {
+        executeStandard(cmd);
+      }
+    } finally {
+      localInfileInputStream = null;
+      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.
+    try {
+      List<Completion> res =
+          con.getClient()
+              .execute(
+                  new PrepareExecutePacket(cmd, parameters, this, localInfileInputStream),
+                  this,
+                  fetchSize,
+                  maxRows,
+                  resultSetConcurrency,
+                  resultSetType,
+                  closeOnCompletion,
+                  false);
+      results = res.subList(1, res.size());
+    } catch (SQLException ex) {
+      results = null;
+      throw ex;
+    }
+  }
+
+  private void executeStandard(String cmd) throws SQLException {
+    // send COM_STMT_PREPARE
+    if (prepareResult == null) {
+      if (canCachePrepStmts) prepareResult = con.getContext().getPrepareCache().get(cmd, this);
+      if (prepareResult == null) {
+        con.getClient().execute(new PreparePacket(cmd), this, true);
+      }
+    }
+    validParameters();
+    // send COM_STMT_EXECUTE
+    ExecutePacket execute =
+        new ExecutePacket(prepareResult, parameters, cmd, this, localInfileInputStream);
+    results =
+        con.getClient()
+            .execute(
+                execute,
+                this,
+                fetchSize,
+                maxRows,
+                resultSetConcurrency,
+                resultSetType,
+                closeOnCompletion,
+                false);
+  }
+
+  private void executeInternalPreparedBatch() throws SQLException {
+    checkNotClosed();
+    String cmd = escapeTimeout(sql);
+    if (batchParameters.size() > 1 && con.getContext().hasServerCapability(STMT_BULK_OPERATIONS)) {
+
+      // ensure pipelining is possible (no LOAD DATA/XML INFILE commands)
+      boolean possibleLoadLocal = con.getContext().hasClientCapability(LOCAL_FILES);
+      if (possibleLoadLocal) {
+        String sqlUpper = sql.toUpperCase(Locale.ROOT);
+        possibleLoadLocal =
+            sqlUpper.contains(" LOCAL ")
+                && sqlUpper.contains("LOAD")
+                && sqlUpper.contains(" INFILE");
+      }
+
+      if (!possibleLoadLocal) {
+        if (con.getContext().getConf().useBulkStmts()
+            && autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS) {
+          executeBatchBulk(cmd);
+        } else {
+          executeBatchPipeline(cmd);
+        }
+        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 void executeBatchBulk(String cmd) throws SQLException {
+    List<Completion> res;
+    if (prepareResult == null && canCachePrepStmts)
+      prepareResult = con.getContext().getPrepareCache().get(cmd, this);
+    try {
+      if (prepareResult == null) {
+        ClientMessage[] packets;
+        packets =
+            new ClientMessage[] {
+              new PreparePacket(cmd), new BulkExecutePacket(null, batchParameters, cmd, this)
+            };
+        res =
+            con.getClient()
+                .executePipeline(
+                    packets,
+                    this,
+                    0,
+                    maxRows,
+                    ResultSet.CONCUR_READ_ONLY,
+                    ResultSet.TYPE_FORWARD_ONLY,
+                    closeOnCompletion,
+                    false);
+
+        // in case of failover, prepare is done in failover, skipping prepare result
+        if (res.get(0) instanceof PrepareResultPacket) {
+          results = res.subList(1, res.size());
+        } else {
+          results = res;
+        }
+      } else {
+        results =
+            con.getClient()
+                .execute(
+                    new BulkExecutePacket(prepareResult, batchParameters, cmd, this),
+                    this,
+                    0,
+                    maxRows,
+                    ResultSet.CONCUR_READ_ONLY,
+                    ResultSet.TYPE_FORWARD_ONLY,
+                    closeOnCompletion,
+                    false);
+      }
+
+    } catch (SQLException bue) {
+      results = null;
+      throw exceptionFactory()
+          .createBatchUpdate(Collections.emptyList(), batchParameters.size(), bue);
+    }
+  }
+
+  /**
+   * Send COM_STMT_PREPARE + X * COM_STMT_EXECUTE, then read for the all answers
+   *
+   * @param cmd command
+   * @throws SQLException if Command error
+   */
+  private void executeBatchPipeline(String cmd) throws SQLException {
+    if (prepareResult == null && canCachePrepStmts)
+      prepareResult = con.getContext().getPrepareCache().get(cmd, this);
+    // 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<Completion> 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;
+
+    } catch (SQLException bue) {
+      results = null;
+      throw exceptionFactory().createBatchUpdate(res, batchParameters.size(), bue);
+    }
+  }
+
+  private List<Completion> 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, localInfileInputStream);
+    }
+    return con.getClient()
+        .executePipeline(
+            packets,
+            this,
+            0,
+            maxRows,
+            ResultSet.CONCUR_READ_ONLY,
+            ResultSet.TYPE_FORWARD_ONLY,
+            closeOnCompletion,
+            false);
+  }
+
+  private List<Completion> 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, localInfileInputStream);
+    }
+    List<Completion> res =
+        con.getClient()
+            .executePipeline(
+                packets,
+                this,
+                0,
+                maxRows,
+                ResultSet.CONCUR_READ_ONLY,
+                ResultSet.TYPE_FORWARD_ONLY,
+                closeOnCompletion,
+                false);
+    // in case of failover, prepare is done in failover, skipping prepare result
+    if (res.get(0) instanceof PrepareResultPacket) {
+      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 void executeBatchStandard(String cmd) throws SQLException {
+    // send COM_STMT_PREPARE
+    List<Completion> tmpResults = new ArrayList<>();
+    SQLException error = null;
+    for (Parameters batchParameter : batchParameters) {
+      // prepare is in loop, because if connection fail, prepare is reset, and need to be re
+      // prepared
+      if (prepareResult == null) {
+        if (canCachePrepStmts) prepareResult = con.getContext().getPrepareCache().get(cmd, this);
+        if (prepareResult == null) {
+          con.getClient().execute(new PreparePacket(cmd), this, false);
+        }
+      }
+      try {
+        ExecutePacket execute =
+            new ExecutePacket(prepareResult, batchParameter, cmd, this, localInfileInputStream);
+        tmpResults.addAll(con.getClient().execute(execute, this, false));
+      } catch (SQLException e) {
+        if (error == null) error = e;
+      }
+    }
+
+    if (error != null) {
+      throw exceptionFactory().createBatchUpdate(tmpResults, batchParameters.size(), error);
+    }
+    this.results = tmpResults;
+  }
+
+  /**
+   * Executes the SQL statement in this <code>PreparedStatement</code> object, which may be any kind
+   * of SQL statement. Some prepared statements return multiple results; the <code>execute</code>
+   * method handles these complex statements as well as the simpler form of statements handled by
+   * the methods <code>executeQuery</code> and <code>executeUpdate</code>.
+   *
+   * <p>The <code>execute</code> method returns a <code>boolean</code> to indicate the form of the
+   * first result. You must call either the method <code>getResultSet</code> or <code>getUpdateCount
+   * </code> to retrieve the result; you must call <code>getMoreResults</code> to move to any
+   * subsequent result(s).
+   *
+   * @return <code>true</code> if the first result is a <code>ResultSet</code> object; <code>false
+   *     </code> 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
+   *     <code>PreparedStatement</code> 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 (canUseServerTimeout && prepareResult != null) {
+      prepareResult.decrementUse(con.getClient(), this);
+      prepareResult = null;
+    }
+  }
+
+  /**
+   * Executes the SQL query in this <code>PreparedStatement</code> object and returns the <code>
+   * ResultSet</code> object generated by the query.
+   *
+   * @return a <code>ResultSet</code> object that contains the data produced by the query; never
+   *     <code>null</code>
+   * @throws SQLException if a database access error occurs; this method is called on a closed
+   *     <code>PreparedStatement</code> or the SQL statement does not return a <code>ResultSet
+   *     </code> 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 ColumnDecoder[0], new byte[0][], con.getContext());
+  }
+
+  /**
+   * Executes the SQL statement in this <code>PreparedStatement</code> object, which must be an SQL
+   * Data Manipulation Language (DML) statement, such as <code>INSERT</code>, <code>UPDATE</code> or
+   * <code>DELETE</code>; 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
+   *     <code>PreparedStatement</code> or the SQL statement returns a <code>ResultSet</code> 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 <code>PreparedStatement</code> object, which must be an SQL
+   * Data Manipulation Language (DML) statement, such as <code>INSERT</code>, <code>UPDATE</code> or
+   * <code>DELETE</code>; or an SQL statement that returns nothing, such as a DDL statement.
+   *
+   * <p>This method should be used when the returned row count may exceed {@link Integer#MAX_VALUE}.
+   *
+   * <p>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
+   *     <code>PreparedStatement</code> or the SQL statement returns a <code>ResultSet</code> 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();
+  }
+
+  /**
+   * Handle output parameter result-set (only for CallableStatement)
+   *
+   * @throws SQLException if any error occurs
+   */
+  protected void handleParameterOutput() throws SQLException {}
+
+  /**
+   * Adds a set of parameters to this <code>PreparedStatement</code> object's batch of commands.
+   *
+   * @throws SQLException if a database access error occurs or this method is called on a closed
+   *     <code>PreparedStatement</code>
+   * @see Statement#addBatch
+   * @since 1.2
+   */
+  @Override
+  public void addBatch() throws SQLException {
+    validParameters();
+    if (batchParameters == null) batchParameters = new ArrayList<>();
+    batchParameters.add(parameters);
+    parameters = parameters.clone();
+  }
+
+  /**
+   * Validated that all parameters have been set.
+   *
+   * @throws SQLException if number of parameters doesn't correspond to expected number
+   */
+  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 <code>ResultSetMetaData</code> object that contains information about the columns
+   * of the <code>ResultSet</code> object that will be returned when this <code>PreparedStatement
+   * </code> object is executed.
+   *
+   * <p>Because a <code>PreparedStatement</code> object is precompiled, it is possible to know about
+   * the <code>ResultSet</code> object that it will return without having to execute it.
+   * Consequently, it is possible to invoke the method <code>getMetaData</code> on a <code>
+   * PreparedStatement</code> object rather than waiting to execute it and then invoking the <code>
+   * ResultSet.getMetaData</code> method on the <code>ResultSet</code> object that is returned.
+   *
+   * <p><B>NOTE:</B> Using this method may be expensive for some drivers due to the lack of
+   * underlying DBMS support.
+   *
+   * @return the description of a <code>ResultSet</code> object's columns or <code>null</code> if
+   *     the driver cannot return a <code>ResultSetMetaData</code> object
+   * @throws SQLException if a database access error occurs or this method is called on a closed
+   *     <code>PreparedStatement</code>
+   * @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) {
+      con.getClient().execute(new PreparePacket(escapeTimeout(sql)), this, true);
+    }
+
+    return new org.mariadb.jdbc.client.result.ResultSetMetaData(
+        exceptionFactory(), prepareResult.getColumns(), con.getContext().getConf(), false);
+  }
+
+  /**
+   * Retrieves the number, types and properties of this <code>PreparedStatement</code> object's
+   * parameters.
+   *
+   * @return a <code>ParameterMetaData</code> object that contains information about the number,
+   *     types and properties for each parameter marker of this <code>PreparedStatement</code>
+   *     object
+   * @throws SQLException if a database access error occurs or this method is called on a closed
+   *     <code>PreparedStatement</code>
+   * @see ParameterMetaData
+   * @since 1.4
+   */
+  @Override
+  public java.sql.ParameterMetaData getParameterMetaData() throws SQLException {
+    // send COM_STMT_PREPARE
+    if (prepareResult == null) {
+      con.getClient().execute(new PreparePacket(escapeTimeout(sql)), this, true);
+    }
+
+    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 {
+      executeInternalPreparedBatch();
+
+      int[] updates = new int[batchParameters.size()];
+      if (results.size() != updates.length) {
+        for (int i = 0; i < updates.length; i++) {
+          updates[i] = Statement.SUCCESS_NO_INFO;
+        }
+      } else {
+        for (int i = 0; i < updates.length; i++) {
+          if (results.get(i) instanceof OkPacket) {
+            updates[i] = (int) ((OkPacket) results.get(i)).getAffectedRows();
+          } else {
+            updates[i] = org.mariadb.jdbc.Statement.SUCCESS_NO_INFO;
+          }
+        }
+      }
+      currResult = results.remove(0);
+      return updates;
+
+    } finally {
+      localInfileInputStream = null;
+      batchParameters.clear();
+      lock.unlock();
+    }
+  }
+
+  @Override
+  public long[] executeLargeBatch() throws SQLException {
+    checkNotClosed();
+    if (batchParameters == null || batchParameters.isEmpty()) return new long[0];
+    lock.lock();
+    try {
+      executeInternalPreparedBatch();
+
+      long[] updates = new long[batchParameters.size()];
+      if (results.size() != updates.length) {
+        for (int i = 0; i < updates.length; i++) {
+          updates[i] = Statement.SUCCESS_NO_INFO;
+        }
+      } else {
+        for (int i = 0; i < updates.length; i++) {
+          if (results.get(i) instanceof OkPacket) {
+            updates[i] = ((OkPacket) results.get(i)).getAffectedRows();
+          } else {
+            updates[i] = org.mariadb.jdbc.Statement.SUCCESS_NO_INFO;
+          }
+        }
+      }
+
+      currResult = results.remove(0);
+      return updates;
+
+    } finally {
+      batchParameters.clear();
+      lock.unlock();
+    }
+  }
+
+  private ExceptionFactory exceptionFactory() {
+    return con.getExceptionFactory().of(this);
+  }
+
+  @Override
+  public void close() throws SQLException {
+    if (prepareResult != null) {
+      prepareResult.decrementUse(con.getClient(), this);
+      prepareResult = null;
+    }
+    con.fireStatementClosed(this);
+    super.close();
+  }
+
+  /**
+   * reset prepare statement in case of a failover. (Command need then to be re-prepared on server)
+   */
+  public void reset() {
+    lock.lock();
+    try {
+      prepareResult = null;
+    } finally {
+      lock.unlock();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "ServerPreparedStatement{" + super.toString() + '}';
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/SimpleParameterMetaData.java b/src/main/java/org/mariadb/jdbc/SimpleParameterMetaData.java
new file mode 100644
index 0000000..96b63fc
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/SimpleParameterMetaData.java
@@ -0,0 +1,209 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.sql.SQLException;
+import org.mariadb.jdbc.export.ExceptionFactory;
+
+/** Simple parameter metadata, when the only reliable think is the number of parameter */
+public class SimpleParameterMetaData implements java.sql.ParameterMetaData {
+
+  private final int paramCount;
+  private final ExceptionFactory exceptionFactory;
+
+  /**
+   * Constructor
+   *
+   * @param exceptionFactory connection exception factory
+   * @param paramCount parameter count
+   */
+  protected SimpleParameterMetaData(ExceptionFactory exceptionFactory, int paramCount) {
+    this.exceptionFactory = exceptionFactory;
+    this.paramCount = paramCount;
+  }
+
+  /**
+   * Retrieves the number of parameters in the <code>PreparedStatement</code> object for which this
+   * <code>ParameterMetaData</code> object contains information.
+   *
+   * @return the number of parameters
+   */
+  @Override
+  public int getParameterCount() {
+    return paramCount;
+  }
+
+  private void checkIndex(int index) throws SQLException {
+    if (index < 1 || index > paramCount) {
+      throw exceptionFactory.create(
+          String.format(
+              "Wrong index position. Is %s but must be in 1-%s range", index, paramCount));
+    }
+  }
+
+  /**
+   * 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 <code>
+   *     ParameterMetaData.parameterNoNulls</code>, <code>ParameterMetaData.parameterNullable</code>
+   * @throws SQLException if wrong index
+   */
+  @Override
+  public int isNullable(int idx) throws SQLException {
+    checkIndex(idx);
+    return java.sql.ParameterMetaData.parameterNullable;
+  }
+
+  /**
+   * Retrieves whether values for the designated parameter can be signed numbers.
+   *
+   * @param idx the first parameter is 1, the second is 2, ...
+   * @return <code>true</code> if so; <code>false</code> otherwise
+   * @throws SQLException if wrong index
+   */
+  @Override
+  public boolean isSigned(int idx) throws SQLException {
+    checkIndex(idx);
+    return true;
+  }
+
+  /**
+   * Retrieves the designated parameter's specified column size.
+   *
+   * <p>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);
+    throw exceptionFactory.create("Unknown parameter metadata precision");
+  }
+
+  /**
+   * 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);
+    throw exceptionFactory.create("Unknown parameter metadata scale");
+  }
+
+  /**
+   * 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 idx the first parameter is 1, the second is 2, ...
+   * @return SQL types from <code>java.sql.Types</code>
+   * @throws SQLException because not supported
+   */
+  @Override
+  public int getParameterType(int idx) throws SQLException {
+    checkIndex(idx);
+    throw exceptionFactory.create("Getting parameter type metadata is 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);
+    throw exceptionFactory.create("Unknown parameter metadata type name");
+  }
+
+  /**
+   * Retrieves the fully-qualified name of the Java class whose instances should be passed to the
+   * method <code>PreparedStatement.setObject</code>.
+   *
+   * @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 <code>PreparedStatement.setObject</code> 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 <code>ParameterMetaData.parameterModeIn</code>, <code>
+   *     ParameterMetaData.parameterModeOut</code>, or <code>ParameterMetaData.parameterModeInOut
+   *     </code> <code>ParameterMetaData.parameterModeUnknown</code>.
+   */
+  @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.
+   *
+   * <p>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 result of
+   * calling <code>unwrap</code> 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 <code>SQLException
+   * </code> is thrown.
+   *
+   * @param iface A Class defining an interface that the result must implement.
+   * @return an object that implements the interface. Maybe a proxy for the actual implementing
+   *     object.
+   * @throws SQLException If no object found that implements the interface
+   */
+  @Override
+  public <T> T unwrap(Class<T> 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 <code>
+   * isWrapperFor</code> 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
+   * <code>unwrap</code> so that callers can use this method to avoid expensive <code>unwrap</code>
+   * calls that may fail. If this method returns true then calling <code>unwrap</code> 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/Statement.java b/src/main/java/org/mariadb/jdbc/Statement.java
new file mode 100644
index 0000000..aaf720b
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/Statement.java
@@ -0,0 +1,1560 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import static org.mariadb.jdbc.util.constants.Capabilities.LOCAL_FILES;
+
+import java.io.InputStream;
+import java.sql.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.locks.ReentrantLock;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.Completion;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.client.result.Result;
+import org.mariadb.jdbc.export.ExceptionFactory;
+import org.mariadb.jdbc.message.client.QueryPacket;
+import org.mariadb.jdbc.message.server.OkPacket;
+import org.mariadb.jdbc.util.NativeSql;
+import org.mariadb.jdbc.util.constants.ColumnFlags;
+import org.mariadb.jdbc.util.constants.ServerStatus;
+
+/** Statement implementation */
+public class Statement implements java.sql.Statement {
+
+  private List<String> batchQueries;
+
+  /** result-set type */
+  protected final int resultSetType;
+  /** concurrency */
+  protected final int resultSetConcurrency;
+  /** thread safe locker */
+  protected final ReentrantLock lock;
+  /** can use server query timeout */
+  protected final boolean canUseServerTimeout;
+  /** can use server row limitation */
+  protected final boolean canUseServerMaxRows;
+  /** connection */
+  protected final Connection con;
+  /** required query timeout */
+  protected int queryTimeout;
+  /** maximum row number */
+  protected long maxRows;
+  /** fetch size */
+  protected int fetchSize;
+  /** automatic generated keys result required */
+  protected int autoGeneratedKeys;
+  /** close statement on resultset completion */
+  protected boolean closeOnCompletion;
+  /** closed flag */
+  protected boolean closed;
+  /** escape processing */
+  protected boolean escape;
+  /** last execution results */
+  protected List<Completion> results;
+  /** current results */
+  protected Completion currResult;
+  /** streaming load data infile data */
+  protected InputStream localInfileInputStream;
+
+  /**
+   * Constructor
+   *
+   * @param con connection
+   * @param lock thread safe locker
+   * @param canUseServerTimeout can use server timeout
+   * @param canUseServerMaxRows can use server row number limitation
+   * @param autoGeneratedKeys automatic generated keys result required
+   * @param resultSetType result-set type
+   * @param resultSetConcurrency concurrency
+   * @param defaultFetchSize fetch size
+   */
+  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;
+  }
+
+  private ExceptionFactory exceptionFactory() {
+    return con.getExceptionFactory().of(this);
+  }
+
+  /**
+   * Set current local infile stream
+   *
+   * @param inputStream stream
+   * @throws SQLException if statement is already closed
+   */
+  public void setLocalInfileInputStream(InputStream inputStream) throws SQLException {
+    checkNotClosed();
+    localInfileInputStream = inputStream;
+  }
+
+  /**
+   * Executes the given SQL statement, which returns a single <code>ResultSet</code> object.
+   *
+   * <p><strong>Note:</strong>This method cannot be called on a <code>PreparedStatement</code> or
+   * <code>CallableStatement</code>.
+   *
+   * @param sql an SQL statement to be sent to the database, typically a static SQL <code>SELECT
+   *     </code> statement
+   * @return a <code>ResultSet</code> object that contains the data produced by the given query;
+   *     never <code>null</code>
+   * @throws SQLException if a database access error occurs, this method is called on a closed
+   *     <code>Statement</code>, the given SQL statement produces anything other than a single
+   *     <code>ResultSet</code> object, the method is called on a <code>PreparedStatement</code> or
+   *     <code>CallableStatement</code>
+   * @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 ColumnDecoder[0], new byte[0][], con.getContext());
+  }
+
+  /**
+   * Executes the given SQL statement, which may be an <code>INSERT</code>, <code>UPDATE</code>, or
+   * <code>DELETE</code> statement or an SQL statement that returns nothing, such as an SQL DDL
+   * statement.
+   *
+   * <p><strong>Note:</strong>This method cannot be called on a <code>PreparedStatement</code> or
+   * <code>CallableStatement</code>.
+   *
+   * @param sql an SQL Data Manipulation Language (DML) statement, such as <code>INSERT</code>,
+   *     <code>UPDATE</code> or <code>DELETE</code>; 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
+   *     <code>Statement</code>, the given SQL statement produces a <code>ResultSet</code> object,
+   *     the method is called on a <code>PreparedStatement</code> or <code>CallableStatement</code>
+   * @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 <code>Statement</code> 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.
+   *
+   * <p>Calling the method <code>close</code> on a <code>Statement</code> object that is already
+   * closed has no effect.
+   *
+   * <p><B>Note:</B>When a <code>Statement</code> object is closed, its current <code>ResultSet
+   * </code> object, if one exists, is also closed.
+   *
+   * @throws SQLException if a database access error occurs
+   */
+  @Override
+  public void close() throws SQLException {
+    if (!closed) {
+      closed = true;
+
+      if (currResult != null && currResult instanceof Result) {
+        ((Result) currResult).closeFromStmtClose(lock);
+      }
+
+      // close result-set
+      if (results != null && !results.isEmpty()) {
+        for (Completion completion : results) {
+          if (completion instanceof Result) {
+            ((Result) completion).closeFromStmtClose(lock);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Abort current command result if streaming. result-set will be incomplete and closed, but ensure
+   * connection state
+   */
+  public void abort() {
+    lock.lock();
+    try {
+      if (!closed) {
+        closed = true;
+
+        if (currResult != null && currResult instanceof Result) {
+          ((Result) currResult).abort();
+        }
+
+        // close result-set
+        if (results != null) {
+          for (Completion completion : results) {
+            if (completion instanceof Result) {
+              ((Result) completion).abort();
+            }
+          }
+        }
+      }
+    } finally {
+      lock.unlock();
+    }
+  }
+
+  /**
+   * Retrieves the maximum number of bytes that can be returned for character and binary column
+   * values in a <code>ResultSet</code> object produced by this <code>Statement</code> object. This
+   * limit applies only to <code>BINARY</code>, <code>VARBINARY</code>, <code>LONGVARBINARY</code>,
+   * <code>CHAR</code>, <code>VARCHAR</code>, <code>NCHAR</code>, <code>NVARCHAR</code>, <code>
+   * LONGNVARCHAR</code> and <code>LONGVARCHAR</code> 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 <code>ResultSet</code> object produced by this
+   * <code>Statement</code> object can contain. If this limit is exceeded, the excess rows are
+   * silently dropped.
+   *
+   * @return the current maximum number of rows for a <code>ResultSet</code> object produced by this
+   *     <code>Statement</code> object; zero means there is no limit
+   * @throws SQLException if a database access error occurs or this method is called on a closed
+   *     <code>Statement</code>
+   * @see #setMaxRows
+   */
+  @Override
+  public int getMaxRows() throws SQLException {
+    checkNotClosed();
+    return (int) maxRows;
+  }
+
+  /**
+   * Sets the limit for the maximum number of rows that any <code>ResultSet</code> object generated
+   * by this <code>Statement</code> 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
+   *     <code>Statement</code> 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.
+   *
+   * <p>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.
+   *
+   * <p>Note: Since prepared statements have usually been parsed prior to making this call,
+   * disabling escape processing for <code>PreparedStatements</code> objects will have no effect.
+   *
+   * @param enable <code>true</code> to enable escape processing; <code>false</code> to disable it
+   * @throws SQLException if a database access error occurs or this method is called on a closed
+   *     <code>Statement</code>
+   */
+  @Override
+  public void setEscapeProcessing(boolean enable) throws SQLException {
+    checkNotClosed();
+    this.escape = enable;
+  }
+
+  /**
+   * Retrieves the number of seconds the driver will wait for a <code>Statement</code> object to
+   * execute. If the limit is exceeded, a <code>SQLException</code> 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
+   *     <code>Statement</code>
+   * @see #setQueryTimeout
+   */
+  @Override
+  public int getQueryTimeout() throws SQLException {
+    checkNotClosed();
+    return queryTimeout;
+  }
+
+  /**
+   * Sets the number of seconds the driver will wait for a <code>Statement</code> 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 <code>SQLTimeoutException</code>
+   * is thrown. A JDBC driver must apply this limit to the <code>execute</code>, <code>executeQuery
+   * </code> and <code>executeUpdate</code> methods.
+   *
+   * <p><strong>Note:</strong> JDBC driver implementations may also apply this limit to {@code
+   * ResultSet} methods (consult your driver vendor documentation for details).
+   *
+   * <p><strong>Note:</strong> In the case of {@code Statement} batching, it is implementation
+   * defined 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
+   *     <code>Statement</code> 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 <code>Statement</code> 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
+   *     <code>Statement</code>
+   * @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 avoids 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 <code>Statement</code> object. Subsequent
+   * <code>Statement</code> object warnings will be chained to this <code>SQLWarning</code> object.
+   *
+   * <p>The warning chain is automatically cleared each time a statement is (re)executed. This
+   * method may not be called on a closed <code>Statement</code> object; doing so will cause an
+   * <code>SQLException</code> to be thrown.
+   *
+   * <p><B>Note:</B> If you are processing a <code>ResultSet</code> object, any warnings associated
+   * with reads on that <code>ResultSet</code> object will be chained on it rather than on the
+   * <code>Statement</code> object that produced it.
+   *
+   * @return the first <code>SQLWarning</code> object or <code>null</code> if there are no warnings
+   * @throws SQLException if a database access error occurs or this method is called on a closed
+   *     <code>Statement</code>
+   */
+  @Override
+  public SQLWarning getWarnings() throws SQLException {
+    return con.getWarnings();
+  }
+
+  /**
+   * Clears all the warnings reported on this <code>Statement</code> object. After a call to this
+   * method, the method <code>getWarnings</code> will return <code>null</code> until a new warning
+   * is reported for this <code>Statement</code> object.
+   */
+  @Override
+  public void clearWarnings() {
+    con.getContext().setWarning(0);
+  }
+
+  /**
+   * Sets the SQL cursor name to the given <code>String</code>, which will be used by subsequent
+   * <code>Statement</code> object <code>execute</code> methods. This name can then be used in SQL
+   * positioned update or delete statements to identify the current row in the <code>ResultSet
+   * </code> object generated by this statement. If the database does not support positioned
+   * update/delete, this method is a noop. To ensure that a cursor has the proper isolation level to
+   * support updates, the cursor's <code>SELECT</code> statement should have the form <code>
+   * SELECT FOR UPDATE</code>. If <code>FOR UPDATE</code> is not present, positioned updates may
+   * fail.
+   *
+   * <p><B>Note:</B> By definition, the execution of positioned updates and deletes must be done by
+   * a different <code>Statement</code> object than the one that generated the <code>ResultSet
+   * </code> 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
+   *     <code>Statement</code>
+   * @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.
+   *
+   * <p>The <code>execute</code> method executes an SQL statement and indicates the form of the
+   * first result. You must then use the methods <code>getResultSet</code> or <code>getUpdateCount
+   * </code> to retrieve the result, and <code>getMoreResults</code> to move to any subsequent
+   * result(s).
+   *
+   * <p><strong>Note:</strong>This method cannot be called on a <code>PreparedStatement</code> or
+   * <code>CallableStatement</code>.
+   *
+   * @param sql any SQL statement
+   * @return <code>true</code> if the first result is a <code>ResultSet</code> object; <code>false
+   *     </code> 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
+   *     <code>Statement</code>, the method is called on a <code>PreparedStatement</code> or <code>
+   *     CallableStatement</code>
+   * @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 <code>ResultSet</code> object. This method should be called
+   * only once per result.
+   *
+   * @return the current result as a <code>ResultSet</code> object or <code>null</code> 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
+   *     <code>Statement</code>
+   * @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 <code>ResultSet</code>
+   * 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 <code>ResultSet
+   *     </code> object or there are no more results
+   * @throws SQLException if a database access error occurs or this method is called on a closed
+   *     <code>Statement</code>
+   * @see #execute
+   */
+  @Override
+  public int getUpdateCount() throws SQLException {
+    checkNotClosed();
+    if (currResult instanceof OkPacket) {
+      return (int) ((OkPacket) currResult).getAffectedRows();
+    }
+    return -1;
+  }
+
+  /**
+   * Moves to this <code>Statement</code> object's next result, returns <code>true</code> if it is a
+   * <code>ResultSet</code> object, and implicitly closes any current <code>ResultSet</code>
+   * object(s) obtained with the method <code>getResultSet</code>.
+   *
+   * <p>There are no more results when the following is true:
+   *
+   * <PRE>{@code
+   * // stmt is a Statement object
+   * ((stmt.getMoreResults() == false) && (stmt.getUpdateCount() == -1))
+   * }</PRE>
+   *
+   * @return <code>true</code> if the next result is a <code>ResultSet</code> object; <code>false
+   *     </code> 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
+   *     <code>Statement</code>
+   * @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 <code>Statement</code> object. If this <code>Statement</code> object
+   * has not set a fetch direction by calling the method <code>setFetchDirection</code>, the return
+   * value is implementation-specific.
+   *
+   * @return the default fetch direction for result sets generated from this <code>Statement</code>
+   *     object
+   * @see #setFetchDirection
+   * @since 1.2
+   */
+  @Override
+  public int getFetchDirection() {
+    return ResultSet.FETCH_FORWARD;
+  }
+
+  /**
+   * Gives the driver a hint as to the direction in which rows will be processed in <code>ResultSet
+   * </code> objects created using this <code>Statement</code> object. The default value is <code>
+   * ResultSet.FETCH_FORWARD</code>.
+   *
+   * <p>Note that this method sets the default fetch direction for result sets generated by this
+   * <code>Statement</code> 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 <code>ResultSet
+   * </code> objects generated from this <code>Statement</code> object. If this <code>Statement
+   * </code> object has not set a fetch size by calling the method <code>setFetchSize</code>, the
+   * return value is implementation-specific.
+   *
+   * @return the default fetch size for result sets generated from this <code>Statement</code>
+   *     object
+   * @throws SQLException if a database access error occurs or this method is called on a closed
+   *     <code>Statement</code>
+   * @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 <code>ResultSet</code> objects generated by this <code>Statement
+   * </code>. 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
+   *     <code>Statement</code> 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 <code>ResultSet</code> objects generated by this
+   * <code>Statement</code> object.
+   *
+   * @return either <code>ResultSet.CONCUR_READ_ONLY</code> or <code>ResultSet.CONCUR_UPDATABLE
+   *     </code>
+   * @throws SQLException if a database access error occurs or this method is called on a closed
+   *     <code>Statement</code>
+   */
+  @Override
+  public int getResultSetConcurrency() throws SQLException {
+    checkNotClosed();
+    return this.resultSetConcurrency;
+  }
+
+  /**
+   * Retrieves the result set type for <code>ResultSet</code> objects generated by this <code>
+   * Statement</code> object.
+   *
+   * @return one of <code>ResultSet.TYPE_FORWARD_ONLY</code>, <code>
+   *     ResultSet.TYPE_SCROLL_INSENSITIVE</code>, or <code>ResultSet.TYPE_SCROLL_SENSITIVE</code>
+   * @since 1.2
+   */
+  @Override
+  public int getResultSetType() {
+    return this.resultSetType;
+  }
+
+  /**
+   * Adds the given SQL command to the current list of commands for this <code>Statement</code>
+   * object. The commands in this list can be executed as a batch by calling the method <code>
+   * executeBatch</code>.
+   *
+   * <p><strong>Note:</strong>This method cannot be called on a <code>PreparedStatement</code> or
+   * <code>CallableStatement</code>.
+   *
+   * @param sql typically this is an SQL <code>INSERT</code> or <code>UPDATE</code> statement
+   * @throws SQLException if a database access error occurs, this method is called on a closed
+   *     <code>Statement</code>, the driver does not support batch updates, the method is called on
+   *     a <code>PreparedStatement</code> or <code>CallableStatement</code>
+   * @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 <code>Statement</code> object's current list of SQL commands.
+   *
+   * @throws SQLException if a database access error occurs, this method is called on a closed
+   *     <code>Statement</code> 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 <code>int</code> 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 <code>executeBatch</code> may be one of the following:
+   *
+   * <OL>
+   *   <LI>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
+   *   <LI>A value of <code>SUCCESS_NO_INFO</code> -- indicates that the command was processed
+   *       successfully but that the number of rows affected is unknown
+   *       <p>If one of the commands in a batch update fails to execute properly, this method throws
+   *       a <code>BatchUpdateException</code>, 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 <code>BatchUpdateException.getUpdateCounts</code> will contain as
+   *       many elements as there are commands in the batch, and at least one of the elements will
+   *       be the following:
+   *   <LI>A value of <code>EXECUTE_FAILED</code> -- indicates that the command failed to execute
+   *       successfully and occurs only if a driver continues to process commands after a command
+   *       fails
+   * </OL>
+   *
+   * <p>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 <code>BatchUpdateException</code> 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
+   *     <code>Statement</code> or the driver does not support batch statements. Throws {@link
+   *     BatchUpdateException} (a subclass of <code>SQLException</code>) 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 {
+      // ensure pipelining is possible (no LOAD DATA/XML INFILE commands)
+      boolean possibleLoadLocal = con.getContext().hasClientCapability(LOCAL_FILES);
+      if (possibleLoadLocal) {
+        possibleLoadLocal = false;
+        for (int i = 0; i < batchQueries.size(); i++) {
+          String sql = batchQueries.get(i).toUpperCase(Locale.ROOT);
+          if (sql.contains(" LOCAL ") && sql.contains("LOAD") && sql.contains(" INFILE")) {
+            possibleLoadLocal = true;
+            break;
+          }
+        }
+      }
+
+      List<Completion> res =
+          possibleLoadLocal ? executeInternalBatchStandard() : executeInternalBatchPipeline();
+
+      results = res;
+
+      int[] updates = new int[res.size()];
+      for (int i = 0; i < res.size(); i++) {
+        if (res.get(i) instanceof OkPacket) {
+          updates[i] = (int) ((OkPacket) res.get(i)).getAffectedRows();
+        } else {
+          updates[i] = org.mariadb.jdbc.Statement.SUCCESS_NO_INFO;
+        }
+      }
+      currResult = results.remove(0);
+      batchQueries.clear();
+      return updates;
+
+    } finally {
+      lock.unlock();
+    }
+  }
+
+  /**
+   * Retrieves the <code>Connection</code> object that produced this <code>Statement</code> object.
+   *
+   * @return the connection that produced this statement
+   * @throws SQLException if a database access error occurs or this method is called on a closed
+   *     <code>Statement</code>
+   * @since 1.2
+   */
+  @Override
+  public Connection getConnection() throws SQLException {
+    checkNotClosed();
+    return con;
+  }
+
+  /**
+   * Moves to this <code>Statement</code> object's next result, deals with any current <code>
+   * ResultSet</code> object(s) according to the instructions specified by the given flag, and
+   * returns <code>true</code> if the next result is a <code>ResultSet</code> object.
+   *
+   * <p>There are no more results when the following is true:
+   *
+   * <PRE>{@code
+   * // stmt is a Statement object
+   * ((stmt.getMoreResults(current) == false) && (stmt.getUpdateCount() == -1))
+   * }</PRE>
+   *
+   * @param current one of the following <code>Statement</code> constants indicating what should
+   *     happen to current <code>ResultSet</code> objects obtained using the method <code>
+   *     getResultSet</code>: <code>Statement.CLOSE_CURRENT_RESULT</code>, <code>
+   *     Statement.KEEP_CURRENT_RESULT</code>, or <code>Statement.CLOSE_ALL_RESULTS</code>
+   * @return <code>true</code> if the next result is a <code>ResultSet</code> object; <code>false
+   *     </code> 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
+   *     <code>Statement</code> or the argument supplied is not one of the following: <code>
+   *     Statement.CLOSE_CURRENT_RESULT</code>, <code>Statement.KEEP_CURRENT_RESULT</code> or <code>
+   *     Statement.CLOSE_ALL_RESULTS</code>
+   * @throws SQLFeatureNotSupportedException if <code>DatabaseMetaData.supportsMultipleOpenResults
+   *     </code> returns <code>false</code> and either <code>Statement.KEEP_CURRENT_RESULT</code> or
+   *     <code>Statement.CLOSE_ALL_RESULTS</code> 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 <code>Statement</code>
+   * object. If this <code>Statement</code> object did not generate any keys, an empty <code>
+   * ResultSet</code> object is returned.
+   *
+   * <p><B>Note:</B>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 <code>ResultSet</code> object containing the auto-generated key(s) generated by the
+   *     execution of this <code>Statement</code> object
+   * @throws SQLException if a database access error occurs or this method is called on a closed
+   *     <code>Statement</code>
+   * @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);
+      if (ok.getLastInsertId() != 0) {
+        List<String[]> insertIds = new ArrayList<>();
+        insertIds.add(new String[] {String.valueOf(ok.getLastInsertId())});
+        for (Completion result : results) {
+          if (result instanceof OkPacket) {
+            insertIds.add(new String[] {String.valueOf(((OkPacket) result).getLastInsertId())});
+          }
+        }
+        String[][] ids = insertIds.toArray(new String[0][]);
+        return CompleteResult.createResultSet(
+            "insert_id",
+            DataType.BIGINT,
+            ids,
+            con.getContext(),
+            ColumnFlags.AUTO_INCREMENT | ColumnFlags.UNSIGNED);
+      }
+    }
+
+    return new CompleteResult(new ColumnDecoder[0], new byte[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 <code>Statement</code> object should be made available for
+   * retrieval. The driver will ignore the flag if the SQL statement is not an <code>INSERT</code>
+   * statement, or an SQL statement able to return auto-generated keys (the list of such statements
+   * is vendor-specific).
+   *
+   * <p><strong>Note:</strong>This method cannot be called on a <code>PreparedStatement</code> or
+   * <code>CallableStatement</code>.
+   *
+   * @param sql an SQL Data Manipulation Language (DML) statement, such as <code>INSERT</code>,
+   *     <code>UPDATE</code> or <code>DELETE</code>; 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: <code>Statement.RETURN_GENERATED_KEYS</code>
+   *     <code>Statement.NO_GENERATED_KEYS</code>
+   * @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
+   *     <code>Statement</code>, the given SQL statement returns a <code>ResultSet</code> object,
+   *     the given constant is not one of those allowed, the method is called on a <code>
+   *     PreparedStatement</code> or <code>CallableStatement</code>
+   * @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, localInfileInputStream),
+                  this,
+                  fetchSize,
+                  maxRows,
+                  resultSetConcurrency,
+                  resultSetType,
+                  closeOnCompletion,
+                  false);
+    } finally {
+      localInfileInputStream = null;
+      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 <code>INSERT</code>
+   * statement, or an SQL statement able to return auto-generated keys (the list of such statements
+   * is vendor-specific).
+   *
+   * <p><strong>Note:</strong>This method cannot be called on a <code>PreparedStatement</code> or
+   * <code>CallableStatement</code>.
+   *
+   * @param sql an SQL Data Manipulation Language (DML) statement, such as <code>INSERT</code>,
+   *     <code>UPDATE</code> or <code>DELETE</code>; 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
+   *     <code>Statement</code>, the SQL statement returns a <code>ResultSet</code> object,the
+   *     second argument supplied to this method is not an <code>int</code> array whose elements are
+   *     valid column indexes, the method is called on a <code>PreparedStatement</code> or <code>
+   *     CallableStatement</code>
+   * @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 <code>INSERT</code> statement,
+   * or an SQL statement able to return auto-generated keys (the list of such statements is
+   * vendor-specific).
+   *
+   * <p><strong>Note:</strong>This method cannot be called on a <code>PreparedStatement</code> or
+   * <code>CallableStatement</code>.
+   *
+   * @param sql an SQL Data Manipulation Language (DML) statement, such as <code>INSERT</code>,
+   *     <code>UPDATE</code> or <code>DELETE</code>; 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 <code>INSERT</code>, <code>UPDATE</code>, or <code>DELETE
+   *     </code> statements, or 0 for SQL statements that return nothing
+   * @throws SQLException if a database access error occurs, this method is called on a closed
+   *     <code>Statement</code>, the SQL statement returns a <code>ResultSet</code> object, the
+   *     second argument supplied to this method is not a <code>String</code> array whose elements
+   *     are valid column names, the method is called on a <code>PreparedStatement</code> or <code>
+   *     CallableStatement</code>
+   * @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 <code>INSERT</code> statement, or an SQL statement
+   * able to return auto-generated keys (the list of such statements is vendor-specific).
+   *
+   * <p>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.
+   *
+   * <p>The <code>execute</code> method executes an SQL statement and indicates the form of the
+   * first result. You must then use the methods <code>getResultSet</code> or <code>getUpdateCount
+   * </code> to retrieve the result, and <code>getMoreResults</code> to move to any subsequent
+   * result(s).
+   *
+   * <p><strong>Note:</strong>This method cannot be called on a <code>PreparedStatement</code> or
+   * <code>CallableStatement</code>.
+   *
+   * @param sql any SQL statement
+   * @param autoGeneratedKeys a constant indicating whether auto-generated keys should be made
+   *     available for retrieval using the method <code>getGeneratedKeys</code>; one of the
+   *     following constants: <code>Statement.RETURN_GENERATED_KEYS</code> or <code>
+   *     Statement.NO_GENERATED_KEYS</code>
+   * @return <code>true</code> if the first result is a <code>ResultSet</code> object; <code>false
+   *     </code> 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
+   *     <code>Statement</code>, the second parameter supplied to this method is not <code>
+   *     Statement.RETURN_GENERATED_KEYS</code> or <code>Statement.NO_GENERATED_KEYS</code>, the
+   *     method is called on a <code>PreparedStatement</code> or <code>CallableStatement</code>
+   * @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;
+  }
+
+  /**
+   * Build sql command to execute :
+   *
+   * <ul>
+   *   <li>Execute escape substitution if needed
+   *   <li>add query timeout prefix if server permits it
+   *   <li>add max row limit prefix if server permits it
+   * </ul>
+   *
+   * @param sql sql command
+   * @return sql command to execute
+   * @throws SQLException if fails to escape sql
+   */
+  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) {
+        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) {
+      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 <code>INSERT</code> statement, or an SQL statement able to return
+   * auto-generated keys (the list of such statements is vendor-specific).
+   *
+   * <p>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.
+   *
+   * <p>The <code>execute</code> method executes an SQL statement and indicates the form of the
+   * first result. You must then use the methods <code>getResultSet</code> or <code>getUpdateCount
+   * </code> to retrieve the result, and <code>getMoreResults</code> to move to any subsequent
+   * result(s).
+   *
+   * <p><strong>Note:</strong>This method cannot be called on a <code>PreparedStatement</code> or
+   * <code>CallableStatement</code>.
+   *
+   * @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 <code>getGeneratedKeys</code>
+   * @return <code>true</code> if the first result is a <code>ResultSet</code> object; <code>false
+   *     </code> 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
+   *     <code>Statement</code>, the elements in the <code>int</code> array passed to this method
+   *     are not valid column indexes, the method is called on a <code>PreparedStatement</code> or
+   *     <code>CallableStatement</code>
+   * @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 <code>INSERT</code> statement, or an SQL statement able to return
+   * auto-generated keys (the list of such statements is vendor-specific).
+   *
+   * <p>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.
+   *
+   * <p>The <code>execute</code> method executes an SQL statement and indicates the form of the
+   * first result. You must then use the methods <code>getResultSet</code> or <code>getUpdateCount
+   * </code> to retrieve the result, and <code>getMoreResults</code> to move to any subsequent
+   * result(s).
+   *
+   * <p><strong>Note:</strong>This method cannot be called on a <code>PreparedStatement</code> or
+   * <code>CallableStatement</code>.
+   *
+   * @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 <code>getGeneratedKeys</code>
+   * @return <code>true</code> if the next result is a <code>ResultSet</code> object; <code>false
+   *     </code> 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
+   *     <code>Statement</code>,the elements of the <code>String</code> array passed to this method
+   *     are not valid column names, the method is called on a <code>PreparedStatement</code> or
+   *     <code>CallableStatement</code>
+   * @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 <code>ResultSet</code> objects generated by this
+   * <code>Statement</code> object.
+   *
+   * @return either <code>ResultSet.HOLD_CURSORS_OVER_COMMIT</code> or <code>
+   *     ResultSet.CLOSE_CURSORS_AT_COMMIT</code>
+   * @since 1.4
+   */
+  @Override
+  public int getResultSetHoldability() {
+    return ResultSet.HOLD_CURSORS_OVER_COMMIT;
+  }
+
+  /**
+   * Retrieves whether this <code>Statement</code> object has been closed. A <code>Statement</code>
+   * is closed if the method close has been called on it, or if it is automatically closed.
+   *
+   * @return true if this <code>Statement</code> object is closed; false if it is still open
+   */
+  @Override
+  public boolean isClosed() {
+    return closed;
+  }
+
+  /**
+   * Returns a value indicating whether the <code>Statement</code> is poolable or not.
+   *
+   * @return <code>true</code> if the <code>Statement</code> is poolable; <code>false</code>
+   *     otherwise
+   * @throws SQLException if this method is called on a closed <code>Statement</code>
+   * @see java.sql.Statement#setPoolable(boolean) setPoolable(boolean)
+   */
+  @Override
+  public boolean isPoolable() throws SQLException {
+    checkNotClosed();
+    return false;
+  }
+
+  /**
+   * Requests that a <code>Statement</code> 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 whether the hint is used.
+   *
+   * <p>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.
+   *
+   * <p>By default, a <code>Statement</code> is not poolable when created, and a <code>
+   * PreparedStatement</code> and <code>CallableStatement</code> 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 <code>Statement</code>
+   */
+  @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.
+   *
+   * <p><strong>Note:</strong> Multiple calls to {@code closeOnCompletion} do not toggle the effect
+   * on this {@code Statement}. However, a call to {@code closeOnCompletion} does affect 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.
+   *
+   * <p>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 result of
+   * calling <code>unwrap</code> 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 <code>SQLException
+   * </code> is thrown.
+   *
+   * @param iface A Class defining an interface that the result must implement.
+   * @return an object that implements the interface. Maybe a proxy for the actual implementing
+   *     object.
+   * @throws SQLException If no object found that implements the interface
+   */
+  @Override
+  @SuppressWarnings("unchecked")
+  public <T> T unwrap(Class<T> 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 <code>
+   * isWrapperFor</code> 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
+   * <code>unwrap</code> so that callers can use this method to avoid expensive <code>unwrap</code>
+   * calls that may fail. If this method returns true then calling <code>unwrap</code> 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.
+   *
+   * <p>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 &gt;= 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 {
+      // ensure pipelining is possible (no LOAD DATA/XML INFILE commands)
+      boolean possibleLoadLocal = con.getContext().hasClientCapability(LOCAL_FILES);
+      if (possibleLoadLocal) {
+        for (int i = 0; i < batchQueries.size(); i++) {
+          String sql = batchQueries.get(i).toUpperCase(Locale.ROOT);
+          if (sql.contains(" LOCAL ") && sql.contains("LOAD") && sql.contains(" INFILE")) {
+            break;
+          }
+        }
+        possibleLoadLocal = false;
+      }
+
+      List<Completion> res =
+          possibleLoadLocal ? executeInternalBatchStandard() : executeInternalBatchPipeline();
+
+      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();
+    }
+  }
+
+  /**
+   * Execute batch pipelining commands (sending all client message, then reading results) (batches
+   * cannot contain results-set, so cannot fill receiving socket buffer while sending buffer is
+   * full)
+   *
+   * @return results
+   * @throws SQLException if any error occurs
+   */
+  public List<Completion> executeInternalBatchPipeline() 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,
+            false);
+  }
+
+  /**
+   * basic implementation Send batch query per query.
+   *
+   * @return results
+   * @throws SQLException if any error occurs
+   */
+  public List<Completion> executeInternalBatchStandard() throws SQLException {
+    List<Completion> results = new ArrayList<>();
+    try {
+      for (String batchQuery : batchQueries) {
+        results.addAll(
+            con.getClient()
+                .execute(
+                    new QueryPacket(batchQuery, localInfileInputStream),
+                    this,
+                    0,
+                    0L,
+                    ResultSet.CONCUR_READ_ONLY,
+                    ResultSet.TYPE_FORWARD_ONLY,
+                    closeOnCompletion,
+                    false));
+      }
+      return results;
+    } catch (SQLException sqle) {
+      int[] updateCounts = new int[batchQueries.size()];
+      for (int i = 0; i < Math.min(results.size(), updateCounts.length); i++) {
+        Completion completion = results.get(i);
+        updateCounts[i] =
+            completion instanceof OkPacket ? (int) ((OkPacket) completion).getAffectedRows() : 0;
+      }
+      throw new BatchUpdateException(
+          sqle.getMessage(), sqle.getSQLState(), sqle.getErrorCode(), updateCounts, sqle);
+    } finally {
+      localInfileInputStream = null;
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/TransactionIsolation.java b/src/main/java/org/mariadb/jdbc/TransactionIsolation.java
new file mode 100644
index 0000000..37b219f
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/TransactionIsolation.java
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc;
+
+import java.sql.Connection;
+
+/** Transaction isolation enumeration */
+public enum TransactionIsolation {
+  /**
+   * dirty reads and non-repeatable reads are prevented; phantom reads can occur. This level
+   * prohibits a transaction from reading a row with uncommitted changes in it, and it also
+   * prohibits the situation where one transaction reads a row, a second transaction alters the row,
+   * and the first transaction rereads the row, getting different values the second time (a
+   * "non-repeatable read").
+   */
+  REPEATABLE_READ("REPEATABLE-READ", java.sql.Connection.TRANSACTION_REPEATABLE_READ),
+  /**
+   * dirty reads are prevented; non-repeatable reads and phantom reads can occur. This level only
+   * prohibits a transaction from reading a row with uncommitted changes in it.
+   */
+  READ_COMMITTED("READ-COMMITTED", Connection.TRANSACTION_READ_COMMITTED),
+  /**
+   * dirty reads, non-repeatable reads and phantom reads can occur. This level allows a row changed
+   * by one transaction to be read by another transaction before any changes in that row have been
+   * committed (a "dirty read"). If any of the changes are rolled back, the second transaction will
+   * have retrieved an invalid row.
+   */
+  READ_UNCOMMITTED("READ-UNCOMMITTED", Connection.TRANSACTION_READ_UNCOMMITTED),
+  /**
+   * dirty reads, non-repeatable reads and phantom reads are prevented. This level includes the
+   * prohibitions in TRANSACTION_REPEATABLE_READ and further prohibits the situation where one
+   * transaction reads all rows that satisfy a WHERE condition, a second transaction inserts a row
+   * that satisfies that WHERE condition, and the first transaction rereads for the same condition,
+   * retrieving the additional "phantom" row in the second read.
+   */
+  SERIALIZABLE("SERIALIZABLE", Connection.TRANSACTION_SERIALIZABLE);
+
+  private final String value;
+  private final int level;
+
+  TransactionIsolation(String value, int level) {
+    this.value = value;
+    this.level = level;
+  }
+
+  /**
+   * Get transaction isolation command
+   *
+   * @return transaction isolation command
+   */
+  public String getValue() {
+    return value;
+  }
+
+  /**
+   * Get transaction isolation level
+   *
+   * @return transaction isolation level
+   */
+  public int getLevel() {
+    return level;
+  }
+
+  /**
+   * Get TransactionIsolation from value
+   *
+   * @param value value
+   * @return transaction isolation
+   */
+  public static TransactionIsolation from(String value) {
+    for (TransactionIsolation transactionIsolation : values()) {
+      if (transactionIsolation
+          .value
+          .replaceAll("[ \\-_]", "")
+          .equalsIgnoreCase(value.replaceAll("[ \\-_]", ""))) {
+        return transactionIsolation;
+      }
+    }
+    throw new IllegalArgumentException(
+        String.format("Wrong argument value '%s' for TransactionIsolation", value));
+  }
+}
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 0000000..95f535b
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/Client.java
@@ -0,0 +1,193 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+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.export.ExceptionFactory;
+import org.mariadb.jdbc.export.Prepare;
+import org.mariadb.jdbc.message.ClientMessage;
+
+/** Client interface */
+public interface Client extends AutoCloseable {
+
+  /**
+   * Send client message and read result
+   *
+   * @param message client message
+   * @param canRedo can client message be redone in case of failover
+   * @return results
+   * @throws SQLException if execution fails
+   */
+  List<Completion> execute(ClientMessage message, boolean canRedo) throws SQLException;
+
+  /**
+   * Send client message and read result
+   *
+   * @param message client message
+   * @param stmt statement
+   * @param canRedo can client message be redone in case of failover
+   * @return results
+   * @throws SQLException if execution fails
+   */
+  List<Completion> execute(ClientMessage message, org.mariadb.jdbc.Statement stmt, boolean canRedo)
+      throws SQLException;
+
+  /**
+   * Send client message and read result
+   *
+   * @param message client message
+   * @param stmt statement
+   * @param fetchSize fetch size
+   * @param maxRows maximum number of rows. 0 = all
+   * @param resultSetConcurrency concurrency
+   * @param resultSetType result-set type
+   * @param closeOnCompletion close statement on completion
+   * @param canRedo can client message be redone in case of failover
+   * @return results
+   * @throws SQLException if any error occurs
+   */
+  List<Completion> execute(
+      ClientMessage message,
+      org.mariadb.jdbc.Statement stmt,
+      int fetchSize,
+      long maxRows,
+      int resultSetConcurrency,
+      int resultSetType,
+      boolean closeOnCompletion,
+      boolean canRedo)
+      throws SQLException;
+
+  /**
+   * Send client messages pipelining and read result
+   *
+   * @param messages client message
+   * @param stmt statement
+   * @param fetchSize fetch size
+   * @param maxRows maximum number of rows. 0 = all
+   * @param resultSetConcurrency concurrency
+   * @param resultSetType result-set type
+   * @param closeOnCompletion close statement on completion
+   * @param canRedo can client message be redone in case of failover
+   * @return results
+   * @throws SQLException if any error occurs
+   */
+  List<Completion> executePipeline(
+      ClientMessage[] messages,
+      org.mariadb.jdbc.Statement stmt,
+      int fetchSize,
+      long maxRows,
+      int resultSetConcurrency,
+      int resultSetType,
+      boolean closeOnCompletion,
+      boolean canRedo)
+      throws SQLException;
+
+  /**
+   * Read results
+   *
+   * @param completions List that will have the new results
+   * @param fetchSize fetch size
+   * @param maxRows maximum number of rows. 0 = all
+   * @param resultSetConcurrency concurrency
+   * @param resultSetType result-set type
+   * @param closeOnCompletion close statement on completion
+   * @throws SQLException if any error occurs
+   */
+  void readStreamingResults(
+      List<Completion> completions,
+      int fetchSize,
+      long maxRows,
+      int resultSetConcurrency,
+      int resultSetType,
+      boolean closeOnCompletion)
+      throws SQLException;
+
+  /**
+   * Close prepare command
+   *
+   * @param prepare prepare command
+   * @throws SQLException if any error occurs
+   */
+  void closePrepare(Prepare prepare) throws SQLException;
+
+  /**
+   * Abort current connection
+   *
+   * @param executor executor
+   * @throws SQLException if any error occurs
+   */
+  void abort(Executor executor) throws SQLException;
+
+  /**
+   * Close client
+   *
+   * @throws SQLException if any error occurs
+   */
+  void close() throws SQLException;
+
+  /**
+   * Switch to a writer/read-only connection, no effet on mono-connection
+   *
+   * @param readOnly must use read-only connection
+   * @throws SQLException if any error occurs
+   */
+  void setReadOnly(boolean readOnly) throws SQLException;
+
+  /**
+   * get socket timeout
+   *
+   * @return socket timeout
+   */
+  int getSocketTimeout();
+
+  /**
+   * Set socket timeout
+   *
+   * @param milliseconds timeout
+   * @throws SQLException if any error occurs
+   */
+  void setSocketTimeout(int milliseconds) throws SQLException;
+
+  /**
+   * Is client closed
+   *
+   * @return close flag
+   */
+  boolean isClosed();
+
+  /** Reset connection */
+  void reset();
+
+  /**
+   * is current client writer or read-only
+   *
+   * @return is primary
+   */
+  boolean isPrimary();
+
+  /**
+   * Get connection context
+   *
+   * @return connection context
+   */
+  Context getContext();
+
+  /**
+   * Get connection exception factory
+   *
+   * @return connection exception factory
+   */
+  ExceptionFactory getExceptionFactory();
+
+  /**
+   * Get connection host
+   *
+   * @return connection host
+   */
+  HostAddress getHostAddress();
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/Column.java b/src/main/java/org/mariadb/jdbc/client/Column.java
new file mode 100644
index 0000000..8032aa8
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/Column.java
@@ -0,0 +1,120 @@
+package org.mariadb.jdbc.client;
+
+public interface Column {
+
+  /**
+   * get column schema
+   *
+   * @return column schema
+   */
+  String getSchema();
+
+  /**
+   * retrieve table alias if any
+   *
+   * @return table alias
+   */
+  String getTableAlias();
+
+  /**
+   * retrieve table name if any
+   *
+   * @return table name
+   */
+  String getTable();
+
+  /**
+   * retrieve column alias if any
+   *
+   * @return column alias
+   */
+  String getColumnAlias();
+
+  /**
+   * retrieve column name if any
+   *
+   * @return column name
+   */
+  String getColumnName();
+
+  /**
+   * column maximum length if known
+   *
+   * @return column maximum length
+   */
+  long getColumnLength();
+
+  /**
+   * server data type
+   *
+   * @return server data type
+   */
+  DataType getType();
+
+  /**
+   * get number of decimal
+   *
+   * @return number of decimal
+   */
+  byte getDecimals();
+
+  /**
+   * Is column signed (for number only)
+   *
+   * @return is signed
+   */
+  boolean isSigned();
+
+  /**
+   * get display size
+   *
+   * @return display sier
+   */
+  int getDisplaySize();
+
+  /**
+   * Is column a primary key
+   *
+   * @return is a primary key
+   */
+  boolean isPrimaryKey();
+
+  /**
+   * Column autoincrement
+   *
+   * @return true if column auto-increment
+   */
+  boolean isAutoIncrement();
+
+  /**
+   * Column has a default value
+   *
+   * @return indicate if has a default value
+   */
+  boolean hasDefault();
+
+  /**
+   * indicate if column is of binary type. doesn't use flag BINARY filter, because char binary and
+   * varchar binary are not binary (handle like string), but have binary flag
+   *
+   * @return is column type binary
+   */
+  boolean isBinary();
+
+  /**
+   * Retrieve metadata flag
+   *
+   * @return metadata flag
+   */
+  int getFlags();
+
+  /**
+   * retrieve extended metadata name if any
+   *
+   * @return extended metadata name
+   */
+  String getExtTypeName();
+
+  /** Use column alias as name */
+  void useAliasAsName();
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/ColumnDecoder.java b/src/main/java/org/mariadb/jdbc/client/ColumnDecoder.java
new file mode 100644
index 0000000..33e1465
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/ColumnDecoder.java
@@ -0,0 +1,425 @@
+package org.mariadb.jdbc.client;
+
+import java.nio.charset.StandardCharsets;
+import java.sql.Date;
+import java.sql.SQLDataException;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.column.UuidColumn;
+import org.mariadb.jdbc.client.impl.StandardReadableByteBuf;
+import org.mariadb.jdbc.util.constants.ColumnFlags;
+
+public interface ColumnDecoder extends Column {
+
+  /**
+   * Returns default class name depending on server column datatype
+   *
+   * @param conf configuration
+   * @return default class name
+   */
+  String defaultClassname(Configuration conf);
+
+  /**
+   * Returns default java.sql.Types depending on server column datatype
+   *
+   * @param conf configuration
+   * @return default java.sql.Types
+   */
+  int getColumnType(Configuration conf);
+
+  /**
+   * Returns server column datatype
+   *
+   * @param conf configuration
+   * @return default server column datatype
+   */
+  String getColumnTypeName(Configuration conf);
+
+  /**
+   * Return decimal precision.
+   *
+   * @return decimal precision
+   */
+  default int getPrecision() {
+    return (int) getColumnLength();
+  }
+
+  /**
+   * Return default Object text encoded
+   *
+   * @param conf configuration
+   * @param buf row buffer
+   * @param length data length
+   * @return default Object
+   * @throws SQLDataException if any decoding error occurs
+   */
+  Object getDefaultText(final Configuration conf, final ReadableByteBuf buf, final int length)
+      throws SQLDataException;
+
+  /**
+   * Return default Object binary encoded
+   *
+   * @param conf configuration
+   * @param buf row buffer
+   * @param length data length
+   * @return default Object
+   * @throws SQLDataException if any decoding error occurs
+   */
+  Object getDefaultBinary(final Configuration conf, final ReadableByteBuf buf, final int length)
+      throws SQLDataException;
+
+  /**
+   * Return String text encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @param cal calendar
+   * @return String value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  String decodeStringText(final ReadableByteBuf buf, final int length, final Calendar cal)
+      throws SQLDataException;
+
+  /**
+   * Return String binary encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @param cal calendar
+   * @return String value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  String decodeStringBinary(final ReadableByteBuf buf, final int length, final Calendar cal)
+      throws SQLDataException;
+
+  /**
+   * Return byte text encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @return byte value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  byte decodeByteText(final ReadableByteBuf buf, final int length) throws SQLDataException;
+
+  /**
+   * Return byte binary encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @return byte value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  byte decodeByteBinary(final ReadableByteBuf buf, final int length) throws SQLDataException;
+
+  /**
+   * Return date text encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @param cal calendar
+   * @return date value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  Date decodeDateText(final ReadableByteBuf buf, final int length, Calendar cal)
+      throws SQLDataException;
+
+  /**
+   * Return date binary encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @param cal calendar
+   * @return date value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  Date decodeDateBinary(final ReadableByteBuf buf, final int length, Calendar cal)
+      throws SQLDataException;
+
+  /**
+   * Return time text encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @param cal calendar
+   * @return time value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  Time decodeTimeText(final ReadableByteBuf buf, final int length, Calendar cal)
+      throws SQLDataException;
+
+  /**
+   * Return time binary encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @param cal calendar
+   * @return time value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  Time decodeTimeBinary(final ReadableByteBuf buf, final int length, Calendar cal)
+      throws SQLDataException;
+
+  /**
+   * Return timestamp text encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @param cal calendar
+   * @return timestamp value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  Timestamp decodeTimestampText(final ReadableByteBuf buf, final int length, Calendar cal)
+      throws SQLDataException;
+
+  /**
+   * Return timestamp binary encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @param cal calendar
+   * @return timestamp value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  Timestamp decodeTimestampBinary(final ReadableByteBuf buf, final int length, Calendar cal)
+      throws SQLDataException;
+  /**
+   * Return boolean text encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @return boolean value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  boolean decodeBooleanText(final ReadableByteBuf buf, final int length) throws SQLDataException;
+
+  /**
+   * Parse boolean binary encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @return boolean value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  boolean decodeBooleanBinary(final ReadableByteBuf buf, final int length) throws SQLDataException;
+  /**
+   * Parse short text encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @return short value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  short decodeShortText(final ReadableByteBuf buf, final int length) throws SQLDataException;
+  /**
+   * Parse short binary encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @return short value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  short decodeShortBinary(final ReadableByteBuf buf, final int length) throws SQLDataException;
+  /**
+   * Parse int text encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @return int value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  int decodeIntText(final ReadableByteBuf buf, final int length) throws SQLDataException;
+
+  /**
+   * Parse int binary encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @return int value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  int decodeIntBinary(final ReadableByteBuf buf, final int length) throws SQLDataException;
+
+  /**
+   * Parse long text encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @return long value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  long decodeLongText(final ReadableByteBuf buf, final int length) throws SQLDataException;
+  /**
+   * Parse long binary encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @return long value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  long decodeLongBinary(final ReadableByteBuf buf, final int length) throws SQLDataException;
+
+  /**
+   * Parse float text encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @return float value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  float decodeFloatText(final ReadableByteBuf buf, final int length) throws SQLDataException;
+
+  /**
+   * Parse float binary encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @return float value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  float decodeFloatBinary(final ReadableByteBuf buf, final int length) throws SQLDataException;
+
+  /**
+   * Parse double text encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @return double value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  double decodeDoubleText(final ReadableByteBuf buf, final int length) throws SQLDataException;
+
+  /**
+   * Parse double binary encoded value
+   *
+   * @param buf row buffer
+   * @param length data length
+   * @return double value
+   * @throws SQLDataException if any decoding error occurs
+   */
+  double decodeDoubleBinary(final ReadableByteBuf buf, final int length) throws SQLDataException;
+
+  /**
+   * Decode Column from mysql packet
+   *
+   * @param buf packet
+   * @param extendedInfo is extended datatype information capability enable
+   * @return column
+   */
+  static ColumnDecoder decode(ReadableByteBuf buf, boolean extendedInfo) {
+    // skip first strings
+    int[] stringPos = new int[5];
+    stringPos[0] = buf.skipIdentifier(); // schema pos
+    stringPos[1] = buf.skipIdentifier(); // table alias pos
+    stringPos[2] = buf.skipIdentifier(); // table pos
+    stringPos[3] = buf.skipIdentifier(); // column alias pos
+    stringPos[4] = buf.skipIdentifier(); // column pos
+    buf.skipIdentifier();
+
+    String extTypeName = null;
+    String extTypeFormat = null;
+    if (extendedInfo) {
+      // fast skipping extended info (usually not set)
+      if (buf.readByte() != 0) {
+        // revert position, because has extended info.
+        buf.pos(buf.pos() - 1);
+
+        ReadableByteBuf subPacket = buf.readLengthBuffer();
+        while (subPacket.readableBytes() > 0) {
+          switch (subPacket.readByte()) {
+            case 0:
+              extTypeName = subPacket.readAscii(subPacket.readLength());
+              break;
+            case 1:
+              extTypeFormat = subPacket.readAscii(subPacket.readLength());
+              break;
+            default: // skip data
+              subPacket.skip(subPacket.readLength());
+              break;
+          }
+        }
+      }
+    }
+
+    buf.skip(); // skip length always 0x0c
+    short charset = buf.readShort();
+    int length = buf.readInt();
+    DataType dataType = DataType.of(buf.readUnsignedByte());
+    int flags = buf.readUnsignedShort();
+    byte decimals = buf.readByte();
+    DataType.ColumnConstructor constructor =
+        (extTypeName != null && extTypeName.equals("uuid"))
+            ? UuidColumn::new
+            : (flags & ColumnFlags.UNSIGNED) == 0
+                ? dataType.getColumnConstructor()
+                : dataType.getUnsignedColumnConstructor();
+    return constructor.create(
+        buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  /**
+   * Create fake MySQL column definition packet with indicated datatype
+   *
+   * @param name column name
+   * @param type data type
+   * @param flags column flags
+   * @return Column
+   */
+  static ColumnDecoder create(String name, DataType type, int flags) {
+    byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8);
+    byte[] arr = new byte[9 + 2 * nameBytes.length];
+    arr[0] = 3;
+    arr[1] = 'D';
+    arr[2] = 'E';
+    arr[3] = 'F';
+
+    int[] stringPos = new int[5];
+    stringPos[0] = 4; // schema pos
+    stringPos[1] = 5; // table alias pos
+    stringPos[2] = 6; // table pos
+
+    // lenenc_str     name
+    // lenenc_str     org_name
+    int pos = 7;
+    for (int i = 0; i < 2; i++) {
+      stringPos[i + 3] = pos;
+      arr[pos++] = (byte) nameBytes.length;
+      System.arraycopy(nameBytes, 0, arr, pos, nameBytes.length);
+      pos += nameBytes.length;
+    }
+    int len;
+
+    /* Sensible predefined length - since we're dealing with I_S here, most char fields are 64 char long */
+    switch (type) {
+      case VARCHAR:
+      case VARSTRING:
+        len = 64 * 3; /* 3 bytes per UTF8 char */
+        break;
+      case SMALLINT:
+        len = 5;
+        break;
+      case NULL:
+        len = 0;
+        break;
+      default:
+        len = 1;
+        break;
+    }
+    DataType.ColumnConstructor constructor =
+        (flags & ColumnFlags.UNSIGNED) == 0
+            ? type.getColumnConstructor()
+            : type.getUnsignedColumnConstructor();
+    return constructor.create(
+        new StandardReadableByteBuf(arr, arr.length),
+        33,
+        len,
+        type,
+        (byte) 0,
+        flags,
+        stringPos,
+        null,
+        null);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/Completion.java b/src/main/java/org/mariadb/jdbc/client/Completion.java
new file mode 100644
index 0000000..678b19d
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/Completion.java
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client;
+
+import org.mariadb.jdbc.message.ServerMessage;
+
+public interface Completion extends ServerMessage {}
diff --git a/src/main/java/org/mariadb/jdbc/client/Context.java b/src/main/java/org/mariadb/jdbc/client/Context.java
new file mode 100644
index 0000000..59c019e
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/Context.java
@@ -0,0 +1,173 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client;
+
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.export.ExceptionFactory;
+
+public interface Context {
+
+  /**
+   * return current thread id
+   *
+   * @return current server thread id
+   */
+  long getThreadId();
+
+  /**
+   * Get connection initial seed
+   *
+   * @return initial seed
+   */
+  byte[] getSeed();
+
+  /**
+   * has server capability
+   *
+   * @param flag capability to check
+   * @return true if server has capability
+   */
+  boolean hasServerCapability(long flag);
+
+  /**
+   * has client capability
+   *
+   * @param flag capability to check
+   * @return true if client has capability
+   */
+  boolean hasClientCapability(long flag);
+
+  /**
+   * Does server and client permit pipeline
+   *
+   * @return true if permitted
+   */
+  boolean permitPipeline();
+
+  /**
+   * Get server connection state
+   *
+   * @return server status
+   */
+  int getServerStatus();
+
+  /**
+   * Set server connection state
+   *
+   * @param serverStatus server status
+   */
+  void setServerStatus(int serverStatus);
+
+  /**
+   * Get current connection database
+   *
+   * @return database
+   */
+  String getDatabase();
+
+  /**
+   * set current database context
+   *
+   * @param database database
+   */
+  void setDatabase(String database);
+
+  /**
+   * Retrieve server version information
+   *
+   * @return server version
+   */
+  ServerVersion getVersion();
+
+  /**
+   * does protocol remove EOF in exchanges
+   *
+   * @return if EOF packet are deprecated
+   */
+  boolean isEofDeprecated();
+
+  /**
+   * Can server skip prepared statement metadata
+   *
+   * @return true if possible
+   */
+  boolean canSkipMeta();
+
+  /**
+   * Does server metadata exchange extended information
+   *
+   * @return use metadata extended information
+   */
+  boolean isExtendedInfo();
+
+  /**
+   * has server warnings
+   *
+   * @return has warnings
+   */
+  int getWarning();
+
+  /**
+   * set server state warning number
+   *
+   * @param warning warning number
+   */
+  void setWarning(int warning);
+
+  /**
+   * Get connection exception factory
+   *
+   * @return exception factory
+   */
+  ExceptionFactory getExceptionFactory();
+
+  /**
+   * Get connection configuration
+   *
+   * @return configuration
+   */
+  Configuration getConf();
+
+  /**
+   * Get connection transaction isolation level
+   *
+   * @return connection transaction isolation level
+   */
+  int getTransactionIsolationLevel();
+
+  /**
+   * Set current connection transaction isolation level
+   *
+   * @param transactionIsolationLevel new connection transaction isolation level
+   */
+  void setTransactionIsolationLevel(int transactionIsolationLevel);
+
+  /**
+   * get LRU prepare cache object
+   *
+   * @return prepare cache
+   */
+  PrepareCache getPrepareCache();
+
+  /** Reset prepare cache (after a failover) */
+  void resetPrepareCache();
+
+  /**
+   * return connection current state change flag
+   *
+   * @return connection current state change flag
+   */
+  int getStateFlag();
+
+  /** reset connection state change flag */
+  void resetStateFlag();
+
+  /**
+   * Indicate connection state (for pooling)
+   *
+   * @param state indicate that some connection state has changed
+   */
+  void addStateFlag(int state);
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/DataType.java b/src/main/java/org/mariadb/jdbc/client/DataType.java
new file mode 100644
index 0000000..4b5c5a2
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/DataType.java
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client;
+
+import org.mariadb.jdbc.client.column.*;
+
+public enum DataType {
+  OLDDECIMAL(0, BigDecimalColumn::new, BigDecimalColumn::new),
+  TINYINT(1, SignedTinyIntColumn::new, UnsignedTinyIntColumn::new),
+  SMALLINT(2, SignedSmallIntColumn::new, UnsignedSmallIntColumn::new),
+  INTEGER(3, SignedIntColumn::new, UnsignedIntColumn::new),
+  FLOAT(4, FloatColumn::new, FloatColumn::new),
+  DOUBLE(5, DoubleColumn::new, DoubleColumn::new),
+  NULL(6, StringColumn::new, StringColumn::new),
+  TIMESTAMP(7, TimestampColumn::new, TimestampColumn::new),
+  BIGINT(8, SignedBigIntColumn::new, UnsignedBigIntColumn::new),
+  MEDIUMINT(9, SignedMediumIntColumn::new, UnsignedMediumIntColumn::new),
+  DATE(10, DateColumn::new, DateColumn::new),
+  TIME(11, TimeColumn::new, TimeColumn::new),
+  DATETIME(12, TimestampColumn::new, TimestampColumn::new),
+  YEAR(13, YearColumn::new, YearColumn::new),
+  NEWDATE(14, DateColumn::new, DateColumn::new),
+  VARCHAR(15, StringColumn::new, StringColumn::new),
+  BIT(16, BitColumn::new, BitColumn::new),
+  JSON(245, JsonColumn::new, JsonColumn::new),
+  DECIMAL(246, BigDecimalColumn::new, BigDecimalColumn::new),
+  ENUM(247, StringColumn::new, StringColumn::new),
+  SET(248, StringColumn::new, StringColumn::new),
+  TINYBLOB(249, BlobColumn::new, BlobColumn::new),
+  MEDIUMBLOB(250, BlobColumn::new, BlobColumn::new),
+  LONGBLOB(251, BlobColumn::new, BlobColumn::new),
+  BLOB(252, BlobColumn::new, BlobColumn::new),
+  VARSTRING(253, StringColumn::new, StringColumn::new),
+  STRING(254, StringColumn::new, StringColumn::new),
+  GEOMETRY(255, GeometryColumn::new, GeometryColumn::new);
+
+  static final DataType[] typeMap;
+
+  static {
+    typeMap = new DataType[256];
+    for (DataType v : values()) {
+      typeMap[v.mariadbType] = v;
+    }
+  }
+
+  private final int mariadbType;
+  private final ColumnConstructor columnConstructor;
+  private final ColumnConstructor unsignedColumnConstructor;
+
+  DataType(
+      int mariadbType,
+      ColumnConstructor columnConstructor,
+      ColumnConstructor unsignedColumnConstructor) {
+    this.mariadbType = mariadbType;
+    this.columnConstructor = columnConstructor;
+    this.unsignedColumnConstructor = unsignedColumnConstructor;
+  }
+
+  public int get() {
+    return mariadbType;
+  }
+
+  public static DataType of(int typeValue) {
+    return typeMap[typeValue];
+  }
+
+  public ColumnConstructor getColumnConstructor() {
+    return columnConstructor;
+  }
+
+  public ColumnConstructor getUnsignedColumnConstructor() {
+    return unsignedColumnConstructor;
+  }
+
+  @FunctionalInterface
+  public interface ColumnConstructor {
+
+    ColumnDecoder create(
+        ReadableByteBuf buf,
+        int charset,
+        long length,
+        DataType dataType,
+        byte decimals,
+        int flags,
+        int[] stringPos,
+        String extTypeName,
+        String extTypeFormat);
+  }
+}
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 0000000..4b7af9f
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/PrepareCache.java
@@ -0,0 +1,30 @@
+package org.mariadb.jdbc.client;
+
+import org.mariadb.jdbc.ServerPreparedStatement;
+import org.mariadb.jdbc.export.Prepare;
+
+/** LRU Prepare cache */
+public interface PrepareCache {
+
+  /**
+   * Get cache value for key
+   *
+   * @param key key
+   * @param preparedStatement prepared statement
+   * @return Prepare value
+   */
+  Prepare get(String key, ServerPreparedStatement preparedStatement);
+
+  /**
+   * Add a prepare cache value
+   *
+   * @param key key
+   * @param result value
+   * @param preparedStatement prepared statement
+   * @return Prepare if was already cached
+   */
+  Prepare put(String key, Prepare result, ServerPreparedStatement preparedStatement);
+
+  /** Reset cache */
+  void reset();
+}
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 0000000..06c61a2
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/ReadableByteBuf.java
@@ -0,0 +1,290 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client;
+
+import org.mariadb.jdbc.MariaDbBlob;
+
+/** Packet buffer interface */
+public interface ReadableByteBuf {
+
+  /**
+   * buffer number of unread bytes
+   *
+   * @return remaining bytes number
+   */
+  int readableBytes();
+
+  /**
+   * Current buffer position
+   *
+   * @return position
+   */
+  int pos();
+
+  /**
+   * buffer
+   *
+   * @return buffer
+   */
+  byte[] buf();
+
+  /**
+   * Reset buffer
+   *
+   * @param buf new buffer
+   * @param limit buffer limit
+   * @param pos initial position
+   */
+  void buf(byte[] buf, int limit, int pos);
+
+  /**
+   * Set position
+   *
+   * @param pos new position
+   */
+  void pos(int pos);
+
+  /** Skip one byte */
+  void skip();
+
+  /**
+   * Skip length value of bytes
+   *
+   * @param length number of position to skip
+   */
+  void skip(int length);
+
+  /** Skip length encoded value */
+  void skipLengthEncoded();
+
+  /**
+   * Read Blob at current position
+   *
+   * @param length blob length
+   * @return Blob
+   */
+  MariaDbBlob readBlob(int length);
+
+  /**
+   * Read byte from buffer at current position, without changing position
+   *
+   * @return byte value
+   */
+  byte getByte();
+
+  /**
+   * Read byte from buffer at indicated index, without changing position
+   *
+   * @param index index
+   * @return byte value
+   */
+  byte getByte(int index);
+
+  /**
+   * Read unsigned byte value at current position, without changing position
+   *
+   * @return short value
+   */
+  short getUnsignedByte();
+
+  /**
+   * Read encoded length value that cannot be null see
+   * https://mariadb.com/kb/en/protocol-data-types/#length-encoded-integers
+   *
+   * @return encoded length
+   */
+  long readLongLengthEncodedNotNull();
+
+  /**
+   * Read encoded length value that cannot be null see
+   * https://mariadb.com/kb/en/protocol-data-types/#length-encoded-integers
+   *
+   * <p>this is readLongLengthEncodedNotNull limited to 32 bits
+   *
+   * @return encoded length
+   */
+  int readIntLengthEncodedNotNull();
+
+  /**
+   * Utility to skip length encoded string, returning initial position
+   *
+   * @return initial position
+   */
+  int skipIdentifier();
+
+  /**
+   * Fast signed long parsing
+   *
+   * @param length data length
+   * @return long value
+   */
+  long atoll(int length);
+
+  /**
+   * Fast unsigned long parsing
+   *
+   * @param length data length
+   * @return long value
+   */
+  long atoull(int length);
+  /**
+   * Read encoded length value see
+   * https://mariadb.com/kb/en/protocol-data-types/#length-encoded-integers
+   *
+   * @return encoded length
+   */
+  Integer readLength();
+
+  /**
+   * Read byte at current position, incrementing position
+   *
+   * @return byte at current position
+   */
+  byte readByte();
+
+  /**
+   * Read unsigned byte value at current position
+   *
+   * @return short value
+   */
+  short readUnsignedByte();
+
+  /**
+   * Read signed 2 bytes value (little endian) at current position
+   *
+   * @return short value
+   */
+  short readShort();
+
+  /**
+   * Read unsigned 2 bytes value (little endian) at current position
+   *
+   * @return short value
+   */
+  int readUnsignedShort();
+
+  /**
+   * Read signed 3 bytes value (little endian) at current position
+   *
+   * @return int value
+   */
+  int readMedium();
+
+  /**
+   * Read unsigned 3 bytes value (little endian) at current position
+   *
+   * @return int value
+   */
+  int readUnsignedMedium();
+
+  /**
+   * Read signed 4 bytes value (little endian) at current position
+   *
+   * @return int value
+   */
+  int readInt();
+
+  /**
+   * Read signed 4 bytes value (big endian) at current position
+   *
+   * @return int value
+   */
+  int readIntBE();
+
+  /**
+   * Read unsigned 4 bytes value (little endian) at current position
+   *
+   * @return long value
+   */
+  long readUnsignedInt();
+
+  /**
+   * Read signed 8 bytes value (little endian) at current position
+   *
+   * @return long value
+   */
+  long readLong();
+
+  /**
+   * Read unsigned 4 bytes value (big endian) at current position
+   *
+   * @return long value
+   */
+  long readLongBE();
+
+  /**
+   * Read as many bytes to fill destination array
+   *
+   * @param dst destination array
+   */
+  void readBytes(byte[] dst);
+
+  /**
+   * Read null-ended encoded bytes. 0x00 null value won't be in return byte, so position is
+   * incremented to returned byte array length + 1
+   *
+   * @return byte array
+   */
+  byte[] readBytesNullEnd();
+
+  /**
+   * Return a length encoded buffer
+   *
+   * @return new buffer
+   */
+  ReadableByteBuf readLengthBuffer();
+
+  /**
+   * Read utf-8 encoded string from length bytes
+   *
+   * @param length length byte to read
+   * @return string value
+   */
+  String readString(int length);
+
+  /**
+   * Read ascii encoded string from length bytes
+   *
+   * @param length length byte to read
+   * @return string value
+   */
+  String readAscii(int length);
+
+  /**
+   * Read null-ended utf-8 encoded string. 0x00 = null represent string ending. Position is
+   * incremented to returned string corresponding bytes + 1
+   *
+   * @return corresponding string
+   */
+  String readStringNullEnd();
+
+  /**
+   * Return the utf-8 string represented by current position to the limit of buffer
+   *
+   * @return string value
+   */
+  String readStringEof();
+
+  /**
+   * Read float encoded on 4 bytes value at current position
+   *
+   * @return float value
+   */
+  float readFloat();
+
+  /**
+   * Read double encoded on 8 bytes value at current position
+   *
+   * @return double value
+   */
+  double readDouble();
+
+  /**
+   * Read double encoded on 8 bytes (big endian) value at current position
+   *
+   * @return double value
+   */
+  double readDoubleBE();
+}
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 0000000..0ef8177
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/ServerVersion.java
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client;
+
+public interface ServerVersion {
+  /**
+   * Get server version string
+   *
+   * @return server version string
+   */
+  String getVersion();
+
+  /**
+   * get server major version, parsed from server version string
+   *
+   * @return server major version
+   */
+  int getMajorVersion();
+
+  /**
+   * get server minor version, parsed from server version string
+   *
+   * @return server minor version
+   */
+  int getMinorVersion();
+
+  /**
+   * get server patch version, parsed from server version string
+   *
+   * @return server patch version
+   */
+  int getPatchVersion();
+
+  /**
+   * get server qualifier, parsed from server version string
+   *
+   * @return server qualifier
+   */
+  String getQualifier();
+
+  /**
+   * 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
+   */
+  boolean versionGreaterOrEqual(int major, int minor, int patch);
+
+  /**
+   * Is server mariadb
+   *
+   * @return true if server is a MariaDB server
+   */
+  boolean isMariaDBServer();
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/SocketHelper.java b/src/main/java/org/mariadb/jdbc/client/SocketHelper.java
new file mode 100644
index 0000000..c8916c8
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/SocketHelper.java
@@ -0,0 +1,35 @@
+package org.mariadb.jdbc.client;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import org.mariadb.jdbc.Configuration;
+
+public class SocketHelper {
+
+  /**
+   * Set socket option
+   *
+   * @param conf configuration
+   * @param socket socket
+   * @throws IOException if any socket error occurs
+   */
+  public static void setSocketOption(final Configuration conf, final Socket socket)
+      throws IOException {
+    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);
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/BigDecimalColumn.java b/src/main/java/org/mariadb/jdbc/client/column/BigDecimalColumn.java
new file mode 100644
index 0000000..527d45b
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/BigDecimalColumn.java
@@ -0,0 +1,245 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.sql.*;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.server.ColumnDefinitionPacket;
+
+/** Column metadata definition */
+public class BigDecimalColumn extends ColumnDefinitionPacket implements ColumnDecoder {
+
+  /**
+   * Constructor for column corresponding to DECIMAL datatype. Class permit specific decoding for
+   * this datatype
+   *
+   * @param buf Column definition MySQL packet buffer
+   * @param charset charset
+   * @param length datatype length
+   * @param dataType data type
+   * @param decimals number of decimals
+   * @param flags column flags
+   * @param stringPos string value position
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public BigDecimalColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    return BigDecimal.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    return Types.DECIMAL;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    return "DECIMAL";
+  }
+
+  public int getPrecision() {
+    // DECIMAL and OLDDECIMAL are  "exact" fixed-point number.
+    // so :
+    // - if is signed, 1 byte is saved for sign
+    // - if decimal > 0, one byte more for dot
+    if (isSigned()) {
+      return (int) (columnLength - ((decimals > 0) ? 2 : 1));
+    } else {
+      return (int) (columnLength - ((decimals > 0) ? 1 : 0));
+    }
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return new BigDecimal(buf.readAscii(length));
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return new BigDecimal(buf.readAscii(length));
+  }
+
+  @Override
+  public boolean decodeBooleanText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return new BigDecimal(buf.readAscii(length)).intValue() != 0;
+  }
+
+  @Override
+  public boolean decodeBooleanBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return decodeBooleanText(buf, length);
+  }
+
+  @Override
+  public byte decodeByteText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String str = buf.readString(length);
+    try {
+      return 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, dataType));
+    }
+  }
+
+  @Override
+  public byte decodeByteBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return decodeByteText(buf, length);
+  }
+
+  @Override
+  public String decodeStringText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public String decodeStringBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public short decodeShortText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result;
+    String str = buf.readString(length);
+    try {
+      result = new BigDecimal(str).setScale(0, RoundingMode.DOWN).longValueExact();
+    } catch (NumberFormatException | ArithmeticException nfe) {
+      throw new SQLDataException(String.format("value '%s' cannot be decoded as Short", str));
+    }
+    if ((short) result != result || (result < 0 && !isSigned())) {
+      throw new SQLDataException("Short overflow");
+    }
+    return (short) result;
+  }
+
+  @Override
+  public short decodeShortBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return decodeShortText(buf, length);
+  }
+
+  @Override
+  public int decodeIntText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result;
+    String str = buf.readString(length);
+    try {
+      result = new BigDecimal(str).setScale(0, RoundingMode.DOWN).longValueExact();
+    } catch (NumberFormatException | ArithmeticException nfe) {
+      throw new SQLDataException(String.format("value '%s' cannot be decoded as Integer", str));
+    }
+    int res = (int) result;
+    if (res != result || (result < 0 && !isSigned())) {
+      throw new SQLDataException("integer overflow");
+    }
+    return res;
+  }
+
+  @Override
+  public int decodeIntBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return decodeIntText(buf, length);
+  }
+
+  @Override
+  public long decodeLongText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String str2 = buf.readAscii(length);
+    try {
+      return new BigDecimal(str2).setScale(0, RoundingMode.DOWN).longValueExact();
+    } catch (NumberFormatException | ArithmeticException nfe) {
+      throw new SQLDataException(String.format("value '%s' cannot be decoded as Long", str2));
+    }
+  }
+
+  @Override
+  public long decodeLongBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    String str = buf.readString(length);
+    try {
+      return new BigDecimal(str).setScale(0, RoundingMode.DOWN).longValueExact();
+    } catch (NumberFormatException | ArithmeticException nfe) {
+      throw new SQLDataException(String.format("value '%s' cannot be decoded as Long", str));
+    }
+  }
+
+  @Override
+  public float decodeFloatText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Float.parseFloat(buf.readAscii(length));
+  }
+
+  @Override
+  public float decodeFloatBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return new BigDecimal(buf.readAscii(length)).floatValue();
+  }
+
+  @Override
+  public double decodeDoubleText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Double.parseDouble(buf.readAscii(length));
+  }
+
+  @Override
+  public double decodeDoubleBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return new BigDecimal(buf.readAscii(length)).doubleValue();
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Time decodeTimeText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Time decodeTimeBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/BitColumn.java b/src/main/java/org/mariadb/jdbc/client/column/BitColumn.java
new file mode 100644
index 0000000..fbf752d
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/BitColumn.java
@@ -0,0 +1,272 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.sql.*;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.server.ColumnDefinitionPacket;
+import org.mariadb.jdbc.plugin.codec.ByteCodec;
+
+/** Column metadata definition */
+public class BitColumn extends ColumnDefinitionPacket implements ColumnDecoder {
+  /**
+   * Constructor for column corresponding to BIT datatype. Class permit specific decoding for this
+   * datatype
+   *
+   * @param buf Column definition MySQL packet buffer
+   * @param charset charset
+   * @param length datatype length
+   * @param dataType data type
+   * @param decimals number of decimals
+   * @param flags column flags
+   * @param stringPos string value position
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public BitColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    return columnLength == 1 ? Boolean.class.getName() : "byte[]";
+  }
+
+  public int getColumnType(Configuration conf) {
+    return columnLength == 1 ? Types.BOOLEAN : Types.VARBINARY;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    return "BIT";
+  }
+
+  public int getPrecision() {
+    return (int) columnLength;
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    if (columnLength == 1) {
+      return ByteCodec.parseBit(buf, length) != 0;
+    }
+    byte[] arr = new byte[length];
+    buf.readBytes(arr);
+    return arr;
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return getDefaultText(conf, buf, length);
+  }
+
+  @Override
+  public boolean decodeBooleanText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return ByteCodec.parseBit(buf, length) != 0;
+  }
+
+  @Override
+  public boolean decodeBooleanBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return ByteCodec.parseBit(buf, length) != 0;
+  }
+
+  @Override
+  public byte decodeByteText(ReadableByteBuf buf, int length) throws SQLDataException {
+    byte val = buf.readByte();
+    if (length > 1) buf.skip(length - 1);
+    return val;
+  }
+
+  @Override
+  public byte decodeByteBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return decodeByteText(buf, length);
+  }
+
+  @Override
+  public String decodeStringText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    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();
+  }
+
+  @Override
+  public String decodeStringBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    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();
+  }
+
+  @Override
+  public short decodeShortText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = 0;
+    for (int i = 0; i < length; i++) {
+      byte b = buf.readByte();
+      result = (result << 8) + (b & 0xff);
+    }
+    if ((short) result != result || (result < 0 && !isSigned())) {
+      throw new SQLDataException("Short overflow");
+    }
+    return (short) result;
+  }
+
+  @Override
+  public short decodeShortBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return decodeShortText(buf, length);
+  }
+
+  @Override
+  public int decodeIntText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = 0;
+    for (int i = 0; i < length; i++) {
+      byte b = buf.readByte();
+      result = (result << 8) + (b & 0xff);
+    }
+    int res = (int) result;
+    if (res != result || (result < 0 && !isSigned())) {
+      throw new SQLDataException("integer overflow");
+    }
+    return res;
+  }
+
+  @Override
+  public int decodeIntBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = 0;
+    for (int i = 0; i < length; i++) {
+      byte b = buf.readByte();
+      result = (result << 8) + (b & 0xff);
+    }
+
+    int res = (int) result;
+    if (res != result) {
+      throw new SQLDataException("integer overflow");
+    }
+
+    return res;
+  }
+
+  @Override
+  public long decodeLongText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = 0;
+    for (int i = 0; i < length; i++) {
+      byte b = buf.readByte();
+      result = (result << 8) + (b & 0xff);
+    }
+    return result;
+  }
+
+  @Override
+  public long decodeLongBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return decodeLongText(buf, length);
+  }
+
+  @Override
+  public float decodeFloatText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Float", dataType));
+  }
+
+  @Override
+  public float decodeFloatBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Float", dataType));
+  }
+
+  @Override
+  public double decodeDoubleText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Double", dataType));
+  }
+
+  @Override
+  public double decodeDoubleBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Double", dataType));
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Time decodeTimeText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Time decodeTimeBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/BlobColumn.java b/src/main/java/org/mariadb/jdbc/client/column/BlobColumn.java
new file mode 100644
index 0000000..a23c873
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/BlobColumn.java
@@ -0,0 +1,338 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.sql.*;
+import java.util.Calendar;
+import java.util.Locale;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.util.CharsetEncodingLength;
+
+/** Column metadata definition */
+public class BlobColumn extends StringColumn implements ColumnDecoder {
+  /**
+   * Constructor for column corresponding to BLOB datatype. Class permit specific decoding for this
+   * datatype
+   *
+   * @param buf Column definition MySQL packet buffer
+   * @param charset charset
+   * @param length datatype length
+   * @param dataType data type
+   * @param decimals number of decimals
+   * @param flags column flags
+   * @param stringPos string value position
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public BlobColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    return isBinary() ? Blob.class.getName() : String.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    if (columnLength <= 0 || getDisplaySize() > 16777215) {
+      return isBinary() ? Types.LONGVARBINARY : Types.LONGVARCHAR;
+    } else {
+      if (dataType == DataType.TINYBLOB || dataType == DataType.BLOB) {
+        return isBinary() ? Types.VARBINARY : Types.VARCHAR;
+      }
+      return isBinary() ? Types.LONGVARBINARY : Types.LONGVARCHAR;
+    }
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    /*
+     map to different blob types based on datatype length
+     see https://mariadb.com/kb/en/library/data-types/
+    */
+    if (extTypeFormat != null) {
+      return extTypeFormat.toUpperCase(Locale.ROOT);
+    }
+    if (isBinary()) {
+      if (columnLength < 0) {
+        return "LONGBLOB";
+      } else if (columnLength <= 255) {
+        return "TINYBLOB";
+      } else if (columnLength <= 65535) {
+        return "BLOB";
+      } else if (columnLength <= 16777215) {
+        return "MEDIUMBLOB";
+      } else {
+        return "LONGBLOB";
+      }
+    } else {
+      if (columnLength < 0) {
+        return "LONGTEXT";
+      } else if (getDisplaySize() <= 65532) {
+        return "VARCHAR";
+      } else if (getDisplaySize() <= 65535) {
+        return "TEXT";
+      } else if (getDisplaySize() <= 16777215) {
+        return "MEDIUMTEXT";
+      } else {
+        return "LONGTEXT";
+      }
+    }
+  }
+
+  public int getPrecision() {
+    if (!isBinary()) {
+      Integer maxWidth2 = CharsetEncodingLength.maxCharlen.get(charset);
+      if (maxWidth2 != null) return (int) (columnLength / maxWidth2);
+    }
+    return (int) columnLength;
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    if (isBinary()) {
+      return buf.readBlob(length);
+    }
+    return buf.readString(length);
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return getDefaultText(conf, buf, length);
+  }
+
+  @Override
+  public boolean decodeBooleanText(ReadableByteBuf buf, int length) throws SQLDataException {
+    if (isBinary()) {
+      buf.skip(length);
+      throw new SQLDataException(
+          String.format("Data type %s cannot be decoded as Boolean", dataType));
+    }
+    String s = buf.readAscii(length);
+    return !"0".equals(s);
+  }
+
+  @Override
+  public boolean decodeBooleanBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return decodeBooleanText(buf, length);
+  }
+
+  @Override
+  public byte decodeByteText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result;
+    if (!isBinary()) {
+      // TEXT column
+      String str2 = buf.readString(length);
+      try {
+        result = new BigDecimal(str2).setScale(0, RoundingMode.DOWN).longValue();
+      } catch (NumberFormatException nfe) {
+        throw new SQLDataException(
+            String.format("value '%s' (%s) cannot be decoded as Byte", str2, dataType));
+      }
+      if ((byte) result != result) {
+        throw new SQLDataException("byte overflow");
+      }
+
+      return (byte) result;
+    }
+    if (length > 0) {
+      byte b = buf.readByte();
+      buf.skip(length - 1);
+      return b;
+    }
+    throw new SQLDataException("empty String value cannot be decoded as Byte");
+  }
+
+  @Override
+  public byte decodeByteBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return decodeByteText(buf, length);
+  }
+
+  @Override
+  public String decodeStringText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public String decodeStringBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public short decodeShortText(ReadableByteBuf buf, int length) throws SQLDataException {
+    if (isBinary()) {
+      buf.skip(length);
+      throw new SQLDataException(
+          String.format("Data type %s cannot be decoded as Short", dataType));
+    }
+    return super.decodeShortText(buf, length);
+  }
+
+  @Override
+  public short decodeShortBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    if (isBinary()) {
+      buf.skip(length);
+      throw new SQLDataException(
+          String.format("Data type %s cannot be decoded as Short", dataType));
+    }
+    return super.decodeShortBinary(buf, length);
+  }
+
+  @Override
+  public int decodeIntText(ReadableByteBuf buf, int length) throws SQLDataException {
+    if (isBinary()) {
+      buf.skip(length);
+      throw new SQLDataException(
+          String.format("Data type %s cannot be decoded as Integer", dataType));
+    }
+    return super.decodeIntText(buf, length);
+  }
+
+  @Override
+  public int decodeIntBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    if (isBinary()) {
+      buf.skip(length);
+      throw new SQLDataException(
+          String.format("Data type %s cannot be decoded as Integer", dataType));
+    }
+    return super.decodeIntBinary(buf, length);
+  }
+
+  @Override
+  public long decodeLongText(ReadableByteBuf buf, int length) throws SQLDataException {
+    if (isBinary()) {
+      buf.skip(length);
+      throw new SQLDataException(String.format("Data type %s cannot be decoded as Long", dataType));
+    }
+    return super.decodeLongText(buf, length);
+  }
+
+  @Override
+  public long decodeLongBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    if (isBinary()) {
+      buf.skip(length);
+      throw new SQLDataException(String.format("Data type %s cannot be decoded as Long", dataType));
+    }
+    return super.decodeLongBinary(buf, length);
+  }
+
+  @Override
+  public float decodeFloatText(ReadableByteBuf buf, int length) throws SQLDataException {
+    if (isBinary()) {
+      buf.skip(length);
+      throw new SQLDataException(
+          String.format("Data type %s cannot be decoded as Float", dataType));
+    }
+    return super.decodeFloatText(buf, length);
+  }
+
+  @Override
+  public float decodeFloatBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    if (isBinary()) {
+      buf.skip(length);
+      throw new SQLDataException(
+          String.format("Data type %s cannot be decoded as Float", dataType));
+    }
+    return super.decodeFloatText(buf, length);
+  }
+
+  @Override
+  public double decodeDoubleText(ReadableByteBuf buf, int length) throws SQLDataException {
+    if (isBinary()) {
+      buf.skip(length);
+      throw new SQLDataException(
+          String.format("Data type %s cannot be decoded as Double", dataType));
+    }
+    return super.decodeDoubleText(buf, length);
+  }
+
+  @Override
+  public double decodeDoubleBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    if (isBinary()) {
+      buf.skip(length);
+      throw new SQLDataException(
+          String.format("Data type %s cannot be decoded as Double", dataType));
+    }
+    return super.decodeDoubleBinary(buf, length);
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    if (isBinary()) {
+      buf.skip(length);
+      throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+    }
+    return super.decodeDateText(buf, length, cal);
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    if (isBinary()) {
+      buf.skip(length);
+      throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+    }
+    return super.decodeDateBinary(buf, length, cal);
+  }
+
+  @Override
+  public Time decodeTimeText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    if (isBinary()) {
+      buf.skip(length);
+      throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+    }
+    return super.decodeTimeText(buf, length, cal);
+  }
+
+  @Override
+  public Time decodeTimeBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    if (isBinary()) {
+      buf.skip(length);
+      throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+    }
+    return super.decodeTimeBinary(buf, length, cal);
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    if (isBinary()) {
+      buf.skip(length);
+      throw new SQLDataException(
+          String.format("Data type %s cannot be decoded as Timestamp", dataType));
+    }
+    return super.decodeTimestampText(buf, length, cal);
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    if (isBinary()) {
+      buf.skip(length);
+      throw new SQLDataException(
+          String.format("Data type %s cannot be decoded as Timestamp", dataType));
+    }
+    return super.decodeTimestampBinary(buf, length, cal);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/DateColumn.java b/src/main/java/org/mariadb/jdbc/client/column/DateColumn.java
new file mode 100644
index 0000000..28a0009
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/DateColumn.java
@@ -0,0 +1,268 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.sql.*;
+import java.time.LocalDate;
+import java.util.Calendar;
+import java.util.TimeZone;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.server.ColumnDefinitionPacket;
+
+/** Column metadata definition */
+public class DateColumn extends ColumnDefinitionPacket implements ColumnDecoder {
+
+  /**
+   * Date metadata type decoder
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param length maximum data length
+   * @param dataType data type. see https://mariadb.com/kb/en/result-set-packets/#field-types
+   * @param decimals decimal length
+   * @param flags flags. see https://mariadb.com/kb/en/result-set-packets/#field-details-flag
+   * @param stringPos string offset position in buffer
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public DateColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    return Date.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    return Types.DATE;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    return "DATE";
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return decodeDateText(buf, length, null);
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return decodeDateBinary(buf, length, null);
+  }
+
+  @Override
+  public boolean decodeBooleanText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Boolean", dataType));
+  }
+
+  @Override
+  public boolean decodeBooleanBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Boolean", dataType));
+  }
+
+  @Override
+  public byte decodeByteText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Byte", dataType));
+  }
+
+  @Override
+  public byte decodeByteBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Byte", dataType));
+  }
+
+  @Override
+  public String decodeStringText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public String decodeStringBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    if (length == 0) return "0000-00-00";
+    int dateYear = buf.readUnsignedShort();
+    int dateMonth = buf.readByte();
+    int dateDay = buf.readByte();
+    return LocalDate.of(dateYear, dateMonth, dateDay).toString();
+  }
+
+  @Override
+  public short decodeShortText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Short", dataType));
+  }
+
+  @Override
+  public short decodeShortBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Short", dataType));
+  }
+
+  @Override
+  public int decodeIntText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Integer", dataType));
+  }
+
+  @Override
+  public int decodeIntBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Integer", dataType));
+  }
+
+  @Override
+  public long decodeLongText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Long", dataType));
+  }
+
+  @Override
+  public long decodeLongBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Long", dataType));
+  }
+
+  @Override
+  public float decodeFloatText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Float", dataType));
+  }
+
+  @Override
+  public float decodeFloatBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Float", dataType));
+  }
+
+  @Override
+  public double decodeDoubleText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Double", dataType));
+  }
+
+  @Override
+  public double decodeDoubleBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Double", dataType));
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    int year = (int) buf.atoull(4);
+    buf.skip(1);
+    int month = (int) buf.atoull(2);
+    buf.skip(1);
+    int dayOfMonth = (int) buf.atoull(2);
+    if (year == 0 && month == 0 && dayOfMonth == 0) return null;
+
+    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());
+    }
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    if (length == 0) return null;
+
+    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());
+    }
+  }
+
+  @Override
+  public Time decodeTimeText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Time decodeTimeBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar calParam)
+      throws SQLDataException {
+    if (calParam == null || calParam.getTimeZone().equals(TimeZone.getDefault())) {
+      String s = buf.readAscii(length);
+      if ("0000-00-00".equals(s)) return null;
+      return new Timestamp(Date.valueOf(s).getTime());
+    }
+
+    String[] datePart = buf.readAscii(length).split("-");
+    synchronized (calParam) {
+      calParam.clear();
+      calParam.set(
+          Integer.parseInt(datePart[0]),
+          Integer.parseInt(datePart[1]) - 1,
+          Integer.parseInt(datePart[2]));
+      return new Timestamp(calParam.getTimeInMillis());
+    }
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar calParam)
+      throws SQLDataException {
+    if (length == 0) return null;
+
+    Calendar cal = calParam == null ? Calendar.getInstance() : calParam;
+    int year;
+    int month;
+    long dayOfMonth;
+
+    year = buf.readUnsignedShort();
+    month = buf.readByte();
+    dayOfMonth = buf.readByte();
+
+    if (year == 0 && month == 0 && dayOfMonth == 0) return null;
+
+    Timestamp timestamp;
+    synchronized (cal) {
+      cal.clear();
+      cal.set(year, month - 1, (int) dayOfMonth, 0, 0, 0);
+      timestamp = new Timestamp(cal.getTimeInMillis());
+    }
+    timestamp.setNanos(0);
+    return timestamp;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/DoubleColumn.java b/src/main/java/org/mariadb/jdbc/client/column/DoubleColumn.java
new file mode 100644
index 0000000..c8571df
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/DoubleColumn.java
@@ -0,0 +1,249 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.sql.*;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.server.ColumnDefinitionPacket;
+
+/** Column metadata definition */
+public class DoubleColumn extends ColumnDefinitionPacket implements ColumnDecoder {
+
+  /**
+   * Double metadata type decoder
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param length maximum data length
+   * @param dataType data type. see https://mariadb.com/kb/en/result-set-packets/#field-types
+   * @param decimals decimal length
+   * @param flags flags. see https://mariadb.com/kb/en/result-set-packets/#field-details-flag
+   * @param stringPos string offset position in buffer
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public DoubleColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    return Double.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    return Types.DOUBLE;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    return "DOUBLE";
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return Double.parseDouble(buf.readAscii(length));
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return buf.readDouble();
+  }
+
+  @Override
+  public boolean decodeBooleanText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String s = buf.readAscii(length);
+    return !"0".equals(s);
+  }
+
+  @Override
+  public boolean decodeBooleanBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return ((int) buf.readDouble()) != 0;
+  }
+
+  @Override
+  public byte decodeByteText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result;
+    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, dataType));
+    }
+    if ((byte) result != result || (result < 0 && !isSigned())) {
+      throw new SQLDataException("byte overflow");
+    }
+    return (byte) result;
+  }
+
+  @Override
+  public byte decodeByteBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = (long) buf.readDouble();
+    if ((byte) result != result) {
+      throw new SQLDataException("byte overflow");
+    }
+
+    return (byte) result;
+  }
+
+  @Override
+  public String decodeStringText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public String decodeStringBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return String.valueOf(buf.readDouble());
+  }
+
+  @Override
+  public short decodeShortText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result;
+    String str = buf.readString(length);
+    try {
+      result = new BigDecimal(str).setScale(0, RoundingMode.DOWN).longValueExact();
+    } catch (NumberFormatException | ArithmeticException nfe) {
+      throw new SQLDataException(String.format("value '%s' cannot be decoded as Short", str));
+    }
+    if ((short) result != result || (result < 0 && !isSigned())) {
+      throw new SQLDataException("Short overflow");
+    }
+
+    return (short) result;
+  }
+
+  @Override
+  public short decodeShortBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = (long) buf.readDouble();
+    if ((short) result != result || (result < 0 && !isSigned())) {
+      throw new SQLDataException("Short overflow");
+    }
+    return (short) result;
+  }
+
+  @Override
+  public int decodeIntText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result;
+    String str = buf.readString(length);
+    try {
+      result = new BigDecimal(str).setScale(0, RoundingMode.DOWN).longValueExact();
+    } catch (NumberFormatException | ArithmeticException nfe) {
+      throw new SQLDataException(String.format("value '%s' cannot be decoded as Integer", str));
+    }
+
+    int res = (int) result;
+    if (res != result || (result < 0 && !isSigned())) {
+      throw new SQLDataException("integer overflow");
+    }
+    return res;
+  }
+
+  @Override
+  public int decodeIntBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = (long) buf.readDouble();
+    int res = (int) result;
+    if (res != result) {
+      throw new SQLDataException("integer overflow");
+    }
+    return res;
+  }
+
+  @Override
+  public long decodeLongText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String str2 = buf.readAscii(length);
+    try {
+      return new BigDecimal(str2).setScale(0, RoundingMode.DOWN).longValueExact();
+    } catch (NumberFormatException | ArithmeticException nfe) {
+      throw new SQLDataException(String.format("value '%s' cannot be decoded as Long", str2));
+    }
+  }
+
+  @Override
+  public long decodeLongBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return (long) buf.readDouble();
+  }
+
+  @Override
+  public float decodeFloatText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Float.parseFloat(buf.readAscii(length));
+  }
+
+  @Override
+  public float decodeFloatBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return (float) buf.readDouble();
+  }
+
+  @Override
+  public double decodeDoubleText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Double.parseDouble(buf.readAscii(length));
+  }
+
+  @Override
+  public double decodeDoubleBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readDouble();
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Time decodeTimeText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Time decodeTimeBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/FloatColumn.java b/src/main/java/org/mariadb/jdbc/client/column/FloatColumn.java
new file mode 100644
index 0000000..365ecaf
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/FloatColumn.java
@@ -0,0 +1,249 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.sql.*;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.server.ColumnDefinitionPacket;
+
+/** Column metadata definition */
+public class FloatColumn extends ColumnDefinitionPacket implements ColumnDecoder {
+
+  /**
+   * Float metadata type decoder
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param length maximum data length
+   * @param dataType data type. see https://mariadb.com/kb/en/result-set-packets/#field-types
+   * @param decimals decimal length
+   * @param flags flags. see https://mariadb.com/kb/en/result-set-packets/#field-details-flag
+   * @param stringPos string offset position in buffer
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public FloatColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    return Float.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    return Types.REAL;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    return "FLOAT";
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return Float.parseFloat(buf.readAscii(length));
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return buf.readFloat();
+  }
+
+  @Override
+  public boolean decodeBooleanText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String s = buf.readAscii(length);
+    return !"0".equals(s);
+  }
+
+  @Override
+  public boolean decodeBooleanBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return ((int) buf.readFloat()) != 0;
+  }
+
+  @Override
+  public byte decodeByteText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result;
+    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, dataType));
+    }
+    if ((byte) result != result || (result < 0 && !isSigned())) {
+      throw new SQLDataException("byte overflow");
+    }
+    return (byte) result;
+  }
+
+  @Override
+  public byte decodeByteBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = (long) buf.readFloat();
+    if ((byte) result != result) {
+      throw new SQLDataException("byte overflow");
+    }
+
+    return (byte) result;
+  }
+
+  @Override
+  public String decodeStringText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public String decodeStringBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return String.valueOf(buf.readFloat());
+  }
+
+  @Override
+  public short decodeShortText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result;
+    String str = buf.readString(length);
+    try {
+      result = new BigDecimal(str).setScale(0, RoundingMode.DOWN).longValueExact();
+    } catch (NumberFormatException | ArithmeticException nfe) {
+      throw new SQLDataException(String.format("value '%s' cannot be decoded as Short", str));
+    }
+    if ((short) result != result || (result < 0 && !isSigned())) {
+      throw new SQLDataException("Short overflow");
+    }
+
+    return (short) result;
+  }
+
+  @Override
+  public short decodeShortBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = (long) buf.readFloat();
+    if ((short) result != result || (result < 0 && !isSigned())) {
+      throw new SQLDataException("Short overflow");
+    }
+    return (short) result;
+  }
+
+  @Override
+  public int decodeIntText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result;
+    String str = buf.readString(length);
+    try {
+      result = new BigDecimal(str).setScale(0, RoundingMode.DOWN).longValueExact();
+    } catch (NumberFormatException | ArithmeticException nfe) {
+      throw new SQLDataException(String.format("value '%s' cannot be decoded as Integer", str));
+    }
+
+    int res = (int) result;
+    if (res != result || (result < 0 && !isSigned())) {
+      throw new SQLDataException("integer overflow");
+    }
+    return res;
+  }
+
+  @Override
+  public int decodeIntBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = (long) buf.readFloat();
+    int res = (int) result;
+    if (res != result) {
+      throw new SQLDataException("integer overflow");
+    }
+    return res;
+  }
+
+  @Override
+  public long decodeLongText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String str2 = buf.readAscii(length);
+    try {
+      return new BigDecimal(str2).setScale(0, RoundingMode.DOWN).longValueExact();
+    } catch (NumberFormatException | ArithmeticException nfe) {
+      throw new SQLDataException(String.format("value '%s' cannot be decoded as Long", str2));
+    }
+  }
+
+  @Override
+  public long decodeLongBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return (long) buf.readFloat();
+  }
+
+  @Override
+  public float decodeFloatText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Float.parseFloat(buf.readAscii(length));
+  }
+
+  @Override
+  public float decodeFloatBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readFloat();
+  }
+
+  @Override
+  public double decodeDoubleText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Double.parseDouble(buf.readAscii(length));
+  }
+
+  @Override
+  public double decodeDoubleBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readFloat();
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Time decodeTimeText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Time decodeTimeBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/GeometryColumn.java b/src/main/java/org/mariadb/jdbc/client/column/GeometryColumn.java
new file mode 100644
index 0000000..37bd06f
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/GeometryColumn.java
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.sql.*;
+import java.util.Calendar;
+import java.util.Locale;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.plugin.codec.*;
+import org.mariadb.jdbc.type.*;
+
+/** Column metadata definition */
+public class GeometryColumn extends BlobColumn {
+
+  /**
+   * Geometry metadata type decoder
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param length maximum data length
+   * @param dataType data type. see https://mariadb.com/kb/en/result-set-packets/#field-types
+   * @param decimals decimal length
+   * @param flags flags. see https://mariadb.com/kb/en/result-set-packets/#field-details-flag
+   * @param stringPos string offset position in buffer
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public GeometryColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    if (conf.geometryDefaultType() != null && "default".equals(conf.geometryDefaultType())) {
+      if (extTypeName != null) {
+        switch (extTypeName) {
+          case "point":
+            return Point.class.getName();
+          case "linestring":
+            return LineString.class.getName();
+          case "polygon":
+            return Polygon.class.getName();
+          case "multipoint":
+            return MultiPoint.class.getName();
+          case "multilinestring":
+            return MultiLineString.class.getName();
+          case "multipolygon":
+            return MultiPolygon.class.getName();
+          case "geometrycollection":
+            return GeometryCollection.class.getName();
+        }
+      }
+      return GeometryCollection.class.getName();
+    }
+    return "byte[]";
+  }
+
+  public int getColumnType(Configuration conf) {
+    return Types.VARBINARY;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    if (extTypeName != null) {
+      return extTypeName.toUpperCase(Locale.ROOT);
+    }
+    return "GEOMETRY";
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    if (conf.geometryDefaultType() != null && "default".equals(conf.geometryDefaultType())) {
+      buf.skip(4); // SRID
+      return Geometry.getGeometry(buf, length - 4, this);
+    }
+    byte[] arr = new byte[length];
+    buf.readBytes(arr);
+    return arr;
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return getDefaultText(conf, buf, length);
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/JsonColumn.java b/src/main/java/org/mariadb/jdbc/client/column/JsonColumn.java
new file mode 100644
index 0000000..a3fb29c
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/JsonColumn.java
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.sql.*;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+
+/** Column metadata definition */
+public class JsonColumn extends StringColumn implements ColumnDecoder {
+
+  /**
+   * JSON metadata type decoder
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param length maximum data length
+   * @param dataType data type. see https://mariadb.com/kb/en/result-set-packets/#field-types
+   * @param decimals decimal length
+   * @param flags flags. see https://mariadb.com/kb/en/result-set-packets/#field-details-flag
+   * @param stringPos string offset position in buffer
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public JsonColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    return String.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    return Types.LONGVARCHAR;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    return "JSON";
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/SignedBigIntColumn.java b/src/main/java/org/mariadb/jdbc/client/column/SignedBigIntColumn.java
new file mode 100644
index 0000000..5afd95b
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/SignedBigIntColumn.java
@@ -0,0 +1,222 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.math.BigInteger;
+import java.sql.*;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.server.ColumnDefinitionPacket;
+
+/** Column metadata definition */
+public class SignedBigIntColumn extends ColumnDefinitionPacket implements ColumnDecoder {
+
+  /**
+   * BIGINT metadata type decoder
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param length maximum data length
+   * @param dataType data type. see https://mariadb.com/kb/en/result-set-packets/#field-types
+   * @param decimals decimal length
+   * @param flags flags. see https://mariadb.com/kb/en/result-set-packets/#field-details-flag
+   * @param stringPos string offset position in buffer
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public SignedBigIntColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    return Long.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    return Types.BIGINT;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    return "BIGINT";
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return buf.atoll(length);
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return buf.readLong();
+  }
+
+  @Override
+  public boolean decodeBooleanText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String s = buf.readAscii(length);
+    return !"0".equals(s);
+  }
+
+  @Override
+  public boolean decodeBooleanBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readLong() != 0;
+  }
+
+  @Override
+  public byte decodeByteText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.atoll(length);
+    if ((byte) result != result) {
+      throw new SQLDataException("byte overflow");
+    }
+    return (byte) result;
+  }
+
+  @Override
+  public byte decodeByteBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.readLong();
+    if ((byte) result != result) {
+      throw new SQLDataException("byte overflow");
+    }
+
+    return (byte) result;
+  }
+
+  @Override
+  public String decodeStringText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public String decodeStringBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return BigInteger.valueOf(buf.readLong()).toString();
+  }
+
+  @Override
+  public short decodeShortText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.atoll(length);
+    if ((short) result != result) {
+      throw new SQLDataException("Short overflow");
+    }
+    return (short) result;
+  }
+
+  @Override
+  public short decodeShortBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.readLong();
+    if ((short) result != result) {
+      throw new SQLDataException("Short overflow");
+    }
+    return (short) result;
+  }
+
+  @Override
+  public int decodeIntText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.atoll(length);
+    int res = (int) result;
+    if (res != result) {
+      throw new SQLDataException("integer overflow");
+    }
+    return res;
+  }
+
+  @Override
+  public int decodeIntBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.readLong();
+    int res = (int) result;
+    if (res != result) {
+      throw new SQLDataException("integer overflow");
+    }
+    return res;
+  }
+
+  @Override
+  public long decodeLongText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.atoll(length);
+  }
+
+  @Override
+  public long decodeLongBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readLong();
+  }
+
+  @Override
+  public float decodeFloatText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Float.parseFloat(buf.readAscii(length));
+  }
+
+  @Override
+  public float decodeFloatBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return (float) buf.readLong();
+  }
+
+  @Override
+  public double decodeDoubleText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Double.parseDouble(buf.readAscii(length));
+  }
+
+  @Override
+  public double decodeDoubleBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readLong();
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Time decodeTimeText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Time decodeTimeBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/SignedIntColumn.java b/src/main/java/org/mariadb/jdbc/client/column/SignedIntColumn.java
new file mode 100644
index 0000000..5cebf4b
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/SignedIntColumn.java
@@ -0,0 +1,211 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.sql.*;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.server.ColumnDefinitionPacket;
+
+/** Column metadata definition */
+public class SignedIntColumn extends ColumnDefinitionPacket implements ColumnDecoder {
+
+  /**
+   * INTEGER metadata type decoder
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param length maximum data length
+   * @param dataType data type. see https://mariadb.com/kb/en/result-set-packets/#field-types
+   * @param decimals decimal length
+   * @param flags flags. see https://mariadb.com/kb/en/result-set-packets/#field-details-flag
+   * @param stringPos string offset position in buffer
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public SignedIntColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    return Integer.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    return Types.INTEGER;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    return "INTEGER";
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return (int) buf.atoll(length);
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return buf.readInt();
+  }
+
+  @Override
+  public boolean decodeBooleanText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String s = buf.readAscii(length);
+    return !"0".equals(s);
+  }
+
+  @Override
+  public boolean decodeBooleanBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readInt() != 0;
+  }
+
+  @Override
+  public byte decodeByteText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.atoll(length);
+    if ((byte) result != result || (result < 0 && !isSigned())) {
+      throw new SQLDataException("byte overflow");
+    }
+    return (byte) result;
+  }
+
+  @Override
+  public byte decodeByteBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.readInt();
+    if ((byte) result != result) {
+      throw new SQLDataException("byte overflow");
+    }
+
+    return (byte) result;
+  }
+
+  @Override
+  public String decodeStringText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public String decodeStringBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return String.valueOf(buf.readInt());
+  }
+
+  @Override
+  public short decodeShortText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.atoll(length);
+    if ((short) result != result) {
+      throw new SQLDataException("Short overflow");
+    }
+    return (short) result;
+  }
+
+  @Override
+  public short decodeShortBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = isSigned() ? buf.readInt() : buf.readUnsignedInt();
+    if ((short) result != result) {
+      throw new SQLDataException("Short overflow");
+    }
+    return (short) result;
+  }
+
+  @Override
+  public int decodeIntText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return (int) buf.atoll(length);
+  }
+
+  @Override
+  public int decodeIntBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readInt();
+  }
+
+  @Override
+  public long decodeLongText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.atoll(length);
+  }
+
+  @Override
+  public long decodeLongBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readInt();
+  }
+
+  @Override
+  public float decodeFloatText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Float.parseFloat(buf.readAscii(length));
+  }
+
+  @Override
+  public float decodeFloatBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return (float) buf.readInt();
+  }
+
+  @Override
+  public double decodeDoubleText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Double.parseDouble(buf.readAscii(length));
+  }
+
+  @Override
+  public double decodeDoubleBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readInt();
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Time decodeTimeText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Time decodeTimeBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/SignedMediumIntColumn.java b/src/main/java/org/mariadb/jdbc/client/column/SignedMediumIntColumn.java
new file mode 100644
index 0000000..2f03e1f
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/SignedMediumIntColumn.java
@@ -0,0 +1,223 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.sql.*;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.server.ColumnDefinitionPacket;
+
+/** Column metadata definition */
+public class SignedMediumIntColumn extends ColumnDefinitionPacket implements ColumnDecoder {
+
+  /**
+   * MEDIUM INTEGER metadata type decoder
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param length maximum data length
+   * @param dataType data type. see https://mariadb.com/kb/en/result-set-packets/#field-types
+   * @param decimals decimal length
+   * @param flags flags. see https://mariadb.com/kb/en/result-set-packets/#field-details-flag
+   * @param stringPos string offset position in buffer
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public SignedMediumIntColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    return Integer.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    return Types.INTEGER;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    return "MEDIUMINT";
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return decodeIntText(buf, length);
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return decodeIntBinary(buf, length);
+  }
+
+  @Override
+  public boolean decodeBooleanText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String s = buf.readAscii(length);
+    return !"0".equals(s);
+  }
+
+  @Override
+  public boolean decodeBooleanBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readInt() != 0;
+  }
+
+  @Override
+  public byte decodeByteText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.atoll(length);
+    if ((byte) result != result) {
+      throw new SQLDataException("byte overflow");
+    }
+    return (byte) result;
+  }
+
+  @Override
+  public byte decodeByteBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.readMedium();
+    buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges !
+    if ((byte) result != result) {
+      throw new SQLDataException("byte overflow");
+    }
+
+    return (byte) result;
+  }
+
+  @Override
+  public String decodeStringText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public String decodeStringBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    String mediumStr = String.valueOf(isSigned() ? buf.readMedium() : buf.readUnsignedMedium());
+    buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges !
+    return mediumStr;
+  }
+
+  @Override
+  public short decodeShortText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.atoll(length);
+    if ((short) result != result) {
+      throw new SQLDataException("Short overflow");
+    }
+    return (short) result;
+  }
+
+  @Override
+  public short decodeShortBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.readMedium();
+    buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges !
+    if ((short) result != result) {
+      throw new SQLDataException("Short overflow");
+    }
+    return (short) result;
+  }
+
+  @Override
+  public int decodeIntText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return (int) buf.atoll(length);
+  }
+
+  @Override
+  public int decodeIntBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    int res = buf.readMedium();
+    buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges !
+    return res;
+  }
+
+  @Override
+  public long decodeLongText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.atoll(length);
+  }
+
+  @Override
+  public long decodeLongBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long l = buf.readMedium();
+    buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges !
+    return l;
+  }
+
+  @Override
+  public float decodeFloatText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Float.parseFloat(buf.readAscii(length));
+  }
+
+  @Override
+  public float decodeFloatBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    float f = buf.readMedium();
+    buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges !
+    return f;
+  }
+
+  @Override
+  public double decodeDoubleText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Double.parseDouble(buf.readAscii(length));
+  }
+
+  @Override
+  public double decodeDoubleBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    double f = buf.readMedium();
+    buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges !
+    return f;
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Time decodeTimeText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Time decodeTimeBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/SignedSmallIntColumn.java b/src/main/java/org/mariadb/jdbc/client/column/SignedSmallIntColumn.java
new file mode 100644
index 0000000..232591d
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/SignedSmallIntColumn.java
@@ -0,0 +1,202 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.sql.*;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.server.ColumnDefinitionPacket;
+
+/** Column metadata definition */
+public class SignedSmallIntColumn extends ColumnDefinitionPacket implements ColumnDecoder {
+
+  /**
+   * SMALLINT metadata type decoder
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param length maximum data length
+   * @param dataType data type. see https://mariadb.com/kb/en/result-set-packets/#field-types
+   * @param decimals decimal length
+   * @param flags flags. see https://mariadb.com/kb/en/result-set-packets/#field-details-flag
+   * @param stringPos string offset position in buffer
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public SignedSmallIntColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    return Short.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    return Types.SMALLINT;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    return isSigned() ? "SMALLINT" : "SMALLINT UNSIGNED";
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return (short) buf.atoll(length);
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return buf.readShort();
+  }
+
+  @Override
+  public boolean decodeBooleanText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String s = buf.readAscii(length);
+    return !"0".equals(s);
+  }
+
+  @Override
+  public boolean decodeBooleanBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readShort() != 0;
+  }
+
+  @Override
+  public byte decodeByteText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.atoll(length);
+    if ((byte) result != result) {
+      throw new SQLDataException("byte overflow");
+    }
+    return (byte) result;
+  }
+
+  @Override
+  public byte decodeByteBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.readShort();
+    if ((byte) result != result) {
+      throw new SQLDataException("byte overflow");
+    }
+    return (byte) result;
+  }
+
+  @Override
+  public String decodeStringText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public String decodeStringBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return String.valueOf(buf.readShort());
+  }
+
+  @Override
+  public short decodeShortText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return (short) buf.atoll(length);
+  }
+
+  @Override
+  public short decodeShortBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readShort();
+  }
+
+  @Override
+  public int decodeIntText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return (int) buf.atoll(length);
+  }
+
+  @Override
+  public int decodeIntBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readShort();
+  }
+
+  @Override
+  public long decodeLongText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.atoll(length);
+  }
+
+  @Override
+  public long decodeLongBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readShort();
+  }
+
+  @Override
+  public float decodeFloatText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Float.parseFloat(buf.readAscii(length));
+  }
+
+  @Override
+  public float decodeFloatBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readShort();
+  }
+
+  @Override
+  public double decodeDoubleText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Double.parseDouble(buf.readAscii(length));
+  }
+
+  @Override
+  public double decodeDoubleBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readShort();
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Time decodeTimeText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Time decodeTimeBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/SignedTinyIntColumn.java b/src/main/java/org/mariadb/jdbc/client/column/SignedTinyIntColumn.java
new file mode 100644
index 0000000..2f79f8c
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/SignedTinyIntColumn.java
@@ -0,0 +1,232 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.sql.*;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.server.ColumnDefinitionPacket;
+
+/** Column metadata definition */
+public class SignedTinyIntColumn extends ColumnDefinitionPacket implements ColumnDecoder {
+
+  /**
+   * TINYINT metadata type decoder
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param length maximum data length
+   * @param dataType data type. see https://mariadb.com/kb/en/result-set-packets/#field-types
+   * @param decimals decimal length
+   * @param flags flags. see https://mariadb.com/kb/en/result-set-packets/#field-details-flag
+   * @param stringPos string offset position in buffer
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public SignedTinyIntColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    if (conf.tinyInt1isBit() && columnLength == 1) return Boolean.class.getName();
+    return Integer.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    if (conf.tinyInt1isBit() && columnLength == 1) {
+      return conf.transformedBitIsBoolean() ? Types.BOOLEAN : Types.BIT;
+    }
+    return isSigned() ? Types.TINYINT : Types.SMALLINT;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    if (conf.tinyInt1isBit() && columnLength == 1) {
+      return conf.transformedBitIsBoolean() ? "BOOLEAN" : "BIT";
+    }
+    return isSigned() ? "TINYINT" : "TINYINT UNSIGNED";
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    if (conf.tinyInt1isBit() && columnLength == 1) {
+      return decodeBooleanText(buf, length);
+    }
+    return (int) buf.atoll(length);
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    if (conf.tinyInt1isBit() && columnLength == 1) {
+      return decodeBooleanBinary(buf, length);
+    }
+    if (isSigned()) {
+      return (int) buf.readByte();
+    }
+    return (int) buf.readUnsignedByte();
+  }
+
+  @Override
+  public boolean decodeBooleanText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String s = buf.readAscii(length);
+    return !"0".equals(s);
+  }
+
+  @Override
+  public boolean decodeBooleanBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readByte() != 0;
+  }
+
+  @Override
+  public byte decodeByteText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.atoll(length);
+    if ((byte) result != result) {
+      throw new SQLDataException("byte overflow");
+    }
+    return (byte) result;
+  }
+
+  @Override
+  public byte decodeByteBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    if (isSigned()) return buf.readByte();
+    long result = buf.readUnsignedByte();
+
+    if ((byte) result != result) {
+      throw new SQLDataException("byte overflow");
+    }
+    return (byte) result;
+  }
+
+  @Override
+  public String decodeStringText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public String decodeStringBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    if (!isSigned()) {
+      return String.valueOf(buf.readUnsignedByte());
+    }
+    return String.valueOf(buf.readByte());
+  }
+
+  @Override
+  public short decodeShortText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return (short) buf.atoll(length);
+  }
+
+  @Override
+  public short decodeShortBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return (isSigned() ? buf.readByte() : buf.readUnsignedByte());
+  }
+
+  @Override
+  public int decodeIntText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return (int) buf.atoll(length);
+  }
+
+  @Override
+  public int decodeIntBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return (isSigned() ? buf.readByte() : buf.readUnsignedByte());
+  }
+
+  @Override
+  public long decodeLongText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.atoll(length);
+  }
+
+  @Override
+  public long decodeLongBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    if (!isSigned()) {
+      return buf.readUnsignedByte();
+    }
+    return buf.readByte();
+  }
+
+  @Override
+  public float decodeFloatText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Float.parseFloat(buf.readAscii(length));
+  }
+
+  @Override
+  public float decodeFloatBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    if (!isSigned()) {
+      return buf.readUnsignedByte();
+    }
+    return buf.readByte();
+  }
+
+  @Override
+  public double decodeDoubleText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Double.parseDouble(buf.readAscii(length));
+  }
+
+  @Override
+  public double decodeDoubleBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    if (!isSigned()) {
+      return buf.readUnsignedByte();
+    }
+    return buf.readByte();
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Time decodeTimeText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Time decodeTimeBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/StringColumn.java b/src/main/java/org/mariadb/jdbc/client/column/StringColumn.java
new file mode 100644
index 0000000..7338478
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/StringColumn.java
@@ -0,0 +1,425 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.RoundingMode;
+import java.sql.*;
+import java.time.DateTimeException;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.server.ColumnDefinitionPacket;
+import org.mariadb.jdbc.plugin.codec.LocalDateTimeCodec;
+import org.mariadb.jdbc.plugin.codec.LocalTimeCodec;
+import org.mariadb.jdbc.util.CharsetEncodingLength;
+
+/** Column metadata definition */
+public class StringColumn extends ColumnDefinitionPacket implements ColumnDecoder {
+
+  /**
+   * VARCHAR/STRING/VARSTRING metadata type decoder
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param length maximum data length
+   * @param dataType data type. see https://mariadb.com/kb/en/result-set-packets/#field-types
+   * @param decimals decimal length
+   * @param flags flags. see https://mariadb.com/kb/en/result-set-packets/#field-details-flag
+   * @param stringPos string offset position in buffer
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public StringColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    return isBinary() ? "byte[]" : String.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    if (dataType == DataType.NULL) {
+      return Types.NULL;
+    }
+    if (dataType == DataType.STRING) {
+      return isBinary() ? Types.VARBINARY : Types.CHAR;
+    }
+    if (columnLength <= 0 || getDisplaySize() > 16777215) {
+      return isBinary() ? Types.LONGVARBINARY : Types.LONGVARCHAR;
+    }
+    return isBinary() ? Types.VARBINARY : Types.VARCHAR;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    switch (dataType) {
+      case STRING:
+        if (isBinary()) {
+          return "BINARY";
+        }
+        return "CHAR";
+      case VARSTRING:
+      case VARCHAR:
+        if (isBinary()) {
+          return "VARBINARY";
+        }
+        if (columnLength < 0) {
+          return "LONGTEXT";
+        } else if (getDisplaySize() <= 65532) {
+          return "VARCHAR";
+        } else if (getDisplaySize() <= 65535) {
+          return "TEXT";
+        } else if (getDisplaySize() <= 16777215) {
+          return "MEDIUMTEXT";
+        } else {
+          return "LONGTEXT";
+        }
+      default:
+        return dataType.name();
+    }
+  }
+
+  public int getPrecision() {
+    Integer maxWidth = CharsetEncodingLength.maxCharlen.get(charset);
+    if (maxWidth == null) {
+      return (int) columnLength;
+    }
+    return (int) (columnLength / maxWidth);
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    if (isBinary()) {
+      byte[] arr = new byte[length];
+      buf.readBytes(arr);
+      return arr;
+    }
+    return buf.readString(length);
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    if (isBinary()) {
+      byte[] arr = new byte[length];
+      buf.readBytes(arr);
+      return arr;
+    }
+    return buf.readString(length);
+  }
+
+  @Override
+  public boolean decodeBooleanText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return !"0".equals(buf.readAscii(length));
+  }
+
+  @Override
+  public boolean decodeBooleanBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return !"0".equals(buf.readAscii(length));
+  }
+
+  @Override
+  public byte decodeByteText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String str = buf.readString(length);
+    long result;
+    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, dataType));
+    }
+    if ((byte) result != result || (result < 0 && !isSigned())) {
+      throw new SQLDataException("byte overflow");
+    }
+    return (byte) result;
+  }
+
+  @Override
+  public byte decodeByteBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return decodeByteText(buf, length);
+  }
+
+  @Override
+  public String decodeStringText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public String decodeStringBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public short decodeShortText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String str = buf.readString(length);
+    try {
+      return new BigDecimal(str).setScale(0, RoundingMode.DOWN).shortValueExact();
+    } catch (NumberFormatException | ArithmeticException nfe) {
+      throw new SQLDataException(String.format("value '%s' cannot be decoded as Short", str));
+    }
+  }
+
+  @Override
+  public short decodeShortBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return decodeShortText(buf, length);
+  }
+
+  @Override
+  public int decodeIntText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String str = buf.readString(length);
+    try {
+      return new BigDecimal(str).setScale(0, RoundingMode.DOWN).intValueExact();
+    } catch (NumberFormatException | ArithmeticException nfe) {
+      throw new SQLDataException(String.format("value '%s' cannot be decoded as Integer", str));
+    }
+  }
+
+  @Override
+  public int decodeIntBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return decodeIntText(buf, length);
+  }
+
+  @Override
+  public long decodeLongText(ReadableByteBuf buf, int length) throws SQLDataException {
+    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));
+    }
+  }
+
+  @Override
+  public long decodeLongBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return decodeLongText(buf, length);
+  }
+
+  @Override
+  public float decodeFloatText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String val = buf.readString(length);
+    try {
+      return Float.parseFloat(val);
+    } catch (NumberFormatException nfe) {
+      throw new SQLDataException(String.format("value '%s' cannot be decoded as Float", val));
+    }
+  }
+
+  @Override
+  public float decodeFloatBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return decodeFloatText(buf, length);
+  }
+
+  @Override
+  public double decodeDoubleText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String str2 = buf.readString(length);
+    try {
+      return Double.parseDouble(str2);
+    } catch (NumberFormatException nfe) {
+      throw new SQLDataException(String.format("value '%s' cannot be decoded as Double", str2));
+    }
+  }
+
+  @Override
+  public double decodeDoubleBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return decodeDoubleText(buf, length);
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    String val = buf.readString(length);
+    if ("0000-00-00".equals(val)) return null;
+    String[] stDatePart = val.split("[- ]");
+    if (stDatePart.length < 3) {
+      throw new SQLDataException(
+          String.format("value '%s' (%s) cannot be decoded as Date", val, dataType));
+    }
+
+    try {
+      int year = Integer.parseInt(stDatePart[0]);
+      int month = Integer.parseInt(stDatePart[1]);
+      int dayOfMonth = Integer.parseInt(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, dataType));
+    }
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return decodeDateText(buf, length, cal);
+  }
+
+  @Override
+  public Time decodeTimeText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    Calendar c = cal == null ? Calendar.getInstance() : cal;
+    int offset = c.getTimeZone().getOffset(0);
+    int[] parts = LocalTimeCodec.parseTime(buf, length, this);
+    long timeInMillis =
+        (parts[1] * 3_600_000L + parts[2] * 60_000L + parts[3] * 1_000L + parts[4] / 1_000_000)
+                * parts[0]
+            - offset;
+    return new Time(timeInMillis);
+  }
+
+  @Override
+  public Time decodeTimeBinary(ReadableByteBuf buf, int length, Calendar calParam)
+      throws SQLDataException {
+    Calendar cal = calParam == null ? Calendar.getInstance() : calParam;
+    int[] parts = LocalTimeCodec.parseTime(buf, length, this);
+    Time t;
+
+    // specific case for TIME, to handle value not in 00:00:00-23:59:59
+    synchronized (cal) {
+      cal.clear();
+      cal.setLenient(true);
+      if (parts[0] == -1) {
+        cal.set(
+            1970,
+            Calendar.JANUARY,
+            1,
+            parts[0] * parts[1],
+            parts[0] * parts[2],
+            parts[0] * parts[3] - 1);
+        t = new Time(cal.getTimeInMillis() + (1000 - parts[4]));
+      } else {
+        cal.set(1970, Calendar.JANUARY, 1, parts[1], parts[2], parts[3]);
+        t = new Time(cal.getTimeInMillis() + parts[4] / 1_000_000);
+      }
+    }
+    return t;
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar calParam)
+      throws SQLDataException {
+    int pos = buf.pos();
+    int nanoBegin = -1;
+    int[] timestampsPart = new int[] {0, 0, 0, 0, 0, 0, 0};
+    int partIdx = 0;
+    for (int begin = 0; begin < length; begin++) {
+      byte b = buf.readByte();
+      if (b == '-' || b == ' ' || b == ':') {
+        partIdx++;
+        continue;
+      }
+      if (b == '.') {
+        partIdx++;
+        nanoBegin = begin;
+        continue;
+      }
+      if (b < '0' || b > '9') {
+        buf.pos(pos);
+        throw new SQLDataException(
+            String.format(
+                "value '%s' (%s) cannot be decoded as Timestamp",
+                buf.readString(length), dataType));
+      }
+
+      timestampsPart[partIdx] = timestampsPart[partIdx] * 10 + b - 48;
+    }
+    if (timestampsPart[0] == 0
+        && timestampsPart[1] == 0
+        && timestampsPart[2] == 0
+        && timestampsPart[3] == 0
+        && timestampsPart[4] == 0
+        && timestampsPart[5] == 0
+        && timestampsPart[6] == 0) {
+      return null;
+    }
+
+    // fix non-leading tray for nanoseconds
+    if (nanoBegin > 0) {
+      for (int begin = 0; begin < 6 - (length - nanoBegin - 1); begin++) {
+        timestampsPart[6] = timestampsPart[6] * 10;
+      }
+    }
+
+    Timestamp timestamp;
+    if (calParam == null) {
+      Calendar c = Calendar.getInstance();
+      c.set(
+          timestampsPart[0],
+          timestampsPart[1] - 1,
+          timestampsPart[2],
+          timestampsPart[3],
+          timestampsPart[4],
+          timestampsPart[5]);
+      timestamp = new Timestamp(c.getTime().getTime());
+      timestamp.setNanos(timestampsPart[6] * 1000);
+    } else {
+      synchronized (calParam) {
+        calParam.clear();
+        calParam.set(
+            timestampsPart[0],
+            timestampsPart[1] - 1,
+            timestampsPart[2],
+            timestampsPart[3],
+            timestampsPart[4],
+            timestampsPart[5]);
+        timestamp = new Timestamp(calParam.getTime().getTime());
+        timestamp.setNanos(timestampsPart[6] * 1000);
+      }
+    }
+    return timestamp;
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar calParam)
+      throws SQLDataException {
+    Calendar cal = calParam == null ? Calendar.getInstance() : calParam;
+
+    String val = buf.readString(length);
+    try {
+      int[] parts = LocalDateTimeCodec.parseTimestamp(val);
+      if (parts == null) return null;
+      int year = parts[0];
+      int month = parts[1];
+      int dayOfMonth = parts[2];
+      int hour = parts[3];
+      int minutes = parts[4];
+      int seconds = parts[5];
+      int microseconds = parts[6] / 1000;
+      Timestamp timestamp;
+      synchronized (cal) {
+        cal.clear();
+        cal.set(year, month - 1, dayOfMonth, hour, minutes, seconds);
+        timestamp = new Timestamp(cal.getTimeInMillis());
+      }
+      timestamp.setNanos(microseconds * 1000);
+      return timestamp;
+
+    } catch (DateTimeException dte) {
+      throw new SQLDataException(
+          String.format("value '%s' (%s) cannot be decoded as Timestamp", val, dataType));
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/TimeColumn.java b/src/main/java/org/mariadb/jdbc/client/column/TimeColumn.java
new file mode 100644
index 0000000..5ac66be
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/TimeColumn.java
@@ -0,0 +1,366 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.sql.*;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.server.ColumnDefinitionPacket;
+import org.mariadb.jdbc.plugin.codec.LocalTimeCodec;
+
+/** Column metadata definition */
+public class TimeColumn extends ColumnDefinitionPacket implements ColumnDecoder {
+
+  /**
+   * TIME metadata type decoder
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param length maximum data length
+   * @param dataType data type. see https://mariadb.com/kb/en/result-set-packets/#field-types
+   * @param decimals decimal length
+   * @param flags flags. see https://mariadb.com/kb/en/result-set-packets/#field-details-flag
+   * @param stringPos string offset position in buffer
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public TimeColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    return Time.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    return Types.TIME;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    return "TIME";
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    Calendar c = Calendar.getInstance();
+    int offset = c.getTimeZone().getOffset(0);
+    int[] parts = LocalTimeCodec.parseTime(buf, length, this);
+    long timeInMillis =
+        (parts[1] * 3_600_000L + parts[2] * 60_000L + parts[3] * 1_000L + parts[4] / 1_000_000)
+                * parts[0]
+            - offset;
+    return new Time(timeInMillis);
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    boolean negate = false;
+    Calendar cal = Calendar.getInstance();
+    long dayOfMonth = 0;
+    int hour = 0;
+    int minutes = 0;
+    int seconds = 0;
+    long microseconds = 0;
+    if (length > 0) {
+      // specific case for TIME, to handle value not in 00:00:00-23:59:59
+      negate = buf.readByte() == 1;
+      dayOfMonth = buf.readUnsignedInt();
+      hour = buf.readByte();
+      minutes = buf.readByte();
+      seconds = buf.readByte();
+      if (length > 8) {
+        microseconds = buf.readUnsignedInt();
+      }
+    }
+    int offset = cal.getTimeZone().getOffset(0);
+    long timeInMillis =
+        ((24 * dayOfMonth + hour) * 3_600_000
+                    + minutes * 60_000
+                    + seconds * 1_000
+                    + microseconds / 1_000)
+                * (negate ? -1 : 1)
+            - offset;
+    return new Time(timeInMillis);
+  }
+
+  @Override
+  public byte decodeByteText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Byte", dataType));
+  }
+
+  @Override
+  public byte decodeByteBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Byte", dataType));
+  }
+
+  @Override
+  public boolean decodeBooleanText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Boolean", dataType));
+  }
+
+  @Override
+  public boolean decodeBooleanBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Boolean", dataType));
+  }
+
+  @Override
+  public String decodeStringText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public String decodeStringBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    long tDays = 0;
+    int tHours = 0;
+    int tMinutes = 0;
+    int tSeconds = 0;
+    long tMicroseconds = 0;
+    if (length == 0) {
+      StringBuilder zeroValue = new StringBuilder("00:00:00");
+      if (getDecimals() > 0) {
+        zeroValue.append(".");
+        for (int i = 0; i < getDecimals(); i++) zeroValue.append("0");
+      }
+      return zeroValue.toString();
+    }
+    boolean 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 (getDecimals() == 0) {
+      if (tMicroseconds == 0) return stTime;
+      // possible for Xpand that doesn't send some metadata
+      // https://jira.mariadb.org/browse/XPT-273
+      StringBuilder stMicro = new StringBuilder(String.valueOf(tMicroseconds));
+      while (stMicro.length() < 6) {
+        stMicro.insert(0, "0");
+      }
+      return stTime + "." + stMicro;
+    }
+    StringBuilder stMicro = new StringBuilder(String.valueOf(tMicroseconds));
+    while (stMicro.length() < getDecimals()) {
+      stMicro.insert(0, "0");
+    }
+    return stTime + "." + stMicro;
+  }
+
+  @Override
+  public short decodeShortText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Short", dataType));
+  }
+
+  @Override
+  public short decodeShortBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Short", dataType));
+  }
+
+  @Override
+  public int decodeIntText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Integer", dataType));
+  }
+
+  @Override
+  public int decodeIntBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Integer", dataType));
+  }
+
+  @Override
+  public long decodeLongText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Long", dataType));
+  }
+
+  @Override
+  public long decodeLongBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Long", dataType));
+  }
+
+  @Override
+  public float decodeFloatText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Float", dataType));
+  }
+
+  @Override
+  public float decodeFloatBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Float", dataType));
+  }
+
+  @Override
+  public double decodeDoubleText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Double", dataType));
+  }
+
+  @Override
+  public double decodeDoubleBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Double", dataType));
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Time decodeTimeText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    Calendar c = cal == null ? Calendar.getInstance() : cal;
+    int offset = c.getTimeZone().getOffset(0);
+    int[] parts = LocalTimeCodec.parseTime(buf, length, this);
+    long timeInMillis =
+        (parts[1] * 3_600_000L + parts[2] * 60_000L + parts[3] * 1_000L + parts[4] / 1_000_000)
+                * parts[0]
+            - offset;
+    return new Time(timeInMillis);
+  }
+
+  @Override
+  public Time decodeTimeBinary(ReadableByteBuf buf, int length, Calendar calParam)
+      throws SQLDataException {
+    Calendar cal = calParam == null ? Calendar.getInstance() : calParam;
+    long dayOfMonth = 0;
+    int hour = 0;
+    int minutes = 0;
+    int seconds = 0;
+    long microseconds = 0;
+    boolean negate = false;
+    if (length > 0) {
+      // specific case for TIME, to handle value not in 00:00:00-23:59:59
+      negate = buf.readByte() == 1;
+      dayOfMonth = buf.readUnsignedInt();
+      hour = buf.readByte();
+      minutes = buf.readByte();
+      seconds = buf.readByte();
+      if (length > 8) {
+        microseconds = buf.readUnsignedInt();
+      }
+    }
+    int offset = cal.getTimeZone().getOffset(0);
+    long timeInMillis =
+        ((24 * dayOfMonth + hour) * 3_600_000
+                    + minutes * 60_000
+                    + seconds * 1_000
+                    + microseconds / 1_000)
+                * (negate ? -1 : 1)
+            - offset;
+    return new Time(timeInMillis);
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar calParam)
+      throws SQLDataException {
+    int[] parts = LocalTimeCodec.parseTime(buf, length, this);
+    Timestamp t;
+
+    // specific case for TIME, to handle value not in 00:00:00-23:59:59
+    Calendar cal = calParam == null ? Calendar.getInstance() : calParam;
+    synchronized (cal) {
+      cal.clear();
+      cal.setLenient(true);
+      if (parts[0] == -1) {
+        cal.set(
+            1970,
+            Calendar.JANUARY,
+            1,
+            parts[0] * parts[1],
+            parts[0] * parts[2],
+            parts[0] * parts[3] - 1);
+        t = new Timestamp(cal.getTimeInMillis());
+        t.setNanos(1_000_000_000 - parts[4]);
+      } else {
+        cal.set(1970, Calendar.JANUARY, 1, parts[1], parts[2], parts[3]);
+        t = new Timestamp(cal.getTimeInMillis());
+        t.setNanos(parts[4]);
+      }
+    }
+    return t;
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar calParam)
+      throws SQLDataException {
+    Calendar cal = calParam == null ? Calendar.getInstance() : calParam;
+    long microseconds = 0;
+
+    // specific case for TIME, to handle value not in 00:00:00-23:59:59
+    boolean negate = buf.readByte() == 1;
+    long dayOfMonth = buf.readUnsignedInt();
+    int hour = buf.readByte();
+    int minutes = buf.readByte();
+    int seconds = buf.readByte();
+    if (length > 8) {
+      microseconds = buf.readUnsignedInt();
+    }
+    int offset = cal.getTimeZone().getOffset(0);
+    long timeInMillis =
+        ((24 * dayOfMonth + hour) * 3_600_000
+                    + minutes * 60_000
+                    + seconds * 1_000
+                    + microseconds / 1_000)
+                * (negate ? -1 : 1)
+            - offset;
+    return new Timestamp(timeInMillis);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/TimestampColumn.java b/src/main/java/org/mariadb/jdbc/client/column/TimestampColumn.java
new file mode 100644
index 0000000..fd9d90e
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/TimestampColumn.java
@@ -0,0 +1,498 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.sql.*;
+import java.time.LocalDateTime;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.server.ColumnDefinitionPacket;
+import org.mariadb.jdbc.plugin.codec.LocalDateTimeCodec;
+import org.mariadb.jdbc.plugin.codec.TimeCodec;
+
+/** Column metadata definition */
+public class TimestampColumn extends ColumnDefinitionPacket implements ColumnDecoder {
+
+  /**
+   * TIMESTAMP metadata type decoder
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param length maximum data length
+   * @param dataType data type. see https://mariadb.com/kb/en/result-set-packets/#field-types
+   * @param decimals decimal length
+   * @param flags flags. see https://mariadb.com/kb/en/result-set-packets/#field-details-flag
+   * @param stringPos string offset position in buffer
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public TimestampColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    return Timestamp.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    return Types.TIMESTAMP;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    return dataType.name();
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return decodeTimestampText(buf, length, null);
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return decodeTimestampBinary(buf, length, null);
+  }
+
+  @Override
+  public boolean decodeBooleanText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Boolean", dataType));
+  }
+
+  @Override
+  public boolean decodeBooleanBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Boolean", dataType));
+  }
+
+  @Override
+  public byte decodeByteText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Byte", dataType));
+  }
+
+  @Override
+  public byte decodeByteBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Byte", dataType));
+  }
+
+  @Override
+  public String decodeStringText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public String decodeStringBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    if (length == 0) {
+      StringBuilder zeroValue = new StringBuilder("0000-00-00 00:00:00");
+      if (getDecimals() > 0) {
+        zeroValue.append(".");
+        for (int i = 0; i < getDecimals(); i++) zeroValue.append("0");
+      }
+      return zeroValue.toString();
+    }
+    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();
+      }
+    }
+
+    // xpand workaround https://jira.mariadb.org/browse/XPT-274
+    if (year == 0 && month == 0 && day == 0) {
+      return "0000-00-00 00:00:00";
+    }
+
+    LocalDateTime dateTime =
+        LocalDateTime.of(year, month, day, hour, minutes, seconds).plusNanos(microseconds * 1000);
+
+    StringBuilder microSecPattern = new StringBuilder();
+    if (getDecimals() > 0 || microseconds > 0) {
+      int decimal = getDecimals() & 0xff;
+      if (decimal == 0) decimal = 6;
+      microSecPattern.append(".");
+      for (int i = 0; i < decimal; i++) microSecPattern.append("S");
+    }
+    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss" + microSecPattern);
+    return dateTime.toLocalDate().toString() + ' ' + dateTime.toLocalTime().format(formatter);
+  }
+
+  @Override
+  public short decodeShortText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Short", dataType));
+  }
+
+  @Override
+  public short decodeShortBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Short", dataType));
+  }
+
+  @Override
+  public int decodeIntText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Integer", dataType));
+  }
+
+  @Override
+  public int decodeIntBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Integer", dataType));
+  }
+
+  @Override
+  public long decodeLongText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Long", dataType));
+  }
+
+  @Override
+  public long decodeLongBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Long", dataType));
+  }
+
+  @Override
+  public float decodeFloatText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Float", dataType));
+  }
+
+  @Override
+  public float decodeFloatBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Float", dataType));
+  }
+
+  @Override
+  public double decodeDoubleText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Double", dataType));
+  }
+
+  @Override
+  public double decodeDoubleBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Double", dataType));
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+
+    int pos = buf.pos();
+    int nanoBegin = -1;
+    int[] timestampsPart = new int[] {0, 0, 0, 0, 0, 0, 0};
+    int partIdx = 0;
+    for (int begin = 0; begin < length; begin++) {
+      byte b = buf.readByte();
+      if (b == '-' || b == ' ' || b == ':') {
+        partIdx++;
+        continue;
+      }
+      if (b == '.') {
+        partIdx++;
+        nanoBegin = begin;
+        continue;
+      }
+      if (b < '0' || b > '9') {
+        buf.pos(pos);
+        throw new SQLDataException(
+            String.format(
+                "value '%s' (%s) cannot be decoded as Timestamp",
+                buf.readString(length), dataType));
+      }
+
+      timestampsPart[partIdx] = timestampsPart[partIdx] * 10 + b - 48;
+    }
+    if (timestampsPart[0] == 0
+        && timestampsPart[1] == 0
+        && timestampsPart[2] == 0
+        && timestampsPart[3] == 0
+        && timestampsPart[4] == 0
+        && timestampsPart[5] == 0
+        && timestampsPart[6] == 0) {
+      return null;
+    }
+
+    // fix non-leading tray for nanoseconds
+    if (nanoBegin > 0) {
+      for (int begin = 0; begin < 6 - (length - nanoBegin - 1); begin++) {
+        timestampsPart[6] = timestampsPart[6] * 10;
+      }
+    }
+
+    Timestamp timestamp;
+    if (cal == null) {
+      Calendar c = Calendar.getInstance();
+      c.set(
+          timestampsPart[0],
+          timestampsPart[1] - 1,
+          timestampsPart[2],
+          timestampsPart[3],
+          timestampsPart[4],
+          timestampsPart[5]);
+      timestamp = new Timestamp(c.getTime().getTime());
+      timestamp.setNanos(timestampsPart[6] * 1000);
+    } else {
+      synchronized (cal) {
+        cal.clear();
+        cal.set(
+            timestampsPart[0],
+            timestampsPart[1] - 1,
+            timestampsPart[2],
+            timestampsPart[3],
+            timestampsPart[4],
+            timestampsPart[5]);
+        timestamp = new Timestamp(cal.getTime().getTime());
+        timestamp.setNanos(timestampsPart[6] * 1000);
+      }
+    }
+
+    String st = timestamp.toString();
+    return Date.valueOf(st.substring(0, 10));
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar calParam)
+      throws SQLDataException {
+    Calendar cal = calParam == null ? Calendar.getInstance() : calParam;
+
+    if (length == 0) return null;
+    int year = buf.readUnsignedShort();
+    int month = buf.readByte();
+    int dayOfMonth = 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();
+      }
+    }
+
+    // xpand workaround https://jira.mariadb.org/browse/XPT-274
+    if (year == 0
+        && month == 0
+        && dayOfMonth == 0
+        && hour == 0
+        && minutes == 0
+        && seconds == 0
+        && microseconds == 0) return null;
+
+    Timestamp timestamp;
+    synchronized (cal) {
+      cal.clear();
+      cal.set(year, month - 1, dayOfMonth, hour, minutes, seconds);
+      timestamp = new Timestamp(cal.getTimeInMillis());
+    }
+    timestamp.setNanos((int) (microseconds * 1000));
+    String st = timestamp.toString();
+    return Date.valueOf(st.substring(0, 10));
+  }
+
+  @Override
+  public Time decodeTimeText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    LocalDateTime lt = LocalDateTimeCodec.INSTANCE.decodeText(buf, length, this, cal);
+    if (lt == null) return null;
+    Calendar cc = cal == null ? Calendar.getInstance() : cal;
+    ZonedDateTime d =
+        TimeCodec.EPOCH_DATE.atTime(lt.toLocalTime()).atZone(cc.getTimeZone().toZoneId());
+    return new Time(d.toEpochSecond() * 1000 + d.getNano() / 1_000_000);
+  }
+
+  @Override
+  public Time decodeTimeBinary(ReadableByteBuf buf, int length, Calendar calParam)
+      throws SQLDataException {
+    if (length == 0) return null;
+    Calendar cal = calParam == null ? Calendar.getInstance() : calParam;
+
+    int year = buf.readUnsignedShort();
+    int month = buf.readByte();
+    int dayOfMonth = 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();
+      }
+    }
+
+    if (year == 0 && month == 0 && dayOfMonth == 0 && hour == 0 && minutes == 0 && seconds == 0) {
+      return null;
+    }
+
+    synchronized (cal) {
+      cal.clear();
+      cal.set(1970, Calendar.JANUARY, 1, hour, minutes, seconds);
+      return new Time(cal.getTimeInMillis() + microseconds / 1_000);
+    }
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar calParam)
+      throws SQLDataException {
+    int pos = buf.pos();
+    int nanoBegin = -1;
+    int[] timestampsPart = new int[] {0, 0, 0, 0, 0, 0, 0};
+    int partIdx = 0;
+    for (int begin = 0; begin < length; begin++) {
+      byte b = buf.readByte();
+      if (b == '-' || b == ' ' || b == ':') {
+        partIdx++;
+        continue;
+      }
+      if (b == '.') {
+        partIdx++;
+        nanoBegin = begin;
+        continue;
+      }
+      if (b < '0' || b > '9') {
+        buf.pos(pos);
+        throw new SQLDataException(
+            String.format(
+                "value '%s' (%s) cannot be decoded as Timestamp",
+                buf.readString(length), dataType));
+      }
+
+      timestampsPart[partIdx] = timestampsPart[partIdx] * 10 + b - 48;
+    }
+    if (timestampsPart[0] == 0
+        && timestampsPart[1] == 0
+        && timestampsPart[2] == 0
+        && timestampsPart[3] == 0
+        && timestampsPart[4] == 0
+        && timestampsPart[5] == 0
+        && timestampsPart[6] == 0) {
+      return null;
+    }
+
+    // fix non-leading tray for nanoseconds
+    if (nanoBegin > 0) {
+      for (int begin = 0; begin < 6 - (length - nanoBegin - 1); begin++) {
+        timestampsPart[6] = timestampsPart[6] * 10;
+      }
+    }
+
+    Timestamp timestamp;
+    if (calParam == null) {
+      Calendar c = Calendar.getInstance();
+      c.set(
+          timestampsPart[0],
+          timestampsPart[1] - 1,
+          timestampsPart[2],
+          timestampsPart[3],
+          timestampsPart[4],
+          timestampsPart[5]);
+      timestamp = new Timestamp(c.getTime().getTime());
+      timestamp.setNanos(timestampsPart[6] * 1000);
+    } else {
+      synchronized (calParam) {
+        calParam.clear();
+        calParam.set(
+            timestampsPart[0],
+            timestampsPart[1] - 1,
+            timestampsPart[2],
+            timestampsPart[3],
+            timestampsPart[4],
+            timestampsPart[5]);
+        timestamp = new Timestamp(calParam.getTime().getTime());
+        timestamp.setNanos(timestampsPart[6] * 1000);
+      }
+    }
+    return timestamp;
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar calParam)
+      throws SQLDataException {
+    if (length == 0) return null;
+    Calendar cal = calParam == null ? Calendar.getInstance() : calParam;
+
+    int year = buf.readUnsignedShort();
+    int month = buf.readByte();
+    int dayOfMonth = 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();
+      }
+    }
+
+    // xpand workaround https://jira.mariadb.org/browse/XPT-274
+    if (year == 0
+        && month == 0
+        && dayOfMonth == 0
+        && hour == 0
+        && minutes == 0
+        && seconds == 0
+        && microseconds == 0) return null;
+    Timestamp timestamp;
+    synchronized (cal) {
+      cal.clear();
+      cal.set(year, month - 1, dayOfMonth, hour, minutes, seconds);
+      timestamp = new Timestamp(cal.getTimeInMillis());
+    }
+    timestamp.setNanos((int) (microseconds * 1000));
+    return timestamp;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/UnsignedBigIntColumn.java b/src/main/java/org/mariadb/jdbc/client/column/UnsignedBigIntColumn.java
new file mode 100644
index 0000000..0eb8a72
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/UnsignedBigIntColumn.java
@@ -0,0 +1,274 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.math.BigInteger;
+import java.sql.*;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.server.ColumnDefinitionPacket;
+
+/** Column metadata definition */
+public class UnsignedBigIntColumn extends ColumnDefinitionPacket implements ColumnDecoder {
+
+  /**
+   * BIGINT UNSIGNED metadata type decoder
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param length maximum data length
+   * @param dataType data type. see https://mariadb.com/kb/en/result-set-packets/#field-types
+   * @param decimals decimal length
+   * @param flags flags. see https://mariadb.com/kb/en/result-set-packets/#field-details-flag
+   * @param stringPos string offset position in buffer
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public UnsignedBigIntColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    return BigInteger.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    return Types.BIGINT;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    return "BIGINT UNSIGNED";
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return new BigInteger(buf.readAscii(length));
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    // 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);
+  }
+
+  @Override
+  public boolean decodeBooleanText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String s = buf.readAscii(length);
+    return !"0".equals(s);
+  }
+
+  @Override
+  public boolean decodeBooleanBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readLong() != 0;
+  }
+
+  @Override
+  public byte decodeByteText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.atoull(length);
+    if ((byte) result != result || result < 0) {
+      throw new SQLDataException("byte overflow");
+    }
+    return (byte) result;
+  }
+
+  @Override
+  public byte decodeByteBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    // 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.byteValueExact();
+    } catch (NumberFormatException | ArithmeticException nfe) {
+      throw new SQLDataException(
+          String.format("value '%s' (%s) cannot be decoded as Byte", val, dataType));
+    }
+  }
+
+  @Override
+  public String decodeStringText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public String decodeStringBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    // 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).toString();
+  }
+
+  @Override
+  public short decodeShortText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.atoull(length);
+    if ((short) result != result || result < 0) {
+      throw new SQLDataException("Short overflow");
+    }
+    return (short) result;
+  }
+
+  @Override
+  public short decodeShortBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.readLong();
+    if ((short) result != result || result < 0) {
+      throw new SQLDataException("Short overflow");
+    }
+    return (short) result;
+  }
+
+  @Override
+  public int decodeIntText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.atoull(length);
+    int res = (int) result;
+    if (res != result || result < 0) {
+      throw new SQLDataException("integer overflow");
+    }
+    return res;
+  }
+
+  @Override
+  public int decodeIntBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+
+    // 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));
+    }
+  }
+
+  @Override
+  public long decodeLongText(ReadableByteBuf buf, int length) throws SQLDataException {
+    if (length < 10) return buf.atoull(length);
+    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));
+    }
+  }
+
+  @Override
+  public long decodeLongBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    if ((buf.getByte(buf.pos() + 7) & 0x80) == 0) {
+      return buf.readLong();
+    } else {
+      // error too big to return a long
+      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));
+      }
+    }
+  }
+
+  @Override
+  public float decodeFloatText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Float.parseFloat(buf.readAscii(length));
+  }
+
+  @Override
+  public float decodeFloatBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    // 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();
+  }
+
+  @Override
+  public double decodeDoubleText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Double.parseDouble(buf.readAscii(length));
+  }
+
+  @Override
+  public double decodeDoubleBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    // 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();
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Time decodeTimeText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Time decodeTimeBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/UnsignedIntColumn.java b/src/main/java/org/mariadb/jdbc/client/column/UnsignedIntColumn.java
new file mode 100644
index 0000000..00a806d
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/UnsignedIntColumn.java
@@ -0,0 +1,222 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.sql.*;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.server.ColumnDefinitionPacket;
+
+/** Column metadata definition */
+public class UnsignedIntColumn extends ColumnDefinitionPacket implements ColumnDecoder {
+
+  /**
+   * INTEGER UNSIGNED metadata type decoder
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param length maximum data length
+   * @param dataType data type. see https://mariadb.com/kb/en/result-set-packets/#field-types
+   * @param decimals decimal length
+   * @param flags flags. see https://mariadb.com/kb/en/result-set-packets/#field-details-flag
+   * @param stringPos string offset position in buffer
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public UnsignedIntColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    return Long.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    return Types.BIGINT;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    return "INTEGER UNSIGNED";
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return buf.atoull(length);
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return buf.readUnsignedInt();
+  }
+
+  @Override
+  public boolean decodeBooleanText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String s = buf.readAscii(length);
+    return !"0".equals(s);
+  }
+
+  @Override
+  public boolean decodeBooleanBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readInt() != 0;
+  }
+
+  @Override
+  public byte decodeByteText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.atoull(length);
+    if ((byte) result != result || (result < 0 && !isSigned())) {
+      throw new SQLDataException("byte overflow");
+    }
+    return (byte) result;
+  }
+
+  @Override
+  public byte decodeByteBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.readUnsignedInt();
+    if ((byte) result != result) {
+      throw new SQLDataException("byte overflow");
+    }
+
+    return (byte) result;
+  }
+
+  @Override
+  public String decodeStringText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public String decodeStringBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return String.valueOf(buf.readUnsignedInt());
+  }
+
+  @Override
+  public short decodeShortText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.atoll(length);
+    if ((short) result != result || result < 0) {
+      throw new SQLDataException("Short overflow");
+    }
+    return (short) result;
+  }
+
+  @Override
+  public short decodeShortBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.readUnsignedInt();
+    if ((short) result != result || result < 0) {
+      throw new SQLDataException("Short overflow");
+    }
+    return (short) result;
+  }
+
+  @Override
+  public int decodeIntText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.atoull(length);
+    int res = (int) result;
+    if (res != result || result < 0) {
+      throw new SQLDataException("integer overflow");
+    }
+    return res;
+  }
+
+  @Override
+  public int decodeIntBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.readUnsignedInt();
+    int res = (int) result;
+    if (res != result) {
+      throw new SQLDataException("integer overflow");
+    }
+
+    return res;
+  }
+
+  @Override
+  public long decodeLongText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.atoull(length);
+  }
+
+  @Override
+  public long decodeLongBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readUnsignedInt();
+  }
+
+  @Override
+  public float decodeFloatText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Float.parseFloat(buf.readAscii(length));
+  }
+
+  @Override
+  public float decodeFloatBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return (float) buf.readUnsignedInt();
+  }
+
+  @Override
+  public double decodeDoubleText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Double.parseDouble(buf.readAscii(length));
+  }
+
+  @Override
+  public double decodeDoubleBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readUnsignedInt();
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Time decodeTimeText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Time decodeTimeBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/UnsignedMediumIntColumn.java b/src/main/java/org/mariadb/jdbc/client/column/UnsignedMediumIntColumn.java
new file mode 100644
index 0000000..d242941
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/UnsignedMediumIntColumn.java
@@ -0,0 +1,223 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.sql.*;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.server.ColumnDefinitionPacket;
+
+/** Column metadata definition */
+public class UnsignedMediumIntColumn extends ColumnDefinitionPacket implements ColumnDecoder {
+
+  /**
+   * MEDIUMINT UNSIGNED metadata type decoder
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param length maximum data length
+   * @param dataType data type. see https://mariadb.com/kb/en/result-set-packets/#field-types
+   * @param decimals decimal length
+   * @param flags flags. see https://mariadb.com/kb/en/result-set-packets/#field-details-flag
+   * @param stringPos string offset position in buffer
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public UnsignedMediumIntColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    return Integer.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    return Types.INTEGER;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    return "MEDIUMINT UNSIGNED";
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return decodeIntText(buf, length);
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return decodeIntBinary(buf, length);
+  }
+
+  @Override
+  public boolean decodeBooleanText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String s = buf.readAscii(length);
+    return !"0".equals(s);
+  }
+
+  @Override
+  public boolean decodeBooleanBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readInt() != 0;
+  }
+
+  @Override
+  public byte decodeByteText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.atoull(length);
+    if ((byte) result != result || result < 0) {
+      throw new SQLDataException("byte overflow");
+    }
+    return (byte) result;
+  }
+
+  @Override
+  public byte decodeByteBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.readUnsignedMedium();
+    buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges !
+    if ((byte) result != result) {
+      throw new SQLDataException("byte overflow");
+    }
+
+    return (byte) result;
+  }
+
+  @Override
+  public String decodeStringText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public String decodeStringBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    String mediumStr = String.valueOf(buf.readUnsignedMedium());
+    buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges !
+    return mediumStr;
+  }
+
+  @Override
+  public short decodeShortText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.atoull(length);
+    if ((short) result != result || result < 0) {
+      throw new SQLDataException("Short overflow");
+    }
+    return (short) result;
+  }
+
+  @Override
+  public short decodeShortBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.readUnsignedMedium();
+    buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges !
+    if ((short) result != result || result < 0) {
+      throw new SQLDataException("Short overflow");
+    }
+    return (short) result;
+  }
+
+  @Override
+  public int decodeIntText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return (int) buf.atoll(length);
+  }
+
+  @Override
+  public int decodeIntBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    int res = buf.readUnsignedMedium();
+    buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges !
+    return res;
+  }
+
+  @Override
+  public long decodeLongText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.atoull(length);
+  }
+
+  @Override
+  public long decodeLongBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long l = buf.readUnsignedMedium();
+    buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges !
+    return l;
+  }
+
+  @Override
+  public float decodeFloatText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Float.parseFloat(buf.readAscii(length));
+  }
+
+  @Override
+  public float decodeFloatBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    float f = buf.readUnsignedMedium();
+    buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges !
+    return f;
+  }
+
+  @Override
+  public double decodeDoubleText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Double.parseDouble(buf.readAscii(length));
+  }
+
+  @Override
+  public double decodeDoubleBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    double f = buf.readUnsignedMedium();
+    buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges !
+    return f;
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Time decodeTimeText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Time decodeTimeBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/UnsignedSmallIntColumn.java b/src/main/java/org/mariadb/jdbc/client/column/UnsignedSmallIntColumn.java
new file mode 100644
index 0000000..b897671
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/UnsignedSmallIntColumn.java
@@ -0,0 +1,210 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.sql.*;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.server.ColumnDefinitionPacket;
+
+/** Column metadata definition */
+public class UnsignedSmallIntColumn extends ColumnDefinitionPacket implements ColumnDecoder {
+
+  /**
+   * SMALLINT UNSIGNED metadata type decoder
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param length maximum data length
+   * @param dataType data type. see https://mariadb.com/kb/en/result-set-packets/#field-types
+   * @param decimals decimal length
+   * @param flags flags. see https://mariadb.com/kb/en/result-set-packets/#field-details-flag
+   * @param stringPos string offset position in buffer
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public UnsignedSmallIntColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    return Integer.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    return Types.INTEGER;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    return "SMALLINT UNSIGNED";
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return (int) buf.atoull(length);
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return buf.readUnsignedShort();
+  }
+
+  @Override
+  public boolean decodeBooleanText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String s = buf.readAscii(length);
+    return !"0".equals(s);
+  }
+
+  @Override
+  public boolean decodeBooleanBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readShort() != 0;
+  }
+
+  @Override
+  public byte decodeByteText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.atoull(length);
+    if ((byte) result != result || result < 0) {
+      throw new SQLDataException("byte overflow");
+    }
+    return (byte) result;
+  }
+
+  @Override
+  public byte decodeByteBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.readUnsignedShort();
+    if ((byte) result != result) {
+      throw new SQLDataException("byte overflow");
+    }
+    return (byte) result;
+  }
+
+  @Override
+  public String decodeStringText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public String decodeStringBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return String.valueOf(buf.readUnsignedShort());
+  }
+
+  @Override
+  public short decodeShortText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.atoull(length);
+    if ((short) result != result) {
+      throw new SQLDataException("Short overflow");
+    }
+    return (short) result;
+  }
+
+  @Override
+  public short decodeShortBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    int result = buf.readUnsignedShort();
+    if ((short) result != result) {
+      throw new SQLDataException("Short overflow");
+    }
+    return (short) result;
+  }
+
+  @Override
+  public int decodeIntText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return (int) buf.atoull(length);
+  }
+
+  @Override
+  public int decodeIntBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readUnsignedShort();
+  }
+
+  @Override
+  public long decodeLongText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.atoull(length);
+  }
+
+  @Override
+  public long decodeLongBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readUnsignedShort();
+  }
+
+  @Override
+  public float decodeFloatText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Float.parseFloat(buf.readAscii(length));
+  }
+
+  @Override
+  public float decodeFloatBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return (float) buf.readUnsignedShort();
+  }
+
+  @Override
+  public double decodeDoubleText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Double.parseDouble(buf.readAscii(length));
+  }
+
+  @Override
+  public double decodeDoubleBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readUnsignedShort();
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Time decodeTimeText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Time decodeTimeBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/UnsignedTinyIntColumn.java b/src/main/java/org/mariadb/jdbc/client/column/UnsignedTinyIntColumn.java
new file mode 100644
index 0000000..29f3cc1
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/UnsignedTinyIntColumn.java
@@ -0,0 +1,215 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.sql.*;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.server.ColumnDefinitionPacket;
+
+/** Column metadata definition */
+public class UnsignedTinyIntColumn extends ColumnDefinitionPacket implements ColumnDecoder {
+
+  /**
+   * TINYINT UNSIGNED metadata type decoder
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param length maximum data length
+   * @param dataType data type. see https://mariadb.com/kb/en/result-set-packets/#field-types
+   * @param decimals decimal length
+   * @param flags flags. see https://mariadb.com/kb/en/result-set-packets/#field-details-flag
+   * @param stringPos string offset position in buffer
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public UnsignedTinyIntColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    if (conf.tinyInt1isBit() && columnLength == 1) return Boolean.class.getName();
+    return Integer.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    if (conf.tinyInt1isBit() && columnLength == 1) {
+      return conf.transformedBitIsBoolean() ? Types.BOOLEAN : Types.BIT;
+    }
+    return Types.SMALLINT;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    if (conf.tinyInt1isBit() && columnLength == 1) {
+      return conf.transformedBitIsBoolean() ? "BOOLEAN" : "BIT";
+    }
+    return "TINYINT UNSIGNED";
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    if (conf.tinyInt1isBit() && columnLength == 1) {
+      return decodeBooleanText(buf, length);
+    }
+    return (int) buf.atoull(length);
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    if (conf.tinyInt1isBit() && columnLength == 1) {
+      return decodeBooleanBinary(buf, length);
+    }
+    return (int) buf.readUnsignedByte();
+  }
+
+  @Override
+  public boolean decodeBooleanText(ReadableByteBuf buf, int length) throws SQLDataException {
+    String s = buf.readAscii(length);
+    return !"0".equals(s);
+  }
+
+  @Override
+  public boolean decodeBooleanBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readByte() != 0;
+  }
+
+  @Override
+  public byte decodeByteText(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.atoull(length);
+    if ((byte) result != result) {
+      throw new SQLDataException("byte overflow");
+    }
+    return (byte) result;
+  }
+
+  @Override
+  public byte decodeByteBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    long result = buf.readUnsignedByte();
+    if ((byte) result != result) {
+      throw new SQLDataException("byte overflow");
+    }
+    return (byte) result;
+  }
+
+  @Override
+  public String decodeStringText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public String decodeStringBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return String.valueOf(buf.readUnsignedByte());
+  }
+
+  @Override
+  public short decodeShortText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return (short) buf.atoull(length);
+  }
+
+  @Override
+  public short decodeShortBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readUnsignedByte();
+  }
+
+  @Override
+  public int decodeIntText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return (int) buf.atoull(length);
+  }
+
+  @Override
+  public int decodeIntBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readUnsignedByte();
+  }
+
+  @Override
+  public long decodeLongText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.atoull(length);
+  }
+
+  @Override
+  public long decodeLongBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readUnsignedByte();
+  }
+
+  @Override
+  public float decodeFloatText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Float.parseFloat(buf.readAscii(length));
+  }
+
+  @Override
+  public float decodeFloatBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readUnsignedByte();
+  }
+
+  @Override
+  public double decodeDoubleText(ReadableByteBuf buf, int length) throws SQLDataException {
+    return Double.parseDouble(buf.readAscii(length));
+  }
+
+  @Override
+  public double decodeDoubleBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    return buf.readUnsignedByte();
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Date", dataType));
+  }
+
+  @Override
+  public Time decodeTimeText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Time decodeTimeBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(String.format("Data type %s cannot be decoded as Time", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Timestamp", dataType));
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/UuidColumn.java b/src/main/java/org/mariadb/jdbc/client/column/UuidColumn.java
new file mode 100644
index 0000000..2294130
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/UuidColumn.java
@@ -0,0 +1,215 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.sql.*;
+import java.util.Calendar;
+import java.util.UUID;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.server.ColumnDefinitionPacket;
+import org.mariadb.jdbc.util.CharsetEncodingLength;
+
+/** Column metadata definition */
+public class UuidColumn extends ColumnDefinitionPacket implements ColumnDecoder {
+
+  /**
+   * UUID metadata type decoder
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param length maximum data length
+   * @param dataType data type. see https://mariadb.com/kb/en/result-set-packets/#field-types
+   * @param decimals decimal length
+   * @param flags flags. see https://mariadb.com/kb/en/result-set-packets/#field-details-flag
+   * @param stringPos string offset position in buffer
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public UuidColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    return conf.uuidAsString() ? String.class.getName() : UUID.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    return conf.uuidAsString() ? Types.CHAR : Types.OTHER;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    return "uuid";
+  }
+
+  public int getPrecision() {
+    Integer maxWidth = CharsetEncodingLength.maxCharlen.get(charset);
+    if (maxWidth == null) {
+      return (int) columnLength;
+    }
+    return (int) (columnLength / maxWidth);
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return conf.uuidAsString() ? buf.readString(length) : UUID.fromString(buf.readAscii(length));
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    return conf.uuidAsString() ? buf.readString(length) : UUID.fromString(buf.readAscii(length));
+  }
+
+  @Override
+  public boolean decodeBooleanText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException("Data type UUID cannot be decoded as Boolean");
+  }
+
+  @Override
+  public boolean decodeBooleanBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException("Data type UUID cannot be decoded as Boolean");
+  }
+
+  @Override
+  public byte decodeByteText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException("Data type UUID cannot be decoded as byte");
+  }
+
+  @Override
+  public byte decodeByteBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException("Data type UUID cannot be decoded as byte");
+  }
+
+  @Override
+  public String decodeStringText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public String decodeStringBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    return buf.readString(length);
+  }
+
+  @Override
+  public short decodeShortText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException("Data type UUID cannot be decoded as Short");
+  }
+
+  @Override
+  public short decodeShortBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException("Data type UUID cannot be decoded as Short");
+  }
+
+  @Override
+  public int decodeIntText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException("Data type UUID cannot be decoded as Integer");
+  }
+
+  @Override
+  public int decodeIntBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException("Data type UUID cannot be decoded as Integer");
+  }
+
+  @Override
+  public long decodeLongText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException("Data type UUID cannot be decoded as Long");
+  }
+
+  @Override
+  public long decodeLongBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException("Data type UUID cannot be decoded as Long");
+  }
+
+  @Override
+  public float decodeFloatText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException("Data type UUID cannot be decoded as Float");
+  }
+
+  @Override
+  public float decodeFloatBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException("Data type UUID cannot be decoded as Float");
+  }
+
+  @Override
+  public double decodeDoubleText(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException("Data type UUID cannot be decoded as Double");
+  }
+
+  @Override
+  public double decodeDoubleBinary(ReadableByteBuf buf, int length) throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException("Data type UUID cannot be decoded as Double");
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException("Data type UUID cannot be decoded as Date");
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException("Data type UUID cannot be decoded as Date");
+  }
+
+  @Override
+  public Time decodeTimeText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException("Data type UUID cannot be decoded as Time");
+  }
+
+  @Override
+  public Time decodeTimeBinary(ReadableByteBuf buf, int length, Calendar calParam)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException("Data type UUID cannot be decoded as Time");
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar calParam)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException("Data type UUID cannot be decoded as Timestamp");
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar calParam)
+      throws SQLDataException {
+    buf.skip(length);
+    throw new SQLDataException("Data type UUID cannot be decoded as Timestamp");
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/column/YearColumn.java b/src/main/java/org/mariadb/jdbc/client/column/YearColumn.java
new file mode 100644
index 0000000..1985d85
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/column/YearColumn.java
@@ -0,0 +1,152 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.column;
+
+import java.sql.*;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+
+/** Column metadata definition */
+public class YearColumn extends UnsignedSmallIntColumn {
+
+  /**
+   * YEAR metadata type decoder
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param length maximum data length
+   * @param dataType data type. see https://mariadb.com/kb/en/result-set-packets/#field-types
+   * @param decimals decimal length
+   * @param flags flags. see https://mariadb.com/kb/en/result-set-packets/#field-details-flag
+   * @param stringPos string offset position in buffer
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public YearColumn(
+      ReadableByteBuf buf,
+      int charset,
+      long length,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    super(buf, charset, length, dataType, decimals, flags, stringPos, extTypeName, extTypeFormat);
+  }
+
+  public String defaultClassname(Configuration conf) {
+    return conf.yearIsDateType() ? Date.class.getName() : Short.class.getName();
+  }
+
+  public int getColumnType(Configuration conf) {
+    return conf.yearIsDateType() ? Types.DATE : Types.SMALLINT;
+  }
+
+  public String getColumnTypeName(Configuration conf) {
+    return "YEAR";
+  }
+
+  @Override
+  public Object getDefaultText(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    if (conf.yearIsDateType()) {
+      short y = (short) buf.atoull(length);
+      if (columnLength == 2) {
+        // YEAR(2) - deprecated
+        if (y <= 69) {
+          y += 2000;
+        } else {
+          y += 1900;
+        }
+      }
+      return Date.valueOf(y + "-01-01");
+    }
+    return decodeShortText(buf, length);
+  }
+
+  @Override
+  public Object getDefaultBinary(final Configuration conf, ReadableByteBuf buf, int length)
+      throws SQLDataException {
+    if (conf.yearIsDateType()) {
+      int v = buf.readShort();
+      if (columnLength == 2) {
+        // YEAR(2) - deprecated
+        if (v <= 69) {
+          v += 2000;
+        } else {
+          v += 1900;
+        }
+      }
+      return Date.valueOf(v + "-01-01");
+    }
+    return decodeShortText(buf, length);
+  }
+
+  @Override
+  public Date decodeDateText(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    short y = (short) buf.atoll(length);
+    if (columnLength == 2) {
+      // YEAR(2) - deprecated
+      if (y <= 69) {
+        y += 2000;
+      } else {
+        y += 1900;
+      }
+    }
+    return Date.valueOf(y + "-01-01");
+  }
+
+  @Override
+  public Date decodeDateBinary(ReadableByteBuf buf, int length, Calendar cal)
+      throws SQLDataException {
+    int v = buf.readShort();
+
+    if (columnLength == 2) {
+      // YEAR(2) - deprecated
+      if (v <= 69) {
+        v += 2000;
+      } else {
+        v += 1900;
+      }
+    }
+    return Date.valueOf(v + "-01-01");
+  }
+
+  @Override
+  public Timestamp decodeTimestampText(ReadableByteBuf buf, int length, Calendar calParam)
+      throws SQLDataException {
+    Calendar cal1 = calParam == null ? Calendar.getInstance() : calParam;
+
+    int year = Integer.parseInt(buf.readAscii(length));
+    if (columnLength <= 2) year += year >= 70 ? 1900 : 2000;
+    synchronized (cal1) {
+      cal1.clear();
+      cal1.set(year, Calendar.JANUARY, 1);
+      return new Timestamp(cal1.getTimeInMillis());
+    }
+  }
+
+  @Override
+  public Timestamp decodeTimestampBinary(ReadableByteBuf buf, int length, Calendar calParam)
+      throws SQLDataException {
+    Calendar cal = calParam == null ? Calendar.getInstance() : calParam;
+
+    int year = buf.readUnsignedShort();
+    if (columnLength <= 2) year += year >= 70 ? 1900 : 2000;
+
+    Timestamp timestamp;
+    synchronized (cal) {
+      cal.clear();
+      cal.set(year, 0, 1, 0, 0, 0);
+      timestamp = new Timestamp(cal.getTimeInMillis());
+    }
+    timestamp.setNanos(0);
+    return timestamp;
+  }
+}
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 0000000..3645d1e
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/context/BaseContext.java
@@ -0,0 +1,174 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.context;
+
+import static org.mariadb.jdbc.util.constants.Capabilities.STMT_BULK_OPERATIONS;
+
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.PrepareCache;
+import org.mariadb.jdbc.client.ServerVersion;
+import org.mariadb.jdbc.export.ExceptionFactory;
+import org.mariadb.jdbc.message.server.InitialHandshakePacket;
+import org.mariadb.jdbc.util.constants.Capabilities;
+
+/** Context (current connection state) of a connection */
+public class BaseContext implements Context {
+
+  private final long threadId;
+  private final long serverCapabilities;
+  private final long clientCapabilities;
+  private final byte[] seed;
+  private final ServerVersion version;
+  private final boolean eofDeprecated;
+  private final boolean skipMeta;
+  private final boolean extendedInfo;
+  private final Configuration conf;
+  private final ExceptionFactory exceptionFactory;
+
+  /** Server status context */
+  protected int serverStatus;
+
+  /** Server current database */
+  private String database;
+
+  /** Server current transaction isolation level */
+  private int transactionIsolationLevel;
+
+  /** Server current warning count */
+  private int warning;
+
+  /** LRU prepare cache object */
+  private final PrepareCache prepareCache;
+
+  /** Connection state use flag */
+  private int stateFlag = 0;
+
+  /**
+   * Constructor of connection context
+   *
+   * @param handshake server handshake
+   * @param clientCapabilities client capabilities
+   * @param conf connection configuration
+   * @param exceptionFactory connection exception factory
+   * @param prepareCache LRU prepare cache
+   */
+  public BaseContext(
+      InitialHandshakePacket handshake,
+      long clientCapabilities,
+      Configuration conf,
+      ExceptionFactory exceptionFactory,
+      PrepareCache prepareCache) {
+    this.threadId = handshake.getThreadId();
+    this.seed = handshake.getSeed();
+    this.serverCapabilities = handshake.getCapabilities();
+    this.serverStatus = handshake.getServerStatus();
+    this.version = handshake.getVersion();
+    this.clientCapabilities = clientCapabilities;
+    this.eofDeprecated = hasClientCapability(Capabilities.CLIENT_DEPRECATE_EOF);
+    this.skipMeta = hasClientCapability(Capabilities.CACHE_METADATA);
+    this.extendedInfo = hasClientCapability(Capabilities.EXTENDED_TYPE_INFO);
+    this.conf = conf;
+    this.database = conf.database();
+    this.exceptionFactory = exceptionFactory;
+    this.prepareCache = prepareCache;
+  }
+
+  public long getThreadId() {
+    return threadId;
+  }
+
+  public byte[] getSeed() {
+    return seed;
+  }
+
+  public boolean hasServerCapability(long flag) {
+    return (serverCapabilities & flag) > 0;
+  }
+
+  public boolean hasClientCapability(long flag) {
+    return (clientCapabilities & flag) > 0;
+  }
+
+  public boolean permitPipeline() {
+    return !conf.disablePipeline() && (clientCapabilities & STMT_BULK_OPERATIONS) > 0;
+  }
+
+  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 boolean isExtendedInfo() {
+    return extendedInfo;
+  }
+
+  public boolean canSkipMeta() {
+    return skipMeta;
+  }
+
+  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() {
+    if (prepareCache != null) prepareCache.reset();
+  }
+
+  public int getStateFlag() {
+    return stateFlag;
+  }
+
+  public void resetStateFlag() {
+    stateFlag = 0;
+  }
+
+  public void addStateFlag(int state) {
+    stateFlag |= state;
+  }
+}
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 0000000..dd809ab
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/context/RedoContext.java
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.context;
+
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.PrepareCache;
+import org.mariadb.jdbc.client.impl.TransactionSaver;
+import org.mariadb.jdbc.export.ExceptionFactory;
+import org.mariadb.jdbc.message.ClientMessage;
+import org.mariadb.jdbc.message.client.RedoableClientMessage;
+import org.mariadb.jdbc.message.server.InitialHandshakePacket;
+import org.mariadb.jdbc.util.constants.ServerStatus;
+
+/** Redo addition to Context */
+public class RedoContext extends BaseContext {
+
+  private final TransactionSaver transactionSaver;
+
+  /**
+   * Constructor
+   *
+   * @param handshake server handshake
+   * @param clientCapabilities client capabilities
+   * @param conf configuration
+   * @param exceptionFactory connection exception factory
+   * @param prepareCache LRU prepare cache
+   */
+  public RedoContext(
+      InitialHandshakePacket handshake,
+      long clientCapabilities,
+      Configuration conf,
+      ExceptionFactory exceptionFactory,
+      PrepareCache prepareCache) {
+    super(handshake, clientCapabilities, conf, exceptionFactory, prepareCache);
+    this.transactionSaver = new TransactionSaver(conf.transactionReplaySize());
+  }
+
+  /**
+   * Set server status
+   *
+   * @param serverStatus server status
+   */
+  public void setServerStatus(int serverStatus) {
+    this.serverStatus = serverStatus;
+    if ((serverStatus & ServerStatus.IN_TRANSACTION) == 0) transactionSaver.clear();
+  }
+
+  /**
+   * Save client message
+   *
+   * @param msg client message
+   */
+  public void saveRedo(ClientMessage msg) {
+    if (msg instanceof RedoableClientMessage) {
+      RedoableClientMessage redoMsg = (RedoableClientMessage) msg;
+      redoMsg.saveParameters();
+      transactionSaver.add(redoMsg);
+    }
+  }
+
+  /**
+   * Save client messages
+   *
+   * @param msgs client messages
+   */
+  public void saveRedo(ClientMessage[] msgs) {
+    for (ClientMessage msg : msgs) saveRedo(msg);
+  }
+
+  /**
+   * Get transaction saver cache
+   *
+   * @return transaction saver cache
+   */
+  public TransactionSaver getTransactionSaver() {
+    return transactionSaver;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/impl/ConnectionHelper.java b/src/main/java/org/mariadb/jdbc/client/impl/ConnectionHelper.java
new file mode 100644
index 0000000..514a117
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/impl/ConnectionHelper.java
@@ -0,0 +1,450 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.impl;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.sql.SQLException;
+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.client.Context;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.client.SocketHelper;
+import org.mariadb.jdbc.client.socket.Reader;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.client.socket.impl.SocketHandlerFunction;
+import org.mariadb.jdbc.client.socket.impl.SocketUtility;
+import org.mariadb.jdbc.export.SslMode;
+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.AuthenticationPlugin;
+import org.mariadb.jdbc.plugin.Credential;
+import org.mariadb.jdbc.plugin.CredentialPlugin;
+import org.mariadb.jdbc.plugin.TlsSocketPlugin;
+import org.mariadb.jdbc.plugin.authentication.AuthenticationPluginLoader;
+import org.mariadb.jdbc.plugin.tls.TlsSocketPluginLoader;
+import org.mariadb.jdbc.util.ConfigurableSocketFactory;
+import org.mariadb.jdbc.util.constants.Capabilities;
+
+/** Connection creation helper class */
+public final class ConnectionHelper {
+
+  private static final SocketHandlerFunction socketHandler;
+
+  static {
+    SocketHandlerFunction init;
+    try {
+      init = SocketUtility.getSocketHandler();
+    } catch (Throwable t) {
+      init = ConnectionHelper::standardSocket;
+    }
+    socketHandler = init;
+  }
+
+  /**
+   * Create socket accordingly to options.
+   *
+   * @param conf Url options
+   * @param hostAddress host ( mandatory but for named pipe / unix socket)
+   * @return a nex socket
+   * @throws IOException if connection error occur
+   * @throws SQLException in case of configuration error
+   */
+  public static Socket createSocket(Configuration conf, HostAddress hostAddress)
+      throws IOException, SQLException {
+    return socketHandler.apply(conf, hostAddress);
+  }
+
+  /**
+   * Use standard socket implementation.
+   *
+   * @param conf url options
+   * @param hostAddress host to connect
+   * @return socket
+   * @throws IOException in case of error establishing socket.
+   * @throws SQLException in case host is null
+   */
+  public static Socket standardSocket(Configuration conf, HostAddress hostAddress)
+      throws IOException, SQLException {
+    SocketFactory socketFactory;
+    String socketFactoryName = conf.socketFactory();
+    if (socketFactoryName != null) {
+      if (hostAddress == null) throw new SQLException("hostname must be set to connect socket");
+      try {
+        @SuppressWarnings("unchecked")
+        Class<? extends SocketFactory> socketFactoryClass =
+            (Class<? extends SocketFactory>) Class.forName(socketFactoryName);
+        Constructor<? extends SocketFactory> constructor = socketFactoryClass.getConstructor();
+        socketFactory = constructor.newInstance();
+        if (socketFactory instanceof ConfigurableSocketFactory) {
+          ((ConfigurableSocketFactory) socketFactory).setConfiguration(conf, hostAddress.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();
+  }
+
+  /**
+   * Connect socket
+   *
+   * @param conf configuration
+   * @param hostAddress host to connect
+   * @return socket
+   * @throws SQLException if hostname is required and not provided, or socket cannot be created
+   */
+  public static Socket connectSocket(final Configuration conf, final HostAddress hostAddress)
+      throws SQLException {
+    Socket socket;
+    try {
+      if (conf.pipe() == null && conf.localSocket() == null && hostAddress == null)
+        throw new SQLException(
+            "hostname must be set to connect socket if not using local socket or pipe");
+      socket = createSocket(conf, hostAddress);
+      SocketHelper.setSocketOption(conf, socket);
+      if (!socket.isConnected()) {
+        InetSocketAddress sockAddr =
+            conf.pipe() == null && conf.localSocket() == null
+                ? new InetSocketAddress(hostAddress.host, hostAddress.port)
+                : null;
+        socket.connect(sockAddr, conf.connectTimeout());
+      }
+      return socket;
+
+    } catch (IOException ioe) {
+      throw new SQLNonTransientConnectionException(
+          String.format(
+              "Socket fail to connect to host:%s. %s",
+              hostAddress == null ? conf.localSocket() : hostAddress, ioe.getMessage()),
+          "08000",
+          ioe);
+    }
+  }
+
+  /**
+   * Initialize client capability according to configuration and server capabilities.
+   *
+   * @param configuration configuration
+   * @param serverCapabilities server capabilities
+   * @param hostAddress host address server
+   * @return client capabilities
+   */
+  public static long initializeClientCapabilities(
+      final Configuration configuration,
+      final long serverCapabilities,
+      final HostAddress hostAddress) {
+    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.EXTENDED_TYPE_INFO;
+
+    // since skipping metadata is only available when using binary protocol,
+    // only set it when server permit it and using binary protocol
+    if (configuration.useServerPrepStmts()
+        && Boolean.parseBoolean(
+            configuration.nonMappedOptions().getProperty("enableSkipMeta", "true"))) {
+      capabilities |= Capabilities.CACHE_METADATA;
+    }
+
+    // remains for compatibility
+    if (Boolean.parseBoolean(
+        configuration.nonMappedOptions().getProperty("interactiveClient", "false"))) {
+      capabilities |= Capabilities.CLIENT_INTERACTIVE;
+    }
+
+    if (configuration.useBulkStmts()) {
+      capabilities |= Capabilities.STMT_BULK_OPERATIONS;
+    }
+
+    if (!configuration.useAffectedRows()) {
+      capabilities |= Capabilities.FOUND_ROWS;
+    }
+
+    if (configuration.allowMultiQueries()) {
+      capabilities |= Capabilities.MULTI_STATEMENTS;
+    }
+
+    if (configuration.allowLocalInfile()) {
+      capabilities |= Capabilities.LOCAL_FILES;
+    }
+
+    // useEof is a technical option
+    boolean deprecateEof =
+        Boolean.parseBoolean(configuration.nonMappedOptions().getProperty("deprecateEof", "true"));
+    if (deprecateEof) {
+      capabilities |= Capabilities.CLIENT_DEPRECATE_EOF;
+    }
+
+    if (configuration.useCompression()) {
+      capabilities |= Capabilities.COMPRESS;
+    }
+
+    // connect to database directly if not needed to be created, or if slave, since cannot be
+    // created
+    if (configuration.database() != null
+        && (!configuration.createDatabaseIfNotExist()
+            || (configuration.createDatabaseIfNotExist()
+                && (hostAddress != null && !hostAddress.primary)))) {
+      capabilities |= Capabilities.CONNECT_WITH_DB;
+    }
+
+    if (configuration.sslMode() != SslMode.DISABLE) {
+      capabilities |= Capabilities.SSL;
+    }
+    return capabilities & serverCapabilities;
+  }
+
+  /**
+   * 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
+    return (byte)
+        ((serverLanguage == 45 // utf8mb4_general_ci
+                || serverLanguage == 46 // utf8mb4_bin
+                || (serverLanguage >= 224 && serverLanguage <= 247))
+            ? serverLanguage
+            : 224); // UTF8MB4_UNICODE_CI;
+  }
+
+  /**
+   * Authentication swtich handler
+   *
+   * @param credential credential
+   * @param writer socket writer
+   * @param reader socket reader
+   * @param context connection context
+   * @throws IOException if any socket error occurs
+   * @throws SQLException if any other kind of issue occurs
+   */
+  public static void authenticationHandler(
+      Credential credential, Writer writer, Reader reader, Context context)
+      throws IOException, SQLException {
+
+    writer.permitTrace(true);
+    Configuration conf = context.getConf();
+    ReadableByteBuf buf = reader.readReusablePacket();
+
+    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);
+          AuthenticationPlugin authenticationPlugin =
+              AuthenticationPluginLoader.get(authSwitchPacket.getPlugin(), conf);
+
+          authenticationPlugin.initialize(
+              credential.getPassword(), authSwitchPacket.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.readLongLengthEncodedNotNull(); // skip affectedRows
+          buf.readLongLengthEncodedNotNull(); // skip insert id
+          // insertId
+          context.setServerStatus(buf.readShort());
+          break authentication_loop;
+
+        default:
+          throw context
+              .getExceptionFactory()
+              .create(
+                  "unexpected data during authentication (header=" + (buf.getUnsignedByte()),
+                  "08000");
+      }
+    }
+    writer.permitTrace(true);
+  }
+
+  /**
+   * Load user/password plugin if configured to.
+   *
+   * @param credentialPlugin configuration credential plugin
+   * @param configuration configuration
+   * @param hostAddress current connection host address
+   * @return credentials
+   * @throws SQLException if configured credential plugin fail
+   */
+  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());
+  }
+
+  /**
+   * Create SSL wrapper
+   *
+   * @param hostAddress host
+   * @param socket socket
+   * @param clientCapabilities client capabilities
+   * @param exchangeCharset connection charset
+   * @param context connection context
+   * @param writer socket writer
+   * @return SSLsocket
+   * @throws IOException if any socket error occurs
+   * @throws SQLException for any other kind of error
+   */
+  public static SSLSocket sslWrapper(
+      final HostAddress hostAddress,
+      final Socket socket,
+      long clientCapabilities,
+      final byte exchangeCharset,
+      Context context,
+      Writer writer)
+      throws IOException, SQLException {
+
+    Configuration conf = context.getConf();
+    if (conf.sslMode() != SslMode.DISABLE) {
+
+      if (!context.hasServerCapability(Capabilities.SSL)) {
+        throw 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 && hostAddress != null) {
+
+        SSLSession session = sslSocket.getSession();
+        try {
+          socketPlugin.verify(hostAddress.host, session, context.getThreadId());
+        } catch (SSLException ex) {
+          throw context
+              .getExceptionFactory()
+              .create(
+                  "SSL hostname verification failed : "
+                      + ex.getMessage()
+                      + "\nThis verification can be disabled using the sslMode to VERIFY_CA "
+                      + "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 &ge; 10.0.15 and &ge; 5.5.41 supports TLSv1.2 if compiled
+   * with openSSL (default). MySQL's community versions &ge; 5.7.10 is compiled 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<String> 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<String> 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);
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/impl/MultiPrimaryClient.java b/src/main/java/org/mariadb/jdbc/client/impl/MultiPrimaryClient.java
new file mode 100644
index 0000000..6f90c1d
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/impl/MultiPrimaryClient.java
@@ -0,0 +1,552 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.impl;
+
+import java.sql.*;
+import java.util.*;
+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.Client;
+import org.mariadb.jdbc.client.Completion;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.context.RedoContext;
+import org.mariadb.jdbc.export.ExceptionFactory;
+import org.mariadb.jdbc.export.Prepare;
+import org.mariadb.jdbc.message.ClientMessage;
+import org.mariadb.jdbc.message.client.ChangeDbPacket;
+import org.mariadb.jdbc.message.client.QueryPacket;
+import org.mariadb.jdbc.message.client.RedoableWithPrepareClientMessage;
+import org.mariadb.jdbc.util.constants.ConnectionState;
+import org.mariadb.jdbc.util.constants.ServerStatus;
+import org.mariadb.jdbc.util.log.Logger;
+import org.mariadb.jdbc.util.log.Loggers;
+
+/**
+ * Handling connection failing automatic reconnection transparently when possible for multi-master
+ * Topology.
+ *
+ * <p>remark: would have been better using proxy, but for AOT compilation, avoiding to using not
+ * supported proxy class.
+ */
+public class MultiPrimaryClient implements Client {
+  private static final Logger logger = Loggers.getLogger(MultiPrimaryClient.class);
+
+  /** temporary blacklisted hosts */
+  protected static final ConcurrentMap<HostAddress, Long> denyList = new ConcurrentHashMap<>();
+
+  /** denied timeout */
+  protected final long deniedListTimeout;
+
+  /** configuration */
+  protected final Configuration conf;
+
+  /** is connections explicitly closed */
+  protected boolean closed = false;
+
+  /** thread locker */
+  protected final ReentrantLock lock;
+
+  /** current client */
+  protected Client currentClient;
+
+  /**
+   * Constructor
+   *
+   * @param conf configuration
+   * @param lock thread locker
+   * @throws SQLException if fail to connect
+   */
+  public MultiPrimaryClient(Configuration conf, ReentrantLock lock) throws SQLException {
+    this.conf = conf;
+    this.lock = lock;
+    deniedListTimeout =
+        Long.parseLong(conf.nonMappedOptions().getProperty("deniedListTimeout", "60000"));
+    currentClient = connectHost(false, false);
+  }
+
+  /**
+   * Trying connecting server.
+   *
+   * <p>searching each connecting primary / replica connection not temporary denied until found one.
+   * searching in temporary denied host if not succeed, until reaching `retriesAllDown` attempts.
+   *
+   * @param readOnly must connect a replica / primary
+   * @param failFast must try only not denied server
+   * @return a valid connection client
+   * @throws SQLException if not succeed to create a connection.
+   */
+  protected Client connectHost(boolean readOnly, boolean failFast) throws SQLException {
+
+    Optional<HostAddress> host;
+    SQLNonTransientConnectionException lastSqle = null;
+    int maxRetries = conf.retriesAllDown();
+
+    while ((host = conf.haMode().getAvailableHost(conf.addresses(), denyList, !readOnly))
+            .isPresent()
+        && maxRetries > 0) {
+
+      try {
+        return conf.transactionReplay()
+            ? new ReplayClient(conf, host.get(), lock, false)
+            : new StandardClient(conf, host.get(), lock, false);
+      } catch (SQLNonTransientConnectionException sqle) {
+        lastSqle = sqle;
+        denyList.putIfAbsent(host.get(), System.currentTimeMillis() + deniedListTimeout);
+        maxRetries--;
+      }
+    }
+
+    if (failFast) {
+      throw (lastSqle != null)
+          ? lastSqle
+          : new SQLNonTransientConnectionException("all hosts are blacklisted");
+    }
+
+    // All server corresponding to type are in deny list
+    // return the one with lower denylist timeout
+    // (check that server is in conf, because denyList is shared for all instances)
+    if (denyList.entrySet().stream()
+        .noneMatch(e -> conf.addresses().contains(e.getKey()) && e.getKey().primary != readOnly))
+      throw new SQLNonTransientConnectionException(
+          String.format("No %s host defined", readOnly ? "replica" : "primary"));
+    while (maxRetries > 0) {
+      try {
+        host =
+            denyList.entrySet().stream()
+                .sorted(Map.Entry.comparingByValue())
+                .filter(
+                    e -> conf.addresses().contains(e.getKey()) && e.getKey().primary != readOnly)
+                .findFirst()
+                .map(Map.Entry::getKey);
+        if (host.isPresent()) {
+          Client client =
+              conf.transactionReplay()
+                  ? new ReplayClient(conf, host.get(), lock, false)
+                  : new StandardClient(conf, host.get(), lock, false);
+          denyList.remove(host.get());
+          return client;
+        }
+        maxRetries--;
+      } catch (SQLNonTransientConnectionException sqle) {
+        lastSqle = sqle;
+        host.ifPresent(
+            hostAddress ->
+                denyList.putIfAbsent(hostAddress, System.currentTimeMillis() + deniedListTimeout));
+        maxRetries--;
+        if (maxRetries > 0) {
+          try {
+            // wait 250ms before looping through
+            Thread.sleep(250);
+          } catch (InterruptedException interrupted) {
+            // interrupted, continue
+          }
+        }
+      }
+    }
+
+    throw lastSqle;
+  }
+
+  /**
+   * Connection loop
+   *
+   * @return client connection
+   * @throws SQLException if fail to connect
+   */
+  protected Client reConnect() throws SQLException {
+
+    denyList.putIfAbsent(
+        currentClient.getHostAddress(), System.currentTimeMillis() + deniedListTimeout);
+    logger.info("Connection error on {}", currentClient.getHostAddress());
+    try {
+      Client oldClient = currentClient;
+      // remove cached prepare from existing server prepare statement
+      oldClient.getContext().resetPrepareCache();
+
+      currentClient = connectHost(false, false);
+      syncNewState(oldClient);
+      return oldClient;
+
+    } catch (SQLNonTransientConnectionException sqle) {
+      currentClient = null;
+      closed = true;
+      throw sqle;
+    }
+  }
+
+  /**
+   * Execute transaction replay if in transaction and configured for it, throw an exception if not
+   *
+   * @param oldClient previous client
+   * @param canRedo if command can be redo even if not in transaction
+   * @throws SQLException if not able to replay
+   */
+  protected void replayIfPossible(Client oldClient, boolean canRedo) throws SQLException {
+    // oldClient is only valued if this occurs on master.
+    if (oldClient != null) {
+      if ((oldClient.getContext().getServerStatus() & ServerStatus.IN_TRANSACTION) > 0) {
+        if (conf.transactionReplay()) {
+          executeTransactionReplay(oldClient);
+        } else {
+          // transaction is lost, 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. In"
+                      + " progress transaction was lost",
+                  oldClient.getHostAddress()),
+              "25S03");
+        }
+      } else if (!canRedo) {
+        // no transaction, 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",
+                oldClient.getHostAddress()),
+            "25S03");
+      }
+    }
+  }
+
+  /**
+   * Execute transaction replay
+   *
+   * @param oldCli previous client
+   * @throws SQLException if not able to replay
+   */
+  protected void executeTransactionReplay(Client oldCli) throws SQLException {
+    // transaction replay
+    RedoContext ctx = (RedoContext) oldCli.getContext();
+    if (ctx.getTransactionSaver().isDirty()) {
+      ctx.getTransactionSaver().clear();
+      throw new SQLTransientConnectionException(
+          String.format(
+              "Driver has reconnect connection after a communications link failure with %s. In"
+                  + " progress transaction was too big to be replayed, and was lost",
+              oldCli.getHostAddress()),
+          "25S03");
+    }
+    ((ReplayClient) currentClient).transactionReplay(ctx.getTransactionSaver());
+  }
+
+  /**
+   * Synchronized previous and new client states.
+   *
+   * @param oldCli previous client
+   * @throws SQLException if error occurs
+   */
+  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")),
+            true);
+      }
+    }
+
+    if ((oldCtx.getStateFlag() & ConnectionState.STATE_DATABASE) > 0
+        && !Objects.equals(currentClient.getContext().getDatabase(), oldCtx.getDatabase())) {
+      currentClient.getContext().addStateFlag(ConnectionState.STATE_DATABASE);
+      if (oldCtx.getDatabase() != null) {
+        currentClient.execute(new ChangeDbPacket(oldCtx.getDatabase()), true);
+      }
+      currentClient.getContext().setDatabase(oldCtx.getDatabase());
+    }
+
+    if ((oldCtx.getStateFlag() & ConnectionState.STATE_NETWORK_TIMEOUT) > 0) {
+      currentClient.setSocketTimeout(oldCli.getSocketTimeout());
+    }
+
+    if ((oldCtx.getStateFlag() & ConnectionState.STATE_READ_ONLY) > 0
+        && !currentClient.getHostAddress().primary
+        && currentClient.getContext().getVersion().versionGreaterOrEqual(5, 6, 5)) {
+      currentClient.execute(new QueryPacket("SET SESSION TRANSACTION READ ONLY"), true);
+    }
+
+    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;
+      }
+      currentClient
+          .getContext()
+          .setTransactionIsolationLevel(oldCtx.getTransactionIsolationLevel());
+      currentClient.execute(new QueryPacket(query), true);
+    }
+  }
+
+  @Override
+  public List<Completion> execute(ClientMessage message, boolean canRedo) throws SQLException {
+    return execute(
+        message,
+        null,
+        0,
+        0L,
+        ResultSet.CONCUR_READ_ONLY,
+        ResultSet.TYPE_FORWARD_ONLY,
+        false,
+        canRedo);
+  }
+
+  @Override
+  public List<Completion> execute(
+      ClientMessage message, org.mariadb.jdbc.Statement stmt, boolean canRedo) throws SQLException {
+    return execute(
+        message,
+        stmt,
+        0,
+        0L,
+        ResultSet.CONCUR_READ_ONLY,
+        ResultSet.TYPE_FORWARD_ONLY,
+        false,
+        canRedo);
+  }
+
+  @Override
+  public List<Completion> execute(
+      ClientMessage message,
+      Statement stmt,
+      int fetchSize,
+      long maxRows,
+      int resultSetConcurrency,
+      int resultSetType,
+      boolean closeOnCompletion,
+      boolean canRedo)
+      throws SQLException {
+
+    if (closed) {
+      throw new SQLNonTransientConnectionException("Connection is closed", "08000", 1220);
+    }
+
+    try {
+      return currentClient.execute(
+          message,
+          stmt,
+          fetchSize,
+          maxRows,
+          resultSetConcurrency,
+          resultSetType,
+          closeOnCompletion,
+          canRedo);
+    } catch (SQLNonTransientConnectionException e) {
+      HostAddress hostAddress = currentClient.getHostAddress();
+      Client oldClient = reConnect();
+
+      if (message instanceof QueryPacket && ((QueryPacket) message).isCommit()) {
+        throw new SQLTransientConnectionException(
+            String.format(
+                "Driver has reconnect connection after a communications failure with %s during a"
+                    + " COMMIT statement",
+                hostAddress),
+            "25S03");
+      }
+
+      replayIfPossible(oldClient, canRedo);
+
+      if (message instanceof RedoableWithPrepareClientMessage) {
+        ((RedoableWithPrepareClientMessage) message).rePrepare(currentClient);
+      }
+      return currentClient.execute(
+          message,
+          stmt,
+          fetchSize,
+          maxRows,
+          resultSetConcurrency,
+          resultSetType,
+          closeOnCompletion,
+          canRedo);
+    }
+  }
+
+  @Override
+  public List<Completion> executePipeline(
+      ClientMessage[] messages,
+      Statement stmt,
+      int fetchSize,
+      long maxRows,
+      int resultSetConcurrency,
+      int resultSetType,
+      boolean closeOnCompletion,
+      boolean canRedo)
+      throws SQLException {
+    if (closed) {
+      throw new SQLNonTransientConnectionException("Connection is closed", "08000", 1220);
+    }
+
+    try {
+      return currentClient.executePipeline(
+          messages,
+          stmt,
+          fetchSize,
+          maxRows,
+          resultSetConcurrency,
+          resultSetType,
+          closeOnCompletion,
+          canRedo);
+    } catch (SQLException e) {
+      if (e instanceof SQLNonTransientConnectionException
+          || (e.getCause() != null && e.getCause() instanceof SQLNonTransientConnectionException)) {
+        Client oldClient = reConnect();
+        replayIfPossible(oldClient, canRedo);
+        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,
+            canRedo);
+      }
+      throw e;
+    }
+  }
+
+  @Override
+  public void readStreamingResults(
+      List<Completion> 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) {
+      try {
+        reConnect();
+      } catch (SQLException e2) {
+        throw getExceptionFactory()
+            .create("Socket error during result streaming", e2.getSQLState(), e2);
+      }
+      throw getExceptionFactory().create("Socket error during result streaming", "HY000", e);
+    }
+  }
+
+  @Override
+  public void closePrepare(Prepare prepare) throws SQLException {
+    if (closed) {
+      throw new SQLNonTransientConnectionException("Connection is closed", "08000", 1220);
+    }
+
+    try {
+      currentClient.closePrepare(prepare);
+    } catch (SQLNonTransientConnectionException e) {
+      reConnect();
+    }
+  }
+
+  @Override
+  public void abort(Executor executor) throws SQLException {
+    if (closed) {
+      throw new SQLNonTransientConnectionException("Connection is closed", "08000", 1220);
+    }
+    currentClient.abort(executor);
+  }
+
+  @Override
+  public void close() throws SQLException {
+    closed = true;
+    currentClient.close();
+  }
+
+  @Override
+  public void setReadOnly(boolean readOnly) throws SQLException {
+    if (closed) {
+      throw new SQLNonTransientConnectionException("Connection is closed", "08000", 1220);
+    }
+  }
+
+  @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 HostAddress getHostAddress() {
+    return currentClient.getHostAddress();
+  }
+
+  public boolean isPrimary() {
+    return true;
+  }
+
+  @Override
+  public void reset() {
+    currentClient.getContext().resetStateFlag();
+    currentClient.getContext().resetPrepareCache();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/impl/MultiPrimaryReplicaClient.java b/src/main/java/org/mariadb/jdbc/client/impl/MultiPrimaryReplicaClient.java
new file mode 100644
index 0000000..8244911
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/impl/MultiPrimaryReplicaClient.java
@@ -0,0 +1,357 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.impl;
+
+import java.sql.SQLException;
+import java.sql.SQLNonTransientConnectionException;
+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.Client;
+import org.mariadb.jdbc.client.Completion;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.export.ExceptionFactory;
+import org.mariadb.jdbc.export.Prepare;
+import org.mariadb.jdbc.message.ClientMessage;
+import org.mariadb.jdbc.util.log.Logger;
+import org.mariadb.jdbc.util.log.Loggers;
+
+/**
+ * Handling connection failing automatic reconnection transparently when possible for replication
+ * Topology.
+ *
+ * <p>remark: would have been better using proxy, but for AOT compilation, avoiding to using not
+ * supported proxy class.
+ */
+public class MultiPrimaryReplicaClient extends MultiPrimaryClient {
+  private static final Logger logger = Loggers.getLogger(MultiPrimaryReplicaClient.class);
+
+  /** timeout before retrying to reconnect failing host */
+  protected long waitTimeout;
+
+  private Client replicaClient;
+  private Client primaryClient;
+  private boolean requestReadOnly;
+  private long nextTryReplica = -1;
+  private long nextTryPrimary = -1;
+
+  /**
+   * Constructor
+   *
+   * @param conf configuration
+   * @param lock thread locker
+   * @throws SQLException if any error occurs
+   */
+  public MultiPrimaryReplicaClient(Configuration conf, ReentrantLock lock) throws SQLException {
+    super(conf, lock);
+    primaryClient = currentClient;
+    waitTimeout =
+        Long.parseLong(conf.nonMappedOptions().getProperty("waitReconnectTimeout", "30000"));
+    try {
+      replicaClient = connectHost(true, false);
+    } catch (SQLException e) {
+      replicaClient = null;
+      nextTryReplica = System.currentTimeMillis() + waitTimeout;
+    }
+  }
+
+  private void reconnectIfNeeded() {
+    if (!closed) {
+
+      // try to reconnect primary
+      if (primaryClient == null && nextTryPrimary < System.currentTimeMillis()) {
+        try {
+          primaryClient = connectHost(false, true);
+          nextTryPrimary = -1;
+        } catch (SQLException e) {
+          nextTryPrimary = System.currentTimeMillis() + waitTimeout;
+        }
+      }
+
+      // try to reconnect replica
+      if (replicaClient == null && nextTryReplica < System.currentTimeMillis()) {
+        try {
+          replicaClient = connectHost(true, true);
+          nextTryReplica = -1;
+          if (requestReadOnly) {
+            syncNewState(primaryClient);
+            currentClient = replicaClient;
+          }
+        } catch (SQLException e) {
+          nextTryReplica = System.currentTimeMillis() + waitTimeout;
+        }
+      }
+    }
+  }
+
+  /**
+   * Reconnect connection, trying to continue transparently if possible. Different possible cases :
+   * replica fails, then reconnect to replica or to master if no replica available
+   *
+   * <p>if reconnect succeed on replica / use master, no problem, continuing without interruption //
+   * if reconnect primary, then replay transaction / throw exception if was in transaction.
+   *
+   * @throws SQLException if exception
+   */
+  @Override
+  protected Client reConnect() throws SQLException {
+    denyList.putIfAbsent(
+        currentClient.getHostAddress(), System.currentTimeMillis() + deniedListTimeout);
+    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().resetPrepareCache();
+
+      try {
+        currentClient = connectHost(requestReadOnly, requestReadOnly);
+        if (requestReadOnly) {
+          nextTryReplica = -1;
+          replicaClient = currentClient;
+        } else {
+          nextTryPrimary = -1;
+          primaryClient = currentClient;
+        }
+
+      } catch (SQLNonTransientConnectionException e) {
+        if (requestReadOnly) {
+          nextTryReplica = System.currentTimeMillis() + waitTimeout;
+          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, 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 reconnect succeed on replica / use master, no problem, continuing without interruption
+      // if reconnect primary, then replay transaction / throw exception if was in transaction.
+      return requestReadOnly ? null : oldClient;
+
+    } catch (SQLNonTransientConnectionException sqle) {
+      currentClient = null;
+      closed = true;
+      if (replicaClient != null) {
+        replicaClient.close();
+      }
+      throw sqle;
+    }
+  }
+
+  @Override
+  public List<Completion> execute(
+      ClientMessage message,
+      Statement stmt,
+      int fetchSize,
+      long maxRows,
+      int resultSetConcurrency,
+      int resultSetType,
+      boolean closeOnCompletion,
+      boolean canRedo)
+      throws SQLException {
+    reconnectIfNeeded();
+    return super.execute(
+        message,
+        stmt,
+        fetchSize,
+        maxRows,
+        resultSetConcurrency,
+        resultSetType,
+        closeOnCompletion,
+        canRedo);
+  }
+
+  @Override
+  public List<Completion> executePipeline(
+      ClientMessage[] messages,
+      Statement stmt,
+      int fetchSize,
+      long maxRows,
+      int resultSetConcurrency,
+      int resultSetType,
+      boolean closeOnCompletion,
+      boolean canRedo)
+      throws SQLException {
+    reconnectIfNeeded();
+    return super.executePipeline(
+        messages,
+        stmt,
+        fetchSize,
+        maxRows,
+        resultSetConcurrency,
+        resultSetType,
+        closeOnCompletion,
+        canRedo);
+  }
+
+  @Override
+  public void readStreamingResults(
+      List<Completion> 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(Prepare 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) {
+      closed = true;
+      try {
+        if (primaryClient != null) primaryClient.close();
+      } catch (SQLException e) {
+        // eat
+      }
+      try {
+        if (replicaClient != null) 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;
+          syncNewState(primaryClient);
+        } else if (nextTryReplica < System.currentTimeMillis()) {
+          try {
+            replicaClient = connectHost(true, true);
+            currentClient = replicaClient;
+            syncNewState(primaryClient);
+          } catch (SQLException e) {
+            nextTryReplica = System.currentTimeMillis() + waitTimeout;
+          }
+        }
+      }
+    } else {
+      // changed ?
+      if (requestReadOnly) {
+        if (primaryClient != null) {
+          currentClient = primaryClient;
+          syncNewState(replicaClient);
+        } else if (nextTryPrimary < System.currentTimeMillis()) {
+          try {
+            primaryClient = connectHost(false, false);
+            nextTryPrimary = -1;
+            syncNewState(replicaClient);
+          } catch (SQLException e) {
+            nextTryPrimary = System.currentTimeMillis() + waitTimeout;
+            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 HostAddress getHostAddress() {
+    reconnectIfNeeded();
+    return super.getHostAddress();
+  }
+
+  public boolean isPrimary() {
+    return getHostAddress().primary;
+  }
+
+  @Override
+  public void reset() {
+    if (replicaClient != null) {
+      replicaClient.getContext().resetStateFlag();
+      replicaClient.getContext().resetPrepareCache();
+    }
+    if (primaryClient != null) {
+      primaryClient.getContext().resetStateFlag();
+      primaryClient.getContext().resetPrepareCache();
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/impl/PrepareCache.java b/src/main/java/org/mariadb/jdbc/client/impl/PrepareCache.java
new file mode 100644
index 0000000..1500643
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/impl/PrepareCache.java
@@ -0,0 +1,93 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.impl;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.mariadb.jdbc.ServerPreparedStatement;
+import org.mariadb.jdbc.export.Prepare;
+import org.mariadb.jdbc.message.server.CachedPrepareResultPacket;
+import org.mariadb.jdbc.message.server.PrepareResultPacket;
+
+/** LRU prepare cache */
+public final class PrepareCache extends LinkedHashMap<String, CachedPrepareResultPacket>
+    implements org.mariadb.jdbc.client.PrepareCache {
+
+  private static final long serialVersionUID = -8922905563713952695L;
+  /** cache maximum size */
+  private final int maxSize;
+  /** client */
+  private final StandardClient con;
+
+  /**
+   * LRU prepare cache constructor
+   *
+   * @param size cache size
+   * @param con client
+   */
+  public PrepareCache(int size, StandardClient con) {
+    super(size, .75f, true);
+    this.maxSize = size;
+    this.con = con;
+  }
+
+  @Override
+  public boolean removeEldestEntry(Map.Entry<String, CachedPrepareResultPacket> eldest) {
+    if (this.size() > maxSize) {
+      eldest.getValue().unCache(con);
+      return true;
+    }
+    return false;
+  }
+
+  public synchronized Prepare get(String key, ServerPreparedStatement preparedStatement) {
+    CachedPrepareResultPacket prepare = super.get(key);
+    if (prepare != null && preparedStatement != null) {
+      prepare.incrementUse(preparedStatement);
+    }
+    return prepare;
+  }
+
+  public synchronized Prepare put(
+      String key, Prepare 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);
+      ((CachedPrepareResultPacket) result).unCache(con);
+      return cached;
+    }
+
+    if (((CachedPrepareResultPacket) result).cache()) {
+      ((CachedPrepareResultPacket) result).incrementUse(preparedStatement);
+      super.put(key, (CachedPrepareResultPacket) result);
+    }
+    return null;
+  }
+
+  public CachedPrepareResultPacket get(Object key) {
+    throw new IllegalStateException("not available method");
+  }
+
+  /**
+   * NOT USED
+   *
+   * @param key key
+   * @param result results
+   * @return will throw an exception
+   */
+  @SuppressWarnings("unused")
+  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/client/impl/ReplayClient.java b/src/main/java/org/mariadb/jdbc/client/impl/ReplayClient.java
new file mode 100644
index 0000000..7278bd8
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/impl/ReplayClient.java
@@ -0,0 +1,165 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.impl;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.concurrent.locks.ReentrantLock;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.HostAddress;
+import org.mariadb.jdbc.client.Completion;
+import org.mariadb.jdbc.client.context.RedoContext;
+import org.mariadb.jdbc.export.MaxAllowedPacketException;
+import org.mariadb.jdbc.export.Prepare;
+import org.mariadb.jdbc.message.ClientMessage;
+import org.mariadb.jdbc.message.client.*;
+import org.mariadb.jdbc.message.server.PrepareResultPacket;
+import org.mariadb.jdbc.util.log.Logger;
+import org.mariadb.jdbc.util.log.Loggers;
+
+/** Replay client wrapper */
+public class ReplayClient extends StandardClient {
+  private static final Logger logger = Loggers.getLogger(ReplayClient.class);
+
+  /**
+   * Constructor
+   *
+   * @param conf configuration
+   * @param hostAddress host
+   * @param lock thread lock object
+   * @param skipPostCommands must skip connection post commands
+   * @throws SQLException if connection fails
+   */
+  public ReplayClient(
+      Configuration conf, HostAddress hostAddress, ReentrantLock lock, boolean skipPostCommands)
+      throws SQLException {
+    super(conf, hostAddress, lock, skipPostCommands);
+  }
+
+  @Override
+  public int sendQuery(ClientMessage message) throws SQLException {
+    checkNotClosed();
+    try {
+      if (message instanceof RedoableClientMessage)
+        ((RedoableClientMessage) message).ensureReplayable(context);
+      return message.encode(writer, context);
+    } catch (IOException ioException) {
+      if (ioException instanceof MaxAllowedPacketException) {
+        if (((MaxAllowedPacketException) ioException).isMustReconnect()) {
+          destroySocket();
+          throw exceptionFactory
+              .withSql(message.description())
+              .create(
+                  "Packet too big for current server max_allowed_packet value",
+                  "08000",
+                  ioException);
+        }
+        throw exceptionFactory
+            .withSql(message.description())
+            .create(
+                "Packet too big for current server max_allowed_packet value", "HZ000", ioException);
+      }
+      destroySocket();
+      throw exceptionFactory
+          .withSql(message.description())
+          .create("Socket error", "08000", ioException);
+    }
+  }
+
+  @Override
+  public List<Completion> executePipeline(
+      ClientMessage[] messages,
+      org.mariadb.jdbc.Statement stmt,
+      int fetchSize,
+      long maxRows,
+      int resultSetConcurrency,
+      int resultSetType,
+      boolean closeOnCompletion,
+      boolean canRedo)
+      throws SQLException {
+    List<Completion> res =
+        super.executePipeline(
+            messages,
+            stmt,
+            fetchSize,
+            maxRows,
+            resultSetConcurrency,
+            resultSetType,
+            closeOnCompletion,
+            canRedo);
+    ((RedoContext) context).saveRedo(messages);
+    return res;
+  }
+
+  @Override
+  public List<Completion> execute(
+      ClientMessage message,
+      org.mariadb.jdbc.Statement stmt,
+      int fetchSize,
+      long maxRows,
+      int resultSetConcurrency,
+      int resultSetType,
+      boolean closeOnCompletion,
+      boolean canRedo)
+      throws SQLException {
+    List<Completion> completions =
+        super.execute(
+            message,
+            stmt,
+            fetchSize,
+            maxRows,
+            resultSetConcurrency,
+            resultSetType,
+            closeOnCompletion,
+            canRedo);
+    ((RedoContext) context).saveRedo(message);
+    return completions;
+  }
+
+  /**
+   * Replay transaction, re-prepare server command if needed
+   *
+   * @param transactionSaver transaction cache
+   * @throws SQLException if any error occurs
+   */
+  public void transactionReplay(TransactionSaver transactionSaver) throws SQLException {
+    RedoableClientMessage[] buffers = transactionSaver.getBuffers();
+    try {
+      // replay all but last
+      Prepare prepare;
+      for (int i = 0; i < transactionSaver.getIdx(); i++) {
+        RedoableClientMessage querySaver = buffers[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);
+            logger.info("replayed command after failover: " + preparePacket.description());
+          }
+          responseNo = querySaver.reEncode(writer, context, prepare);
+        } else {
+          responseNo = querySaver.reEncode(writer, context, null);
+        }
+        logger.info("replayed command after failover: " + querySaver.description());
+        for (int j = 0; j < responseNo; j++) {
+          readResponse(querySaver);
+        }
+      }
+    } catch (IOException e) {
+      throw context
+          .getExceptionFactory()
+          .create("Socket error during transaction replay", "08000", e);
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/impl/StandardClient.java b/src/main/java/org/mariadb/jdbc/client/impl/StandardClient.java
new file mode 100644
index 0000000..d67b79e
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/impl/StandardClient.java
@@ -0,0 +1,1016 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.impl;
+
+import java.io.*;
+import java.net.Socket;
+import java.net.SocketException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.SQLNonTransientConnectionException;
+import java.time.DateTimeException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.util.*;
+import java.util.concurrent.Executor;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Collectors;
+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.Client;
+import org.mariadb.jdbc.client.Completion;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.client.context.BaseContext;
+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.client.socket.Reader;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.client.socket.impl.*;
+import org.mariadb.jdbc.client.util.MutableByte;
+import org.mariadb.jdbc.export.ExceptionFactory;
+import org.mariadb.jdbc.export.MaxAllowedPacketException;
+import org.mariadb.jdbc.export.Prepare;
+import org.mariadb.jdbc.message.ClientMessage;
+import org.mariadb.jdbc.message.client.*;
+import org.mariadb.jdbc.message.server.ErrorPacket;
+import org.mariadb.jdbc.message.server.InitialHandshakePacket;
+import org.mariadb.jdbc.message.server.PrepareResultPacket;
+import org.mariadb.jdbc.plugin.Credential;
+import org.mariadb.jdbc.plugin.CredentialPlugin;
+import org.mariadb.jdbc.util.Security;
+import org.mariadb.jdbc.util.constants.Capabilities;
+import org.mariadb.jdbc.util.constants.ServerStatus;
+import org.mariadb.jdbc.util.log.Logger;
+import org.mariadb.jdbc.util.log.Loggers;
+
+/** Connection client */
+public class StandardClient implements Client, AutoCloseable {
+  private static final Logger logger = Loggers.getLogger(StandardClient.class);
+  private final Socket socket;
+  private final MutableByte sequence = new MutableByte();
+  private final MutableByte compressionSequence = new MutableByte();
+  private final ReentrantLock lock;
+  private final Configuration conf;
+  private final HostAddress hostAddress;
+  private boolean closed = false;
+  private Reader reader;
+  private org.mariadb.jdbc.Statement streamStmt = null;
+  private ClientMessage streamMsg = null;
+  private int socketTimeout;
+  private final boolean disablePipeline;
+
+  /** connection context */
+  protected Context context;
+
+  /** connection exception factory */
+  protected final ExceptionFactory exceptionFactory;
+
+  /** packet writer */
+  protected Writer writer;
+
+  /**
+   * Constructor
+   *
+   * @param conf configuration
+   * @param hostAddress host
+   * @param lock thread locker
+   * @param skipPostCommands must connection post command be skipped
+   * @throws SQLException if connection fails
+   */
+  public StandardClient(
+      Configuration conf, HostAddress hostAddress, ReentrantLock lock, boolean skipPostCommands)
+      throws SQLException {
+
+    this.conf = conf;
+    this.lock = lock;
+    this.hostAddress = hostAddress;
+    this.exceptionFactory = new ExceptionFactory(conf, hostAddress);
+    this.disablePipeline = conf.disablePipeline();
+
+    String host = hostAddress != null ? hostAddress.host : null;
+    this.socketTimeout = conf.socketTimeout();
+    this.socket = ConnectionHelper.connectSocket(conf, hostAddress);
+
+    try {
+      // **********************************************************************
+      // creating socket
+      // **********************************************************************
+      OutputStream out = new BufferedOutputStream(socket.getOutputStream(), 16384);
+      InputStream in =
+          conf.useReadAheadInput()
+              ? new ReadAheadBufferedStream(socket.getInputStream())
+              : new BufferedInputStream(socket.getInputStream(), 16384);
+
+      assignStream(out, in, conf, null);
+
+      if (conf.socketTimeout() > 0) setSocketTimeout(conf.socketTimeout());
+
+      // read server handshake
+      ReadableByteBuf buf = reader.readReusablePacket(logger.isTraceEnabled());
+      if (buf.getByte() == -1) {
+        ErrorPacket errorPacket = new ErrorPacket(buf, null);
+        throw this.exceptionFactory.create(
+            errorPacket.getMessage(), errorPacket.getSqlState(), errorPacket.getErrorCode());
+      }
+      final InitialHandshakePacket handshake = InitialHandshakePacket.decode(buf);
+
+      this.exceptionFactory.setThreadId(handshake.getThreadId());
+      long clientCapabilities =
+          ConnectionHelper.initializeClientCapabilities(
+              conf, handshake.getCapabilities(), hostAddress);
+      this.context =
+          conf.transactionReplay()
+              ? new RedoContext(
+                  handshake,
+                  clientCapabilities,
+                  conf,
+                  this.exceptionFactory,
+                  new PrepareCache(conf.prepStmtCacheSize(), this))
+              : new BaseContext(
+                  handshake,
+                  clientCapabilities,
+                  conf,
+                  this.exceptionFactory,
+                  conf.cachePrepStmts() ? new PrepareCache(conf.prepStmtCacheSize(), this) : null);
+
+      this.reader.setServerThreadId(handshake.getThreadId(), hostAddress);
+      this.writer.setServerThreadId(handshake.getThreadId(), hostAddress);
+
+      byte exchangeCharset = ConnectionHelper.decideLanguage(handshake);
+
+      // **********************************************************************
+      // changing to SSL socket if needed
+      // **********************************************************************
+      SSLSocket sslSocket =
+          ConnectionHelper.sslWrapper(
+              hostAddress, socket, clientCapabilities, exchangeCharset, context, writer);
+
+      if (sslSocket != null) {
+        out = new BufferedOutputStream(sslSocket.getOutputStream(), 16384);
+        in =
+            conf.useReadAheadInput()
+                ? new ReadAheadBufferedStream(sslSocket.getInputStream())
+                : new BufferedInputStream(sslSocket.getInputStream(), 16384);
+        assignStream(out, in, conf, handshake.getThreadId());
+      }
+
+      // **********************************************************************
+      // handling authentication
+      // **********************************************************************
+      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(
+              credential,
+              authenticationPluginType,
+              context.getSeed(),
+              conf,
+              host,
+              clientCapabilities,
+              exchangeCharset)
+          .encode(writer, context);
+      writer.flush();
+
+      ConnectionHelper.authenticationHandler(credential, writer, reader, context);
+
+      // **********************************************************************
+      // activate compression if required
+      // **********************************************************************
+      if ((clientCapabilities & Capabilities.COMPRESS) != 0) {
+        assignStream(
+            new CompressOutputStream(out, compressionSequence),
+            new CompressInputStream(in, compressionSequence),
+            conf,
+            handshake.getThreadId());
+      }
+
+      // **********************************************************************
+      // post queries
+      // **********************************************************************
+      if (!skipPostCommands) {
+        postConnectionQueries();
+      }
+
+    } 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(OutputStream out, InputStream in, Configuration conf, Long threadId) {
+    this.writer =
+        new PacketWriter(
+            out, conf.maxQuerySizeToLog(), conf.maxAllowedPacket(), sequence, compressionSequence);
+    this.writer.setServerThreadId(threadId, hostAddress);
+
+    this.reader = new PacketReader(in, conf, sequence);
+    this.reader.setServerThreadId(threadId, hostAddress);
+  }
+
+  /** Closing socket in case of Connection error after socket creation. */
+  protected 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
+    }
+  }
+
+  /**
+   * load server timezone and ensure this corresponds to client timezone
+   *
+   * @throws SQLException if any socket error.
+   */
+  private String handleTimezone() throws SQLException {
+    if (!"disable".equalsIgnoreCase(conf.timezone())) {
+      String timeZone = null;
+      try {
+        Result res =
+            (Result)
+                execute(new QueryPacket("SELECT @@time_zone, @@system_time_zone"), true).get(0);
+        res.next();
+        timeZone = res.getString(1);
+        if ("SYSTEM".equals(timeZone)) {
+          timeZone = res.getString(2);
+        }
+      } catch (SQLException sqle) {
+        Result res =
+            (Result)
+                execute(
+                        new QueryPacket(
+                            "SHOW VARIABLES WHERE Variable_name in ("
+                                + "'system_time_zone',"
+                                + "'time_zone')"),
+                        true)
+                    .get(0);
+        String systemTimeZone = null;
+        while (res.next()) {
+          if ("system_time_zone".equals(res.getString(1))) {
+            systemTimeZone = res.getString(2);
+          } else {
+            timeZone = res.getString(2);
+          }
+        }
+        if ("SYSTEM".equals(timeZone)) {
+          timeZone = systemTimeZone;
+        }
+      }
+      return timeZone;
+    }
+    return null;
+  }
+
+  private void postConnectionQueries() throws SQLException {
+    List<String> commands = new ArrayList<>();
+
+    List<String> galeraAllowedStates =
+        conf.galeraAllowedState() == null
+            ? Collections.emptyList()
+            : Arrays.asList(conf.galeraAllowedState().split(","));
+
+    if (hostAddress != null
+        && Boolean.TRUE.equals(hostAddress.primary)
+        && !galeraAllowedStates.isEmpty()) {
+      commands.add("show status like 'wsrep_local_state'");
+    }
+
+    String serverTz = conf.timezone() != null ? handleTimezone() : null;
+    String sessionVariableQuery = createSessionVariableQuery(serverTz);
+    if (sessionVariableQuery != null) commands.add(sessionVariableQuery);
+
+    if (hostAddress != null
+        && !hostAddress.primary
+        && context.getVersion().versionGreaterOrEqual(5, 6, 5)) {
+      commands.add("SET SESSION TRANSACTION READ ONLY");
+    }
+
+    if (conf.database() != null
+        && conf.createDatabaseIfNotExist()
+        && (hostAddress == null || hostAddress.primary)) {
+      String escapedDb = conf.database().replace("`", "``");
+      commands.add(String.format("CREATE DATABASE IF NOT EXISTS `%s`", escapedDb));
+      commands.add(String.format("USE `%s`", escapedDb));
+    }
+
+    if (conf.initSql() != null) {
+      commands.add(conf.initSql());
+    }
+
+    if (conf.nonMappedOptions().containsKey("initSql")) {
+      String[] initialCommands = conf.nonMappedOptions().get("initSql").toString().split(";");
+      for (String cmd : initialCommands) {
+        commands.add(cmd);
+      }
+    }
+
+    if (!commands.isEmpty()) {
+      try {
+        List<Completion> res;
+        ClientMessage[] msgs = new ClientMessage[commands.size()];
+        for (int i = 0; i < commands.size(); i++) {
+          msgs[i] = new QueryPacket(commands.get(i));
+        }
+        res =
+            executePipeline(
+                msgs,
+                null,
+                0,
+                0L,
+                ResultSet.CONCUR_READ_ONLY,
+                ResultSet.TYPE_FORWARD_ONLY,
+                false,
+                true);
+
+        if (hostAddress != null
+            && Boolean.TRUE.equals(hostAddress.primary)
+            && !galeraAllowedStates.isEmpty()) {
+          ResultSet rs = (ResultSet) res.get(0);
+          if (rs.next()) {
+            if (!galeraAllowedStates.contains(rs.getString(2))) {
+              throw exceptionFactory.create(
+                  String.format("fail to validate Galera state (State is %s)", rs.getString(2)));
+            }
+          } else {
+            throw exceptionFactory.create(
+                "fail to validate Galera state (unknown 'wsrep_local_state' state)");
+          }
+          res.remove(0);
+        }
+
+      } catch (SQLException sqlException) {
+
+        if (conf.timezone() != null && !"disable".equalsIgnoreCase(conf.timezone())) {
+          // timezone is not valid
+          throw exceptionFactory.create(
+              String.format(
+                  "Setting configured timezone '%s' fail on server.\n"
+                      + "Look 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()),
+              "HY000",
+              sqlException);
+        }
+        throw exceptionFactory.create("Initialization command fail", "08000", sqlException);
+      }
+    }
+  }
+
+  /**
+   * Create session variable if configuration requires additional commands.
+   *
+   * @param serverTz server timezone
+   * @return sql setting session command
+   */
+  public String createSessionVariableQuery(String serverTz) {
+    // 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
+    List<String> sessionCommands = new ArrayList<>();
+    if (conf.autocommit() != null) {
+      sessionCommands.add("autocommit=" + (conf.autocommit() ? "1" : "0"));
+    }
+
+    // add configured session variable if configured
+    if (conf.sessionVariables() != null) {
+      sessionCommands.add(Security.parseSessionVariables(conf.sessionVariables()));
+    }
+
+    // force client timezone to connection to ensure result of now(), ...
+    if (conf.timezone() != null && !"disable".equalsIgnoreCase(conf.timezone())) {
+      boolean mustSetTimezone = true;
+      TimeZone connectionTz =
+          "auto".equalsIgnoreCase(conf.timezone())
+              ? TimeZone.getDefault()
+              : TimeZone.getTimeZone(ZoneId.of(conf.timezone()).normalized());
+      ZoneId clientZoneId = connectionTz.toZoneId();
+
+      // try to avoid timezone consideration if server use the same one
+      try {
+        ZoneId serverZoneId = ZoneId.of(serverTz);
+        if (serverZoneId.normalized().equals(clientZoneId)
+            || ZoneId.of(serverTz, ZoneId.SHORT_IDS).equals(clientZoneId)) {
+          mustSetTimezone = false;
+        }
+      } catch (DateTimeException e) {
+        // eat
+      }
+
+      if (mustSetTimezone) {
+        if (clientZoneId.getRules().isFixedOffset()) {
+          ZoneOffset zoneOffset = clientZoneId.getRules().getOffset(Instant.now());
+          if (zoneOffset.getTotalSeconds() == 0) {
+            // specific for UTC timezone, server permitting only SYSTEM/UTC offset or named time
+            // zone
+            // not 'UTC'/'Z'
+            sessionCommands.add("time_zone='+00:00'");
+          } else {
+            sessionCommands.add("time_zone='" + zoneOffset.getId() + "'");
+          }
+        } else {
+          sessionCommands.add("time_zone='" + clientZoneId.normalized() + "'");
+        }
+      }
+    }
+
+    if (conf.transactionIsolation() != null) {
+      int major = context.getVersion().getMajorVersion();
+      if (!context.getVersion().isMariaDBServer()
+          && ((major >= 8 && context.getVersion().versionGreaterOrEqual(8, 0, 3))
+              || (major < 8 && context.getVersion().versionGreaterOrEqual(5, 7, 20)))) {
+        sessionCommands.add(
+            "transaction_isolation='" + conf.transactionIsolation().getValue() + "'");
+      } else {
+        sessionCommands.add("tx_isolation='" + conf.transactionIsolation().getValue() + "'");
+      }
+    }
+
+    if (!sessionCommands.isEmpty()) {
+      return "set " + sessionCommands.stream().collect(Collectors.joining(","));
+    }
+    return null;
+  }
+
+  public void setReadOnly(boolean readOnly) throws SQLException {
+    if (closed) {
+      throw new SQLNonTransientConnectionException("Connection is closed", "08000", 1220);
+    }
+  }
+
+  /**
+   * Send client message to server
+   *
+   * @param message client message
+   * @return number of command send
+   * @throws SQLException if socket error occurs
+   */
+  public int sendQuery(ClientMessage message) throws SQLException {
+    checkNotClosed();
+    try {
+      if (logger.isDebugEnabled() && message.description() != null) {
+        logger.debug("execute query: {}", message.description());
+      }
+      return message.encode(writer, context);
+    } catch (IOException ioException) {
+      if (ioException instanceof MaxAllowedPacketException) {
+        if (((MaxAllowedPacketException) ioException).isMustReconnect()) {
+          destroySocket();
+          throw exceptionFactory
+              .withSql(message.description())
+              .create(
+                  "Packet too big for current server max_allowed_packet value",
+                  "08000",
+                  ioException);
+        }
+        throw exceptionFactory
+            .withSql(message.description())
+            .create(
+                "Packet too big for current server max_allowed_packet value", "HZ000", ioException);
+      }
+      destroySocket();
+      throw exceptionFactory
+          .withSql(message.description())
+          .create("Socket error", "08000", ioException);
+    }
+  }
+
+  public List<Completion> execute(ClientMessage message, boolean canRedo) throws SQLException {
+    return execute(
+        message,
+        null,
+        0,
+        0L,
+        ResultSet.CONCUR_READ_ONLY,
+        ResultSet.TYPE_FORWARD_ONLY,
+        false,
+        canRedo);
+  }
+
+  public List<Completion> execute(
+      ClientMessage message, org.mariadb.jdbc.Statement stmt, boolean canRedo) throws SQLException {
+    return execute(
+        message,
+        stmt,
+        0,
+        0L,
+        ResultSet.CONCUR_READ_ONLY,
+        ResultSet.TYPE_FORWARD_ONLY,
+        false,
+        canRedo);
+  }
+
+  public List<Completion> executePipeline(
+      ClientMessage[] messages,
+      org.mariadb.jdbc.Statement stmt,
+      int fetchSize,
+      long maxRows,
+      int resultSetConcurrency,
+      int resultSetType,
+      boolean closeOnCompletion,
+      boolean canRedo)
+      throws SQLException {
+    List<Completion> results = new ArrayList<>();
+
+    int readCounter = 0;
+    int[] responseMsg = new int[messages.length];
+    try {
+      if (disablePipeline) {
+        for (readCounter = 0; readCounter < messages.length; readCounter++) {
+          results.addAll(
+              execute(
+                  messages[readCounter],
+                  stmt,
+                  fetchSize,
+                  maxRows,
+                  resultSetConcurrency,
+                  resultSetType,
+                  closeOnCompletion,
+                  canRedo));
+        }
+      } else {
+        for (int i = 0; i < messages.length; i++) {
+          responseMsg[i] = sendQuery(messages[i]);
+        }
+        while (readCounter < messages.length) {
+          readCounter++;
+          for (int j = 0; j < responseMsg[readCounter - 1]; j++) {
+            results.addAll(
+                readResponse(
+                    stmt,
+                    messages[readCounter - 1],
+                    fetchSize,
+                    maxRows,
+                    resultSetConcurrency,
+                    resultSetType,
+                    closeOnCompletion));
+          }
+        }
+      }
+      return results;
+    } catch (SQLException sqlException) {
+      if (!closed) {
+        // read remaining results
+        for (int i = readCounter; i < messages.length; i++) {
+          for (int j = 0; j < responseMsg[i]; j++) {
+            try {
+              results.addAll(
+                  readResponse(
+                      stmt,
+                      messages[i],
+                      fetchSize,
+                      maxRows,
+                      resultSetConcurrency,
+                      resultSetType,
+                      closeOnCompletion));
+            } catch (SQLException e) {
+              // eat
+            }
+          }
+        }
+
+        // prepare associated to PrepareStatement need to be uncached
+        for (Completion result : results) {
+          if (result instanceof PrepareResultPacket && stmt instanceof ServerPreparedStatement) {
+            try {
+              ((PrepareResultPacket) result).decrementUse(this, (ServerPreparedStatement) stmt);
+            } catch (SQLException e) {
+              // eat
+            }
+          }
+        }
+      }
+
+      int batchUpdateLength = 0;
+      for (ClientMessage message : messages) {
+        batchUpdateLength += message.batchUpdateLength();
+      }
+      throw exceptionFactory.createBatchUpdate(
+          results, batchUpdateLength, responseMsg, sqlException);
+    }
+  }
+
+  public List<Completion> execute(
+      ClientMessage message,
+      org.mariadb.jdbc.Statement stmt,
+      int fetchSize,
+      long maxRows,
+      int resultSetConcurrency,
+      int resultSetType,
+      boolean closeOnCompletion,
+      boolean canRedo)
+      throws SQLException {
+    int nbResp = sendQuery(message);
+    if (nbResp == 1) {
+      return readResponse(
+          stmt,
+          message,
+          fetchSize,
+          maxRows,
+          resultSetConcurrency,
+          resultSetType,
+          closeOnCompletion);
+    } else {
+      if (streamStmt != null) {
+        streamStmt.fetchRemaining();
+        streamStmt = null;
+      }
+      List<Completion> completions = new ArrayList<>();
+      try {
+        while (nbResp-- > 0) {
+          readResults(
+              stmt,
+              message,
+              completions,
+              fetchSize,
+              maxRows,
+              resultSetConcurrency,
+              resultSetType,
+              closeOnCompletion);
+        }
+        return completions;
+      } catch (SQLException e) {
+        while (nbResp-- > 0) {
+          try {
+            readResults(
+                stmt,
+                message,
+                completions,
+                fetchSize,
+                maxRows,
+                resultSetConcurrency,
+                resultSetType,
+                closeOnCompletion);
+          } catch (SQLException ee) {
+            // eat
+          }
+        }
+        throw e;
+      }
+    }
+  }
+
+  /**
+   * Read server responses for a client message
+   *
+   * @param stmt statement that issue the message
+   * @param message client message sent
+   * @param fetchSize fetch size
+   * @param maxRows maximum number of rows
+   * @param resultSetConcurrency concurrency
+   * @param resultSetType result-set type
+   * @param closeOnCompletion close statement on resultset completion
+   * @return list of result
+   * @throws SQLException if any error occurs
+   */
+  public List<Completion> 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<Completion> completions = new ArrayList<>();
+    readResults(
+        stmt,
+        message,
+        completions,
+        fetchSize,
+        maxRows,
+        resultSetConcurrency,
+        resultSetType,
+        closeOnCompletion);
+    return completions;
+  }
+
+  /**
+   * Read server response
+   *
+   * @param message client message that was sent
+   * @throws SQLException if any error occurs
+   */
+  public void readResponse(ClientMessage message) throws SQLException {
+    checkNotClosed();
+    if (streamStmt != null) {
+      streamStmt.fetchRemaining();
+      streamStmt = null;
+    }
+    List<Completion> completions = new ArrayList<>();
+    readResults(
+        null,
+        message,
+        completions,
+        0,
+        0L,
+        ResultSet.CONCUR_READ_ONLY,
+        ResultSet.TYPE_FORWARD_ONLY,
+        false);
+  }
+
+  public void closePrepare(Prepare 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 readStreamingResults(
+      List<Completion> 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<Completion> 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));
+    }
+  }
+
+  /**
+   * Read a MySQL packet from socket
+   *
+   * @param message client message issuing the result
+   * @return a mysql result
+   * @throws SQLException if any error occurs
+   */
+  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 <a href="https://mariadb.com/kb/en/mariadb/4-server-response-packets/">server response
+   *     packets</a>
+   * @param stmt current statement (null if internal)
+   * @param message current message
+   * @param fetchSize default fetch size
+   * @param maxRows maximum row number
+   * @param resultSetConcurrency concurrency
+   * @param resultSetType type
+   * @param closeOnCompletion must resultset close statement on completion
+   * @return 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 {
+      boolean traceEnable = logger.isTraceEnabled();
+      Completion completion =
+          message.readPacket(
+              stmt,
+              fetchSize,
+              maxRows,
+              resultSetConcurrency,
+              resultSetType,
+              closeOnCompletion,
+              reader,
+              writer,
+              context,
+              exceptionFactory,
+              lock,
+              traceEnable,
+              message);
+      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);
+    }
+  }
+
+  /**
+   * Throw an exception if client is closed
+   *
+   * @throws SQLException if closed
+   */
+  protected 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 {
+
+    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;
+      logger.debug("aborting connection {}", context.getThreadId());
+      if (!lockStatus) {
+        // lock not available : query is running
+        // force end by executing an KILL connection
+        try (StandardClient cli =
+            new StandardClient(conf, hostAddress, new ReentrantLock(), true)) {
+          cli.execute(new QueryPacket("KILL " + context.getThreadId()), false);
+        } catch (SQLException e) {
+          // eat
+        }
+      } else {
+        try {
+          QuitPacket.INSTANCE.encode(writer, context);
+        } catch (IOException e) {
+          // eat
+        }
+      }
+      if (streamStmt != null) {
+        streamStmt.abort();
+      }
+      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", "42000", se);
+    }
+  }
+
+  public void close() {
+    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 HostAddress getHostAddress() {
+    return hostAddress;
+  }
+
+  public void reset() {
+    context.resetStateFlag();
+    context.resetPrepareCache();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/impl/StandardReadableByteBuf.java b/src/main/java/org/mariadb/jdbc/client/impl/StandardReadableByteBuf.java
new file mode 100644
index 0000000..62746b5
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/impl/StandardReadableByteBuf.java
@@ -0,0 +1,324 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.impl;
+
+import java.nio.charset.StandardCharsets;
+import org.mariadb.jdbc.MariaDbBlob;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+
+/** Packet buffer */
+public final class StandardReadableByteBuf implements ReadableByteBuf {
+  /** row data limit */
+  private int limit;
+
+  /** buffer */
+  public byte[] buf;
+
+  /** current position reading buffer */
+  public int pos;
+
+  /**
+   * Packet buffer constructor
+   *
+   * @param buf buffer
+   * @param limit buffer limit
+   */
+  public StandardReadableByteBuf(byte[] buf, int limit) {
+    this.pos = 0;
+    this.buf = buf;
+    this.limit = limit;
+  }
+
+  /**
+   * Packet buffer constructor, limit being the buffer length
+   *
+   * @param buf buffer
+   */
+  public StandardReadableByteBuf(byte[] buf) {
+    this.pos = 0;
+    this.buf = buf;
+    this.limit = buf.length;
+  }
+
+  public int readableBytes() {
+    return limit - pos;
+  }
+
+  public int pos() {
+    return pos;
+  }
+
+  public byte[] buf() {
+    return buf;
+  }
+
+  public void buf(byte[] buf, int limit, int pos) {
+    this.buf = buf;
+    this.limit = limit;
+    this.pos = pos;
+  }
+
+  public void pos(int pos) {
+    this.pos = pos;
+  }
+
+  public void skip() {
+    pos++;
+  }
+
+  public void skip(int length) {
+    pos += length;
+  }
+
+  public void skipLengthEncoded() {
+    byte len = buf[pos++];
+    switch (len) {
+      case (byte) 251:
+        return;
+      case (byte) 252:
+        skip(readUnsignedShort());
+        return;
+      case (byte) 253:
+        skip(readUnsignedMedium());
+        return;
+      case (byte) 254:
+        skip((int) (4 + readUnsignedInt()));
+        return;
+      default:
+        pos += len & 0xff;
+        return;
+    }
+  }
+
+  public MariaDbBlob readBlob(int length) {
+    pos += length;
+    return MariaDbBlob.safeMariaDbBlob(buf, pos - length, length);
+  }
+
+  public long atoll(int length) {
+    boolean negate = false;
+    int idx = 0;
+    long result = 0;
+
+    if (length > 0 && buf[pos] == 45) { // minus sign
+      negate = true;
+      pos++;
+      idx++;
+    }
+
+    while (idx++ < length) {
+      result = result * 10 + buf[pos++] - 48;
+    }
+
+    return (negate) ? -1 * result : result;
+  }
+
+  public long atoull(int length) {
+    long result = 0;
+    for (int idx = 0; idx < length; idx++) {
+      result = result * 10 + buf[pos++] - 48;
+    }
+    return result;
+  }
+
+  public byte getByte() {
+    return buf[pos];
+  }
+
+  public byte getByte(int index) {
+    return buf[index];
+  }
+
+  public short getUnsignedByte() {
+    return (short) (buf[pos] & 0xff);
+  }
+
+  public long readLongLengthEncodedNotNull() {
+    int type = (buf[pos++] & 0xff);
+    if (type < 251) return type;
+    switch (type) {
+      case 252: // 0xfc
+        return readUnsignedShort();
+      case 253: // 0xfd
+        return readUnsignedMedium();
+      default: // 0xfe
+        return readLong();
+    }
+  }
+
+  public int readIntLengthEncodedNotNull() {
+    int type = (buf[pos++] & 0xff);
+    if (type < 251) return type;
+    switch (type) {
+      case 252:
+        return readUnsignedShort();
+      case 253:
+        return readUnsignedMedium();
+      case 254:
+        return (int) readLong();
+      default:
+        return type;
+    }
+  }
+
+  /**
+   * Identifier can have a max length of 256 (alias) So no need to check whole length encoding.
+   *
+   * @return current pos
+   */
+  public int skipIdentifier() {
+    int len = readIntLengthEncodedNotNull();
+    pos += len;
+    return pos;
+  }
+
+  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 int readIntBE() {
+    return (((buf[pos++] & 0xff) << 24)
+        + ((buf[pos++] & 0xff) << 16)
+        + ((buf[pos++] & 0xff) << 8)
+        + (buf[pos++] & 0xff));
+  }
+
+  public long readUnsignedInt() {
+    return ((buf[pos++] & 0xff)
+            + ((buf[pos++] & 0xff) << 8)
+            + ((buf[pos++] & 0xff) << 16)
+            + ((long) (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 long readLongBE() {
+    return (((buf[pos++] & 0xffL) << 56)
+        + ((buf[pos++] & 0xffL) << 48)
+        + ((buf[pos++] & 0xffL) << 40)
+        + ((buf[pos++] & 0xffL) << 32)
+        + ((buf[pos++] & 0xffL) << 24)
+        + ((buf[pos++] & 0xffL) << 16)
+        + ((buf[pos++] & 0xffL) << 8)
+        + (buf[pos++] & 0xffL));
+  }
+
+  public void readBytes(byte[] dst) {
+    System.arraycopy(buf, pos, dst, 0, dst.length);
+    pos += dst.length;
+  }
+
+  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 StandardReadableByteBuf readLengthBuffer() {
+    int len = this.readIntLengthEncodedNotNull();
+    byte[] tmp = new byte[len];
+    readBytes(tmp);
+    return new StandardReadableByteBuf(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());
+  }
+
+  public double readDoubleBE() {
+    return Double.longBitsToDouble(readLongBE());
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/impl/TransactionSaver.java b/src/main/java/org/mariadb/jdbc/client/impl/TransactionSaver.java
new file mode 100644
index 0000000..a9c8f54
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/impl/TransactionSaver.java
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.impl;
+
+import java.util.Arrays;
+import org.mariadb.jdbc.message.client.RedoableClientMessage;
+
+/**
+ * Transaction cache Huge command are not cached, cache is limited to configuration
+ * transactionReplaySize commands
+ */
+public class TransactionSaver {
+  private final RedoableClientMessage[] buffers;
+  private int idx = 0;
+  private boolean dirty = false;
+
+  /**
+   * Constructor
+   *
+   * @param transactionReplaySize maximum number of command cached
+   */
+  public TransactionSaver(int transactionReplaySize) {
+    buffers = new RedoableClientMessage[transactionReplaySize];
+  }
+
+  /**
+   * Add a command to cache.
+   *
+   * @param clientMessage client message
+   */
+  public void add(RedoableClientMessage clientMessage) {
+    if (idx < buffers.length) {
+      buffers[idx++] = clientMessage;
+    } else {
+      dirty = true;
+    }
+  }
+
+  /** Transaction finished, clearing cache */
+  public void clear() {
+    Arrays.fill(buffers, null);
+    dirty = false;
+    idx = 0;
+  }
+
+  /**
+   * Current transaction cache length
+   *
+   * @return cache length
+   */
+  public int getIdx() {
+    return idx;
+  }
+
+  /**
+   * Is cache not valid (some commands have not been cached)
+   *
+   * @return is dirty
+   */
+  public boolean isDirty() {
+    return dirty;
+  }
+
+  /**
+   * cache buffer
+   *
+   * @return cached messages
+   */
+  public RedoableClientMessage[] getBuffers() {
+    return buffers;
+  }
+}
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 0000000..e407ed5
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/result/CompleteResult.java
@@ -0,0 +1,318 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+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 java.util.concurrent.locks.ReentrantLock;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.socket.Reader;
+
+/** Result-set that will retrieve all rows immediately before returning the result-set. */
+public class CompleteResult extends Result {
+
+  /** before first row position = initial position */
+  protected static final int BEFORE_FIRST_POS = -1;
+
+  /**
+   * Constructor from exchanges
+   *
+   * @param stmt current statement
+   * @param binaryProtocol does exchanges uses binary protocol
+   * @param maxRows maximum number of rows
+   * @param metadataList metadata
+   * @param reader packet reader
+   * @param context connection context
+   * @param resultSetType result set type
+   * @param closeOnCompletion close statement on completion
+   * @param traceEnable network trace exchange possible
+   * @throws IOException if Socket error occurs
+   * @throws SQLException for all other kind of errors
+   */
+  public CompleteResult(
+      Statement stmt,
+      boolean binaryProtocol,
+      long maxRows,
+      ColumnDecoder[] metadataList,
+      Reader reader,
+      Context context,
+      int resultSetType,
+      boolean closeOnCompletion,
+      boolean traceEnable)
+      throws IOException, SQLException {
+
+    super(
+        stmt,
+        binaryProtocol,
+        maxRows,
+        metadataList,
+        reader,
+        context,
+        resultSetType,
+        closeOnCompletion,
+        traceEnable);
+    this.data = new byte[10][];
+    if (maxRows > 0) {
+      while (readNext() && dataSize < maxRows) {}
+      if (!loaded) skipRemaining();
+    } else {
+      while (readNext()) {}
+    }
+    loaded = true;
+  }
+
+  /**
+   * Specific constructor for internal build result-set, empty resultset, or generated key
+   * result-set.
+   *
+   * @param metadataList metadata
+   * @param data result-set data
+   * @param context connection context
+   */
+  public CompleteResult(ColumnDecoder[] metadataList, byte[][] data, Context context) {
+    super(metadataList, data, context);
+  }
+
+  /**
+   * Specific constructor for generating generated key result-set.
+   *
+   * @param columnName column key
+   * @param columnType column key type
+   * @param data values
+   * @param context connection context
+   * @param flags column flags
+   * @return result-set
+   */
+  public static ResultSet createResultSet(
+      String columnName, DataType columnType, String[][] data, Context context, int flags) {
+    return createResultSet(
+        new String[] {columnName}, new DataType[] {columnType}, data, context, flags);
+  }
+
+  /**
+   * 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
+   * @param flags column flags
+   * @return resultset
+   */
+  public static ResultSet createResultSet(
+      String[] columnNames, DataType[] columnTypes, String[][] data, Context context, int flags) {
+
+    int columnNameLength = columnNames.length;
+    ColumnDecoder[] columns = new ColumnDecoder[columnNameLength];
+
+    for (int i = 0; i < columnNameLength; i++) {
+      columns[i] = ColumnDecoder.create(columnNames[i], columnTypes[i], flags);
+    }
+
+    List<byte[]> rows = new ArrayList<>();
+    for (String[] rowData : data) {
+      ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+      for (String rowDatum : rowData) {
+
+        if (rowDatum != null) {
+          byte[] bb = rowDatum.getBytes();
+          int len = bb.length;
+          if (len < 251) {
+            baos.write((byte) len);
+          } else {
+            // assume length cannot be > 65536
+            baos.write((byte) 0xfc);
+            baos.write((byte) len);
+            baos.write((byte) (len >>> 8));
+          }
+          baos.write(bb, 0, bb.length);
+        } else {
+          baos.write((byte) 0xfb);
+        }
+      }
+      byte[] bb = baos.toByteArray();
+      rows.add(bb);
+    }
+    return new CompleteResult(columns, rows.toArray(new byte[0][0]), context);
+  }
+
+  @Override
+  public boolean next() throws SQLException {
+    if (rowPointer < dataSize - 1) {
+      setRow(data[++rowPointer]);
+      return true;
+    } else {
+      // all data are reads and pointer is after last
+      setNullRowBuf();
+      rowPointer = dataSize;
+      return false;
+    }
+  }
+
+  @Override
+  public boolean streaming() {
+    return false;
+  }
+
+  @Override
+  public void fetchRemaining() {}
+
+  @Override
+  public void closeFromStmtClose(ReentrantLock lock) {
+    this.closed = true;
+  }
+
+  @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 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;
+    setNullRowBuf();
+  }
+
+  @Override
+  public void afterLast() throws SQLException {
+    checkClose();
+    setNullRowBuf();
+    rowPointer = dataSize;
+  }
+
+  @Override
+  public boolean first() throws SQLException {
+    checkClose();
+    rowPointer = 0;
+    if (dataSize == 0) {
+      setNullRowBuf();
+      return false;
+    }
+    setRow(data[rowPointer]);
+    return true;
+  }
+
+  @Override
+  public boolean last() throws SQLException {
+    checkClose();
+    rowPointer = dataSize - 1;
+    if (rowPointer == BEFORE_FIRST_POS) {
+      setNullRowBuf();
+      return false;
+    }
+    setRow(data[rowPointer]);
+    return true;
+  }
+
+  @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;
+      setNullRowBuf();
+      return false;
+    }
+
+    if (idx > 0) {
+      rowPointer = idx - 1;
+      setRow(data[rowPointer]);
+      return true;
+    } else {
+      if (dataSize + idx >= 0) {
+        // absolute position reverse from ending resultSet
+        rowPointer = dataSize + idx;
+        setRow(data[rowPointer]);
+        return true;
+      }
+      rowPointer = BEFORE_FIRST_POS;
+      setNullRowBuf();
+      return false;
+    }
+  }
+
+  @Override
+  public boolean relative(int rows) throws SQLException {
+    checkClose();
+    int newPos = rowPointer + rows;
+    if (newPos <= -1) {
+      rowPointer = BEFORE_FIRST_POS;
+      setNullRowBuf();
+      return false;
+    } else if (newPos >= dataSize) {
+      rowPointer = dataSize;
+      setNullRowBuf();
+      return false;
+    } else {
+      rowPointer = newPos;
+      setRow(data[rowPointer]);
+      return true;
+    }
+  }
+
+  @Override
+  public boolean previous() throws SQLException {
+    checkClose();
+    if (rowPointer > BEFORE_FIRST_POS) {
+      rowPointer--;
+      if (rowPointer != BEFORE_FIRST_POS) {
+        setRow(data[rowPointer]);
+        return true;
+      }
+    }
+    setNullRowBuf();
+    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 0000000..1ed6171
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/result/Result.java
@@ -0,0 +1,1718 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.result;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.RoundingMode;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.sql.*;
+import java.sql.Date;
+import java.util.*;
+import java.util.concurrent.locks.ReentrantLock;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.impl.StandardReadableByteBuf;
+import org.mariadb.jdbc.client.result.rowdecoder.BinaryRowDecoder;
+import org.mariadb.jdbc.client.result.rowdecoder.RowDecoder;
+import org.mariadb.jdbc.client.result.rowdecoder.TextRowDecoder;
+import org.mariadb.jdbc.client.util.MutableInt;
+import org.mariadb.jdbc.export.ExceptionFactory;
+import org.mariadb.jdbc.message.server.ErrorPacket;
+import org.mariadb.jdbc.plugin.Codec;
+import org.mariadb.jdbc.plugin.codec.*;
+import org.mariadb.jdbc.util.constants.ServerStatus;
+
+/** Result-set common */
+public abstract class Result implements ResultSet, Completion {
+  private static BinaryRowDecoder BINARY_ROW_DECODER = new BinaryRowDecoder();
+  private static TextRowDecoder TEXT_ROW_DECODER = new TextRowDecoder();
+  /** null length value */
+  public static final int NULL_LENGTH = -1;
+
+  private final int maxIndex;
+  private final boolean closeOnCompletion;
+  private boolean forceAlias;
+  private final boolean traceEnable;
+
+  /** result-set type */
+  protected final int resultSetType;
+
+  /** connection exception factory */
+  protected final ExceptionFactory exceptionFactory;
+
+  /** packet reader */
+  protected final org.mariadb.jdbc.client.socket.Reader reader;
+
+  /** connection context */
+  protected final Context context;
+
+  /** columns metadata */
+  protected final ColumnDecoder[] metadataList;
+
+  /** binary/text row decoder */
+  protected final RowDecoder rowDecoder;
+
+  /** data size */
+  protected int dataSize = 0;
+
+  /** rows */
+  protected byte[][] data;
+
+  private byte[] nullBitmap;
+
+  /** reusable row buffer decoder */
+  protected final StandardReadableByteBuf rowBuf = new StandardReadableByteBuf(null, 0);
+
+  private int fieldLength;
+
+  /** mutable field index */
+  protected MutableInt fieldIndex = new MutableInt();
+
+  private Map<String, Integer> mapper = null;
+
+  /** is fully loaded */
+  protected boolean loaded;
+
+  /** is an output parameter result-set */
+  protected boolean outputParameter;
+
+  /** current row pointer */
+  protected int rowPointer = -1;
+
+  /** is result-set closed */
+  protected boolean closed;
+
+  /** statement that initiate this result */
+  protected Statement statement;
+
+  /** row number limit */
+  protected long maxRows;
+
+  /**
+   * Constructor for server's data
+   *
+   * @param stmt statement that initiate this result
+   * @param binaryProtocol binary encoded rows
+   * @param maxRows row number limit
+   * @param metadataList columns metadata
+   * @param reader packet reader
+   * @param context connection context
+   * @param resultSetType result-set type
+   * @param closeOnCompletion close statement on completion
+   * @param traceEnable logger enabled
+   */
+  public Result(
+      org.mariadb.jdbc.Statement stmt,
+      boolean binaryProtocol,
+      long maxRows,
+      ColumnDecoder[] metadataList,
+      org.mariadb.jdbc.client.socket.Reader reader,
+      Context context,
+      int resultSetType,
+      boolean closeOnCompletion,
+      boolean traceEnable) {
+    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;
+    this.traceEnable = traceEnable;
+    if (binaryProtocol) {
+      rowDecoder = BINARY_ROW_DECODER;
+      nullBitmap = new byte[(maxIndex + 9) / 8];
+    } else {
+      rowDecoder = TEXT_ROW_DECODER;
+    }
+  }
+
+  /**
+   * Internal constructed result-set
+   *
+   * @param metadataList column metadata
+   * @param data raw data
+   * @param context connection context
+   */
+  public Result(ColumnDecoder[] metadataList, byte[][] data, Context context) {
+    this.metadataList = metadataList;
+    this.maxIndex = this.metadataList.length;
+    this.reader = null;
+    this.loaded = true;
+    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;
+    this.traceEnable = false;
+    rowDecoder = TEXT_ROW_DECODER;
+  }
+
+  /**
+   * Read new row
+   *
+   * @return true if fully loaded
+   * @throws IOException if any socket error occurs
+   * @throws SQLException for all other type of errors
+   */
+  @SuppressWarnings("fallthrough")
+  protected boolean readNext() throws IOException, SQLException {
+    byte[] buf = reader.readPacket(traceEnable);
+    switch (buf[0]) {
+      case (byte) 0xFF:
+        loaded = true;
+        ErrorPacket errorPacket = new ErrorPacket(reader.readableBufFromArray(buf), context);
+        throw exceptionFactory.create(
+            errorPacket.getMessage(), errorPacket.getSqlState(), errorPacket.getErrorCode());
+
+      case (byte) 0xFE:
+        if ((context.isEofDeprecated() && buf.length < 16777215)
+            || (!context.isEofDeprecated() && buf.length < 8)) {
+          ReadableByteBuf readBuf = reader.readableBufFromArray(buf);
+          readBuf.skip(); // skip header
+          int serverStatus;
+          int warnings;
+
+          if (!context.isEofDeprecated()) {
+            // EOF_Packet
+            warnings = readBuf.readUnsignedShort();
+            serverStatus = readBuf.readUnsignedShort();
+          } else {
+            // OK_Packet with a 0xFE header
+            readBuf.readLongLengthEncodedNotNull(); // skip update count
+            readBuf.readLongLengthEncodedNotNull(); // skip insert id
+            serverStatus = readBuf.readUnsignedShort();
+            warnings = readBuf.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;
+  }
+
+  /**
+   * Skip remaining rows to keep connection state ok, without needing remaining data.
+   *
+   * @throws IOException if socket error occurs
+   * @throws SQLException for other kind of error
+   */
+  @SuppressWarnings("fallthrough")
+  protected void skipRemaining() throws IOException, SQLException {
+    while (true) {
+      ReadableByteBuf buf = reader.readReusablePacket(traceEnable);
+      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.readLongLengthEncodedNotNull(); // skip update count
+              buf.readLongLengthEncodedNotNull(); // 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);
+    byte[][] newData = new byte[newCapacity][];
+    System.arraycopy(data, 0, newData, 0, data.length);
+    data = newData;
+  }
+
+  /**
+   * Position resultset to next row
+   *
+   * @return true if next row exists
+   * @throws SQLException if any error occurs
+   */
+  @Override
+  public abstract boolean next() throws SQLException;
+
+  /**
+   * Indicate of current result-set is a streaming result-set
+   *
+   * @return if streaming result-set
+   */
+  public abstract boolean streaming();
+
+  /**
+   * Fetch remaining results.
+   *
+   * @throws SQLException if issue occurs during data retrieving
+   */
+  public abstract void fetchRemaining() throws SQLException;
+
+  /**
+   * Is result-set fully loaded or still streaming
+   *
+   * @return true if fully loaded
+   */
+  public boolean loaded() {
+    return loaded;
+  }
+
+  /**
+   * Does result-set contain output parameters
+   *
+   * @return true if containing output parameters
+   */
+  public boolean isOutputParameter() {
+    return outputParameter;
+  }
+
+  /**
+   * Close current result-set
+   *
+   * @throws SQLException if socket error occurs
+   */
+  @Override
+  public void close() throws SQLException {
+    if (!loaded) {
+      try {
+        skipRemaining();
+      } catch (IOException ioe) {
+        throw exceptionFactory.create("Error while streaming resultSet data", "08000", ioe);
+      }
+    }
+    this.closed = true;
+    if (closeOnCompletion) {
+      statement.close();
+    }
+  }
+
+  /**
+   * Closing result-set due to closing statement that issue command.
+   *
+   * @param lock thread locker object
+   * @throws SQLException if any error occurs
+   */
+  public void closeFromStmtClose(ReentrantLock lock) throws SQLException {
+    lock.lock();
+    try {
+      this.fetchRemaining();
+      this.closed = true;
+    } finally {
+      lock.unlock();
+    }
+  }
+
+  /** Aborting result-set, without any consideration for connection state. */
+  public void abort() {
+    this.closed = true;
+  }
+
+  /**
+   * return current row RAW data
+   *
+   * @return current row RAW data
+   */
+  protected byte[] getCurrentRowData() {
+    return data[0];
+  }
+
+  /**
+   * Add a row
+   *
+   * @param buf add row
+   */
+  protected void addRowData(byte[] buf) {
+    if (dataSize + 1 > data.length) {
+      growDataArray();
+    }
+    data[dataSize++] = buf;
+  }
+
+  /**
+   * Update current row
+   *
+   * @param rawData new row
+   */
+  protected void updateRowData(byte[] rawData) {
+    data[rowPointer] = rawData;
+    if (rawData == null) {
+      setNullRowBuf();
+    } else {
+      setRow(rawData);
+      fieldIndex.set(-1);
+    }
+  }
+
+  private void checkIndex(int index) throws SQLException {
+    if (index < 1 || index > maxIndex) {
+      throw new SQLException(
+          String.format("Wrong index position. Is %s but must be in 1-%s range", index, maxIndex));
+    }
+    if (rowBuf.buf == null) {
+      throw new SQLDataException("wrong row position", "22023");
+    }
+  }
+
+  /**
+   * has last data getter return a null value
+   *
+   * @return true if was null
+   */
+  @Override
+  public boolean wasNull() {
+    return rowDecoder.wasNull(nullBitmap, fieldIndex, fieldLength);
+  }
+
+  @Override
+  public String getString(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+    return rowDecoder.decodeString(metadataList, fieldIndex, rowBuf, fieldLength);
+  }
+
+  @Override
+  public boolean getBoolean(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return false;
+    }
+    return rowDecoder.decodeBoolean(metadataList, fieldIndex, rowBuf, fieldLength);
+  }
+
+  @Override
+  public byte getByte(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return 0;
+    }
+    return rowDecoder.decodeByte(metadataList, fieldIndex, rowBuf, fieldLength);
+  }
+
+  @Override
+  public short getShort(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return 0;
+    }
+    return rowDecoder.decodeShort(metadataList, fieldIndex, rowBuf, fieldLength);
+  }
+
+  @Override
+  public int getInt(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return 0;
+    }
+    return rowDecoder.decodeInt(metadataList, fieldIndex, rowBuf, fieldLength);
+  }
+
+  @Override
+  public long getLong(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return 0L;
+    }
+    return rowDecoder.decodeLong(metadataList, fieldIndex, rowBuf, fieldLength);
+  }
+
+  /**
+   * Retrieves the value of the designated column in the current row of this ResultSet object as a
+   * BigInteger.
+   *
+   * @param columnIndex index
+   * @return BigInteger value
+   * @throws SQLException if cannot be decoded as a BigInteger
+   */
+  public BigInteger getBigInteger(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+    return rowDecoder.decode(
+        BigIntegerCodec.INSTANCE, null, rowBuf, fieldLength, metadataList, fieldIndex);
+  }
+
+  /**
+   * Retrieves the value of the designated column in the current row of this ResultSet object as a
+   * BigInteger.
+   *
+   * @param columnLabel column label
+   * @return BigInteger value
+   * @throws SQLException if cannot be decoded as a BigInteger
+   */
+  public BigInteger getBigInteger(String columnLabel) throws SQLException {
+    return getBigInteger(findColumn(columnLabel));
+  }
+
+  @Override
+  public float getFloat(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return 0F;
+    }
+    return rowDecoder.decodeFloat(metadataList, fieldIndex, rowBuf, fieldLength);
+  }
+
+  @Override
+  public double getDouble(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return 0D;
+    }
+    return rowDecoder.decodeDouble(metadataList, fieldIndex, rowBuf, fieldLength);
+  }
+
+  @Override
+  @Deprecated
+  public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+    BigDecimal d =
+        rowDecoder.decode(
+            BigDecimalCodec.INSTANCE, null, rowBuf, fieldLength, metadataList, fieldIndex);
+    if (d == null) return null;
+    return d.setScale(scale, RoundingMode.HALF_DOWN);
+  }
+
+  @Override
+  public byte[] getBytes(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+    return rowDecoder.decode(
+        ByteArrayCodec.INSTANCE, null, rowBuf, fieldLength, metadataList, fieldIndex);
+  }
+
+  @Override
+  public Date getDate(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+    return rowDecoder.decodeDate(metadataList, fieldIndex, rowBuf, fieldLength, null);
+  }
+
+  @Override
+  public Time getTime(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+    return rowDecoder.decodeTime(metadataList, fieldIndex, rowBuf, fieldLength, null);
+  }
+
+  @Override
+  public Timestamp getTimestamp(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+    return rowDecoder.decodeTimestamp(metadataList, fieldIndex, rowBuf, fieldLength, null);
+  }
+
+  @Override
+  public InputStream getAsciiStream(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+    return rowDecoder.decode(
+        StreamCodec.INSTANCE, null, rowBuf, fieldLength, metadataList, fieldIndex);
+  }
+
+  @Override
+  @Deprecated
+  public InputStream getUnicodeStream(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+    return rowDecoder.decode(
+        StreamCodec.INSTANCE, null, rowBuf, fieldLength, metadataList, fieldIndex);
+  }
+
+  @Override
+  public InputStream getBinaryStream(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+    return rowDecoder.decode(
+        StreamCodec.INSTANCE, null, rowBuf, fieldLength, metadataList, fieldIndex);
+  }
+
+  @Override
+  public String getString(String columnLabel) throws SQLException {
+    return getString(findColumn(columnLabel));
+  }
+
+  @Override
+  public boolean getBoolean(String columnLabel) throws SQLException {
+    return getBoolean(findColumn(columnLabel));
+  }
+
+  @Override
+  public byte getByte(String columnLabel) throws SQLException {
+    return getByte(findColumn(columnLabel));
+  }
+
+  @Override
+  public short getShort(String columnLabel) throws SQLException {
+    return getShort(findColumn(columnLabel));
+  }
+
+  @Override
+  public int getInt(String columnLabel) throws SQLException {
+    return getInt(findColumn(columnLabel));
+  }
+
+  @Override
+  public long getLong(String columnLabel) throws SQLException {
+    return getLong(findColumn(columnLabel));
+  }
+
+  @Override
+  public float getFloat(String columnLabel) throws SQLException {
+    return getFloat(findColumn(columnLabel));
+  }
+
+  @Override
+  public double getDouble(String columnLabel) throws SQLException {
+    return getDouble(findColumn(columnLabel));
+  }
+
+  @Override
+  @Deprecated
+  public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException {
+    return getBigDecimal(findColumn(columnLabel), scale);
+  }
+
+  @Override
+  public byte[] getBytes(String columnLabel) throws SQLException {
+    return getBytes(findColumn(columnLabel));
+  }
+
+  @Override
+  public Date getDate(String columnLabel) throws SQLException {
+    return getDate(findColumn(columnLabel));
+  }
+
+  @Override
+  public Time getTime(String columnLabel) throws SQLException {
+    return getTime(findColumn(columnLabel));
+  }
+
+  @Override
+  public Timestamp getTimestamp(String columnLabel) throws SQLException {
+    return getTimestamp(findColumn(columnLabel));
+  }
+
+  @Override
+  public InputStream getAsciiStream(String columnLabel) throws SQLException {
+    return getAsciiStream(findColumn(columnLabel));
+  }
+
+  @Override
+  @Deprecated
+  public InputStream getUnicodeStream(String columnLabel) throws SQLException {
+    return getUnicodeStream(findColumn(columnLabel));
+  }
+
+  @Override
+  public InputStream getBinaryStream(String columnLabel) throws SQLException {
+    return getBinaryStream(findColumn(columnLabel));
+  }
+
+  @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);
+  }
+
+  @Override
+  public Object getObject(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+    return rowDecoder.defaultDecode(
+        context.getConf(), metadataList, fieldIndex, rowBuf, fieldLength);
+  }
+
+  @Override
+  public Object getObject(String columnLabel) throws SQLException {
+    return getObject(findColumn(columnLabel));
+  }
+
+  @Override
+  public Reader getCharacterStream(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+    return rowDecoder.decode(
+        ReaderCodec.INSTANCE, null, rowBuf, fieldLength, metadataList, fieldIndex);
+  }
+
+  @Override
+  public Reader getCharacterStream(String columnLabel) throws SQLException {
+    return getCharacterStream(findColumn(columnLabel));
+  }
+
+  @Override
+  public BigDecimal getBigDecimal(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+    return rowDecoder.decode(
+        BigDecimalCodec.INSTANCE, null, rowBuf, fieldLength, metadataList, fieldIndex);
+  }
+
+  @Override
+  public BigDecimal getBigDecimal(String columnLabel) throws SQLException {
+    return getBigDecimal(findColumn(columnLabel));
+  }
+
+  /**
+   * Verify that result-set is not closed, throwing an exception if closed
+   *
+   * @throws SQLException if closed
+   */
+  protected void checkClose() throws SQLException {
+    if (closed) {
+      throw exceptionFactory.create("Operation not permit on a closed resultSet", "HY000");
+    }
+  }
+
+  /**
+   * Throw an exception if result-set type is ResultSet.TYPE_FORWARD_ONLY
+   *
+   * @throws SQLException throw error if type is ResultSet.TYPE_FORWARD_ONLY
+   */
+  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() {
+    return statement;
+  }
+
+  /**
+   * Update statement that initiate this result-set
+   *
+   * @param stmt statement
+   */
+  public void setStatement(Statement stmt) {
+    statement = stmt;
+  }
+
+  /** Force using alias as name */
+  public void useAliasAsName() {
+    for (Column packet : metadataList) {
+      packet.useAliasAsName();
+    }
+    forceAlias = true;
+  }
+
+  @Override
+  public Object getObject(int columnIndex, Map<String, Class<?>> map) throws SQLException {
+    if (map == null || map.isEmpty()) {
+      return getObject(columnIndex);
+    }
+    throw exceptionFactory.notSupported(
+        "Method ResultSet.getObject(int columnIndex, Map<String, Class<?>> map) not supported for"
+            + " non empty map");
+  }
+
+  @Override
+  public Ref getRef(int columnIndex) throws SQLException {
+    throw exceptionFactory.notSupported("Method ResultSet.getRef not supported");
+  }
+
+  @Override
+  public Blob getBlob(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+    return rowDecoder.decode(
+        BlobCodec.INSTANCE, null, rowBuf, fieldLength, metadataList, fieldIndex);
+  }
+
+  @Override
+  public Clob getClob(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+    return rowDecoder.decode(
+        ClobCodec.INSTANCE, null, rowBuf, fieldLength, metadataList, fieldIndex);
+  }
+
+  @Override
+  public Array getArray(int columnIndex) throws SQLException {
+    throw exceptionFactory.notSupported("Method ResultSet.getArray not supported");
+  }
+
+  @Override
+  public Object getObject(String columnLabel, Map<String, Class<?>> map) throws SQLException {
+    if (map == null || map.isEmpty()) {
+      return getObject(columnLabel);
+    }
+    throw exceptionFactory.notSupported(
+        "Method ResultSet.getObject(String columnLabel, Map<String, Class<?>> 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 getBlob(findColumn(columnLabel));
+  }
+
+  @Override
+  public Clob getClob(String columnLabel) throws SQLException {
+    return getClob(findColumn(columnLabel));
+  }
+
+  @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 {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+    return rowDecoder.decodeDate(metadataList, fieldIndex, rowBuf, fieldLength, cal);
+  }
+
+  @Override
+  public Date getDate(String columnLabel, Calendar cal) throws SQLException {
+    return getDate(findColumn(columnLabel), cal);
+  }
+
+  @Override
+  public Time getTime(int columnIndex, Calendar cal) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+    return rowDecoder.decodeTime(metadataList, fieldIndex, rowBuf, fieldLength, cal);
+  }
+
+  @Override
+  public Time getTime(String columnLabel, Calendar cal) throws SQLException {
+    return getTime(findColumn(columnLabel), cal);
+  }
+
+  @Override
+  public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+    return rowDecoder.decodeTimestamp(metadataList, fieldIndex, rowBuf, fieldLength, cal);
+  }
+
+  @Override
+  public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException {
+    return getTimestamp(findColumn(columnLabel), cal);
+  }
+
+  @Override
+  public URL getURL(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+
+    String s =
+        rowDecoder.decode(
+            StringCodec.INSTANCE, null, rowBuf, fieldLength, metadataList, fieldIndex);
+    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(findColumn(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() {
+    return ResultSet.HOLD_CURSORS_OVER_COMMIT;
+  }
+
+  @Override
+  public boolean isClosed() {
+    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 {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+    return (NClob)
+        rowDecoder.decode(ClobCodec.INSTANCE, null, rowBuf, fieldLength, metadataList, fieldIndex);
+  }
+
+  @Override
+  public NClob getNClob(String columnLabel) throws SQLException {
+    return getNClob(findColumn(columnLabel));
+  }
+
+  @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("Method ResultSet.updateSQLXML not supported");
+  }
+
+  @Override
+  public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException {
+    throw exceptionFactory.notSupported("Method ResultSet.updateSQLXML not supported");
+  }
+
+  @Override
+  public String getNString(int columnIndex) throws SQLException {
+    return getString(columnIndex);
+  }
+
+  @Override
+  public String getNString(String columnLabel) throws SQLException {
+    return getString(columnLabel);
+  }
+
+  @Override
+  public Reader getNCharacterStream(int columnIndex) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    if (fieldLength == NULL_LENGTH) {
+      return null;
+    }
+    return rowDecoder.decode(
+        ReaderCodec.INSTANCE, null, rowBuf, fieldLength, metadataList, fieldIndex);
+  }
+
+  @Override
+  public Reader getNCharacterStream(String columnLabel) throws SQLException {
+    return getNCharacterStream(findColumn(columnLabel));
+  }
+
+  @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
+  @SuppressWarnings("unchecked")
+  public <T> T getObject(int columnIndex, Class<T> type) throws SQLException {
+    checkIndex(columnIndex);
+    fieldLength =
+        rowDecoder.setPosition(
+            columnIndex - 1, fieldIndex, maxIndex, rowBuf, nullBitmap, metadataList);
+    Calendar calendar = null;
+    if (wasNull()) {
+      if (type.isPrimitive()) {
+        throw new SQLException(
+            String.format("Cannot return null for primitive %s", type.getName()));
+      }
+      return null;
+    }
+    Configuration conf = context.getConf();
+    ColumnDecoder column = metadataList[columnIndex - 1];
+    // type generic, return "natural" java type
+    if (Object.class.equals(type) || type == null) {
+      return (T) rowDecoder.defaultDecode(conf, metadataList, fieldIndex, rowBuf, fieldLength);
+    }
+
+    for (Codec<?> codec : conf.codecs()) {
+      if (codec.canDecode(column, type)) {
+        return rowDecoder.decode(
+            (Codec<T>) codec, calendar, rowBuf, fieldLength, metadataList, fieldIndex);
+      }
+    }
+    rowBuf.skip(fieldLength);
+    throw new SQLException(
+        String.format("Type %s not supported type for %s type", type, column.getType().name()));
+  }
+
+  @Override
+  public <T> T getObject(String columnLabel, Class<T> type) throws SQLException {
+    return getObject(findColumn(columnLabel), type);
+  }
+
+  @Override
+  public <T> T unwrap(Class<T> 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) {
+    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");
+  }
+
+  /** Set row buffer to null (no row) */
+  protected void setNullRowBuf() {
+    rowBuf.buf(null, 0, 0);
+  }
+
+  /**
+   * set row decoder to current row data
+   *
+   * @param row row
+   */
+  public void setRow(byte[] row) {
+    rowBuf.buf(row, row.length, 0);
+    fieldIndex.set(-1);
+  }
+
+  public int findColumn(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 < maxIndex; i++) {
+        Column ci = metadataList[i];
+        String columnAlias = ci.getColumnAlias();
+        if (columnAlias != null) {
+          columnAlias = columnAlias.toLowerCase(Locale.ROOT);
+          mapper.putIfAbsent(columnAlias, i + 1);
+          String tableAlias = ci.getTableAlias();
+          String tableLabel = tableAlias != null ? tableAlias : ci.getTable();
+          mapper.putIfAbsent(tableLabel.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/client/result/ResultSetMetaData.java b/src/main/java/org/mariadb/jdbc/client/result/ResultSetMetaData.java
new file mode 100644
index 0000000..2f9628c
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/result/ResultSetMetaData.java
@@ -0,0 +1,341 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.result;
+
+import java.sql.SQLException;
+import java.sql.Types;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.Column;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.export.ExceptionFactory;
+import org.mariadb.jdbc.util.constants.ColumnFlags;
+
+/** Result-set metadata */
+public class ResultSetMetaData implements java.sql.ResultSetMetaData {
+
+  private final ExceptionFactory exceptionFactory;
+  private final ColumnDecoder[] fieldPackets;
+  private final Configuration conf;
+  private final boolean forceAlias;
+
+  /**
+   * Constructor.
+   *
+   * @param exceptionFactory default exception handler
+   * @param fieldPackets column informations
+   * @param conf connection options
+   * @param forceAlias force table and column name alias as original data
+   */
+  public ResultSetMetaData(
+      final ExceptionFactory exceptionFactory,
+      final ColumnDecoder[] fieldPackets,
+      final Configuration conf,
+      final boolean forceAlias) {
+    this.exceptionFactory = exceptionFactory;
+    this.fieldPackets = fieldPackets;
+    this.conf = conf;
+    this.forceAlias = forceAlias;
+  }
+
+  /**
+   * Returns the number of columns in this <code>ResultSet</code> object.
+   *
+   * @return the number of columns
+   */
+  public int getColumnCount() {
+    return fieldPackets.length;
+  }
+
+  /**
+   * Indicates whether the designated column is automatically numbered.
+   *
+   * @param column the first column is 1, the second is 2, ...
+   * @return <code>true</code> if so; <code>false</code> otherwise
+   * @throws SQLException if a database access error occurs
+   */
+  public boolean isAutoIncrement(final int column) throws SQLException {
+    return (getColumn(column).getFlags() & ColumnFlags.AUTO_INCREMENT) != 0;
+  }
+
+  /**
+   * Indicates whether a column's case matters.
+   *
+   * @param column the first column is 1, the second is 2, ...
+   * @return <code>true</code> if so; <code>false</code> otherwise
+   */
+  public boolean isCaseSensitive(final int column) {
+    return true;
+  }
+
+  /**
+   * Indicates whether the designated column can be used in a where clause.
+   *
+   * @param column the first column is 1, the second is 2, ...
+   * @return <code>true</code> if so; <code>false</code> otherwise
+   */
+  public boolean isSearchable(final int column) {
+    return true;
+  }
+
+  /**
+   * Indicates whether the designated column is a cash value.
+   *
+   * @param column the first column is 1, the second is 2, ...
+   * @return <code>true</code> if so; <code>false</code> otherwise
+   */
+  public boolean isCurrency(final int column) {
+    return false;
+  }
+
+  /**
+   * Indicates the nullability of values in the designated column.
+   *
+   * @param column the first column is 1, the second is 2, ...
+   * @return the nullability status of the given column; one of <code>columnNoNulls</code>, <code>
+   *     columnNullable</code> or <code>columnNullableUnknown</code>
+   * @throws SQLException if a database access error occurs
+   */
+  public int isNullable(final int column) throws SQLException {
+    if ((getColumn(column).getFlags() & ColumnFlags.NOT_NULL) == 0) {
+      return java.sql.ResultSetMetaData.columnNullable;
+    } else {
+      return java.sql.ResultSetMetaData.columnNoNulls;
+    }
+  }
+
+  /**
+   * Indicates whether values in the designated column are signed numbers.
+   *
+   * @param column the first column is 1, the second is 2, ...
+   * @return <code>true</code> if so; <code>false</code> otherwise
+   * @throws SQLException if a database access error occurs
+   */
+  public boolean isSigned(int column) throws SQLException {
+    return getColumn(column).isSigned();
+  }
+
+  /**
+   * Indicates the designated column's normal maximum width in characters.
+   *
+   * @param column the first column is 1, the second is 2, ...
+   * @return the normal maximum number of characters allowed as the width of the designated column
+   * @throws SQLException if a database access error occurs
+   */
+  public int getColumnDisplaySize(final int column) throws SQLException {
+    return getColumn(column).getDisplaySize();
+  }
+
+  /**
+   * Gets the designated column's suggested title for use in printouts and displays. The suggested
+   * title is usually specified by the SQL <code>AS</code> clause. If an SQL <code>AS</code> is not
+   * specified, the value returned from <code>getColumnLabel</code> will be the same as the value
+   * returned by the <code>getColumnName</code> method.
+   *
+   * @param column the first column is 1, the second is 2, ...
+   * @return the suggested column title
+   * @throws SQLException if a database access error occurs
+   */
+  public String getColumnLabel(final int column) throws SQLException {
+    return getColumn(column).getColumnAlias();
+  }
+
+  /**
+   * Get the designated column's name.
+   *
+   * @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 idx) throws SQLException {
+    Column column = getColumn(idx);
+    String columnName = column.getColumnName();
+    if ("".equals(columnName) || forceAlias) {
+      return column.getColumnAlias();
+    }
+    return columnName;
+  }
+
+  /**
+   * Get the designated column's table's schema.
+   *
+   * @param column the first column is 1, the second is 2, ...
+   * @return schema name or "" if not applicable
+   * @throws SQLException if a database access error occurs
+   */
+  public String getCatalogName(int column) throws SQLException {
+    return getColumn(column).getSchema();
+  }
+
+  /**
+   * Get the designated column's specified column size. 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 column the first column is 1, the second is 2, ...
+   * @return precision
+   * @throws SQLException if a database access error occurs
+   */
+  public int getPrecision(final int column) throws SQLException {
+    return 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 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 index) throws SQLException {
+    return getColumn(index).getDecimals();
+  }
+
+  /**
+   * Gets the designated column's table name.
+   *
+   * @param column the first column is 1, the second is 2, ...
+   * @return table name or "" if not applicable
+   * @throws SQLException if a database access error occurs
+   */
+  public String getTableName(final int column) throws SQLException {
+    if (forceAlias) {
+      return getColumn(column).getTableAlias();
+    }
+
+    if (conf.blankTableNameMeta()) {
+      return "";
+    }
+
+    return getColumn(column).getTable();
+  }
+
+  public String getSchemaName(int column) {
+    return "";
+  }
+
+  /**
+   * Retrieves the designated column's SQL type.
+   *
+   * @param column the first column is 1, the second is 2, ...
+   * @return SQL type from java.sql.Types
+   * @throws SQLException if a database access error occurs
+   * @see Types
+   */
+  public int getColumnType(final int column) throws SQLException {
+    return getColumn(column).getColumnType(conf);
+  }
+
+  /**
+   * Retrieves the designated column's database-specific type name.
+   *
+   * @param index the first column is 1, the second is 2, ...
+   * @return type name used by the database. If the column type is a user-defined type, then a
+   *     fully-qualified type name is returned.
+   * @throws SQLException if a database access error occurs
+   */
+  public String getColumnTypeName(final int index) throws SQLException {
+    return getColumn(index).getColumnTypeName(conf);
+  }
+
+  /**
+   * Indicates whether the designated column is definitely not writable.
+   *
+   * @param column the first column is 1, the second is 2, ...
+   * @return <code>true</code> if so; <code>false</code> otherwise
+   * @throws SQLException if a database access error occurs or in case of wrong index
+   */
+  public boolean isReadOnly(final int column) throws SQLException {
+    Column ci = getColumn(column);
+    return ci.getColumnName().isEmpty();
+  }
+
+  /**
+   * Indicates whether it is possible for writing on the designated column to succeed.
+   *
+   * @param column the first column is 1, the second is 2, ...
+   * @return <code>true</code> if so; <code>false</code> otherwise
+   * @throws SQLException if a database access error occurs or in case of wrong index
+   */
+  public boolean isWritable(final int column) throws SQLException {
+    return !isReadOnly(column);
+  }
+
+  /**
+   * Indicates whether writing on the designated column will definitely succeed.
+   *
+   * @param column the first column is 1, the second is 2, ...
+   * @return <code>true</code> if so; <code>false</code> otherwise
+   * @throws SQLException if a database access error occurs or in case of wrong index
+   */
+  public boolean isDefinitelyWritable(final int column) throws SQLException {
+    return !isReadOnly(column);
+  }
+
+  /**
+   * Returns the fully-qualified name of the Java class whose instances are manufactured if the
+   * method <code>ResultSet.getObject</code> is called to retrieve a value from the column. <code>
+   * ResultSet.getObject</code> may return a subclass of the class returned by this method.
+   *
+   * @param column the first column 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 <code>ResultSet.getObject</code> to retrieve the value in the specified
+   *     column. This is the class name used for custom mapping.
+   * @throws SQLException if a database access error occurs
+   */
+  public String getColumnClassName(int column) throws SQLException {
+    return getColumn(column).defaultClassname(conf);
+  }
+
+  private ColumnDecoder getColumn(int column) throws SQLException {
+    if (column >= 1 && column <= fieldPackets.length) {
+      return fieldPackets[column - 1];
+    }
+    throw exceptionFactory.create(String.format("wrong column index %s", column));
+  }
+
+  /**
+   * Returns an object that implements the given interface to allow access to non-standard methods,
+   * or standard methods not exposed by the proxy. <br>
+   * 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 result of
+   * calling <code>unwrap</code> 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 <code>SQLException
+   * </code> is thrown.
+   *
+   * @param iface A Class defining an interface that the result must implement.
+   * @return an object that implements the interface. Maybe a proxy for the actual implementing
+   *     object.
+   * @throws SQLException If no object found that implements the interface
+   */
+  public <T> T unwrap(final Class<T> 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 <code>
+   * isWrapperFor</code> 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
+   * <code>unwrap</code> so that callers can use this method to avoid expensive <code>unwrap</code>
+   * calls that may fail. If this method returns true then calling <code>unwrap</code> 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.
+   */
+  public boolean isWrapperFor(final Class<?> iface) {
+    return iface.isInstance(this);
+  }
+}
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 0000000..24678c6
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/result/StreamingResult.java
@@ -0,0 +1,397 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+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.ColumnDecoder;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.socket.Reader;
+
+/**
+ * Streaming result-set implementation. Implementation rely on reading as many rows than fetch size
+ * required, keeping remaining rows in TCP-IP buffer
+ *
+ * <p>The server usually expects clients to read off the result set relatively quickly. The
+ * net_write_timeout server variable controls this behavior (defaults to 60s).
+ *
+ * <p>If you don't expect results to be handled in this amount of time there is a different
+ * possibility:
+ *
+ * <ul>
+ *   <li>With &gt; MariaDB server, 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).
+ *   <li>for non mariadb servers, a specific query will have to temporarily set net_write_timeout
+ *       ("SET STATEMENT net_write_timeout=..."), and set it back afterward.
+ *   <li>if your application usually uses a lot of long queries with fetch size, the connection can
+ *       be set using option "sessionVariables=net_write_timeout=xxx"
+ * </ul>
+ *
+ * <p>Even using setFetchSize, the server will send all results to the client.
+ *
+ * <p>If another query is executed on the same connection when a streaming result-set has not been
+ * fully read, the connector will put the whole remaining streaming result-set in memory in order to
+ * execute the next query. This can lead to OutOfMemoryError if not handled.
+ */
+public class StreamingResult extends Result {
+
+  private final ReentrantLock lock;
+  private int dataFetchTime;
+  private int fetchSize;
+
+  /**
+   * Constructor
+   *
+   * @param stmt statement that initiate this result
+   * @param binaryProtocol is result-set binary encoded
+   * @param maxRows maximum row number
+   * @param metadataList column metadata
+   * @param reader packet reader
+   * @param context connection context
+   * @param fetchSize fetch size
+   * @param lock thread safe locker
+   * @param resultSetType result-set type
+   * @param closeOnCompletion close statement on completion
+   * @param traceEnable can network log be logged
+   * @throws SQLException if any error occurs
+   */
+  public StreamingResult(
+      Statement stmt,
+      boolean binaryProtocol,
+      long maxRows,
+      ColumnDecoder[] metadataList,
+      Reader reader,
+      Context context,
+      int fetchSize,
+      ReentrantLock lock,
+      int resultSetType,
+      boolean closeOnCompletion,
+      boolean traceEnable)
+      throws SQLException {
+
+    super(
+        stmt,
+        binaryProtocol,
+        maxRows,
+        metadataList,
+        reader,
+        context,
+        resultSetType,
+        closeOnCompletion,
+        traceEnable);
+    this.lock = lock;
+    this.dataFetchTime = 0;
+    this.fetchSize = fetchSize;
+    this.data = new byte[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 && (long) 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++;
+      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) {
+            setRow(data[rowPointer]);
+            return true;
+          }
+        } else {
+          // cursor can move backward, so driver must keep the results.
+          // results have been added to current resultSet
+          rowPointer++;
+          if (dataSize > rowPointer) {
+            setRow(data[rowPointer]);
+            return true;
+          }
+        }
+        setNullRowBuf();
+        return false;
+      }
+
+      // all data are reads and pointer is after last
+      rowPointer = dataSize;
+      setNullRowBuf();
+      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 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();
+    if (resultSetType == TYPE_FORWARD_ONLY) {
+      return rowPointer == 0 && dataSize > 0 && dataFetchTime == 1;
+    } else {
+      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
+      addStreamingValue();
+
+      if (loaded) {
+        // now driver is sure when data ends.
+        return rowPointer == dataSize - 1;
+      }
+
+      // There is data remaining
+      return false;
+    }
+  }
+
+  @Override
+  public void beforeFirst() throws SQLException {
+    checkClose();
+    checkNotForwardOnly();
+    setNullRowBuf();
+    rowPointer = -1;
+  }
+
+  @Override
+  public void afterLast() throws SQLException {
+    checkClose();
+    checkNotForwardOnly();
+    fetchRemaining();
+    setNullRowBuf();
+    rowPointer = dataSize;
+  }
+
+  @Override
+  public boolean first() throws SQLException {
+    checkClose();
+    checkNotForwardOnly();
+
+    rowPointer = 0;
+    if (dataSize > 0) {
+      setRow(data[rowPointer]);
+      return true;
+    }
+    setNullRowBuf();
+    return false;
+  }
+
+  @Override
+  public boolean last() throws SQLException {
+    checkClose();
+    fetchRemaining();
+    rowPointer = dataSize - 1;
+    if (dataSize > 0) {
+      setRow(data[rowPointer]);
+      return true;
+    }
+    setNullRowBuf();
+    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;
+      setNullRowBuf();
+      return false;
+    }
+
+    if (idx > 0 && idx <= dataSize) {
+      rowPointer = idx - 1;
+      setRow(data[rowPointer]);
+      return true;
+    }
+
+    // if streaming, must read additional results.
+    fetchRemaining();
+
+    if (idx > 0) {
+      if (idx <= dataSize) {
+        rowPointer = idx - 1;
+        setRow(data[rowPointer]);
+        return true;
+      }
+
+      rowPointer = dataSize; // go to afterLast() position
+      setNullRowBuf();
+
+    } else {
+
+      if (dataSize + idx >= 0) {
+        // absolute position reverse from ending resultSet
+        rowPointer = dataSize + idx;
+        setRow(data[rowPointer]);
+        return true;
+      }
+      setNullRowBuf();
+      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;
+      setNullRowBuf();
+      return false;
+    }
+
+    while (newPos >= dataSize) {
+      if (loaded) {
+        rowPointer = dataSize;
+        setNullRowBuf();
+        return false;
+      }
+      addStreamingValue();
+    }
+
+    rowPointer = newPos;
+    setRow(data[rowPointer]);
+    return true;
+  }
+
+  @Override
+  public boolean previous() throws SQLException {
+    checkClose();
+    checkNotForwardOnly();
+    if (rowPointer > -1) {
+      rowPointer--;
+      if (rowPointer != -1) {
+        setRow(data[rowPointer]);
+        return true;
+      }
+    }
+    setNullRowBuf();
+    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 0000000..0947ba9
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/result/UpdatableResult.java
@@ -0,0 +1,1135 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.mariadb.jdbc.BasePreparedStatement;
+import org.mariadb.jdbc.Connection;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.Column;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.result.rowdecoder.BinaryRowDecoder;
+import org.mariadb.jdbc.codec.*;
+import org.mariadb.jdbc.plugin.Codec;
+import org.mariadb.jdbc.plugin.codec.*;
+import org.mariadb.jdbc.util.ParameterList;
+
+/** Updatable result implementation */
+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 static final int STATE_INSERTED = 4;
+
+  private String database;
+  private String table;
+  private boolean canInsert;
+  private boolean canUpdate;
+  private String sqlStateError = "HY000";
+  private boolean isAutoincrementPk;
+  private int savedRowPointer;
+  private String changeError;
+  private int state = STATE_STANDARD;
+  private ParameterList parameters;
+  private String[] primaryCols;
+
+  /**
+   * Constructor
+   *
+   * @param stmt statement that initiate this result
+   * @param binaryProtocol are rows binary encoded
+   * @param maxRows maximum rows
+   * @param metadataList columns metadata
+   * @param reader packet reader
+   * @param context connection context
+   * @param resultSetType result-set type
+   * @param closeOnCompletion close on completion
+   * @param traceEnable must network exchanges be logged
+   * @throws IOException if any socket error occurs
+   * @throws SQLException for other kind of error
+   */
+  public UpdatableResult(
+      Statement stmt,
+      boolean binaryProtocol,
+      long maxRows,
+      ColumnDecoder[] metadataList,
+      org.mariadb.jdbc.client.socket.Reader reader,
+      Context context,
+      int resultSetType,
+      boolean closeOnCompletion,
+      boolean traceEnable)
+      throws IOException, SQLException {
+    super(
+        stmt,
+        binaryProtocol,
+        maxRows,
+        metadataList,
+        reader,
+        context,
+        resultSetType,
+        closeOnCompletion,
+        traceEnable);
+    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 (Column columnDefinition : metadataList) {
+      if (columnDefinition.getTable().isEmpty()) {
+        cannotUpdateInsertRow(
+            "The result-set contains fields without without any database/table information");
+        sqlStateError = "0A000";
+        return;
+      }
+
+      if (database != null && !database.equals(columnDefinition.getSchema())) {
+        cannotUpdateInsertRow("The result-set contains more than one database");
+        sqlStateError = "0A000";
+        return;
+      }
+      database = columnDefinition.getSchema();
+
+      if (table != null && !table.equals(columnDefinition.getTable())) {
+        cannotUpdateInsertRow("The result-set contains fields on different tables");
+        sqlStateError = "0A000";
+        return;
+      }
+      table = columnDefinition.getTable();
+    }
+
+    // check that listed column contain primary field
+    for (Column col : metadataList) {
+      if (col.isPrimaryKey()) {
+        isAutoincrementPk = col.isAutoIncrement();
+        if (isAutoincrementPk) {
+          primaryCols = new String[] {col.getColumnName()};
+          return;
+        }
+      }
+    }
+
+    // check that table contains a generated primary field
+    // to check if insert are still possible
+    ResultSet rs =
+        statement
+            .getConnection()
+            .createStatement()
+            .executeQuery("SHOW COLUMNS FROM `" + database + "`.`" + table + "`");
+    List<String> primaryColumns = new ArrayList<>();
+    while (rs.next()) {
+      if ("PRI".equals(rs.getString("Key"))) {
+        primaryColumns.add(rs.getString("Field"));
+        boolean keyPresent = false;
+        for (Column col : metadataList) {
+          if (rs.getString("Field").equals(col.getColumnName())) {
+            keyPresent = true;
+          }
+        }
+        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 (!keyPresent && !canBeNull && !hasDefault && !generated) {
+          canInsert = false;
+          changeError =
+              String.format("primary field `%s` is not present in query", rs.getString("Field"));
+        }
+        if (!keyPresent) {
+          canUpdate = false;
+          changeError =
+              String.format(
+                  "Cannot update rows, since primary field %s is not present in query",
+                  rs.getString("Field"));
+        }
+      }
+    }
+
+    if (primaryColumns.isEmpty()) {
+      canUpdate = false;
+      changeError = "Cannot update rows, since no primary field is present in query";
+    } else {
+      primaryCols = primaryColumns.toArray(new String[0]);
+    }
+  }
+
+  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_UPDATED) {
+      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, sqlStateError);
+      }
+    }
+  }
+
+  @Override
+  public boolean rowUpdated() {
+    return state == STATE_UPDATED;
+  }
+
+  @Override
+  public boolean rowInserted() {
+    return state == STATE_INSERTED;
+  }
+
+  @Override
+  public boolean rowDeleted() {
+    return false;
+  }
+
+  @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(findColumn(columnLabel));
+  }
+
+  @Override
+  public void updateBoolean(String columnLabel, boolean x) throws SQLException {
+    updateBoolean(findColumn(columnLabel), x);
+  }
+
+  @Override
+  public void updateByte(String columnLabel, byte x) throws SQLException {
+    updateByte(findColumn(columnLabel), x);
+  }
+
+  @Override
+  public void updateShort(String columnLabel, short x) throws SQLException {
+    updateShort(findColumn(columnLabel), x);
+  }
+
+  @Override
+  public void updateInt(String columnLabel, int x) throws SQLException {
+    updateInt(findColumn(columnLabel), x);
+  }
+
+  @Override
+  public void updateLong(String columnLabel, long x) throws SQLException {
+    updateLong(findColumn(columnLabel), x);
+  }
+
+  @Override
+  public void updateFloat(String columnLabel, float x) throws SQLException {
+    updateFloat(findColumn(columnLabel), x);
+  }
+
+  @Override
+  public void updateDouble(String columnLabel, double x) throws SQLException {
+    updateDouble(findColumn(columnLabel), x);
+  }
+
+  @Override
+  public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException {
+    updateBigDecimal(findColumn(columnLabel), x);
+  }
+
+  @Override
+  public void updateString(String columnLabel, String x) throws SQLException {
+    updateString(findColumn(columnLabel), x);
+  }
+
+  @Override
+  public void updateBytes(String columnLabel, byte[] x) throws SQLException {
+    updateBytes(findColumn(columnLabel), x);
+  }
+
+  @Override
+  public void updateDate(String columnLabel, Date x) throws SQLException {
+    updateDate(findColumn(columnLabel), x);
+  }
+
+  @Override
+  public void updateTime(String columnLabel, Time x) throws SQLException {
+    updateTime(findColumn(columnLabel), x);
+  }
+
+  @Override
+  public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException {
+    updateTimestamp(findColumn(columnLabel), x);
+  }
+
+  @Override
+  public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException {
+    updateAsciiStream(findColumn(columnLabel), x, length);
+  }
+
+  @Override
+  public void updateBinaryStream(String columnLabel, InputStream x, int length)
+      throws SQLException {
+    updateBinaryStream(findColumn(columnLabel), x, length);
+  }
+
+  @Override
+  public void updateCharacterStream(String columnLabel, Reader reader, int length)
+      throws SQLException {
+    updateCharacterStream(findColumn(columnLabel), reader, length);
+  }
+
+  @Override
+  public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException {
+    updateObject(findColumn(columnLabel), x, scaleOrLength);
+  }
+
+  @Override
+  public void updateObject(String columnLabel, Object x) throws SQLException {
+    updateObject(findColumn(columnLabel), x);
+  }
+
+  @Override
+  public void insertRow() throws SQLException {
+    if (state == STATE_INSERT || state == STATE_INSERTED) {
+
+      // 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,
+                  rowDecoder instanceof BinaryRowDecoder)) {
+
+        int paramPos = 0;
+        for (int pos = 0; pos < metadataList.length; pos++) {
+          Column colInfo = metadataList[pos];
+          org.mariadb.jdbc.client.util.Parameter param =
+              parameters.size() > pos ? parameters.get(pos) : null;
+          if (param != null) {
+            ((BasePreparedStatement) insertPreparedStatement).setParameter(paramPos++, param);
+          } else if (!Arrays.asList(primaryCols).contains(colInfo.getColumnName())
+              && !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()) {
+            byte[] 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());
+      state = STATE_INSERTED;
+    }
+  }
+
+  /**
+   * Build insert query
+   *
+   * @return insert sql
+   * @throws SQLException exception
+   */
+  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++) {
+      Column colInfo = metadataList[pos];
+
+      if (pos != 0) {
+        returningClause.append(", ");
+      }
+      returningClause.append("`").append(colInfo.getColumnName()).append("`");
+
+      org.mariadb.jdbc.client.util.Parameter param =
+          parameters.size() > pos ? parameters.get(pos) : null;
+      if (param != null) {
+        if (!firstParam) {
+          insertSql.append(",");
+          valueClause.append(", ");
+        }
+        insertSql.append("`").append(colInfo.getColumnName()).append("`");
+        valueClause.append("?");
+        firstParam = false;
+      } else {
+        if (Arrays.asList(primaryCols).contains(colInfo.getColumnName())) {
+          boolean isAutoIncrement =
+              colInfo.isAutoIncrement() || (primaryCols.length == 1 && isAutoincrementPk);
+          if (isAutoIncrement || colInfo.hasDefault()) {
+            if (!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.getColumnName()));
+            }
+          } else {
+            throw exceptionFactory.create(
+                String.format(
+                    "Cannot call insertRow() not setting value for primary key %s",
+                    colInfo.getColumnName()));
+          }
+        } else if (!colInfo.hasDefault()) {
+          if (!firstParam) {
+            insertSql.append(",");
+            valueClause.append(", ");
+          }
+          firstParam = false;
+          insertSql.append("`").append(colInfo.getColumnName()).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++) {
+      Column colInfo = metadataList[pos];
+      if (pos != 0) {
+        selectSql.append(",");
+      }
+      selectSql.append("`").append(colInfo.getColumnName()).append("`");
+
+      if (Arrays.asList(primaryCols).contains(colInfo.getColumnName())) {
+        if (!firstPrimary) {
+          whereClause.append("AND ");
+        }
+        firstPrimary = false;
+        whereClause.append("`").append(colInfo.getColumnName()).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,
+            rowDecoder instanceof BinaryRowDecoder);
+  }
+
+  private byte[] refreshRawData() throws SQLException {
+    int fieldsPrimaryIndex = 0;
+    try (PreparedStatement refreshPreparedStatement = prepareRefreshStmt()) {
+
+      for (int pos = 0; pos < metadataList.length; pos++) {
+        Column colInfo = metadataList[pos];
+        if (Arrays.asList(primaryCols).contains(colInfo.getColumnName())) {
+          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.
+            org.mariadb.jdbc.client.util.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;
+
+    for (int pos = 0; pos < primaryCols.length; pos++) {
+      String key = primaryCols[pos];
+      if (pos != 0) {
+        whereClause.append("AND ");
+      }
+      whereClause.append("`").append(key).append("` = ? ");
+    }
+
+    for (int pos = 0; pos < metadataList.length; pos++) {
+      Column colInfo = metadataList[pos];
+
+      if (parameters.size() > pos && parameters.get(pos) != null) {
+        if (!firstUpdate) {
+          updateSql.append(",");
+        }
+        firstUpdate = false;
+        updateSql.append("`").append(colInfo.getColumnName()).append("` = ? ");
+      }
+    }
+    if (firstUpdate) return null;
+    return updateSql.append(whereClause).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 == STATE_UPDATED) {
+
+      // 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,
+                    rowDecoder instanceof BinaryRowDecoder)) {
+
+          int fieldsIndex = 0;
+          for (int pos = 0; pos < metadataList.length; pos++) {
+            if (parameters.size() > pos) {
+              org.mariadb.jdbc.client.util.Parameter param = parameters.get(pos);
+              if (param != null) {
+                ((BasePreparedStatement) preparedStatement).setParameter(fieldsIndex++, param);
+              }
+            }
+          }
+
+          for (int pos = 0; pos < metadataList.length; pos++) {
+            Column colInfo = metadataList[pos];
+            if (Arrays.asList(primaryCols).contains(colInfo.getColumnName())) {
+              preparedStatement.setObject(++fieldsIndex, getObject(pos + 1));
+            }
+          }
+
+          preparedStatement.execute();
+        }
+        refreshRow();
+      }
+      parameters = new ParameterList(parameters.size());
+      state = STATE_UPDATED;
+    }
+  }
+
+  @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, sqlStateError);
+    }
+    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 (Column colInfo : metadataList) {
+      if (Arrays.asList(primaryCols).contains(colInfo.getColumnName())) {
+        if (!firstPrimary) {
+          deleteSql.append("AND ");
+        }
+        firstPrimary = false;
+        deleteSql.append("`").append(colInfo.getColumnName()).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++) {
+        Column colInfo = metadataList[pos];
+        if (Arrays.asList(primaryCols).contains(colInfo.getColumnName())) {
+          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 refreshRow() 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() {
+    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, sqlStateError);
+    }
+    parameters = new ParameterList(parameters.size());
+    state = STATE_INSERT;
+    savedRowPointer = rowPointer;
+  }
+
+  @Override
+  public void moveToCurrentRow() {
+    state = STATE_STANDARD;
+    resetToRowPointer();
+  }
+
+  @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(findColumn(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(findColumn(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 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(findColumn(columnLabel), x, length);
+  }
+
+  @Override
+  public void updateBinaryStream(String columnLabel, InputStream x, long length)
+      throws SQLException {
+    updateBinaryStream(findColumn(columnLabel), x, length);
+  }
+
+  @Override
+  public void updateCharacterStream(String columnLabel, Reader reader, long length)
+      throws SQLException {
+    updateCharacterStream(findColumn(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(findColumn(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 {
+    updateClob(findColumn(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(findColumn(columnLabel), x);
+  }
+
+  @Override
+  public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException {
+    updateBinaryStream(findColumn(columnLabel), x);
+  }
+
+  @Override
+  public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException {
+    updateCharacterStream(findColumn(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(findColumn(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(findColumn(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 : context.getConf().codecs()) {
+      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(findColumn(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(findColumn(columnLabel), x, targetSqlType);
+  }
+
+  @Override
+  public int getConcurrency() {
+    return CONCUR_UPDATABLE;
+  }
+
+  private void resetToRowPointer() {
+    rowPointer = savedRowPointer;
+    if (rowPointer != BEFORE_FIRST_POS && rowPointer < dataSize - 1) {
+      setRow(data[rowPointer]);
+    } else {
+      // all data are reads and pointer is after last
+      setNullRowBuf();
+    }
+    savedRowPointer = -1;
+  }
+
+  @Override
+  public void beforeFirst() throws SQLException {
+    if (state == STATE_INSERT) {
+      resetToRowPointer();
+    }
+    state = STATE_STANDARD;
+    super.beforeFirst();
+  }
+
+  @Override
+  public boolean first() throws SQLException {
+    if (state == STATE_INSERT) {
+      resetToRowPointer();
+    }
+    state = STATE_STANDARD;
+    return super.first();
+  }
+
+  @Override
+  public boolean last() throws SQLException {
+    if (state == STATE_INSERT) {
+      resetToRowPointer();
+    }
+    state = STATE_STANDARD;
+    return super.last();
+  }
+
+  @Override
+  public void afterLast() throws SQLException {
+    if (state == STATE_INSERT) {
+      resetToRowPointer();
+    }
+    state = STATE_STANDARD;
+    super.afterLast();
+  }
+
+  @Override
+  public boolean absolute(int row) throws SQLException {
+    if (state == STATE_INSERT) {
+      resetToRowPointer();
+    }
+    state = STATE_STANDARD;
+    return super.absolute(row);
+  }
+
+  @Override
+  public boolean relative(int rows) throws SQLException {
+    if (state == STATE_INSERT) {
+      resetToRowPointer();
+    }
+    state = STATE_STANDARD;
+    return super.relative(rows);
+  }
+
+  @Override
+  public boolean next() throws SQLException {
+    if (state == STATE_INSERT) {
+      resetToRowPointer();
+    }
+    state = STATE_STANDARD;
+    return super.next();
+  }
+
+  @Override
+  public boolean previous() throws SQLException {
+    if (state == STATE_INSERT) {
+      resetToRowPointer();
+    }
+    state = STATE_STANDARD;
+    return super.previous();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/result/rowdecoder/BinaryRowDecoder.java b/src/main/java/org/mariadb/jdbc/client/result/rowdecoder/BinaryRowDecoder.java
new file mode 100644
index 0000000..dffb6ff
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/result/rowdecoder/BinaryRowDecoder.java
@@ -0,0 +1,261 @@
+package org.mariadb.jdbc.client.result.rowdecoder;
+
+import static org.mariadb.jdbc.client.result.Result.NULL_LENGTH;
+
+import java.sql.Date;
+import java.sql.SQLException;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.impl.StandardReadableByteBuf;
+import org.mariadb.jdbc.client.util.MutableInt;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** BINARY rows decoder */
+public class BinaryRowDecoder implements RowDecoder {
+
+  /**
+   * Binary decode data according to data type.
+   *
+   * @param codec current codec
+   * @param cal calendar
+   * @param rowBuf row buffer
+   * @param fieldLength field length
+   * @param metadataList metadatas
+   * @param fieldIndex field index
+   * @return default object according to metadata
+   * @param <T> Codec default return type
+   * @throws SQLException if any decoding error occurs
+   */
+  public <T> T decode(
+      Codec<T> codec,
+      Calendar cal,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength,
+      ColumnDecoder[] metadataList,
+      final MutableInt fieldIndex)
+      throws SQLException {
+    return codec.decodeBinary(rowBuf, fieldLength, metadataList[fieldIndex.get()], cal);
+  }
+
+  @Override
+  public Object defaultDecode(
+      Configuration conf,
+      ColumnDecoder[] metadataList,
+      final MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].getDefaultBinary(conf, rowBuf, fieldLength);
+  }
+
+  public String decodeString(
+      ColumnDecoder[] metadataList,
+      final MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeStringBinary(rowBuf, fieldLength, null);
+  }
+
+  public byte decodeByte(
+      ColumnDecoder[] metadataList,
+      final MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeByteBinary(rowBuf, fieldLength);
+  }
+
+  public boolean decodeBoolean(
+      ColumnDecoder[] metadataList,
+      final MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeBooleanBinary(rowBuf, fieldLength);
+  }
+
+  public Date decodeDate(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength,
+      Calendar cal)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeDateBinary(rowBuf, fieldLength, cal);
+  }
+
+  public Time decodeTime(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength,
+      Calendar cal)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeTimeBinary(rowBuf, fieldLength, cal);
+  }
+
+  public Timestamp decodeTimestamp(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength,
+      Calendar cal)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeTimestampBinary(rowBuf, fieldLength, cal);
+  }
+
+  public short decodeShort(
+      ColumnDecoder[] metadataList,
+      final MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeShortBinary(rowBuf, fieldLength);
+  }
+
+  public int decodeInt(
+      ColumnDecoder[] metadataList,
+      final MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeIntBinary(rowBuf, fieldLength);
+  }
+
+  public long decodeLong(
+      ColumnDecoder[] metadataList,
+      final MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeLongBinary(rowBuf, fieldLength);
+  }
+
+  public float decodeFloat(
+      ColumnDecoder[] metadataList,
+      final MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeFloatBinary(rowBuf, fieldLength);
+  }
+
+  public double decodeDouble(
+      ColumnDecoder[] metadataList,
+      final MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeDoubleBinary(rowBuf, fieldLength);
+  }
+
+  public boolean wasNull(byte[] nullBitmap, final MutableInt fieldIndex, int fieldLength) {
+    return (nullBitmap[(fieldIndex.get() + 2) / 8] & (1 << ((fieldIndex.get() + 2) % 8))) > 0;
+  }
+
+  /**
+   * Set length and pos indicator to asked index.
+   *
+   * @param newIndex index (0 is first).
+   */
+  @Override
+  public int setPosition(
+      int newIndex,
+      final MutableInt fieldIndex,
+      int maxIndex,
+      StandardReadableByteBuf rowBuf,
+      byte[] nullBitmap,
+      ColumnDecoder[] metadataList) {
+
+    if (fieldIndex.get() >= newIndex) {
+      fieldIndex.set(0);
+      rowBuf.pos(1);
+      rowBuf.readBytes(nullBitmap);
+    } else {
+      fieldIndex.incrementAndGet();
+      if (fieldIndex.get() == 0) {
+        // skip header + null-bitmap
+        rowBuf.pos(1);
+        rowBuf.readBytes(nullBitmap);
+      }
+    }
+
+    while (fieldIndex.get() < newIndex) {
+      if ((nullBitmap[(fieldIndex.get() + 2) / 8] & (1 << ((fieldIndex.get() + 2) % 8))) == 0) {
+        // skip bytes
+        switch (metadataList[fieldIndex.get()].getType()) {
+          case BIGINT:
+          case DOUBLE:
+            rowBuf.skip(8);
+            break;
+
+          case INTEGER:
+          case MEDIUMINT:
+          case FLOAT:
+            rowBuf.skip(4);
+            break;
+
+          case SMALLINT:
+          case YEAR:
+            rowBuf.skip(2);
+            break;
+
+          case TINYINT:
+            rowBuf.skip(1);
+            break;
+
+          default:
+            rowBuf.skipLengthEncoded();
+            break;
+        }
+      }
+      fieldIndex.incrementAndGet();
+    }
+
+    if (wasNull(nullBitmap, fieldIndex, 0)) {
+      return NULL_LENGTH;
+    }
+
+    // read asked field position and length
+    switch (metadataList[fieldIndex.get()].getType()) {
+      case BIGINT:
+      case DOUBLE:
+        return 8;
+
+      case INTEGER:
+      case MEDIUMINT:
+      case FLOAT:
+        return 4;
+
+      case SMALLINT:
+      case YEAR:
+        return 2;
+
+      case TINYINT:
+        return 1;
+
+      default:
+        // field with variable length
+        byte len = rowBuf.readByte();
+        switch (len) {
+          case (byte) 252:
+            // length is encoded on 3 bytes (0xfc header + 2 bytes indicating length)
+            return rowBuf.readUnsignedShort();
+
+          case (byte) 253:
+            // length is encoded on 4 bytes (0xfd header + 3 bytes indicating length)
+            return rowBuf.readUnsignedMedium();
+
+          case (byte) 254:
+            // length is encoded on 9 bytes (0xfe header + 8 bytes indicating length)
+            return (int) rowBuf.readLong();
+          default:
+            return len & 0xff;
+        }
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/result/rowdecoder/RowDecoder.java b/src/main/java/org/mariadb/jdbc/client/result/rowdecoder/RowDecoder.java
new file mode 100644
index 0000000..ec173d8
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/result/rowdecoder/RowDecoder.java
@@ -0,0 +1,269 @@
+package org.mariadb.jdbc.client.result.rowdecoder;
+
+import java.sql.Date;
+import java.sql.SQLException;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.impl.StandardReadableByteBuf;
+import org.mariadb.jdbc.client.util.MutableInt;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** Row decoder interface */
+public interface RowDecoder {
+  /**
+   * Indicate if last decoded value was null
+   *
+   * @param nullBitmap null bitmap
+   * @param fieldIndex field index
+   * @param fieldLength field length
+   * @return true if last value was null
+   */
+  boolean wasNull(byte[] nullBitmap, MutableInt fieldIndex, int fieldLength);
+
+  /**
+   * Position the read index on buffer to data at indicated index.
+   *
+   * @param newIndex new data index
+   * @param fieldIndex current field index
+   * @param maxIndex maximum index
+   * @param rowBuf row buffer
+   * @param nullBitmap null bitmap
+   * @param metadataList metadata list
+   * @return new index to read data
+   */
+  int setPosition(
+      int newIndex,
+      MutableInt fieldIndex,
+      int maxIndex,
+      StandardReadableByteBuf rowBuf,
+      byte[] nullBitmap,
+      ColumnDecoder[] metadataList);
+
+  /**
+   * Decode data according to data type.
+   *
+   * @param codec current codec
+   * @param calendar calendar
+   * @param rowBuf row buffer
+   * @param fieldLength field length
+   * @param metadataList metadatas
+   * @param fieldIndex field index
+   * @return default object according to metadata
+   * @param <T> Codec default return type
+   * @throws SQLException if any decoding error occurs
+   */
+  <T> T decode(
+      Codec<T> codec,
+      Calendar calendar,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength,
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex)
+      throws SQLException;
+
+  /**
+   * Decode data according to data type.
+   *
+   * @param conf configuration
+   * @param metadataList metadata list
+   * @param fieldIndex field index
+   * @param rowBuf row buffer
+   * @param fieldLength field length
+   * @return data
+   * @throws SQLException if any decoding error occurs
+   */
+  Object defaultDecode(
+      Configuration conf,
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException;
+
+  /**
+   * Decode data according to byte.
+   *
+   * @param metadataList metadata list
+   * @param fieldIndex field index
+   * @param rowBuf row buffer
+   * @param fieldLength field length
+   * @return data
+   * @throws SQLException if data type cannot be decoded to byte value
+   */
+  byte decodeByte(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException;
+
+  /**
+   * Decode data according to boolean.
+   *
+   * @param metadataList metadata list
+   * @param fieldIndex field index
+   * @param rowBuf row buffer
+   * @param fieldLength field length
+   * @return data
+   * @throws SQLException if data type cannot be decoded to boolean value
+   */
+  boolean decodeBoolean(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException;
+  /**
+   * Decode data according to Date.
+   *
+   * @param metadataList metadata list
+   * @param fieldIndex field index
+   * @param rowBuf row buffer
+   * @param fieldLength field length
+   * @param cal calendar
+   * @return data
+   * @throws SQLException if data type cannot be decoded to Date value
+   */
+  Date decodeDate(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength,
+      Calendar cal)
+      throws SQLException;
+  /**
+   * Decode data according to Time.
+   *
+   * @param metadataList metadata list
+   * @param fieldIndex field index
+   * @param rowBuf row buffer
+   * @param fieldLength field length
+   * @param cal calendar
+   * @return data
+   * @throws SQLException if data type cannot be decoded to Time value
+   */
+  Time decodeTime(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength,
+      Calendar cal)
+      throws SQLException;
+  /**
+   * Decode data according to Timestamp.
+   *
+   * @param metadataList metadata list
+   * @param fieldIndex field index
+   * @param rowBuf row buffer
+   * @param fieldLength field length
+   * @param cal calendar
+   * @return data
+   * @throws SQLException if data type cannot be decoded to Timestamp value
+   */
+  Timestamp decodeTimestamp(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength,
+      Calendar cal)
+      throws SQLException;
+  /**
+   * Decode data according to short.
+   *
+   * @param metadataList metadata list
+   * @param fieldIndex field index
+   * @param rowBuf row buffer
+   * @param fieldLength field length
+   * @return data
+   * @throws SQLException if data type cannot be decoded to short value
+   */
+  short decodeShort(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException;
+  /**
+   * Decode data according to int.
+   *
+   * @param metadataList metadata list
+   * @param fieldIndex field index
+   * @param rowBuf row buffer
+   * @param fieldLength field length
+   * @return data
+   * @throws SQLException if data type cannot be decoded to int value
+   */
+  int decodeInt(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException;
+  /**
+   * Decode data according to String.
+   *
+   * @param metadataList metadata list
+   * @param fieldIndex field index
+   * @param rowBuf row buffer
+   * @param fieldLength field length
+   * @return data
+   * @throws SQLException if data type cannot be decoded to String value
+   */
+  String decodeString(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException;
+  /**
+   * Decode data according to long.
+   *
+   * @param metadataList metadata list
+   * @param fieldIndex field index
+   * @param rowBuf row buffer
+   * @param fieldLength field length
+   * @return data
+   * @throws SQLException if data type cannot be decoded to long value
+   */
+  long decodeLong(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException;
+  /**
+   * Decode data according to float.
+   *
+   * @param metadataList metadata list
+   * @param fieldIndex field index
+   * @param rowBuf row buffer
+   * @param fieldLength field length
+   * @return data
+   * @throws SQLException if data type cannot be decoded to float value
+   */
+  float decodeFloat(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException;
+  /**
+   * Decode data according to double.
+   *
+   * @param metadataList metadata list
+   * @param fieldIndex field index
+   * @param rowBuf row buffer
+   * @param fieldLength field length
+   * @return data
+   * @throws SQLException if data type cannot be decoded to double value
+   */
+  double decodeDouble(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException;
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/result/rowdecoder/TextRowDecoder.java b/src/main/java/org/mariadb/jdbc/client/result/rowdecoder/TextRowDecoder.java
new file mode 100644
index 0000000..5f08db3
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/result/rowdecoder/TextRowDecoder.java
@@ -0,0 +1,190 @@
+package org.mariadb.jdbc.client.result.rowdecoder;
+
+import static org.mariadb.jdbc.client.result.Result.NULL_LENGTH;
+
+import java.sql.Date;
+import java.sql.SQLException;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Calendar;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.impl.StandardReadableByteBuf;
+import org.mariadb.jdbc.client.util.MutableInt;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** TEXT rows decoder */
+public class TextRowDecoder implements RowDecoder {
+
+  @Override
+  public <T> T decode(
+      Codec<T> codec,
+      Calendar cal,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength,
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex)
+      throws SQLException {
+    return codec.decodeText(rowBuf, fieldLength, metadataList[fieldIndex.get()], cal);
+  }
+
+  @Override
+  public Object defaultDecode(
+      Configuration conf,
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].getDefaultText(conf, rowBuf, fieldLength);
+  }
+
+  @Override
+  public String decodeString(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeStringText(rowBuf, fieldLength, null);
+  }
+
+  public byte decodeByte(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeByteText(rowBuf, fieldLength);
+  }
+
+  public boolean decodeBoolean(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeBooleanText(rowBuf, fieldLength);
+  }
+
+  public Date decodeDate(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength,
+      Calendar cal)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeDateText(rowBuf, fieldLength, cal);
+  }
+
+  public Time decodeTime(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength,
+      Calendar cal)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeTimeText(rowBuf, fieldLength, cal);
+  }
+
+  public Timestamp decodeTimestamp(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength,
+      Calendar cal)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeTimestampText(rowBuf, fieldLength, cal);
+  }
+
+  public short decodeShort(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeShortText(rowBuf, fieldLength);
+  }
+
+  public int decodeInt(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeIntText(rowBuf, fieldLength);
+  }
+
+  public long decodeLong(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeLongText(rowBuf, fieldLength);
+  }
+
+  public float decodeFloat(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeFloatText(rowBuf, fieldLength);
+  }
+
+  public double decodeDouble(
+      ColumnDecoder[] metadataList,
+      MutableInt fieldIndex,
+      StandardReadableByteBuf rowBuf,
+      int fieldLength)
+      throws SQLException {
+    return metadataList[fieldIndex.get()].decodeDoubleText(rowBuf, fieldLength);
+  }
+
+  public boolean wasNull(byte[] nullBitmap, MutableInt fieldIndex, int fieldLength) {
+    return fieldLength == NULL_LENGTH;
+  }
+
+  /**
+   * Set length and pos indicator to asked index.
+   *
+   * @param newIndex index (1 is first).
+   */
+  @Override
+  public int setPosition(
+      int newIndex,
+      final MutableInt fieldIndex,
+      final int maxIndex,
+      final StandardReadableByteBuf rowBuf,
+      final byte[] nullBitmap,
+      final ColumnDecoder[] metadataList) {
+    if (fieldIndex.get() >= newIndex) {
+      fieldIndex.set(0);
+      rowBuf.pos(0);
+    } else {
+      fieldIndex.incrementAndGet();
+    }
+
+    while (fieldIndex.get() < newIndex) {
+      rowBuf.skipLengthEncoded();
+      fieldIndex.incrementAndGet();
+    }
+
+    byte len = rowBuf.buf[rowBuf.pos++];
+    switch (len) {
+      case (byte) 251:
+        return NULL_LENGTH;
+      case (byte) 252:
+        return rowBuf.readUnsignedShort();
+      case (byte) 253:
+        return rowBuf.readUnsignedMedium();
+      case (byte) 254:
+        int fieldLength = (int) rowBuf.readUnsignedInt();
+        rowBuf.skip(4);
+        return fieldLength;
+      default:
+        return len & 0xff;
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/socket/Reader.java b/src/main/java/org/mariadb/jdbc/client/socket/Reader.java
new file mode 100644
index 0000000..5dbf3f3
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/socket/Reader.java
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.socket;
+
+import java.io.IOException;
+import org.mariadb.jdbc.HostAddress;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.client.util.MutableByte;
+
+/** Packet Reader */
+public interface Reader {
+
+  /**
+   * Get next MySQL packet. Packet is expected to have size &lt; 16M and will use if possible an
+   * internal cached buffer. This packet bytes are expect to be read immediately
+   *
+   * @param traceEnable must trace pacjet
+   * @return Readable byte array packet.
+   * @throws IOException if socket exception occur.
+   */
+  ReadableByteBuf readReusablePacket(boolean traceEnable) throws IOException;
+
+  /**
+   * Get next MySQL packet. Packet is expected to have size &lt; 16M and will use if possible an
+   * internal cached buffer. This packet bytes are expect to be read immediately
+   *
+   * @return Readable byte array packet.
+   * @throws IOException if socket exception occur.
+   */
+  ReadableByteBuf readReusablePacket() throws IOException;
+
+  /**
+   * Get next MySQL packet. If packet is more than 16M, read as many packet needed to finish reading
+   * MySQL packet. (first that has not length = 16Mb)
+   *
+   * @param traceEnable must trace packet.
+   * @return array packet.
+   * @throws IOException if socket exception occur.
+   */
+  byte[] readPacket(boolean traceEnable) throws IOException;
+
+  /**
+   * Get a readable byte array from byte array. This packet is expected to be read immediately,
+   * since no lock is set on this packet.
+   *
+   * @param buf byte array to be parsed
+   * @return array packet.
+   */
+  ReadableByteBuf readableBufFromArray(byte[] buf);
+
+  /**
+   * Skip next MySQL packet. Packet is expected to have size &lt; 16M
+   *
+   * @throws IOException if socket exception occur.
+   */
+  void skipPacket() throws IOException;
+
+  /**
+   * Get current sequence object
+   *
+   * @return current sequence
+   */
+  MutableByte getSequence();
+
+  /**
+   * Close stream
+   *
+   * @throws IOException if any error occurs
+   */
+  void close() throws IOException;
+
+  /**
+   * Set server thread id.
+   *
+   * @param serverThreadId current server thread id.
+   * @param hostAddress host information
+   */
+  void setServerThreadId(Long serverThreadId, HostAddress hostAddress);
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/socket/Writer.java b/src/main/java/org/mariadb/jdbc/client/socket/Writer.java
new file mode 100644
index 0000000..a9482e2
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/socket/Writer.java
@@ -0,0 +1,249 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.socket;
+
+import java.io.IOException;
+import org.mariadb.jdbc.HostAddress;
+
+/** Packet Writer interface */
+public interface Writer {
+  /**
+   * current buffer position
+   *
+   * @return current buffer position
+   */
+  int pos();
+
+  /**
+   * Current buffer
+   *
+   * @return current buffer
+   */
+  byte[] buf();
+
+  /**
+   * Set current buffer position
+   *
+   * @param pos position
+   * @throws IOException if buffer cannot grow to position
+   */
+  void pos(int pos) throws IOException;
+
+  /**
+   * Write byte into buf, flush buf to socket if needed.
+   *
+   * @param value byte to send
+   * @throws IOException if socket error occur.
+   */
+  void writeByte(int value) throws IOException;
+
+  /**
+   * Write short value into buf. flush buf if too small.
+   *
+   * @param value short value
+   * @throws IOException if socket error occur
+   */
+  void writeShort(short value) throws IOException;
+
+  /**
+   * Write int value into buf. flush buf if too small.
+   *
+   * @param value int value
+   * @throws IOException if socket error occur
+   */
+  void writeInt(int value) throws IOException;
+
+  /**
+   * Write long value into buf. flush buf if too small.
+   *
+   * @param value long value
+   * @throws IOException if socket error occur
+   */
+  void writeLong(long value) throws IOException;
+
+  /**
+   * Write Double binary value to buffer
+   *
+   * @param value double value
+   * @throws IOException if socket error occur
+   */
+  void writeDouble(double value) throws IOException;
+
+  /**
+   * Write float binary value to buffer
+   *
+   * @param value float value
+   * @throws IOException if socket error occur
+   */
+  void writeFloat(float value) throws IOException;
+
+  /**
+   * Write byte array to buffer
+   *
+   * @param arr bytes
+   * @throws IOException if socket error occur
+   */
+  void writeBytes(byte[] arr) throws IOException;
+
+  /**
+   * Write byte array to buffer at a specific position
+   *
+   * @param arr bytes
+   * @param pos position
+   */
+  void writeBytesAtPos(byte[] arr, int pos);
+
+  /**
+   * 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
+   */
+  void writeBytes(byte[] arr, int off, int len) throws IOException;
+
+  /**
+   * Write field length into buf, flush socket if needed.
+   *
+   * @param length field length
+   * @throws IOException if socket error occur.
+   */
+  void writeLength(long length) throws IOException;
+
+  /**
+   * Write ascii string to buffer
+   *
+   * @param str string
+   * @throws IOException if socket error occurs
+   */
+  void writeAscii(String str) throws IOException;
+
+  /**
+   * Write utf8 string to buffer
+   *
+   * @param str string
+   * @throws IOException if socket error occurs
+   */
+  void writeString(String str) throws IOException;
+
+  /**
+   * Write string to socket.
+   *
+   * @param str string
+   * @param noBackslashEscapes escape method
+   * @throws IOException if socket error occur
+   */
+  void writeStringEscaped(String str, boolean noBackslashEscapes) throws IOException;
+
+  /**
+   * Write escape bytes to socket.
+   *
+   * @param bytes bytes
+   * @param len len to write
+   * @param noBackslashEscapes escape method
+   * @throws IOException if socket error occur
+   */
+  void writeBytesEscaped(byte[] bytes, int len, boolean noBackslashEscapes) throws IOException;
+
+  /**
+   * Send empty packet.
+   *
+   * @throws IOException if socket error occur.
+   */
+  void writeEmptyPacket() throws IOException;
+
+  /**
+   * Send packet to socket.
+   *
+   * @throws IOException if socket error occur.
+   */
+  void flush() throws IOException;
+
+  /**
+   * Send packet to buffered outputstream without flushing
+   *
+   * @throws IOException if socket error occur.
+   */
+  void flushPipeline() throws IOException;
+
+  /**
+   * must a max allowed length exception be thrown
+   *
+   * @param length command length
+   * @return true if too big
+   */
+  boolean throwMaxAllowedLength(int length);
+
+  /**
+   * Get current command length
+   *
+   * @return command length
+   */
+  long getCmdLength();
+
+  /**
+   * Indicate if logging trace are permitted
+   *
+   * @param permitTrace permits trace to be logged
+   */
+  void permitTrace(boolean permitTrace);
+
+  /**
+   * Set server thread id.
+   *
+   * @param serverThreadId current server thread id.
+   * @param hostAddress host information
+   */
+  void setServerThreadId(Long serverThreadId, HostAddress hostAddress);
+
+  /** mark position */
+  void mark();
+
+  /**
+   * has some position been marked
+   *
+   * @return is marked
+   */
+  boolean isMarked();
+
+  /**
+   * Current command has flushed packet to socket
+   *
+   * @return indicate if some packet have been flushed
+   */
+  boolean hasFlushed();
+
+  /**
+   * Flush to last mark.
+   *
+   * @throws IOException if flush fail.
+   */
+  void flushBufferStopAtMark() throws IOException;
+
+  /**
+   * Buffer has data after marked position
+   *
+   * @return indicate if there is data after marked position
+   */
+  boolean bufIsDataAfterMark();
+
+  /**
+   * Reset mark flag and send bytes after mark flag.
+   *
+   * @return bytes after mark flag
+   */
+  byte[] resetMark();
+
+  /** reset sequences and position for sending a new packet */
+  void initPacket();
+
+  /**
+   * Close socket stream
+   *
+   * @throws IOException if any error occurs
+   */
+  void close() throws IOException;
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/socket/impl/CompressInputStream.java b/src/main/java/org/mariadb/jdbc/client/socket/impl/CompressInputStream.java
new file mode 100644
index 0000000..c716901
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/socket/impl/CompressInputStream.java
@@ -0,0 +1,338 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.socket.impl;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.DataFormatException;
+import java.util.zip.Inflater;
+import org.mariadb.jdbc.client.util.MutableByte;
+
+/**
+ * Compression handler, permitting decompression of mysql packet if needed. When compression is set,
+ * using a 7 byte header to identify is packet is compressed or not.
+ */
+public class CompressInputStream extends InputStream {
+  private final InputStream in;
+  private final MutableByte sequence;
+
+  private final byte[] header = new byte[7];
+
+  private int end;
+  private int pos;
+  private byte[] buf;
+
+  /**
+   * Constructor. When this handler is used, driver expect packet with 7 byte compression header
+   *
+   * @param in socket input stream
+   * @param compressionSequence compression sequence
+   */
+  public CompressInputStream(InputStream in, MutableByte compressionSequence) {
+    this.in = in;
+    this.sequence = compressionSequence;
+  }
+
+  /**
+   * Reads up to <code>len</code> bytes of data from the input stream into an array of bytes. An
+   * attempt is made to read as many as <code>len</code> bytes, but a smaller number may be read.
+   * The number of bytes actually read is returned as an integer.
+   *
+   * <p>This method blocks until input data is available, end of file is detected, or an exception
+   * is thrown.
+   *
+   * <p>If <code>len</code> is zero, then no bytes are read and <code>0</code> is returned;
+   * otherwise, there is an attempt to read at least one byte. If no byte is available because the
+   * stream is at end of file, the value <code>-1</code> is returned; otherwise, at least one byte
+   * is read and stored into <code>b</code>.
+   *
+   * <p>The first byte read is stored into element <code>b[off]</code>, the next one into <code>
+   * b[off+1]</code>, and so on. The number of bytes read is, at most, equal to <code>len</code>.
+   * Let <i>k</i> be the number of bytes actually read; these bytes will be stored in elements
+   * <code>b[off]</code> through <code>b[off+</code><i>k</i><code>-1]</code>, leaving elements
+   * <code>b[off+</code><i>k</i><code>]</code> through <code>b[off+len-1]</code> unaffected.
+   *
+   * <p>In every case, elements <code>b[0]</code> through <code>b[off]</code> and elements <code>
+   * b[off+len]</code> through <code>b[b.length-1]</code> are unaffected.
+   *
+   * <p>The <code>read(b,</code> <code>off,</code> <code>len)</code> method for class <code>
+   * InputStream</code> simply calls the method <code>read()</code> repeatedly. If the first such
+   * call results in an <code>IOException</code>, that exception is returned from the call to the
+   * <code>read(b,</code> <code>off,</code> <code>len)</code> method. If any subsequent call to
+   * <code>read()</code> results in a <code>IOException</code>, the exception is caught and treated
+   * as if it were end of file; the bytes read up to that point are stored into <code>b</code> and
+   * the number of bytes read before the exception occurred is returned. The default implementation
+   * of this method blocks until the requested amount of input data <code>len</code> has been read,
+   * end of file is detected, or an exception is thrown. Subclasses are encouraged to provide a more
+   * efficient implementation of this method.
+   *
+   * @param b the buffer into which the data is read.
+   * @param off the start offset in array <code>b</code> at which the data is written.
+   * @param len the maximum number of bytes to read.
+   * @return the total number of bytes read into the buffer, or <code>-1</code> if there is no more
+   *     data because the end of the stream has been reached.
+   * @throws IOException If the first byte cannot be read for any reason other than end of file, or
+   *     if the input stream has been closed, or if some other I/O error occurs.
+   * @throws NullPointerException If <code>b</code> is <code>null</code>.
+   * @throws IndexOutOfBoundsException If <code>off</code> is negative, <code>len</code> is
+   *     negative, or <code>len</code> is greater than <code>b.length - off</code>
+   * @see InputStream#read()
+   */
+  @Override
+  public int read(byte[] b, int off, int len) throws IOException {
+    if (len == 0) {
+      return 0;
+    }
+
+    int totalReads = 0;
+
+    do {
+      if (end - pos <= 0) {
+        retrieveBuffer();
+      }
+      // copy internal value to buf.
+      int copyLength = Math.min(len - totalReads, end - pos);
+      System.arraycopy(buf, pos, b, off + totalReads, copyLength);
+      pos += copyLength;
+      totalReads += copyLength;
+    } while (totalReads < len && super.available() > 0);
+
+    return totalReads;
+  }
+
+  private void retrieveBuffer() throws IOException {
+    // ***************************************************
+    // Read header
+    // ***************************************************
+    int remaining = 7;
+    int readOffset = 0;
+    do {
+      int count = in.read(header, readOffset, remaining);
+      if (count < 0) {
+        throw new EOFException(
+            "unexpected end of stream, read "
+                + readOffset
+                + " bytes from 7 (socket was closed by server)");
+      }
+      remaining -= count;
+      readOffset += count;
+    } while (remaining > 0);
+
+    int compressedPacketLength =
+        (header[0] & 0xff) + ((header[1] & 0xff) << 8) + ((header[2] & 0xff) << 16);
+    sequence.set(header[3]);
+    int packetLength = (header[4] & 0xff) + ((header[5] & 0xff) << 8) + ((header[6] & 0xff) << 16);
+    boolean compressed = (packetLength != 0);
+    remaining = compressedPacketLength;
+    byte[] intermediaryBuf = new byte[remaining];
+
+    // ***************************************************
+    // Read content
+    // ***************************************************
+
+    readOffset = 0;
+    do {
+      int count = in.read(intermediaryBuf, readOffset, remaining);
+      if (count < 0) {
+        throw new EOFException(
+            "unexpected end of stream, read "
+                + ((compressed ? compressedPacketLength : packetLength) - remaining)
+                + " bytes from "
+                + (compressed ? compressedPacketLength : packetLength)
+                + " (socket was closed by server)");
+      }
+      remaining -= count;
+      readOffset += count;
+    } while (remaining > 0);
+
+    if (compressed) {
+      buf = new byte[packetLength];
+      Inflater inflater = new Inflater();
+      inflater.setInput(intermediaryBuf);
+      try {
+        int actualUncompressBytes = inflater.inflate(buf);
+        if (actualUncompressBytes != packetLength) {
+          throw new IOException(
+              "Invalid exception length after decompression "
+                  + actualUncompressBytes
+                  + ",expected "
+                  + packetLength);
+        }
+      } catch (DataFormatException dfe) {
+        throw new IOException(dfe);
+      }
+      inflater.end();
+      end = packetLength;
+    } else {
+      buf = intermediaryBuf;
+      end = compressedPacketLength;
+    }
+    pos = 0;
+  }
+
+  /**
+   * Skips over and discards <code>n</code> bytes of data from this input stream. The <code>skip
+   * </code> method may, for a variety of reasons, end up skipping over some smaller number of
+   * bytes, possibly <code>0</code>. This may result from any of a number of conditions; reaching
+   * end of file before <code>n</code> bytes have been skipped is only one possibility. The actual
+   * number of bytes skipped is returned. If {@code n} is negative, the {@code skip} method for
+   * class {@code InputStream} always returns 0, and no bytes are skipped. Subclasses may handle the
+   * negative value differently.
+   *
+   * <p>The <code>skip</code> method of this class creates a byte array and then repeatedly reads
+   * into it until <code>n</code> bytes have been read or the end of the stream has been reached.
+   * Subclasses are encouraged to provide a more efficient implementation of this method. For
+   * instance, the implementation may depend on the ability to seek.
+   *
+   * @param n the number of bytes to be skipped.
+   * @return the actual number of bytes skipped.
+   * @throws IOException if the stream does not support seek, or if some other I/O error occurs.
+   */
+  @Override
+  public long skip(long n) throws IOException {
+    return read(new byte[(int) n], 0, (int) n);
+  }
+
+  /**
+   * Returns an estimate of the number of bytes that can be read (or skipped over) from this input
+   * stream without blocking by the next invocation of a method for this input stream. The next
+   * invocation might be the same thread or another thread. A single read or skip of this many bytes
+   * will not block, but may read or skip fewer bytes.
+   *
+   * <p>Note that while some implementations of {@code InputStream} will return the total number of
+   * bytes in the stream, many will not. It is never correct to use the return value of this method
+   * to allocate a buffer intended to hold all data in this stream.
+   *
+   * <p>A subclass' implementation of this method may choose to throw an {@link IOException} if this
+   * input stream has been closed by invoking the {@link #close()} method.
+   *
+   * <p>The {@code available} method for class {@code InputStream} always returns {@code 0}.
+   *
+   * <p>This method should be overridden by subclasses.
+   *
+   * @return an estimate of the number of bytes that can be read (or skipped over) from this input
+   *     stream without blocking or {@code 0} when it reaches the end of the input stream.
+   * @throws IOException if an I/O error occurs.
+   */
+  @Override
+  public int available() throws IOException {
+    return in.available();
+  }
+
+  /**
+   * Closes this input stream and releases any system resources associated with the stream.
+   *
+   * <p>The <code>close</code> method of <code>InputStream</code> does nothing.
+   *
+   * @throws IOException if an I/O error occurs.
+   */
+  @Override
+  public void close() throws IOException {
+    in.close();
+  }
+
+  /**
+   * Marks the current position in this input stream. A subsequent call to the <code>reset</code>
+   * method repositions this stream at the last marked position so that subsequent reads re-read the
+   * same bytes.
+   *
+   * <p>The <code>readlimit</code> arguments tells this input stream to allow that many bytes to be
+   * read before the mark position gets invalidated.
+   *
+   * <p>The general contract of <code>mark</code> is that, if the method <code>markSupported</code>
+   * returns <code>true</code>, the stream somehow remembers all the bytes read after the call to
+   * <code>mark</code> and stands ready to supply those same bytes again if and whenever the method
+   * <code>reset</code> is called. However, the stream is not required to remember any data at all
+   * if more than <code>readlimit</code> bytes are read from the stream before <code>reset</code> is
+   * called.
+   *
+   * <p>Marking a closed stream should not have any effect on the stream.
+   *
+   * <p>The <code>mark</code> method of <code>InputStream</code> does nothing.
+   *
+   * @param readlimit the maximum limit of bytes that can be read before the mark position becomes
+   *     invalid.
+   * @see InputStream#reset()
+   */
+  @Override
+  public synchronized void mark(int readlimit) {
+    in.mark(readlimit);
+  }
+
+  /**
+   * Repositions this stream to the position at the time the <code>mark</code> method was last
+   * called on this input stream.
+   *
+   * <p>The general contract of <code>reset</code> is:
+   *
+   * <ul>
+   *   <li>If the method <code>markSupported</code> returns <code>true</code>, then:
+   *       <ul>
+   *         <li>If the method <code>mark</code> has not been called since the stream was created,
+   *             or the number of bytes read from the stream since <code>mark</code> was last called
+   *             is larger than the argument to <code>mark</code> at that last call, then an <code>
+   *             IOException</code> might be thrown.
+   *         <li>If such an <code>IOException</code> is not thrown, then the stream is reset to a
+   *             state such that all the bytes read since the most recent call to <code>mark</code>
+   *             (or since the start of the file, if <code>mark</code> has not been called) will be
+   *             resupplied to subsequent callers of the <code>read</code> method, followed by any
+   *             bytes that otherwise would have been the next input data as of the time of the call
+   *             to <code>reset</code>.
+   *       </ul>
+   *   <li>If the method <code>markSupported</code> returns <code>false</code>, then:
+   *       <ul>
+   *         <li>The call to <code>reset</code> may throw an <code>IOException</code>.
+   *         <li>If an <code>IOException</code> is not thrown, then the stream is reset to a fixed
+   *             state that depends on the particular type of the input stream and how it was
+   *             created. The bytes that will be supplied to subsequent callers of the <code>read
+   *             </code> method depend on the particular type of the input stream.
+   *       </ul>
+   * </ul>
+   *
+   * <p>The method <code>reset</code> for class <code>InputStream</code> does nothing except throw
+   * an <code>IOException</code>.
+   *
+   * @throws IOException if this stream has not been marked or if the mark has been invalidated.
+   * @see InputStream#mark(int)
+   * @see IOException
+   */
+  @Override
+  public synchronized void reset() throws IOException {
+    in.reset();
+  }
+
+  /**
+   * Tests if this input stream supports the <code>mark</code> and <code>reset</code> methods.
+   * Whether <code>mark</code> and <code>reset</code> are supported is an invariant property of a
+   * particular input stream instance. The <code>markSupported</code> method of <code>
+   * InputStream</code> returns <code>false</code>.
+   *
+   * @return <code>true</code> if this stream instance supports the mark and reset methods; <code>
+   *     false</code> otherwise.
+   * @see InputStream#mark(int)
+   * @see InputStream#reset()
+   */
+  @Override
+  public boolean markSupported() {
+    return in.markSupported();
+  }
+
+  /**
+   * Reads the next byte of data from the input stream. The value byte is returned as an <code>int
+   * </code> in the range <code>0</code> to <code>255</code>. If no byte is available because the
+   * end of the stream has been reached, the value <code>-1</code> is returned. This method blocks
+   * until input data is available, the end of the stream is detected, or an exception is thrown.
+   *
+   * <p>A subclass must provide an implementation of this method.
+   *
+   * @return the next byte of data, or <code>-1</code> if the end of the stream is reached.
+   * @throws IOException if an I/O error occurs.
+   */
+  @Override
+  public int read() throws IOException {
+    throw new IOException("NOT IMPLEMENTED !");
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/socket/impl/CompressOutputStream.java b/src/main/java/org/mariadb/jdbc/client/socket/impl/CompressOutputStream.java
new file mode 100644
index 0000000..cd239c7
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/socket/impl/CompressOutputStream.java
@@ -0,0 +1,195 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.socket.impl;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.zip.DeflaterOutputStream;
+import org.mariadb.jdbc.client.util.MutableByte;
+
+/**
+ * Compression writer handler Permit to wrap standard packet to compressed packet ( 7 byte header).
+ * Driver will compress packet only if packet size is meaningful (1536 bytes) &gt; to one TCP
+ * packet.
+ */
+public class CompressOutputStream extends OutputStream {
+  private static final int MIN_COMPRESSION_SIZE = 1536; // TCP-IP single packet
+  private final OutputStream out;
+  private final MutableByte sequence;
+  private final byte[] header = new byte[7];
+  private byte[] longPacketBuffer = null;
+
+  /**
+   * Constructor.
+   *
+   * @param out socket output stream
+   * @param compressionSequence compression sequence
+   */
+  public CompressOutputStream(OutputStream out, MutableByte compressionSequence) {
+    this.out = out;
+    this.sequence = compressionSequence;
+  }
+
+  /**
+   * Writes <code>len</code> bytes from the specified byte array starting at offset <code>off</code>
+   * to this output stream. The general contract for <code>write(b, off, len)</code> is that some
+   * bytes in the array <code>b</code> are written to the output stream in order; element <code>
+   * b[off]</code> is the first byte written and <code>b[off+len-1]</code> is the last byte written
+   * by this operation.
+   *
+   * <p>The <code>write</code> method of <code>OutputStream</code> calls the write method of one
+   * argument on each of the bytes to be written out. Subclasses are encouraged to override this
+   * method and provide a more efficient implementation.
+   *
+   * <p>If <code>b</code> is <code>null</code>, a <code>NullPointerException</code> is thrown.
+   *
+   * <p>If <code>off</code> is negative, or <code>len</code> is negative, or <code>off+len</code> is
+   * greater than the length of the array <code>b</code>, then an IndexOutOfBoundsException is
+   * thrown.
+   *
+   * @param b the data.
+   * @param off the start offset in the data.
+   * @param len the number of bytes to write.
+   * @throws IOException if an I/O error occurs. In particular, an <code>IOException</code> is
+   *     thrown if the output stream is closed.
+   */
+  @Override
+  public void write(byte[] b, int off, int len) throws IOException {
+    if (len + ((longPacketBuffer != null) ? longPacketBuffer.length : 0) < MIN_COMPRESSION_SIZE) {
+      // *******************************************************************************
+      // small packet, no compression
+      // *******************************************************************************
+
+      if (longPacketBuffer != null) {
+        header[0] = (byte) (len + longPacketBuffer.length);
+        header[1] = (byte) ((len + longPacketBuffer.length) >>> 8);
+        header[2] = 0;
+        header[3] = sequence.incrementAndGet();
+        header[4] = 0;
+        header[5] = 0;
+        header[6] = 0;
+        out.write(header, 0, 7);
+        out.write(longPacketBuffer, 0, longPacketBuffer.length);
+        out.write(b, off, len);
+        longPacketBuffer = null;
+        return;
+      }
+
+      header[0] = (byte) len;
+      header[1] = (byte) (len >>> 8);
+      header[2] = 0;
+      header[3] = sequence.incrementAndGet();
+      header[4] = 0;
+      header[5] = 0;
+      header[6] = 0;
+      out.write(header, 0, 7);
+      out.write(b, off, len);
+
+    } else {
+      // *******************************************************************************
+      // compressing packet
+      // *******************************************************************************
+      int sent = 0;
+      try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+        try (DeflaterOutputStream deflater = new DeflaterOutputStream(baos)) {
+
+          /**
+           * For multi packet, len will be 0x00ffffff + 4 bytes for header. but compression can only
+           * compress up to 0x00ffffff bytes (header initial length size cannot be > 3 bytes) so,
+           * for this specific case, a buffer will save remaining data
+           */
+          if (longPacketBuffer != null) {
+            deflater.write(longPacketBuffer, 0, longPacketBuffer.length);
+            sent = longPacketBuffer.length;
+            longPacketBuffer = null;
+          }
+          if (len + sent > 0x00ffffff) {
+            int remaining = len + sent - 0x00ffffff;
+            longPacketBuffer = new byte[remaining];
+            System.arraycopy(b, off + 0x00ffffff - sent, longPacketBuffer, 0, remaining);
+          }
+
+          int bufLenSent = Math.min(0x00ffffff - sent, len);
+          deflater.write(b, off, bufLenSent);
+          sent += bufLenSent;
+          deflater.finish();
+        }
+
+        byte[] compressedBytes = baos.toByteArray();
+
+        int compressLen = compressedBytes.length;
+
+        header[0] = (byte) compressLen;
+        header[1] = (byte) (compressLen >>> 8);
+        header[2] = (byte) (compressLen >>> 16);
+        header[3] = sequence.incrementAndGet();
+        header[4] = (byte) sent;
+        header[5] = (byte) (sent >>> 8);
+        header[6] = (byte) (sent >>> 16);
+
+        out.write(header, 0, 7);
+        out.write(compressedBytes, 0, compressLen);
+        out.flush();
+      }
+    }
+  }
+
+  /**
+   * Flushes this output stream and forces any buffered output bytes to be written out. The general
+   * contract of <code>flush</code> is that calling it is an indication that, if any bytes
+   * previously written have been buffered by the implementation of the output stream, such bytes
+   * should immediately be written to their intended destination.
+   *
+   * <p>If the intended destination of this stream is an abstraction provided by the underlying
+   * operating system, for example a file, then flushing the stream guarantees only that bytes
+   * previously written to the stream are passed to the operating system for writing; it does not
+   * guarantee that they are actually written to a physical device such as a disk drive.
+   *
+   * <p>The <code>flush</code> method of <code>OutputStream</code> does nothing.
+   *
+   * @throws IOException if an I/O error occurs.
+   */
+  @Override
+  public void flush() throws IOException {
+    if (longPacketBuffer != null) {
+      byte[] b = longPacketBuffer;
+      longPacketBuffer = null;
+      write(b, 0, b.length);
+    }
+    out.flush();
+    sequence.set((byte) -1);
+  }
+
+  /**
+   * Closes this output stream and releases any system resources associated with this stream. The
+   * general contract of <code>close</code> is that it closes the output stream. A closed stream
+   * cannot perform output operations and cannot be reopened.
+   *
+   * <p>The <code>close</code> method of <code>OutputStream</code> does nothing.
+   *
+   * @throws IOException if an I/O error occurs.
+   */
+  @Override
+  public void close() throws IOException {
+    out.close();
+  }
+
+  /**
+   * Writes the specified byte to this output stream. The general contract for <code>write</code> is
+   * that one byte is written to the output stream. The byte to be written is the eight low-order
+   * bits of the argument <code>b</code>. The 24 high-order bits of <code>b</code> are ignored.
+   *
+   * <p>Subclasses of <code>OutputStream</code> must provide an implementation for this method.
+   *
+   * @param b the <code>byte</code>.
+   * @throws IOException if an I/O error occurs. In particular, an <code>IOException</code> may be
+   *     thrown if the output stream has been closed.
+   */
+  @Override
+  public void write(int b) throws IOException {
+    throw new IOException("NOT EXPECTED !");
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/socket/impl/NamedPipeSocket.java b/src/main/java/org/mariadb/jdbc/client/socket/impl/NamedPipeSocket.java
new file mode 100644
index 0000000..4c8fd55
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/socket/impl/NamedPipeSocket.java
@@ -0,0 +1,151 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.socket.impl;
+
+import com.sun.jna.platform.win32.Kernel32;
+import java.io.*;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.util.concurrent.TimeUnit;
+
+/** Windows named pipe socket implementation */
+@SuppressWarnings("UnnecessaryInitCause")
+public class NamedPipeSocket extends Socket {
+
+  private final String host;
+  private final String name;
+
+  private RandomAccessFile file;
+  private InputStream is;
+  private OutputStream os;
+
+  /**
+   * Constructor
+   *
+   * @param host host
+   * @param name pipe name
+   */
+  public NamedPipeSocket(String host, String name) {
+    this.host = host;
+    this.name = name;
+  }
+
+  @Override
+  public void close() throws IOException {
+    if (file != null) {
+      file.close();
+      file = null;
+    }
+  }
+
+  /**
+   * Name pipe connection.
+   *
+   * @param endpoint endPoint
+   * @param timeout timeout in milliseconds
+   * @throws IOException exception
+   */
+  public void connect(SocketAddress endpoint, int timeout) throws IOException {
+    String filename =
+        String.format(
+            "\\\\%s\\pipe\\%s", (host == null || host.equals("localhost")) ? "." : host, name);
+
+    // use a default timeout of 100ms if no timeout set.
+    int usedTimeout = timeout == 0 ? 100 : timeout;
+    long initialNano = System.nanoTime();
+    do {
+      try {
+        file = new RandomAccessFile(filename, "rw");
+        break;
+      } catch (FileNotFoundException fileNotFoundException) {
+        try {
+          // using JNA if available
+          Kernel32.INSTANCE.WaitNamedPipe(filename, timeout);
+          // then retry connection
+          file = new RandomAccessFile(filename, "rw");
+        } catch (Throwable cle) {
+          // in case JNA not on classpath, then wait 10ms before next try.
+          if (System.nanoTime() - initialNano > TimeUnit.MILLISECONDS.toNanos(usedTimeout)) {
+            if (timeout == 0) {
+              throw new FileNotFoundException(
+                  fileNotFoundException.getMessage()
+                      + "\n"
+                      + "please consider set connectTimeout option, so connection can retry having"
+                      + " access to named pipe. \n"
+                      + "(Named pipe can throw ERROR_PIPE_BUSY error)");
+            }
+            throw fileNotFoundException;
+          }
+          try {
+            TimeUnit.MILLISECONDS.sleep(5);
+          } catch (InterruptedException interrupted) {
+            IOException ioException =
+                new IOException("Interruption during connection to named pipe");
+            ioException.initCause(interrupted);
+            throw ioException;
+          }
+        }
+      }
+    } while (true);
+
+    is =
+        new InputStream() {
+          @Override
+          public int read(byte[] bytes, int off, int len) throws IOException {
+            return file.read(bytes, off, len);
+          }
+
+          @Override
+          public int read() throws IOException {
+            return file.read();
+          }
+
+          @Override
+          public int read(byte[] bytes) throws IOException {
+            return file.read(bytes);
+          }
+        };
+
+    os =
+        new OutputStream() {
+          @Override
+          public void write(byte[] bytes, int off, int len) throws IOException {
+            file.write(bytes, off, len);
+          }
+
+          @Override
+          public void write(int value) {}
+        };
+  }
+
+  public InputStream getInputStream() {
+    return is;
+  }
+
+  public OutputStream getOutputStream() {
+    return os;
+  }
+
+  public void setTcpNoDelay(boolean bool) {
+    // do nothing
+  }
+
+  public void setKeepAlive(boolean bool) {
+    // do nothing
+  }
+
+  public void setSoLinger(boolean bool, int value) {
+    // do nothing
+  }
+
+  @Override
+  public void setSoTimeout(int timeout) {
+    // do nothing
+  }
+
+  public void shutdownOutput() {
+    // do nothing
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/socket/impl/PacketReader.java b/src/main/java/org/mariadb/jdbc/client/socket/impl/PacketReader.java
new file mode 100644
index 0000000..26c09f1
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/socket/impl/PacketReader.java
@@ -0,0 +1,315 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.socket.impl;
+
+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.client.ReadableByteBuf;
+import org.mariadb.jdbc.client.impl.StandardReadableByteBuf;
+import org.mariadb.jdbc.client.socket.Reader;
+import org.mariadb.jdbc.client.util.MutableByte;
+import org.mariadb.jdbc.util.log.Logger;
+import org.mariadb.jdbc.util.log.LoggerHelper;
+import org.mariadb.jdbc.util.log.Loggers;
+
+/** Packet reader */
+public class PacketReader implements Reader {
+
+  private StandardReadableByteBuf readBuf = new StandardReadableByteBuf(null, 0);
+
+  private static final int REUSABLE_BUFFER_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_BUFFER_LENGTH];
+  private final InputStream inputStream;
+  private final int maxQuerySizeToLog;
+
+  private final MutableByte sequence;
+  private String serverThreadLog = "";
+
+  /**
+   * Constructor of standard socket MySQL packet stream reader.
+   *
+   * @param in stream
+   * @param conf connection options
+   * @param sequence current increment sequence
+   */
+  public PacketReader(InputStream in, Configuration conf, MutableByte sequence) {
+    this.inputStream = in;
+    this.maxQuerySizeToLog = conf.maxQuerySizeToLog();
+    this.sequence = sequence;
+  }
+
+  public ReadableByteBuf readableBufFromArray(byte[] buf) {
+    readBuf.buf(buf, buf.length, 0);
+    return readBuf;
+  }
+
+  public ReadableByteBuf readReusablePacket() throws IOException {
+    return readReusablePacket(logger.isTraceEnabled());
+  }
+
+  public ReadableByteBuf readReusablePacket(boolean traceEnable) 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);
+
+    int lastPacketLength =
+        (header[0] & 0xff) + ((header[1] & 0xff) << 8) + ((header[2] & 0xff) << 16);
+    sequence.set(header[3]);
+
+    // prepare array
+    byte[] rawBytes;
+    if (lastPacketLength < REUSABLE_BUFFER_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 (traceEnable) {
+      logger.trace(
+          "read: {}\n{}",
+          serverThreadLog,
+          LoggerHelper.hex(header, rawBytes, 0, lastPacketLength, maxQuerySizeToLog));
+    }
+
+    readBuf.buf(rawBytes, lastPacketLength, 0);
+    return readBuf;
+  }
+
+  /**
+   * Get next MySQL packet. If packet is more than 16M, read as many packet needed to finish reading
+   * MySQL packet. (first that has not length = 16Mb)
+   *
+   * @param traceEnable must trace packet.
+   * @return array packet.
+   * @throws IOException if socket exception occur.
+   */
+  public byte[] readPacket(boolean traceEnable) 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);
+
+    int lastPacketLength =
+        (header[0] & 0xff) + ((header[1] & 0xff) << 8) + ((header[2] & 0xff) << 16);
+
+    // prepare array
+    byte[] 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 (traceEnable) {
+      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);
+
+        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 (traceEnable) {
+          logger.trace(
+              "read: {}\n{}",
+              serverThreadLog,
+              LoggerHelper.hex(
+                  header, rawBytes, currentbufLength, packetLength, maxQuerySizeToLog));
+        }
+
+        lastPacketLength += packetLength;
+      } while (packetLength == MAX_PACKET_SIZE);
+    }
+
+    return rawBytes;
+  }
+
+  public void skipPacket() throws IOException {
+    if (logger.isTraceEnabled()) {
+      readReusablePacket(logger.isTraceEnabled());
+      return;
+    }
+
+    // ***************************************************
+    // 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);
+
+    int lastPacketLength =
+        (header[0] & 0xff) + ((header[1] & 0xff) << 8) + ((header[2] & 0xff) << 16);
+
+    remaining = lastPacketLength;
+    do {
+      remaining -= inputStream.skip(remaining);
+    } while (remaining > 0);
+
+    // ***************************************************
+    // 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);
+
+        remaining = packetLength;
+        do {
+          remaining -= inputStream.skip(remaining);
+        } while (remaining > 0);
+
+        lastPacketLength += packetLength;
+      } while (packetLength == MAX_PACKET_SIZE);
+    }
+  }
+
+  public MutableByte 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 == null ? "-1" : serverThreadId)
+            + ((isMaster != null) ? " (" + (isMaster ? "M" : "S") + ")" : "");
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/socket/impl/PacketWriter.java b/src/main/java/org/mariadb/jdbc/client/socket/impl/PacketWriter.java
new file mode 100644
index 0000000..7504cc2
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/socket/impl/PacketWriter.java
@@ -0,0 +1,882 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.socket.impl;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import org.mariadb.jdbc.HostAddress;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.client.util.MutableByte;
+import org.mariadb.jdbc.export.MaxAllowedPacketException;
+import org.mariadb.jdbc.util.log.Logger;
+import org.mariadb.jdbc.util.log.LoggerHelper;
+import org.mariadb.jdbc.util.log.Loggers;
+
+/** Packet writer */
+@SuppressWarnings("SameReturnValue")
+public class PacketWriter implements Writer {
+
+  /** initial buffer size */
+  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 MEDIUM_BUFFER_SIZE = 128 * 1024;
+  private static final int LARGE_BUFFER_SIZE = 1024 * 1024;
+  private static final int MAX_PACKET_LENGTH = 0x00ffffff + 4;
+  private final int maxQuerySizeToLog;
+  private final OutputStream out;
+  private int maxPacketLength = MAX_PACKET_LENGTH;
+  private Integer maxAllowedPacket;
+  private long cmdLength;
+  private boolean permitTrace = true;
+  private String serverThreadLog = "";
+  private int mark = -1;
+  private boolean bufContainDataAfterMark = false;
+
+  /** internal buffer */
+  protected byte[] buf;
+  /** buffer position */
+  protected int pos = 4;
+  /** packet sequence */
+  protected final MutableByte sequence;
+  /** compressed packet sequence */
+  protected final MutableByte compressSequence;
+
+  /**
+   * Common feature to write data into socket, creating MariaDB Packet.
+   *
+   * @param out output stream
+   * @param maxQuerySizeToLog maximum query size to log
+   * @param maxAllowedPacket max allowed packet value if known
+   * @param sequence packet sequence
+   * @param compressSequence compressed packet sequence
+   */
+  public PacketWriter(
+      OutputStream out,
+      int maxQuerySizeToLog,
+      Integer maxAllowedPacket,
+      MutableByte sequence,
+      MutableByte compressSequence) {
+    this.out = out;
+    this.buf = new byte[SMALL_BUFFER_SIZE];
+    this.maxQuerySizeToLog = maxQuerySizeToLog;
+    this.cmdLength = 0;
+    this.sequence = sequence;
+    this.compressSequence = compressSequence;
+    this.maxAllowedPacket = maxAllowedPacket;
+  }
+
+  /**
+   * get current position
+   *
+   * @return current position
+   */
+  public int pos() {
+    return pos;
+  }
+
+  /**
+   * position setter
+   *
+   * @param pos new position
+   * @throws IOException if buffer is not big enough to contains new position
+   */
+  public void pos(int pos) throws IOException {
+    if (pos > buf.length) growBuffer(pos);
+    this.pos = pos;
+  }
+
+  /**
+   * get current command length
+   *
+   * @return current command length
+   */
+  public long getCmdLength() {
+    return cmdLength;
+  }
+
+  /**
+   * Write byte into buf, flush buf to socket if needed.
+   *
+   * @param value byte to send
+   * @throws IOException if socket error occur.
+   */
+  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);
+      }
+    }
+    buf[pos++] = (byte) value;
+  }
+
+  /**
+   * Write short value into buf. flush buf if too small.
+   *
+   * @param value short value
+   * @throws IOException if socket error occur
+   */
+  public void writeShort(short value) throws IOException {
+    if (2 > buf.length - pos) {
+      // not enough space remaining
+      writeBytes(new byte[] {(byte) value, (byte) (value >> 8)}, 0, 2);
+      return;
+    }
+
+    buf[pos] = (byte) value;
+    buf[pos + 1] = (byte) (value >> 8);
+    pos += 2;
+  }
+
+  /**
+   * Write int value into buf. flush buf if too small.
+   *
+   * @param value int value
+   * @throws IOException if socket error occur
+   */
+  public void writeInt(int value) throws IOException {
+    if (4 > buf.length - pos) {
+      // not enough space remaining
+      byte[] arr = new byte[4];
+      arr[0] = (byte) value;
+      arr[1] = (byte) (value >> 8);
+      arr[2] = (byte) (value >> 16);
+      arr[3] = (byte) (value >> 24);
+      writeBytes(arr, 0, 4);
+      return;
+    }
+
+    buf[pos] = (byte) value;
+    buf[pos + 1] = (byte) (value >> 8);
+    buf[pos + 2] = (byte) (value >> 16);
+    buf[pos + 3] = (byte) (value >> 24);
+    pos += 4;
+  }
+
+  /**
+   * Write long value into buf. flush buf if too small.
+   *
+   * @param value long value
+   * @throws IOException if socket error occur
+   */
+  public void writeLong(long value) throws IOException {
+    if (8 > buf.length - pos) {
+      // not enough space remaining
+      byte[] arr = new byte[8];
+      arr[0] = (byte) value;
+      arr[1] = (byte) (value >> 8);
+      arr[2] = (byte) (value >> 16);
+      arr[3] = (byte) (value >> 24);
+      arr[4] = (byte) (value >> 32);
+      arr[5] = (byte) (value >> 40);
+      arr[6] = (byte) (value >> 48);
+      arr[7] = (byte) (value >> 56);
+      writeBytes(arr, 0, 8);
+      return;
+    }
+
+    buf[pos] = (byte) value;
+    buf[pos + 1] = (byte) (value >> 8);
+    buf[pos + 2] = (byte) (value >> 16);
+    buf[pos + 3] = (byte) (value >> 24);
+    buf[pos + 4] = (byte) (value >> 32);
+    buf[pos + 5] = (byte) (value >> 40);
+    buf[pos + 6] = (byte) (value >> 48);
+    buf[pos + 7] = (byte) (value >> 56);
+    pos += 8;
+  }
+
+  public void writeDouble(double value) throws IOException {
+    writeLong(Double.doubleToLongBits(value));
+  }
+
+  public void writeFloat(float value) throws IOException {
+    writeInt(Float.floatToIntBits(value));
+  }
+
+  public void writeBytes(byte[] arr) throws IOException {
+    writeBytes(arr, 0, arr.length);
+  }
+
+  public void writeBytesAtPos(byte[] arr, int pos) {
+    System.arraycopy(arr, 0, buf, pos, arr.length);
+  }
+
+  /**
+   * 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();
+          }
+        }
+
+        if (len > buf.length - pos) {
+          // 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 writeLength(long length) throws IOException {
+    if (length < 251) {
+      writeByte((byte) length);
+      return;
+    }
+
+    if (length < 65536) {
+
+      if (3 > buf.length - pos) {
+        // not enough space remaining
+        byte[] arr = new byte[3];
+        arr[0] = (byte) 0xfc;
+        arr[1] = (byte) length;
+        arr[2] = (byte) (length >>> 8);
+        writeBytes(arr, 0, 3);
+        return;
+      }
+
+      buf[pos] = (byte) 0xfc;
+      buf[pos + 1] = (byte) length;
+      buf[pos + 2] = (byte) (length >>> 8);
+      pos += 3;
+      return;
+    }
+
+    if (length < 16777216) {
+
+      if (4 > buf.length - pos) {
+        // not enough space remaining
+        byte[] arr = new byte[4];
+        arr[0] = (byte) 0xfd;
+        arr[1] = (byte) length;
+        arr[2] = (byte) (length >>> 8);
+        arr[3] = (byte) (length >>> 16);
+        writeBytes(arr, 0, 4);
+        return;
+      }
+
+      buf[pos] = (byte) 0xfd;
+      buf[pos + 1] = (byte) length;
+      buf[pos + 2] = (byte) (length >>> 8);
+      buf[pos + 3] = (byte) (length >>> 16);
+      pos += 4;
+      return;
+    }
+
+    if (9 > buf.length - pos) {
+      // not enough space remaining
+      byte[] arr = new byte[9];
+      arr[0] = (byte) 0xfe;
+      arr[1] = (byte) length;
+      arr[2] = (byte) (length >>> 8);
+      arr[3] = (byte) (length >>> 16);
+      arr[4] = (byte) (length >>> 24);
+      arr[5] = (byte) (length >>> 32);
+      arr[6] = (byte) (length >>> 40);
+      arr[7] = (byte) (length >>> 48);
+      arr[8] = (byte) (length >>> 56);
+      writeBytes(arr, 0, 9);
+      return;
+    }
+
+    buf[pos] = (byte) 0xfe;
+    buf[pos + 1] = (byte) length;
+    buf[pos + 2] = (byte) (length >>> 8);
+    buf[pos + 3] = (byte) (length >>> 16);
+    buf[pos + 4] = (byte) (length >>> 24);
+    buf[pos + 5] = (byte) (length >>> 32);
+    buf[pos + 6] = (byte) (length >>> 40);
+    buf[pos + 7] = (byte) (length >>> 48);
+    buf[pos + 8] = (byte) (length >>> 56);
+    pos += 9;
+  }
+
+  public void writeAscii(String str) throws IOException {
+    int len = str.length();
+    if (len > buf.length - pos) {
+      byte[] arr = str.getBytes(StandardCharsets.US_ASCII);
+      writeBytes(arr, 0, arr.length);
+      return;
+    }
+    for (int off = 0; off < len; ) {
+      this.buf[this.pos++] = (byte) str.charAt(off++);
+    }
+  }
+
+  public void writeString(String str) throws IOException {
+    int charsLength = str.length();
+
+    // not enough space remaining
+    if (charsLength * 3 >= buf.length - pos) {
+      byte[] arr = str.getBytes(StandardCharsets.UTF_8);
+      writeBytes(arr, 0, arr.length);
+      return;
+    }
+
+    // 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;
+
+    // quick loop if only ASCII chars for faster escape
+    for (;
+        charsOffset < charsLength && (currChar = str.charAt(charsOffset)) < 0x80;
+        charsOffset++) {
+      buf[pos++] = (byte) currChar;
+    }
+
+    // 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 {
+              // must have low surrogate
+              buf[pos++] = (byte) 0x3f;
+            }
+          }
+        } 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));
+      }
+    }
+  }
+
+  /**
+   * Current buffer
+   *
+   * @return current buffer
+   */
+  public byte[] buf() {
+    return buf;
+  }
+
+  /**
+   * Write string to socket.
+   *
+   * @param str string
+   * @param noBackslashEscapes escape method
+   * @throws IOException if socket error occur
+   */
+  public void writeStringEscaped(String str, boolean noBackslashEscapes) throws IOException {
+
+    int charsLength = str.length();
+
+    // not enough space remaining
+    if (charsLength * 3 >= buf.length - pos) {
+      byte[] arr = str.getBytes(StandardCharsets.UTF_8);
+      writeBytesEscaped(arr, arr.length, noBackslashEscapes);
+      return;
+    }
+
+    // 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;
+
+    // quick loop if only ASCII chars for faster escape
+    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 quick loop not finished
+    while (charsOffset < charsLength) {
+      currChar = str.charAt(charsOffset++);
+      if (currChar < 0x80) {
+        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) {
+        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 {
+              // must have low surrogate
+              buf[pos++] = (byte) 0x3f;
+            }
+          }
+        } 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));
+      }
+    }
+  }
+
+  /**
+   * Write escape bytes to socket.
+   *
+   * @param bytes bytes
+   * @param len len to write
+   * @param noBackslashEscapes escape method
+   * @throws IOException if socket error occur
+   */
+  public void writeBytesEscaped(byte[] bytes, int len, boolean noBackslashEscapes)
+      throws IOException {
+    if (len * 2 > buf.length - pos) {
+
+      // makes buf bigger (up to 16M)
+      if (buf.length != maxPacketLength) {
+        growBuffer(len * 2);
+      }
+
+      // 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) {
+          growBuffer(len * 2);
+          if (mark != -1) {
+            flushBufferStopAtMark();
+          }
+
+        } else {
+          // not enough space in buf, will fill buf
+          if (buf.length <= pos) {
+            writeSocket(false);
+          }
+          if (noBackslashEscapes) {
+            for (int i = 0; i < len; i++) {
+              if (QUOTE == bytes[i]) {
+                buf[pos++] = QUOTE;
+                if (buf.length <= pos) {
+                  writeSocket(false);
+                }
+              }
+              buf[pos++] = bytes[i];
+              if (buf.length <= pos) {
+                writeSocket(false);
+              }
+            }
+          } else {
+            for (int i = 0; i < len; i++) {
+              if (bytes[i] == QUOTE
+                  || bytes[i] == BACKSLASH
+                  || bytes[i] == DBL_QUOTE
+                  || bytes[i] == ZERO_BYTE) {
+                buf[pos++] = '\\';
+                if (buf.length <= pos) {
+                  writeSocket(false);
+                }
+              }
+              buf[pos++] = bytes[i];
+              if (buf.length <= pos) {
+                writeSocket(false);
+              }
+            }
+          }
+          return;
+        }
+      }
+    }
+
+    // sure to have enough place filling buf directly
+    if (noBackslashEscapes) {
+      for (int i = 0; i < len; i++) {
+        if (QUOTE == bytes[i]) {
+          buf[pos++] = QUOTE;
+        }
+        buf[pos++] = bytes[i];
+      }
+    } else {
+      for (int i = 0; i < len; i++) {
+        if (bytes[i] == QUOTE
+            || bytes[i] == BACKSLASH
+            || bytes[i] == '"'
+            || bytes[i] == ZERO_BYTE) {
+          buf[pos++] = BACKSLASH; // add escape slash
+        }
+        buf[pos++] = bytes[i];
+      }
+    }
+  }
+
+  /**
+   * buf growing use 4 size only to avoid creating/copying that are expensive operations. possible
+   * size
+   *
+   * <ol>
+   *   <li>SMALL_buf_SIZE = 8k (default)
+   *   <li>MEDIUM_buf_SIZE = 128k
+   *   <li>LARGE_buf_SIZE = 1M
+   *   <li>getMaxPacketLength = 16M (+ 4 if using no compression)
+   * </ol>
+   *
+   * @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 (len + pos > newCapacity) {
+      if (mark != -1) {
+        // 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 (bufLength == maxPacketLength) return;
+        if (len + pos > newCapacity) {
+          newCapacity = Math.min(maxPacketLength, 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] = this.sequence.incrementAndGet();
+    out.write(buf, 0, 4);
+
+    if (logger.isTraceEnabled()) {
+      logger.trace(
+          "send com : content length=0 {}\n{}", 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);
+
+    // 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 void flushPipeline() throws IOException {
+    writeSocket(false);
+
+    // 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;
+  }
+
+  /**
+   * 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 sent.
+   */
+  private void checkMaxAllowedLength(int length) throws MaxAllowedPacketException {
+    if (maxAllowedPacket != null) {
+      if (cmdLength + length >= maxAllowedPacket) {
+        // launch exception only if no packet has been sent.
+        throw new MaxAllowedPacketException(
+            "query size ("
+                + (cmdLength + length)
+                + ") is >= to max_allowed_packet ("
+                + maxAllowedPacket
+                + ")",
+            cmdLength != 0);
+      }
+    }
+  }
+
+  public boolean throwMaxAllowedLength(int length) {
+    if (maxAllowedPacket != null) return cmdLength + length >= maxAllowedPacket;
+    return false;
+  }
+
+  public void permitTrace(boolean permitTrace) {
+    this.permitTrace = permitTrace;
+  }
+
+  /**
+   * 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 == null ? "-1" : serverThreadId)
+            + ((isMaster != null) ? " (" + (isMaster ? "M" : "S") + ")" : "");
+  }
+
+  public void mark() {
+    mark = pos;
+  }
+
+  public boolean isMarked() {
+    return mark != -1;
+  }
+
+  public boolean hasFlushed() {
+    return sequence.get() != -1;
+  }
+
+  /**
+   * Flush to last mark.
+   *
+   * @throws IOException if flush fail.
+   */
+  public void flushBufferStopAtMark() throws IOException {
+    final int end = pos;
+    pos = mark;
+    writeSocket(true);
+    out.flush();
+    initPacket();
+
+    System.arraycopy(buf, mark, buf, pos, end - mark);
+    pos += end - mark;
+    mark = -1;
+    bufContainDataAfterMark = true;
+  }
+
+  public boolean bufIsDataAfterMark() {
+    return bufContainDataAfterMark;
+  }
+
+  /**
+   * Reset mark flag and send bytes after mark flag.
+   *
+   * @return bytes after mark flag
+   */
+  public byte[] resetMark() {
+    pos = mark;
+    mark = -1;
+
+    if (bufContainDataAfterMark) {
+      byte[] data = Arrays.copyOfRange(buf, 4, pos);
+      initPacket();
+      bufContainDataAfterMark = false;
+      return data;
+    }
+    return null;
+  }
+
+  public void initPacket() {
+    sequence.set((byte) -1);
+    compressSequence.set((byte) -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] = this.sequence.incrementAndGet();
+      checkMaxAllowedLength(pos - 4);
+      out.write(buf, 0, pos);
+      if (commandEnd) out.flush();
+      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=<hidden>", 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();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/socket/impl/ReadAheadBufferedStream.java b/src/main/java/org/mariadb/jdbc/client/socket/impl/ReadAheadBufferedStream.java
new file mode 100644
index 0000000..83c9fb4
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/socket/impl/ReadAheadBufferedStream.java
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.socket.impl;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * 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 final byte[] buf;
+  private int end;
+  private int pos;
+
+  /**
+   * Constructor
+   *
+   * @param in socket input stream
+   */
+  public ReadAheadBufferedStream(InputStream in) {
+    super(in);
+    buf = new byte[BUF_SIZE];
+    end = 0;
+    pos = 0;
+  }
+
+  /**
+   * Returning byte array, from cache of reading socket if needed.
+   *
+   * @param externalBuf buf to fill
+   * @param off offset
+   * @param len length to read
+   * @return number of added bytes
+   * @throws IOException if exception during socket reading
+   */
+  public synchronized int read(byte[] externalBuf, int off, int len) throws IOException {
+
+    if (len == 0) {
+      return 0;
+    }
+
+    int totalReads = 0;
+    while (true) {
+
+      // read
+      if (end - pos <= 0) {
+        if (len - totalReads >= buf.length) {
+          // 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;
+          }
+          return totalReads + reads;
+
+        } else {
+
+          // filling internal buf
+          fillingBuffer(len - totalReads);
+          if (end <= 0) {
+            return (totalReads == 0) ? -1 : totalReads;
+          }
+        }
+      }
+
+      // copy internal value to buf.
+      int copyLength = Math.min(len - totalReads, end - pos);
+      System.arraycopy(buf, pos, externalBuf, off + totalReads, copyLength);
+      pos += copyLength;
+      totalReads += copyLength;
+
+      if (totalReads >= len || super.available() <= 0) {
+        return totalReads;
+      }
+    }
+  }
+
+  /**
+   * Fill buf with required length, or available bytes.
+   *
+   * @param minNeededBytes asked number of bytes
+   * @throws IOException in case of failing reading stream.
+   */
+  private void fillingBuffer(int minNeededBytes) throws IOException {
+    int lengthToReallyRead = Math.min(BUF_SIZE, Math.max(super.available(), minNeededBytes));
+    end = super.read(buf, 0, lengthToReallyRead);
+    pos = 0;
+  }
+
+  public boolean markSupported() {
+    return false;
+  }
+
+  public void close() throws IOException {
+    super.close();
+    end = 0;
+    pos = 0;
+  }
+
+  public synchronized int available() throws IOException {
+    return end - pos + super.available();
+  }
+
+  public int read() throws IOException {
+    throw new IOException("read() from socket not implemented");
+  }
+
+  public long skip(long n) throws IOException {
+    throw new IOException("Skip from socket not implemented");
+  }
+
+  public void reset() throws IOException {
+    throw new IOException("reset from socket not implemented");
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/socket/impl/SocketHandlerFunction.java b/src/main/java/org/mariadb/jdbc/client/socket/impl/SocketHandlerFunction.java
new file mode 100644
index 0000000..33d8d51
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/socket/impl/SocketHandlerFunction.java
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.socket.impl;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.sql.SQLException;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.HostAddress;
+
+/** Construct socket depending on configuration helper */
+@FunctionalInterface
+public interface SocketHandlerFunction {
+  /**
+   * Create socket
+   *
+   * @param conf configuration
+   * @param hostAddress host
+   * @return socket
+   * @throws IOException if any socket issue occurs
+   * @throws SQLException for other kind of error
+   */
+  Socket apply(Configuration conf, HostAddress hostAddress) throws IOException, SQLException;
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/socket/impl/SocketUtility.java b/src/main/java/org/mariadb/jdbc/client/socket/impl/SocketUtility.java
new file mode 100644
index 0000000..b1b3d25
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/socket/impl/SocketUtility.java
@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.socket.impl;
+
+import com.sun.jna.Platform;
+import java.io.IOException;
+import org.mariadb.jdbc.client.impl.ConnectionHelper;
+
+/**
+ * Socket Utility, to defined function that will create socket according to dependency and
+ * configuration
+ */
+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() {
+    // forcing use of JNA to ensure AOT compilation
+    Platform.getOSType();
+
+    return (conf, hostAddress) -> {
+      if (conf.pipe() != null) {
+        return new NamedPipeSocket(hostAddress != null ? hostAddress.host : null, 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, hostAddress);
+      }
+    };
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/socket/impl/UnixDomainSocket.java b/src/main/java/org/mariadb/jdbc/client/socket/impl/UnixDomainSocket.java
new file mode 100644
index 0000000..1898894
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/socket/impl/UnixDomainSocket.java
@@ -0,0 +1,272 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.socket.impl;
+
+import com.sun.jna.LastErrorException;
+import com.sun.jna.Native;
+import com.sun.jna.Platform;
+import com.sun.jna.Structure;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** Unix IPC socket */
+public class UnixDomainSocket extends Socket {
+
+  private static final int AF_UNIX = 1;
+  private static final int SOCK_STREAM = 1;
+  private static final int PROTOCOL = 0;
+
+  static {
+    if (!Platform.isWindows() && !Platform.isWindowsCE()) {
+      Native.register("c");
+    }
+  }
+
+  private final AtomicBoolean closeLock = new AtomicBoolean();
+  private final SockAddr sockaddr;
+  private final int fd;
+  private InputStream is;
+  private OutputStream os;
+  private boolean connected;
+
+  /**
+   * Constructor
+   *
+   * @param path unix path
+   * @throws IOException if any error occurs
+   */
+  public UnixDomainSocket(String path) throws IOException {
+    if (Platform.isWindows()) {
+      throw new IOException("Unix domain sockets are not supported on Windows");
+    }
+    sockaddr = new SockAddr(path);
+    closeLock.set(false);
+    try {
+      fd = socket(AF_UNIX, SOCK_STREAM, PROTOCOL);
+    } catch (LastErrorException lee) {
+      throw new IOException("native socket() failed : " + formatError(lee));
+    }
+  }
+
+  /**
+   * creates an endpoint for communication and returns a file descriptor that refers to that
+   * endpoint. see https://man7.org/linux/man-pages/man2/socket.2.html
+   *
+   * @param domain domain
+   * @param type type
+   * @param protocol protocol
+   * @return file descriptor
+   * @throws LastErrorException if any error occurs
+   */
+  public static native int socket(int domain, int type, int protocol) throws LastErrorException;
+
+  /**
+   * Connect socket
+   *
+   * @param sockfd file descriptor
+   * @param sockaddr socket address
+   * @param addrlen address length
+   * @return zero on success. -1 on error
+   * @throws LastErrorException if error occurs
+   */
+  public static native int connect(int sockfd, SockAddr sockaddr, int addrlen)
+      throws LastErrorException;
+
+  /**
+   * Receive a message from a socket
+   *
+   * @param fd file descriptor
+   * @param buffer buffer
+   * @param count length
+   * @param flags flag. see https://man7.org/linux/man-pages/man2/recvmsg.2.html
+   * @return zero on success. -1 on error
+   * @throws LastErrorException if error occurs
+   */
+  public static native int recv(int fd, byte[] buffer, int count, int flags)
+      throws LastErrorException;
+
+  /**
+   * Send a message to a socket
+   *
+   * @param fd file descriptor
+   * @param buffer buffer
+   * @param count length
+   * @param flags flag. see https://man7.org/linux/man-pages/man2/sendmsg.2.html
+   * @return zero on success. -1 on error
+   * @throws LastErrorException if error occurs
+   */
+  public static native int send(int fd, byte[] buffer, int count, int flags)
+      throws LastErrorException;
+
+  /**
+   * Close socket
+   *
+   * @param fd file descriptor
+   * @return zero on success. -1 on error
+   * @throws LastErrorException if error occurs
+   */
+  public static native int close(int fd) throws LastErrorException;
+
+  /**
+   * return a description of the error code passed in the argument errnum.
+   *
+   * @param errno error pointer
+   * @return error description
+   */
+  public static native String strerror(int errno);
+
+  private static String formatError(LastErrorException lee) {
+    try {
+      return strerror(lee.getErrorCode());
+    } catch (Throwable t) {
+      return lee.getMessage();
+    }
+  }
+
+  @Override
+  public boolean isConnected() {
+    return connected;
+  }
+
+  @Override
+  public void close() throws IOException {
+    if (!closeLock.getAndSet(true)) {
+      try {
+        close(fd);
+      } catch (LastErrorException lee) {
+        throw new IOException("native close() failed : " + formatError(lee));
+      }
+      connected = false;
+    }
+  }
+
+  public void connect(SocketAddress endpoint, int timeout) throws IOException {
+    try {
+      int ret = connect(fd, sockaddr, sockaddr.size());
+      if (ret != 0) {
+        throw new IOException(strerror(Native.getLastError()));
+      }
+      connected = true;
+    } catch (LastErrorException lee) {
+      try {
+        close();
+      } catch (IOException e) {
+      }
+
+      throw new IOException("native connect() failed : " + formatError(lee));
+    }
+    is = new UnixSocketInputStream();
+    os = new UnixSocketOutputStream();
+  }
+
+  public InputStream getInputStream() {
+    return is;
+  }
+
+  public OutputStream getOutputStream() {
+    return os;
+  }
+
+  public void setTcpNoDelay(boolean b) {
+    // do nothing
+  }
+
+  public void setKeepAlive(boolean b) {
+    // do nothing
+  }
+
+  public void setSoLinger(boolean b, int i) {
+    // do nothing
+  }
+
+  public void setSoTimeout(int timeout) {
+    // do nothing
+  }
+
+  public void shutdownInput() {
+    // do nothing
+  }
+
+  public void shutdownOutput() {
+    // do nothing
+  }
+
+  /** Socket address */
+  public static class SockAddr extends Structure {
+    /** socket family */
+    public short sun_family = AF_UNIX;
+    /** pathname */
+    public byte[] sun_path;
+
+    /**
+     * Constructor.
+     *
+     * @param sunPath path
+     */
+    public SockAddr(String sunPath) {
+      byte[] arr = sunPath.getBytes();
+      sun_path = new byte[arr.length + 1];
+      System.arraycopy(arr, 0, sun_path, 0, Math.min(sun_path.length - 1, arr.length));
+      allocateMemory();
+    }
+
+    @Override
+    protected java.util.List<String> getFieldOrder() {
+      return Arrays.asList("sun_family", "sun_path");
+    }
+  }
+
+  class UnixSocketInputStream extends InputStream {
+
+    @Override
+    public int read(byte[] bytesEntry, int off, int len) throws IOException {
+      try {
+        return recv(fd, bytesEntry, len, 0);
+      } catch (LastErrorException lee) {
+        throw new IOException("native read() failed : " + formatError(lee));
+      }
+    }
+
+    @Override
+    public int read() throws IOException {
+      byte[] bytes = new byte[1];
+      int bytesRead = read(bytes);
+      if (bytesRead == 0) {
+        return -1;
+      }
+      return bytes[0] & 0xff;
+    }
+
+    @Override
+    public int read(byte[] bytes) throws IOException {
+      return read(bytes, 0, bytes.length);
+    }
+  }
+
+  class UnixSocketOutputStream extends OutputStream {
+
+    @Override
+    public void write(byte[] bytesEntry, int off, int len) throws IOException {
+      int bytes;
+      try {
+        bytes = send(fd, bytesEntry, len, 0);
+
+        if (bytes != len) {
+          throw new IOException("can't write " + len + "bytes");
+        }
+      } catch (LastErrorException lee) {
+        throw new IOException("native write() failed : " + formatError(lee));
+      }
+    }
+
+    @Override
+    public void write(int value) throws IOException {}
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/tls/HostnameVerifier.java b/src/main/java/org/mariadb/jdbc/client/tls/HostnameVerifier.java
new file mode 100644
index 0000000..393f57f
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/tls/HostnameVerifier.java
@@ -0,0 +1,383 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.tls;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+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;
+import javax.net.ssl.SSLException;
+import javax.security.auth.x500.X500Principal;
+import org.mariadb.jdbc.util.log.Logger;
+import org.mariadb.jdbc.util.log.Loggers;
+
+/** SSL host verification */
+public class HostnameVerifier {
+
+  private static final Logger logger = Loggers.getLogger(HostnameVerifier.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])\\.)"
+              + "(([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
+   * more than one identity of a given type is present in the certificate (e.g., more than one
+   * dNSName name, a match in any one of the set is considered acceptable.) Names may contain the
+   * wildcard character * which is considered to match any single domain name component or component
+   * fragment. E.g., *.a.com matches foo.a.com but not bar.foo.a.com. f*.com matches foo.com but not
+   * bar.com.
+   *
+   * @param hostname hostname
+   * @param tlsDnsPattern DNS pattern (may contain wildcard)
+   * @return true if matching
+   */
+  private static boolean matchDns(String hostname, String tlsDnsPattern) throws SSLException {
+    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()) {
+      return false;
+    }
+
+    try {
+      while (hostnameSt.hasMoreTokens()) {
+        if (!matchWildCards(hostIsIp, hostnameSt.nextToken(), templateSt.nextToken())) {
+          return false;
+        }
+      }
+    } catch (SSLException exception) {
+      throw new SSLException(
+          normalizedHostMsg(hostname)
+              + " doesn't correspond to certificate CN \""
+              + tlsDnsPattern
+              + "\" : wildcards not possible for IPs");
+    }
+    return true;
+  }
+
+  private static boolean matchWildCards(boolean hostIsIp, String hostnameToken, String tlsDnsToken)
+      throws SSLException {
+    int wildcardIndex = tlsDnsToken.indexOf("*");
+    String token = hostnameToken;
+    if (wildcardIndex != -1) {
+      if (hostIsIp) {
+        throw new SSLException("WildCards not possible when using IP's");
+      }
+      boolean first = true;
+      String beforeWildcard;
+      String afterWildcard = tlsDnsToken;
+
+      while (wildcardIndex != -1) {
+        beforeWildcard = afterWildcard.substring(0, wildcardIndex);
+        afterWildcard = afterWildcard.substring(wildcardIndex + 1);
+
+        int beforeStartIdx = token.indexOf(beforeWildcard);
+        if ((beforeStartIdx == -1) || (first && beforeStartIdx != 0)) {
+          return false;
+        }
+
+        first = false;
+
+        token = token.substring(beforeStartIdx + beforeWildcard.length());
+        wildcardIndex = afterWildcard.indexOf("*");
+      }
+      return token.endsWith(afterWildcard);
+    }
+
+    // no wildcard -> token must be equal.
+    return token.equals(tlsDnsToken);
+  }
+
+  private static String extractCommonName(String principal) throws SSLException {
+    if (principal == null) {
+      return null;
+    }
+    try {
+      LdapName ldapName = new LdapName(principal);
+
+      for (Rdn rdn : ldapName.getRdns()) {
+        if (rdn.getType().equalsIgnoreCase("CN")) {
+          Object obj = rdn.getValue();
+          if (obj != null) {
+            return obj.toString();
+          }
+        }
+      }
+      return null;
+    } catch (InvalidNameException e) {
+      throw new SSLException("DN value \"" + principal + "\" is invalid");
+    }
+  }
+
+  private static String normaliseAddress(String hostname) {
+    try {
+      if (hostname == null) {
+        return null;
+      }
+      InetAddress inetAddress = InetAddress.getByName(hostname);
+      return inetAddress.getHostAddress();
+    } catch (UnknownHostException unexpected) {
+      return hostname;
+    }
+  }
+
+  private static String normalizedHostMsg(String normalizedHost) {
+    StringBuilder msg = new StringBuilder();
+    if (isIPv4(normalizedHost)) {
+      msg.append("IPv4 host \"");
+    } else if (isIPv6(normalizedHost)) {
+      msg.append("IPv6 host \"");
+    } else {
+      msg.append("DNS host \"");
+    }
+    msg.append(normalizedHost).append("\"");
+    return msg.toString();
+  }
+
+  /**
+   * check if ip correspond to IPV4
+   *
+   * @param ip ip value
+   * @return if ip is using IPV4 format
+   */
+  public static boolean isIPv4(final String ip) {
+    return IP_V4.matcher(ip).matches();
+  }
+
+  /**
+   * check if ip correspond to IPV6
+   *
+   * @param ip ip value
+   * @return if ip is using IPV6 format
+   */
+  public static boolean isIPv6(final String ip) {
+    return IP_V6.matcher(ip).matches() || IP_V6_COMPRESSED.matcher(ip).matches();
+  }
+
+  private static SubjectAltNames getSubjectAltNames(X509Certificate cert)
+      throws CertificateParsingException {
+    Collection<List<?>> entries = cert.getSubjectAlternativeNames();
+    SubjectAltNames subjectAltNames = new SubjectAltNames();
+    if (entries != null) {
+      for (List<?> entry : entries) {
+        if (entry.size() >= 2) {
+          int type = (Integer) entry.get(0);
+
+          if (type == 2) { // DNS
+            String altNameDns = (String) entry.get(1);
+            if (altNameDns != null) {
+              String normalizedSubjectAlt = altNameDns.toLowerCase(Locale.ROOT);
+              subjectAltNames.add(new GeneralName(normalizedSubjectAlt, Extension.DNS));
+            }
+          }
+
+          if (type == 7) { // IP
+            String altNameIp = (String) entry.get(1);
+            if (altNameIp != null) {
+              subjectAltNames.add(new GeneralName(altNameIp, Extension.IP));
+            }
+          }
+        }
+      }
+    }
+    return subjectAltNames;
+  }
+
+  /**
+   * Verification that throw an exception with a detailed error message in case of error.
+   *
+   * @param host hostname
+   * @param cert certificate
+   * @param serverThreadId server thread Identifier to identify connection in logs
+   * @throws SSLException exception
+   */
+  public static void verify(String host, X509Certificate cert, long serverThreadId)
+      throws SSLException {
+    if (host == null) {
+      return; // no validation if no host (possible for name pipe)
+    }
+    String lowerCaseHost = host.toLowerCase(Locale.ROOT);
+    try {
+      // ***********************************************************
+      // RFC 6125 : check Subject Alternative Name (SAN)
+      // ***********************************************************
+      SubjectAltNames subjectAltNames = getSubjectAltNames(cert);
+      if (!subjectAltNames.isEmpty()) {
+
+        // ***********************************************************
+        // Host is IPv4 : Check corresponding entries in subject alternative names
+        // ***********************************************************
+        if (isIPv4(lowerCaseHost)) {
+          for (GeneralName entry : subjectAltNames.getGeneralNames()) {
+            if (logger.isTraceEnabled()) {
+              logger.trace(
+                  "Conn={}. IPv4 verification of hostname : type={} value={} to {}",
+                  serverThreadId,
+                  entry.extension,
+                  entry.value,
+                  lowerCaseHost);
+            }
+
+            if (entry.extension == Extension.IP && lowerCaseHost.equals(entry.value)) {
+              return;
+            }
+          }
+        } else if (isIPv6(lowerCaseHost)) {
+          // ***********************************************************
+          // Host is IPv6 : Check corresponding entries in subject alternative names
+          // ***********************************************************
+          String normalisedHost = normaliseAddress(lowerCaseHost);
+          for (GeneralName entry : subjectAltNames.getGeneralNames()) {
+            if (logger.isTraceEnabled()) {
+              logger.trace(
+                  "Conn={}. IPv6 verification of hostname : type={} value={} to {}",
+                  serverThreadId,
+                  entry.extension,
+                  entry.value,
+                  lowerCaseHost);
+            }
+
+            if (entry.extension == Extension.IP
+                && !isIPv4(entry.value)
+                && normalisedHost.equals(normaliseAddress(entry.value))) {
+              return;
+            }
+          }
+        } else {
+          // ***********************************************************
+          // Host is not IP = DNS : Check corresponding entries in alternative subject names
+          // ***********************************************************
+          for (GeneralName entry : subjectAltNames.getGeneralNames()) {
+            if (logger.isTraceEnabled()) {
+              logger.trace(
+                  "Conn={}. DNS verification of hostname : type={} value={} to {}",
+                  serverThreadId,
+                  entry.extension,
+                  entry.value,
+                  lowerCaseHost);
+            }
+
+            if (entry.extension == Extension.DNS
+                && matchDns(lowerCaseHost, entry.value.toLowerCase(Locale.ROOT))) {
+              return;
+            }
+          }
+        }
+      }
+
+      // ***********************************************************
+      // RFC 2818 : legacy fallback using CN (recommendation is using alt-names)
+      // ***********************************************************
+      X500Principal subjectPrincipal = cert.getSubjectX500Principal();
+      String cn = extractCommonName(subjectPrincipal.getName(X500Principal.RFC2253));
+
+      if (cn == null) {
+        if (subjectAltNames.isEmpty()) {
+          throw new SSLException(
+              "CN not found in certificate principal \""
+                  + subjectPrincipal
+                  + "\" and certificate doesn't contain SAN");
+        } else {
+          throw new SSLException(
+              "CN not found in certificate principal \""
+                  + subjectPrincipal
+                  + "\" and "
+                  + normalizedHostMsg(lowerCaseHost)
+                  + " doesn't correspond to "
+                  + subjectAltNames);
+        }
+      }
+
+      String normalizedCn = cn.toLowerCase(Locale.ROOT);
+      if (logger.isTraceEnabled()) {
+        logger.trace(
+            "Conn={}. DNS verification of hostname : CN={} to {}",
+            serverThreadId,
+            normalizedCn,
+            lowerCaseHost);
+      }
+      if (!matchDns(lowerCaseHost, normalizedCn)) {
+        String errorMsg =
+            normalizedHostMsg(lowerCaseHost)
+                + " doesn't correspond to certificate CN \""
+                + normalizedCn
+                + "\"";
+        if (!subjectAltNames.isEmpty()) {
+          errorMsg += " and " + subjectAltNames;
+        }
+        throw new SSLException(errorMsg);
+      }
+
+    } catch (CertificateParsingException cpe) {
+      throw new SSLException("certificate parsing error : " + cpe.getMessage());
+    }
+  }
+
+  private enum Extension {
+    DNS,
+    IP
+  }
+
+  private static class GeneralName {
+
+    private final String value;
+    private final Extension extension;
+
+    public GeneralName(String value, Extension extension) {
+      this.value = value;
+      this.extension = extension;
+    }
+
+    @Override
+    public String toString() {
+      return "{" + extension + ":\"" + value + "\"}";
+    }
+  }
+
+  private static class SubjectAltNames {
+
+    private final List<GeneralName> generalNames = new ArrayList<>();
+
+    @Override
+    public String toString() {
+      StringBuilder sb = new StringBuilder("SAN[");
+      boolean first = true;
+
+      for (GeneralName generalName : generalNames) {
+        if (!first) {
+          sb.append(",");
+        }
+        first = false;
+        sb.append(generalName.toString());
+      }
+      sb.append("]");
+      return sb.toString();
+    }
+
+    public List<GeneralName> getGeneralNames() {
+      return generalNames;
+    }
+
+    public void add(GeneralName generalName) {
+      generalNames.add(generalName);
+    }
+
+    public boolean isEmpty() {
+      return generalNames.isEmpty();
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/tls/MariaDbX509KeyManager.java b/src/main/java/org/mariadb/jdbc/client/tls/MariaDbX509KeyManager.java
new file mode 100644
index 0000000..e879285
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/tls/MariaDbX509KeyManager.java
@@ -0,0 +1,149 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.tls;
+
+import java.net.Socket;
+import java.security.*;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.*;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.X509ExtendedKeyManager;
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * Key manager implementation that implement only client verification and rely only on private key
+ * for mutual authentication, without Server Name Indication (SNI) verification.
+ */
+public class MariaDbX509KeyManager extends X509ExtendedKeyManager {
+
+  private final Hashtable<String, KeyStore.PrivateKeyEntry> privateKeyHash = new Hashtable<>();
+
+  /**
+   * Creates Key manager.
+   *
+   * @param keyStore keyStore (must have been initialized)
+   * @param pwd keyStore password
+   * @throws KeyStoreException if keyStore hasn't been initialized.
+   */
+  public MariaDbX509KeyManager(KeyStore keyStore, char[] pwd) throws KeyStoreException {
+    super();
+    Enumeration<String> aliases = keyStore.aliases();
+    while (aliases.hasMoreElements()) {
+      String alias = aliases.nextElement();
+      if (keyStore.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class)) {
+        try {
+          privateKeyHash.put(
+              alias,
+              (KeyStore.PrivateKeyEntry)
+                  keyStore.getEntry(alias, new KeyStore.PasswordProtection(pwd)));
+        } catch (UnrecoverableEntryException | NoSuchAlgorithmException unrecoverableEntryEx) {
+          // password invalid | algorithm error
+        }
+      }
+    }
+  }
+
+  @Override
+  public String[] getClientAliases(String keyType, Principal[] issuers) {
+    List<String> accurateAlias = searchAccurateAliases(new String[] {keyType}, issuers);
+    if (accurateAlias.size() == 0) {
+      return null;
+    }
+    return accurateAlias.toArray(new String[0]);
+  }
+
+  @Override
+  public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
+    List<String> accurateAlias = searchAccurateAliases(keyType, issuers);
+    return accurateAlias == null || accurateAlias.isEmpty() ? null : accurateAlias.get(0);
+  }
+
+  @Override
+  public X509Certificate[] getCertificateChain(String alias) {
+    KeyStore.PrivateKeyEntry keyEntry = privateKeyHash.get(alias);
+    if (keyEntry == null) {
+      return null;
+    }
+
+    Certificate[] certs = keyEntry.getCertificateChain();
+    if (certs.length > 0 && certs[0] instanceof X509Certificate) {
+      return Arrays.copyOf(certs, certs.length, X509Certificate[].class);
+    }
+
+    return null;
+  }
+
+  @Override
+  public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) {
+    return chooseClientAlias(keyType, issuers, null);
+  }
+
+  @Override
+  public PrivateKey getPrivateKey(String alias) {
+    KeyStore.PrivateKeyEntry keyEntry = privateKeyHash.get(alias);
+    if (keyEntry == null) {
+      return null;
+    }
+    return keyEntry.getPrivateKey();
+  }
+
+  /**
+   * Search aliases corresponding to algorithms and issuers.
+   *
+   * @param keyTypes list of algorithms
+   * @param issuers list of issuers;
+   * @return list of corresponding aliases
+   */
+  private ArrayList<String> searchAccurateAliases(String[] keyTypes, Principal[] issuers) {
+    if (keyTypes == null || keyTypes.length == 0) {
+      return null;
+    }
+
+    ArrayList<String> accurateAliases = new ArrayList<>();
+    for (Map.Entry<String, KeyStore.PrivateKeyEntry> mapEntry : privateKeyHash.entrySet()) {
+
+      Certificate[] certs = mapEntry.getValue().getCertificateChain();
+      String alg = certs[0].getPublicKey().getAlgorithm();
+
+      for (String keyType : keyTypes) {
+        if (alg.equals(keyType)) {
+          if (issuers != null && issuers.length != 0) {
+            checkLoop:
+            for (Certificate cert : certs) {
+              if (cert instanceof X509Certificate) {
+                X500Principal certificateIssuer = ((X509Certificate) cert).getIssuerX500Principal();
+                for (Principal issuer : issuers) {
+                  if (certificateIssuer.equals(issuer)) {
+                    accurateAliases.add(mapEntry.getKey());
+                    break checkLoop;
+                  }
+                }
+              }
+            }
+          } else {
+            accurateAliases.add(mapEntry.getKey());
+          }
+        }
+      }
+    }
+    return accurateAliases;
+  }
+
+  @Override
+  public String[] getServerAliases(String keyType, Principal[] issuers) {
+    return null; // Driver use only client side
+  }
+
+  @Override
+  public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
+    return null; // Driver use only client side
+  }
+
+  @Override
+  public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
+    return null; // Driver use only client side
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/tls/MariaDbX509TrustingManager.java b/src/main/java/org/mariadb/jdbc/client/tls/MariaDbX509TrustingManager.java
new file mode 100644
index 0000000..15c7b27
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/tls/MariaDbX509TrustingManager.java
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.tls;
+
+import java.security.cert.X509Certificate;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * Class to accept any server certificate.
+ *
+ * <p>This permit to have network encrypted, BUT client doesn't validate server identity !!
+ */
+public class MariaDbX509TrustingManager implements X509TrustManager {
+
+  /** Constructor */
+  public MariaDbX509TrustingManager() {}
+
+  @Override
+  public void checkClientTrusted(X509Certificate[] x509Certificates, String string) {}
+
+  @Override
+  public void checkServerTrusted(X509Certificate[] x509Certificates, String string) {}
+
+  public X509Certificate[] getAcceptedIssuers() {
+    return null;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/util/MutableByte.java b/src/main/java/org/mariadb/jdbc/client/util/MutableByte.java
new file mode 100644
index 0000000..ccec43b
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/util/MutableByte.java
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.util;
+
+/** Mutable int, permitting to update packet sequence */
+public class MutableByte {
+  private byte value = -1;
+
+  /**
+   * Set new sequence value
+   *
+   * @param value new value
+   */
+  public void set(byte value) {
+    this.value = value;
+  }
+
+  /**
+   * Get current sequence value
+   *
+   * @return value
+   */
+  public byte get() {
+    return this.value;
+  }
+
+  /**
+   * Increment sequence and get new value
+   *
+   * @return new value
+   */
+  public byte incrementAndGet() {
+    return ++value;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/util/MutableInt.java b/src/main/java/org/mariadb/jdbc/client/util/MutableInt.java
new file mode 100644
index 0000000..a9657f6
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/util/MutableInt.java
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.util;
+
+/** Mutable int */
+public class MutableInt {
+  private int value = -1;
+
+  /**
+   * Set new sequence value
+   *
+   * @param value new value
+   */
+  public void set(int value) {
+    this.value = value;
+  }
+
+  /**
+   * Get current sequence value
+   *
+   * @return value
+   */
+  public int get() {
+    return this.value;
+  }
+
+  /**
+   * Increment sequence and get new value
+   *
+   * @return new value
+   */
+  public int incrementAndGet() {
+    return ++value;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/util/Parameter.java b/src/main/java/org/mariadb/jdbc/client/util/Parameter.java
new file mode 100644
index 0000000..0f41bef
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/util/Parameter.java
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.util;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.socket.Writer;
+
+/** Parameter */
+public interface Parameter {
+
+  /**
+   * Encode parameter in text format
+   *
+   * @param encoder packet writer
+   * @param context connection context
+   * @throws IOException if socket error occurs
+   * @throws SQLException if other kind of error occurs
+   */
+  void encodeText(Writer encoder, Context context) throws IOException, SQLException;
+
+  /**
+   * Encode parameter in binary format
+   *
+   * @param encoder packet writer
+   * @throws IOException if socket error occurs
+   * @throws SQLException if other kind of error occurs
+   */
+  void encodeBinary(Writer encoder) throws IOException, SQLException;
+
+  /**
+   * Encode parameter in binary long format
+   *
+   * @param encoder packet writer
+   * @throws IOException if socket error occurs
+   * @throws SQLException if other kind of error occurs
+   */
+  void encodeLongData(Writer encoder) throws IOException, SQLException;
+
+  /**
+   * transform parameter in byte array
+   *
+   * @return bytes
+   * @throws IOException if socket error occurs
+   * @throws SQLException if other kind of error occurs
+   */
+  byte[] encodeData() throws IOException, SQLException;
+
+  /**
+   * Can parameter be encoded in binary long format
+   *
+   * @return can parameter be encoded in binary long format
+   */
+  boolean canEncodeLongData();
+
+  /**
+   * return binary encoding type
+   *
+   * @return binary encoding type
+   */
+  int getBinaryEncodeType();
+
+  /**
+   * is parameter null
+   *
+   * @return is null
+   */
+  boolean isNull();
+
+  /**
+   * Methods to return parameter as string if possible (Streaming parameter will return null)
+   *
+   * @param context current connection context
+   * @return null if not available.
+   */
+  String bestEffortStringValue(Context context);
+}
diff --git a/src/main/java/org/mariadb/jdbc/client/util/Parameters.java b/src/main/java/org/mariadb/jdbc/client/util/Parameters.java
new file mode 100644
index 0000000..dc42f18
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/client/util/Parameters.java
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.client.util;
+
+/** Parameters list */
+public interface Parameters {
+
+  /**
+   * get parameter at index
+   *
+   * @param index index
+   * @return parameter
+   */
+  Parameter get(int index);
+
+  /**
+   * is there a parameter at requested index
+   *
+   * @param index index
+   * @return indicate if there is a parameter at index
+   */
+  boolean containsKey(int index);
+
+  /**
+   * Set parameter at index
+   *
+   * @param index index
+   * @param element parameter
+   */
+  void set(int index, Parameter element);
+
+  /**
+   * list size
+   *
+   * @return list size
+   */
+  int size();
+
+  /**
+   * Clone parameter list
+   *
+   * @return parameter list
+   */
+  Parameters clone();
+}
diff --git a/src/main/java/org/mariadb/jdbc/codec/NonNullParameter.java b/src/main/java/org/mariadb/jdbc/codec/NonNullParameter.java
new file mode 100644
index 0000000..aca1704
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/codec/NonNullParameter.java
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.codec;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/**
+ * Parameter wrapper for primitive, knowing that value cannot be null, permitting fast path for few
+ * methods.
+ *
+ * @param <T> value type
+ */
+public class NonNullParameter<T> extends Parameter<T> {
+
+  public NonNullParameter(Codec<T> codec, T value) {
+    super(codec, value);
+  }
+
+  @Override
+  public void encodeText(Writer encoder, Context context) throws IOException, SQLException {
+    codec.encodeText(encoder, context, this.value, null, length);
+  }
+
+  @Override
+  public boolean isNull() {
+    return false;
+  }
+}
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 0000000..3367bf4
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/codec/Parameter.java
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.codec;
+
+import java.io.IOException;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.sql.SQLException;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.client.socket.impl.PacketWriter;
+import org.mariadb.jdbc.plugin.Codec;
+
+public class Parameter<T> implements org.mariadb.jdbc.client.util.Parameter {
+  @SuppressWarnings({"rawtypes", "unchecked"})
+  public static final Parameter<?> NULL_PARAMETER =
+      new Parameter(null, null) {
+        @Override
+        public int getBinaryEncodeType() {
+          return DataType.VARCHAR.get();
+        }
+
+        @Override
+        public boolean isNull() {
+          return true;
+        }
+      };
+
+  protected final Codec<T> codec;
+  protected final T value;
+  protected final Long length;
+
+  public Parameter(Codec<T> codec, T value) {
+    this.codec = codec;
+    this.value = value;
+    this.length = null;
+  }
+
+  public Parameter(Codec<T> codec, T value, Long length) {
+    this.codec = codec;
+    this.value = value;
+    this.length = length;
+  }
+
+  public void encodeText(Writer encoder, Context context) throws IOException, SQLException {
+    if (value == null) {
+      encoder.writeAscii("null");
+    } else {
+      codec.encodeText(encoder, context, this.value, null, length);
+    }
+  }
+
+  public void encodeBinary(Writer encoder) throws IOException, SQLException {
+    codec.encodeBinary(encoder, this.value, null, length);
+  }
+
+  public void encodeLongData(Writer encoder) throws IOException, SQLException {
+    codec.encodeLongData(encoder, this.value, length);
+  }
+
+  public byte[] encodeData() throws IOException, SQLException {
+    return codec.encodeData(this.value, length);
+  }
+
+  public boolean canEncodeLongData() {
+    return codec.canEncodeLongData();
+  }
+
+  public int getBinaryEncodeType() {
+    return codec.getBinaryEncodeType();
+  }
+
+  public boolean isNull() {
+    return value == null;
+  }
+
+  public String bestEffortStringValue(Context context) {
+    if (isNull()) return "null";
+    if (codec.canEncodeLongData()) {
+      Type it = codec.getClass().getGenericInterfaces()[0];
+      ParameterizedType parameterizedType = (ParameterizedType) it;
+      Type typeParameter = parameterizedType.getActualTypeArguments()[0];
+      return "<" + typeParameter + ">";
+    }
+    try {
+      PacketWriter writer = new PacketWriter(null, 0, 0xffffff, null, null);
+      codec.encodeText(writer, context, this.value, null, this.length);
+      return new String(writer.buf(), 4, writer.pos() - 4, StandardCharsets.UTF_8);
+    } catch (Throwable t) {
+      return null;
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/codec/ParameterWithCal.java b/src/main/java/org/mariadb/jdbc/codec/ParameterWithCal.java
new file mode 100644
index 0000000..bfcd18a
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/codec/ParameterWithCal.java
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.codec;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.Calendar;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+public class ParameterWithCal<T> extends Parameter<T> {
+
+  private final Calendar cal;
+
+  public ParameterWithCal(Codec<T> codec, T value, Calendar cal) {
+    super(codec, value);
+    this.cal = cal;
+  }
+
+  @Override
+  public void encodeText(Writer encoder, Context context) throws IOException, SQLException {
+    codec.encodeText(encoder, context, this.value, this.cal, length);
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder) throws IOException, SQLException {
+    codec.encodeBinary(encoder, this.value, this.cal, length);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/export/ExceptionFactory.java b/src/main/java/org/mariadb/jdbc/export/ExceptionFactory.java
new file mode 100644
index 0000000..a90aaee
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/export/ExceptionFactory.java
@@ -0,0 +1,415 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.export;
+
+import java.sql.*;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.Connection;
+import org.mariadb.jdbc.HostAddress;
+import org.mariadb.jdbc.MariaDbPoolConnection;
+import org.mariadb.jdbc.client.Completion;
+import org.mariadb.jdbc.message.server.OkPacket;
+
+/**
+ * Exception factory. This permit common error logging, with thread id, dump query, and specific
+ * dead-lock additional information
+ */
+public class ExceptionFactory {
+
+  private static final Set<Integer> LOCK_DEADLOCK_ERROR_CODES =
+      new HashSet<>(Arrays.asList(1205, 1213, 1614));
+  private final Configuration conf;
+  private final HostAddress hostAddress;
+  private Connection connection;
+  private MariaDbPoolConnection poolConnection;
+  private long threadId;
+  private Statement statement;
+
+  /**
+   * Connection Exception factory constructor
+   *
+   * @param conf configuration
+   * @param hostAddress current host
+   */
+  public ExceptionFactory(Configuration conf, HostAddress hostAddress) {
+    this.conf = conf;
+    this.hostAddress = hostAddress;
+  }
+
+  private ExceptionFactory(
+      Connection connection,
+      MariaDbPoolConnection poolConnection,
+      Configuration conf,
+      HostAddress hostAddress,
+      long threadId,
+      Statement statement) {
+    this.connection = connection;
+    this.poolConnection = poolConnection;
+    this.conf = conf;
+    this.hostAddress = hostAddress;
+    this.threadId = threadId;
+    this.statement = statement;
+  }
+
+  private static String buildMsgText(
+      String initialMessage,
+      long threadId,
+      Configuration conf,
+      String sql,
+      int errorCode,
+      Connection connection) {
+
+    StringBuilder msg = new StringBuilder();
+
+    if (threadId != 0L) {
+      msg.append("(conn=").append(threadId).append(") ");
+    }
+    msg.append(initialMessage);
+
+    if (conf.dumpQueriesOnException() && sql != null) {
+      if (conf.maxQuerySizeToLog() != 0 && sql.length() > conf.maxQuerySizeToLog() - 3) {
+        msg.append("\nQuery is: ").append(sql, 0, conf.maxQuerySizeToLog() - 3).append("...");
+      } else {
+        msg.append("\nQuery is: ").append(sql);
+      }
+    }
+
+    if (conf.includeInnodbStatusInDeadlockExceptions()
+        && LOCK_DEADLOCK_ERROR_CODES.contains(errorCode)
+        && connection != null) {
+      Statement stmt = connection.createStatement();
+      try {
+        ResultSet rs = stmt.executeQuery("SHOW ENGINE INNODB STATUS");
+        rs.next();
+        msg.append("\ndeadlock information: ").append(rs.getString(3));
+      } catch (SQLException sqle) {
+        // eat
+      }
+    }
+
+    if (conf.includeThreadDumpInDeadlockExceptions()
+        && LOCK_DEADLOCK_ERROR_CODES.contains(errorCode)) {
+      msg.append("\nthread name: ").append(Thread.currentThread().getName());
+      msg.append("\ncurrent threads: ");
+      Thread.getAllStackTraces()
+          .forEach(
+              (thread, traces) -> {
+                msg.append("\n  name:\"")
+                    .append(thread.getName())
+                    .append("\" pid:")
+                    .append(thread.getId())
+                    .append(" status:")
+                    .append(thread.getState());
+                for (StackTraceElement trace : traces) {
+                  msg.append("\n    ").append(trace);
+                }
+              });
+    }
+
+    return msg.toString();
+  }
+
+  /**
+   * Set connection
+   *
+   * @param oldExceptionFactory previous connection exception factory
+   */
+  public void setConnection(ExceptionFactory oldExceptionFactory) {
+    this.connection = oldExceptionFactory.connection;
+  }
+
+  /**
+   * Set connection to factory
+   *
+   * @param connection connection
+   * @return this {@link ExceptionFactory}
+   */
+  public ExceptionFactory setConnection(Connection connection) {
+    this.connection = connection;
+    return this;
+  }
+
+  /**
+   * Set pool connection to factory
+   *
+   * @param internalPoolConnection internal pool connection
+   * @return this {@link ExceptionFactory}
+   */
+  public ExceptionFactory setPoolConnection(MariaDbPoolConnection internalPoolConnection) {
+    this.poolConnection = internalPoolConnection;
+    return this;
+  }
+
+  /**
+   * Set connection thread id
+   *
+   * @param threadId connection thread id
+   */
+  public void setThreadId(long threadId) {
+    this.threadId = threadId;
+  }
+
+  /**
+   * Create a BatchUpdateException, filling successful updates
+   *
+   * @param res completion list
+   * @param length expected size
+   * @param sqle exception
+   * @return BatchUpdateException object
+   */
+  public BatchUpdateException createBatchUpdate(
+      List<Completion> res, int length, SQLException sqle) {
+    int[] updateCounts = new int[length];
+    for (int i = 0; i < length; i++) {
+      if (i < res.size()) {
+        if (res.get(i) instanceof OkPacket) {
+          updateCounts[i] = (int) ((OkPacket) res.get(i)).getAffectedRows();
+        } else {
+          updateCounts[i] = org.mariadb.jdbc.Statement.SUCCESS_NO_INFO;
+        }
+      } else {
+        updateCounts[i] = org.mariadb.jdbc.Statement.EXECUTE_FAILED;
+      }
+    }
+    return new BatchUpdateException(
+        sqle.getMessage(), sqle.getSQLState(), sqle.getErrorCode(), updateCounts, sqle);
+  }
+
+  /**
+   * Create a BatchUpdateException, filling successful updates
+   *
+   * @param res completion list
+   * @param length expected length
+   * @param responseMsg successful response
+   * @param sqle exception
+   * @return BatchUpdateException object
+   */
+  public BatchUpdateException createBatchUpdate(
+      List<Completion> res, int length, int[] responseMsg, SQLException sqle) {
+    int[] updateCounts = new int[length];
+
+    int responseIncrement = 0;
+    for (int i = 0; i < length; i++) {
+      if (i >= responseMsg.length) {
+        Arrays.fill(updateCounts, i, length, Statement.EXECUTE_FAILED);
+        break;
+      }
+      int MsgResponseNo = responseMsg[i];
+      if (MsgResponseNo < 1) {
+        updateCounts[responseIncrement++] = Statement.EXECUTE_FAILED;
+        return new BatchUpdateException(updateCounts, sqle);
+      } else if (MsgResponseNo == 1 && res.size() > i && res.get(i) instanceof OkPacket) {
+        updateCounts[i] = (int) ((OkPacket) res.get(i)).getAffectedRows();
+      } else {
+        // unknown.
+        updateCounts[i] = Statement.SUCCESS_NO_INFO;
+      }
+    }
+    return new BatchUpdateException(
+        sqle.getMessage(), sqle.getSQLState(), sqle.getErrorCode(), updateCounts, sqle);
+  }
+
+  /**
+   * Construct an Exception factory from this + adding current statement
+   *
+   * @param statement current statement
+   * @return new Exception factory
+   */
+  public ExceptionFactory of(Statement statement) {
+    return new ExceptionFactory(
+        this.connection,
+        this.poolConnection,
+        this.conf,
+        this.hostAddress,
+        this.threadId,
+        statement);
+  }
+
+  /**
+   * Construct an Exception factory from this + adding current SQL
+   *
+   * @param sql current sql command
+   * @return new Exception factory
+   */
+  public ExceptionFactory withSql(String sql) {
+    return new SqlExceptionFactory(
+        this.connection,
+        this.poolConnection,
+        this.conf,
+        this.hostAddress,
+        this.threadId,
+        statement,
+        sql);
+  }
+
+  private SQLException createException(
+      String initialMessage, String sqlState, int errorCode, Exception cause) {
+
+    String msg = buildMsgText(initialMessage, threadId, conf, getSql(), errorCode, connection);
+
+    if ("70100".equals(sqlState)) { // ER_QUERY_INTERRUPTED
+      return new SQLTimeoutException(msg, sqlState, errorCode);
+    }
+    // 4166 : mariadb load data infile disable
+    // 1148 : 10.2 mariadb load data infile disable
+    // 3948 : mysql load data infile disable
+    if ((errorCode == 4166 || errorCode == 3948 || errorCode == 1148) && !conf.allowLocalInfile()) {
+      return new SQLException(
+          "Local infile is disabled by connector. Enable `allowLocalInfile` to allow local infile"
+              + " commands",
+          sqlState,
+          errorCode,
+          cause);
+    }
+
+    SQLException returnEx;
+    String sqlClass = sqlState == null ? "42" : sqlState.substring(0, 2);
+    switch (sqlClass) {
+      case "0A":
+        returnEx = new SQLFeatureNotSupportedException(msg, sqlState, errorCode, cause);
+        break;
+      case "22":
+      case "26":
+      case "2F":
+      case "20":
+      case "42":
+      case "XA":
+        returnEx = new SQLSyntaxErrorException(msg, sqlState, errorCode, cause);
+        break;
+      case "25":
+      case "28":
+        returnEx = new SQLInvalidAuthorizationSpecException(msg, sqlState, errorCode, cause);
+        break;
+      case "21":
+      case "23":
+        returnEx = new SQLIntegrityConstraintViolationException(msg, sqlState, errorCode, cause);
+        break;
+      case "08":
+        returnEx = new SQLNonTransientConnectionException(msg, sqlState, errorCode, cause);
+        break;
+      case "40":
+        returnEx = new SQLTransactionRollbackException(msg, sqlState, errorCode, cause);
+        break;
+      case "HY":
+        returnEx = new SQLException(msg, sqlState, errorCode, cause);
+        break;
+      default:
+        returnEx = new SQLTransientConnectionException(msg, sqlState, errorCode, cause);
+        break;
+    }
+
+    if (poolConnection != null) {
+      if (statement != null && statement instanceof PreparedStatement) {
+        poolConnection.fireStatementErrorOccurred((PreparedStatement) statement, returnEx);
+      }
+      if (returnEx instanceof SQLNonTransientConnectionException
+          || returnEx instanceof SQLTransientConnectionException) {
+        poolConnection.fireConnectionErrorOccurred(returnEx);
+      }
+    }
+
+    return returnEx;
+  }
+
+  /**
+   * fast creation of SQLFeatureNotSupportedException exception
+   *
+   * @param message error message
+   * @return exception to be thrown
+   */
+  public SQLException notSupported(String message) {
+    return createException(message, "0A000", -1, null);
+  }
+
+  /**
+   * Creation of an exception
+   *
+   * @param message error message
+   * @return exception to be thrown
+   */
+  public SQLException create(String message) {
+    return createException(message, "42000", -1, null);
+  }
+
+  /**
+   * Creation of an exception
+   *
+   * @param message error message
+   * @param sqlState sql state
+   * @return exception to be thrown
+   */
+  public SQLException create(String message, String sqlState) {
+    return createException(message, sqlState, -1, null);
+  }
+
+  /**
+   * Creation of an exception
+   *
+   * @param message error message
+   * @param sqlState sql state
+   * @param cause initial exception
+   * @return exception to be thrown
+   */
+  public SQLException create(String message, String sqlState, Exception cause) {
+    return createException(message, sqlState, -1, cause);
+  }
+  /**
+   * Creation of an exception
+   *
+   * @param message error message
+   * @param sqlState sql state
+   * @param errorCode error code
+   * @return exception to be thrown
+   */
+  public SQLException create(String message, String sqlState, int errorCode) {
+    return createException(message, sqlState, errorCode, null);
+  }
+
+  /**
+   * get SQL command
+   *
+   * @return sql command
+   */
+  public String getSql() {
+    return null;
+  }
+
+  /** Exception with SQL command */
+  public class SqlExceptionFactory extends ExceptionFactory {
+    private final String sql;
+
+    /**
+     * Constructor of Exception factory with SQL
+     *
+     * @param connection connection
+     * @param poolConnection pool connection
+     * @param conf configuration
+     * @param hostAddress host
+     * @param threadId connection thread id
+     * @param statement statement
+     * @param sql sql
+     */
+    public SqlExceptionFactory(
+        Connection connection,
+        MariaDbPoolConnection poolConnection,
+        Configuration conf,
+        HostAddress hostAddress,
+        long threadId,
+        Statement statement,
+        String sql) {
+      super(connection, poolConnection, conf, hostAddress, threadId, statement);
+      this.sql = sql;
+    }
+
+    @Override
+    public String getSql() {
+      return sql;
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/export/HaMode.java b/src/main/java/org/mariadb/jdbc/export/HaMode.java
new file mode 100644
index 0000000..4a1267c
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/export/HaMode.java
@@ -0,0 +1,162 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.export;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentMap;
+import org.mariadb.jdbc.HostAddress;
+
+/** Failover (High-availability) mode */
+public enum HaMode {
+  /** replication mode : first is primary, other are replica */
+  REPLICATION("replication") {
+    public Optional<HostAddress> getAvailableHost(
+        List<HostAddress> hostAddresses,
+        ConcurrentMap<HostAddress, Long> denyList,
+        boolean primary) {
+      return HaMode.getAvailableRoundRobinHost(this, hostAddresses, denyList, primary);
+    }
+  },
+  /** sequential: driver will always connect according to connection string order */
+  SEQUENTIAL("sequential") {
+    public Optional<HostAddress> getAvailableHost(
+        List<HostAddress> hostAddresses,
+        ConcurrentMap<HostAddress, Long> denyList,
+        boolean primary) {
+      return getAvailableHostInOrder(hostAddresses, denyList, primary);
+    }
+  },
+  /**
+   * load-balance: driver will connect to any host using round-robin, permitting balancing
+   * connections
+   */
+  LOADBALANCE("load-balance") {
+    public Optional<HostAddress> getAvailableHost(
+        List<HostAddress> hostAddresses,
+        ConcurrentMap<HostAddress, Long> denyList,
+        boolean primary) {
+      return HaMode.getAvailableRoundRobinHost(this, hostAddresses, denyList, primary);
+    }
+  },
+  /** no ha-mode. Connect to first host only */
+  NONE("") {
+    public Optional<HostAddress> getAvailableHost(
+        List<HostAddress> hostAddresses,
+        ConcurrentMap<HostAddress, Long> denyList,
+        boolean primary) {
+      return hostAddresses.isEmpty() ? Optional.empty() : Optional.of(hostAddresses.get(0));
+    }
+  };
+
+  private final String value;
+  private HostAddress lastRoundRobinPrimaryHost = null;
+  private HostAddress lastRoundRobinSecondaryHost = null;
+
+  HaMode(String value) {
+    this.value = value;
+  }
+
+  /**
+   * Get HAMode from values or aliases
+   *
+   * @param value value or alias
+   * @return HaMode if corresponding mode is found
+   */
+  public static HaMode from(String value) {
+    for (HaMode haMode : values()) {
+      if (haMode.value.equalsIgnoreCase(value) || haMode.name().equalsIgnoreCase(value)) {
+        return haMode;
+      }
+    }
+    throw new IllegalArgumentException(
+        String.format("Wrong argument value '%s' for HaMode", value));
+  }
+
+  /**
+   * return hosts of corresponding type (primary or not) without blacklisted hosts. hosts in
+   * blacklist reaching blacklist timeout will be present. order corresponds to connection string
+   * order.
+   *
+   * @param hostAddresses hosts
+   * @param denyList blacklist
+   * @param primary returns primary hosts or replica
+   * @return list without denied hosts
+   */
+  public static Optional<HostAddress> getAvailableHostInOrder(
+      List<HostAddress> hostAddresses, ConcurrentMap<HostAddress, Long> denyList, boolean primary) {
+    // use in order not blacklisted server
+    for (HostAddress hostAddress : hostAddresses) {
+      if (hostAddress.primary == primary) {
+        if (!denyList.containsKey(hostAddress)) return Optional.of(hostAddress);
+        if (denyList.get(hostAddress) < System.currentTimeMillis()) {
+          // timeout reached
+          denyList.remove(hostAddress);
+          return Optional.of(hostAddress);
+        }
+      }
+    }
+    return Optional.empty();
+  }
+
+  /**
+   * return hosts of corresponding type (primary or not) without blacklisted hosts. hosts in
+   * blacklist reaching blacklist timeout will be present, RoundRobin Order.
+   *
+   * @param haMode current haMode
+   * @param hostAddresses hosts
+   * @param denyList blacklist
+   * @param primary returns primary hosts or replica
+   * @return list without denied hosts
+   */
+  public static Optional<HostAddress> getAvailableRoundRobinHost(
+      HaMode haMode,
+      List<HostAddress> hostAddresses,
+      ConcurrentMap<HostAddress, Long> denyList,
+      boolean primary) {
+    HostAddress lastChosenHost =
+        primary ? haMode.lastRoundRobinPrimaryHost : haMode.lastRoundRobinSecondaryHost;
+
+    List<HostAddress> loopList;
+    if (lastChosenHost == null) {
+      loopList = hostAddresses;
+    } else {
+      int lastChosenIndex = hostAddresses.indexOf(lastChosenHost);
+      loopList = new ArrayList<>();
+      loopList.addAll(hostAddresses.subList(lastChosenIndex + 1, hostAddresses.size()));
+      loopList.addAll(hostAddresses.subList(0, lastChosenIndex + 1));
+    }
+
+    for (HostAddress hostAddress : loopList) {
+      if (hostAddress.primary == primary) {
+        if (denyList.containsKey(hostAddress)) {
+          // take in account denied server that have reached denied timeout
+          if (denyList.get(hostAddress) > System.currentTimeMillis()) {
+            continue;
+          } else {
+            denyList.remove(hostAddress);
+          }
+        }
+        if (primary) {
+          haMode.lastRoundRobinPrimaryHost = hostAddress;
+        } else {
+          haMode.lastRoundRobinSecondaryHost = hostAddress;
+        }
+        return Optional.of(hostAddress);
+      }
+    }
+    return Optional.empty();
+  }
+
+  /**
+   * List of hosts without blacklist entries, ordered according to HA mode
+   *
+   * @param hostAddresses hosts
+   * @param denyList hosts temporary denied
+   * @param primary type
+   * @return list without denied hosts
+   */
+  public abstract Optional<HostAddress> getAvailableHost(
+      List<HostAddress> hostAddresses, ConcurrentMap<HostAddress, Long> denyList, boolean primary);
+}
diff --git a/src/main/java/org/mariadb/jdbc/export/MaxAllowedPacketException.java b/src/main/java/org/mariadb/jdbc/export/MaxAllowedPacketException.java
new file mode 100644
index 0000000..253e7e9
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/export/MaxAllowedPacketException.java
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.export;
+
+import java.io.IOException;
+
+/**
+ * Dedicate exception when error is due to sending packet with size &gt; to server
+ * max_allowed_packet, that would cause server to drop connection
+ */
+public class MaxAllowedPacketException extends IOException {
+
+  private static final long serialVersionUID = 5669184960442818475L;
+  /** is connection in wrong state */
+  private final boolean mustReconnect;
+
+  /**
+   * Constructor
+   *
+   * @param message error message
+   * @param mustReconnect is connection state unsure
+   */
+  public MaxAllowedPacketException(String message, boolean mustReconnect) {
+    super(message);
+    this.mustReconnect = mustReconnect;
+  }
+
+  /**
+   * Indicate that connection state is unsure
+   *
+   * @return must driver reconnect connection
+   */
+  public boolean isMustReconnect() {
+    return mustReconnect;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/export/Prepare.java b/src/main/java/org/mariadb/jdbc/export/Prepare.java
new file mode 100644
index 0000000..bb451af
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/export/Prepare.java
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.export;
+
+import java.sql.SQLException;
+import org.mariadb.jdbc.ServerPreparedStatement;
+import org.mariadb.jdbc.client.Client;
+import org.mariadb.jdbc.client.ColumnDecoder;
+
+/** Prepare packet COM_STMT_PREPARE (see https://mariadb.com/kb/en/com_stmt_prepare/) */
+public interface Prepare {
+
+  /**
+   * Close Prepared command
+   *
+   * @param con current connection
+   * @throws SQLException if prepare close fails
+   */
+  void close(Client con) throws SQLException;
+
+  /**
+   * Decrement use of prepare. In case not used anymore, and not in cache, will be close.
+   *
+   * @param con connection
+   * @param preparedStatement current prepared statement that was using prepare object
+   * @throws SQLException if close fails
+   */
+  void decrementUse(Client con, ServerPreparedStatement preparedStatement) throws SQLException;
+
+  /**
+   * Get current prepare statement id
+   *
+   * @return statement id
+   */
+  int getStatementId();
+
+  /**
+   * Prepare parameters
+   *
+   * @return parameters metadata
+   */
+  ColumnDecoder[] getParameters();
+
+  /**
+   * Prepare result-set columns
+   *
+   * @return result-set columns metadata
+   */
+  ColumnDecoder[] getColumns();
+
+  /**
+   * set prepare result-set columns
+   *
+   * @param columns set result-set columns metadata
+   */
+  void setColumns(ColumnDecoder[] columns);
+}
diff --git a/src/main/java/org/mariadb/jdbc/export/SslMode.java b/src/main/java/org/mariadb/jdbc/export/SslMode.java
new file mode 100644
index 0000000..1878f19
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/export/SslMode.java
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.export;
+
+/** SSl requirement */
+public enum SslMode {
+  /** NO SSL */
+  DISABLE("disable", new String[] {"DISABLED", "0", "false"}),
+
+  /** Encryption only (no certificate and hostname validation) (DEVELOPMENT ONLY) */
+  TRUST("trust", new String[] {"REQUIRED"}),
+
+  /** Encryption, certificates validation, BUT no hostname verification */
+  VERIFY_CA("verify-ca", new String[] {"VERIFY_CA"}),
+
+  /** Standard SSL use: Encryption, certificate validation and hostname validation */
+  VERIFY_FULL("verify-full", new String[] {"VERIFY_IDENTITY", "1", "true"});
+
+  private final String value;
+  private final String[] aliases;
+
+  SslMode(String value, String[] aliases) {
+    this.value = value;
+    this.aliases = aliases;
+  }
+
+  /**
+   * Create SSLMode from enumeration value, or aliases
+   *
+   * @param value enumeration value or alias
+   * @return SSLMode if corresponding enumeration value or alias found
+   */
+  public static SslMode from(String value) {
+    for (SslMode sslMode : values()) {
+      if (sslMode.value.equalsIgnoreCase(value) || sslMode.name().equalsIgnoreCase(value)) {
+        return sslMode;
+      }
+      for (String alias : sslMode.aliases) {
+        if (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/message/ClientMessage.java b/src/main/java/org/mariadb/jdbc/message/ClientMessage.java
new file mode 100644
index 0000000..f23af0c
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/ClientMessage.java
@@ -0,0 +1,333 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.regex.Pattern;
+import org.mariadb.jdbc.BasePreparedStatement;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.impl.StandardReadableByteBuf;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.client.result.StreamingResult;
+import org.mariadb.jdbc.client.result.UpdatableResult;
+import org.mariadb.jdbc.client.socket.Reader;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.client.util.Parameters;
+import org.mariadb.jdbc.export.ExceptionFactory;
+import org.mariadb.jdbc.message.server.ErrorPacket;
+import org.mariadb.jdbc.message.server.OkPacket;
+import org.mariadb.jdbc.util.constants.ServerStatus;
+
+public interface ClientMessage {
+
+  /**
+   * Encode client message to socket.
+   *
+   * @param writer socket writer
+   * @param context connection context
+   * @return number of client message written
+   * @throws IOException if socket error occur
+   * @throws SQLException if any issue occurs
+   */
+  int encode(Writer writer, Context context) throws IOException, SQLException;
+
+  /**
+   * Number of parameter rows, and so expected return length
+   *
+   * @return batch update length
+   */
+  default int batchUpdateLength() {
+    return 0;
+  }
+
+  /**
+   * Message description
+   *
+   * @return description
+   */
+  default String description() {
+    return null;
+  }
+
+  /**
+   * Are return value encoded in binary protocol
+   *
+   * @return use binary protocol
+   */
+  default boolean binaryProtocol() {
+    return false;
+  }
+
+  /**
+   * Can skip metadata
+   *
+   * @return can skip metadata
+   */
+  default boolean canSkipMeta() {
+    return false;
+  }
+
+  /**
+   * default packet resultset parser
+   *
+   * @param stmt caller
+   * @param fetchSize fetch size
+   * @param maxRows maximum number of rows
+   * @param resultSetConcurrency resultset concurrency
+   * @param resultSetType resultset type
+   * @param closeOnCompletion must close caller on result parsing end
+   * @param reader packet reader
+   * @param writer packet writer
+   * @param context connection context
+   * @param exceptionFactory connection exception factory
+   * @param lock thread safe locks
+   * @param traceEnable is logging trace enable
+   * @param message client message
+   * @return results
+   * @throws IOException if any socket error occurs
+   * @throws SQLException for other kind of errors
+   */
+  default Completion readPacket(
+      Statement stmt,
+      int fetchSize,
+      long maxRows,
+      int resultSetConcurrency,
+      int resultSetType,
+      boolean closeOnCompletion,
+      Reader reader,
+      Writer writer,
+      Context context,
+      ExceptionFactory exceptionFactory,
+      ReentrantLock lock,
+      boolean traceEnable,
+      ClientMessage message)
+      throws IOException, SQLException {
+
+    ReadableByteBuf buf = reader.readReusablePacket(traceEnable);
+
+    switch (buf.getByte()) {
+
+        // *********************************************************************************************************
+        // * OK response
+        // *********************************************************************************************************
+      case (byte) 0x00:
+        return new OkPacket(buf, context);
+
+        // *********************************************************************************************************
+        // * ERROR response
+        // *********************************************************************************************************
+      case (byte) 0xff:
+        // force current status to in transaction to ensure rollback/commit, since command may
+        // have issue a transaction
+        ErrorPacket errorPacket = new ErrorPacket(buf, context);
+        throw exceptionFactory
+            .withSql(this.description())
+            .create(
+                errorPacket.getMessage(), errorPacket.getSqlState(), errorPacket.getErrorCode());
+      case (byte) 0xfb:
+        buf.skip(1); // skip header
+        SQLException exception = null;
+        reader.getSequence().set((byte) 1);
+        InputStream is = getLocalInfileInputStream();
+        if (is == null) {
+          String fileName = buf.readStringNullEnd();
+          if (!message.validateLocalFileName(fileName, context)) {
+            exception =
+                exceptionFactory
+                    .withSql(this.description())
+                    .create(
+                        String.format(
+                            "LOAD DATA LOCAL INFILE asked for file '%s' that doesn't correspond to"
+                                + " initial query %s. Possible malicious proxy changing server"
+                                + " answer ! Command interrupted",
+                            fileName, this.description()),
+                        "HY000");
+          } else {
+
+            try {
+              is = new FileInputStream(fileName);
+            } catch (FileNotFoundException f) {
+              exception =
+                  exceptionFactory
+                      .withSql(this.description())
+                      .create("Could not send file : " + f.getMessage(), "HY000", f);
+            }
+          }
+        }
+
+        // sending stream
+        if (is != null) {
+          try {
+            byte[] fileBuf = new byte[8192];
+            int len;
+            while ((len = is.read(fileBuf)) > 0) {
+              writer.writeBytes(fileBuf, 0, len);
+              writer.flush();
+            }
+          } finally {
+            is.close();
+          }
+        }
+
+        // after file send / having an error, sending an empty packet to keep connection state ok
+        writer.writeEmptyPacket();
+        Completion completion =
+            readPacket(
+                stmt,
+                fetchSize,
+                maxRows,
+                resultSetConcurrency,
+                resultSetType,
+                closeOnCompletion,
+                reader,
+                writer,
+                context,
+                exceptionFactory,
+                lock,
+                traceEnable,
+                message);
+        if (exception != null) {
+          throw exception;
+        }
+        return completion;
+
+        // *********************************************************************************************************
+        // * ResultSet
+        // *********************************************************************************************************
+      default:
+        int fieldCount = buf.readIntLengthEncodedNotNull();
+
+        ColumnDecoder[] ci;
+        boolean canSkipMeta = context.canSkipMeta() && this.canSkipMeta();
+        boolean skipMeta = canSkipMeta ? buf.readByte() == 0 : false;
+        if (canSkipMeta && skipMeta) {
+          ci = ((BasePreparedStatement) stmt).getMeta();
+        } else {
+          // read columns information's
+          ci = new ColumnDecoder[fieldCount];
+          for (int i = 0; i < fieldCount; i++) {
+            ci[i] =
+                ColumnDecoder.decode(
+                    new StandardReadableByteBuf(reader.readPacket(traceEnable)),
+                    context.isExtendedInfo());
+          }
+        }
+        if (canSkipMeta && !skipMeta) ((BasePreparedStatement) stmt).updateMeta(ci);
+
+        // intermediate EOF
+        if (!context.isEofDeprecated()) {
+          reader.skipPacket();
+        }
+
+        // read resultSet
+        if (resultSetConcurrency == ResultSet.CONCUR_UPDATABLE) {
+          return new UpdatableResult(
+              stmt,
+              binaryProtocol(),
+              maxRows,
+              ci,
+              reader,
+              context,
+              resultSetType,
+              closeOnCompletion,
+              traceEnable);
+        }
+
+        if (fetchSize != 0) {
+          if ((context.getServerStatus() & ServerStatus.MORE_RESULTS_EXISTS) > 0) {
+            context.setServerStatus(context.getServerStatus() - ServerStatus.MORE_RESULTS_EXISTS);
+          }
+
+          return new StreamingResult(
+              stmt,
+              binaryProtocol(),
+              maxRows,
+              ci,
+              reader,
+              context,
+              fetchSize,
+              lock,
+              resultSetType,
+              closeOnCompletion,
+              traceEnable);
+        } else {
+          return new CompleteResult(
+              stmt,
+              binaryProtocol(),
+              maxRows,
+              ci,
+              reader,
+              context,
+              resultSetType,
+              closeOnCompletion,
+              traceEnable);
+        }
+    }
+  }
+
+  /**
+   * Get current local infile input stream.
+   *
+   * @return default to null
+   */
+  default InputStream getLocalInfileInputStream() {
+    return null;
+  }
+
+  /**
+   * Request for local file to be validated from current query.
+   *
+   * @param fileName server file request path
+   * @param context current connection context
+   * @return true if file name correspond to demand and query is a load local infile
+   */
+  default boolean validateLocalFileName(String fileName, Context context) {
+    return false;
+  }
+
+  /**
+   * Check that file requested correspond to request.
+   *
+   * @param sql current command sql
+   * @param parameters current command parameter
+   * @param fileName file path request
+   * @param context current connection context
+   * @return true if file name correspond to demand and query is a load local infile
+   */
+  static boolean validateLocalFileName(
+      String sql, Parameters parameters, String fileName, Context context) {
+    Pattern pattern =
+        Pattern.compile(
+            "^(\\s*\\/\\*([^\\*]|\\*[^\\/])*\\*\\/)*\\s*LOAD\\s+(DATA|XML)\\s+((LOW_PRIORITY|CONCURRENT)\\s+)?LOCAL\\s+INFILE\\s+'"
+                + fileName
+                + "'",
+            Pattern.CASE_INSENSITIVE);
+    if (pattern.matcher(sql).find()) {
+      return true;
+    }
+
+    if (parameters != null) {
+      pattern =
+          Pattern.compile(
+              "^(\\s*\\/\\*([^\\*]|\\*[^\\/])*\\*\\/)*\\s*LOAD\\s+(DATA|XML)\\s+((LOW_PRIORITY|CONCURRENT)\\s+)?LOCAL\\s+INFILE\\s+\\?",
+              Pattern.CASE_INSENSITIVE);
+      if (pattern.matcher(sql).find() && parameters.size() > 0) {
+        String paramString = parameters.get(0).bestEffortStringValue(context);
+        if (paramString != null) {
+          return paramString.toLowerCase().equals("'" + fileName.toLowerCase() + "'");
+        }
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/ServerMessage.java b/src/main/java/org/mariadb/jdbc/message/ServerMessage.java
new file mode 100644
index 0000000..ad40b11
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/ServerMessage.java
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message;
+
+public interface ServerMessage {}
diff --git a/src/main/java/org/mariadb/jdbc/message/client/AuthMoreRawPacket.java b/src/main/java/org/mariadb/jdbc/message/client/AuthMoreRawPacket.java
new file mode 100644
index 0000000..3edacc0
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/client/AuthMoreRawPacket.java
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.client;
+
+import java.io.IOException;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.message.ClientMessage;
+
+/** plugin additional exchanges. raw data with mysql header packet */
+public final class AuthMoreRawPacket implements ClientMessage {
+
+  private final byte[] raw;
+
+  /**
+   * Constructor
+   *
+   * @param raw plugin exchange raw data
+   */
+  public AuthMoreRawPacket(byte[] raw) {
+    this.raw = raw;
+  }
+
+  @Override
+  public int encode(Writer writer, Context context) throws IOException {
+    if (raw.length == 0) {
+      writer.writeEmptyPacket();
+    } else {
+      writer.writeBytes(raw);
+      writer.flush();
+    }
+    return 0;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/client/BulkExecutePacket.java b/src/main/java/org/mariadb/jdbc/message/client/BulkExecutePacket.java
new file mode 100644
index 0000000..d0a1a69
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/client/BulkExecutePacket.java
@@ -0,0 +1,206 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.client;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import org.mariadb.jdbc.ServerPreparedStatement;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.client.util.Parameter;
+import org.mariadb.jdbc.client.util.Parameters;
+import org.mariadb.jdbc.export.MaxAllowedPacketException;
+import org.mariadb.jdbc.export.Prepare;
+import org.mariadb.jdbc.message.server.PrepareResultPacket;
+
+/**
+ * batch execution. This relies on COM_STMT_BULK_EXECUTE see
+ * https://mariadb.com/kb/en/com_stmt_bulk_execute/
+ */
+public final class BulkExecutePacket implements RedoableWithPrepareClientMessage {
+  private List<Parameters> batchParameterList;
+  private final String command;
+  private final ServerPreparedStatement prep;
+  private Prepare prepareResult;
+
+  /**
+   * Constructor
+   *
+   * @param prepareResult prepare result
+   * @param batchParameterList batch parameter list
+   * @param command sql command
+   * @param prep object creator
+   */
+  public BulkExecutePacket(
+      Prepare prepareResult,
+      List<Parameters> batchParameterList,
+      String command,
+      ServerPreparedStatement prep) {
+    this.batchParameterList = batchParameterList;
+    this.prepareResult = prepareResult;
+    this.command = command;
+    this.prep = prep;
+  }
+
+  public void saveParameters() {
+    List<Parameters> savedList = new ArrayList<>(batchParameterList.size());
+    for (Parameters parameterList : batchParameterList) {
+      savedList.add(parameterList.clone());
+    }
+    this.batchParameterList = savedList;
+  }
+
+  public int encode(Writer writer, Context context, Prepare newPrepareResult)
+      throws IOException, SQLException {
+
+    int statementId =
+        (newPrepareResult != null && newPrepareResult.getStatementId() != -1)
+            ? newPrepareResult.getStatementId()
+            : (this.prepareResult != null ? this.prepareResult.getStatementId() : -1);
+
+    Iterator<Parameters> paramIterator = batchParameterList.iterator();
+    Parameters parameters = paramIterator.next();
+    int parameterCount = parameters.size();
+
+    @SuppressWarnings("rawtypes")
+    Parameter[] parameterHeaderType = new Parameter[parameterCount];
+    // set header type
+    for (int i = 0; i < parameterCount; i++) {
+      parameterHeaderType[i] = parameters.get(i);
+    }
+    byte[] lastCmdData = null;
+    int bulkPacketNo = 0;
+
+    // Implementation After writing a bunch of parameter to buffer is marked. then : - when writing
+    // next bunch of parameter, if buffer grow more than max_allowed_packet, send buffer up to mark,
+    // then create a new packet with current bunch of data - if a bunch of parameter data type
+    // changes
+    // send buffer up to mark, then create a new packet with new data type.
+    // Problem remains if a bunch of parameter is bigger than max_allowed_packet
+    main_loop:
+    while (true) {
+      bulkPacketNo++;
+
+      writer.initPacket();
+      writer.writeByte(0xfa); // COM_STMT_BULK_EXECUTE
+      writer.writeInt(statementId);
+      writer.writeShort((short) 128); // always SEND_TYPES_TO_SERVER
+
+      for (int i = 0; i < parameterCount; i++) {
+        writer.writeShort((short) parameterHeaderType[i].getBinaryEncodeType());
+      }
+
+      if (lastCmdData != null) {
+        if (writer.throwMaxAllowedLength(lastCmdData.length)) {
+          throw new MaxAllowedPacketException(
+              "query size is >= to max_allowed_packet", writer.getCmdLength() != 0);
+        }
+        writer.writeBytes(lastCmdData);
+        writer.mark();
+        lastCmdData = null;
+        if (!paramIterator.hasNext()) {
+          break;
+        }
+        parameters = paramIterator.next();
+      }
+
+      parameter_loop:
+      while (true) {
+        for (int i = 0; i < parameterCount; i++) {
+          Parameter param = parameters.get(i);
+          if (param.isNull()) {
+            writer.writeByte(0x01); // value is null
+          } else {
+            writer.writeByte(0x00); // value follow
+            param.encodeBinary(writer);
+          }
+        }
+
+        if (!writer.isMarked() && writer.hasFlushed()) {
+          // parameter were too big to fit in a MySQL packet
+          // need to finish the packet separately
+          writer.flush();
+          if (!paramIterator.hasNext()) {
+            break main_loop;
+          }
+          parameters = paramIterator.next();
+          // reset header type
+          for (int j = 0; j < parameterCount; j++) {
+            parameterHeaderType[j] = parameters.get(j);
+          }
+          break;
+        }
+
+        if (writer.isMarked() && writer.throwMaxAllowedLength(writer.pos())) {
+          // for max_allowed_packet < 16Mb
+          // packet length was ok at last mark, but won't with new data
+          writer.flushBufferStopAtMark();
+          writer.mark();
+          lastCmdData = writer.resetMark();
+          break;
+        }
+
+        writer.mark();
+
+        if (writer.bufIsDataAfterMark()) {
+          // flush has been done
+          lastCmdData = writer.resetMark();
+          break;
+        }
+
+        if (!paramIterator.hasNext()) {
+          break main_loop;
+        }
+
+        parameters = paramIterator.next();
+
+        // ensure type has not changed
+        for (int i = 0; i < parameterCount; i++) {
+          if (parameterHeaderType[i].getBinaryEncodeType()
+                  != parameters.get(i).getBinaryEncodeType()
+              && !parameters.get(i).isNull()) {
+            writer.flush();
+            // reset header type
+            for (int j = 0; j < parameterCount; j++) {
+              parameterHeaderType[j] = parameters.get(j);
+            }
+            break parameter_loop;
+          }
+        }
+      }
+    }
+
+    writer.flush();
+
+    return bulkPacketNo;
+  }
+
+  public int batchUpdateLength() {
+    return batchParameterList.size();
+  }
+
+  public String getCommand() {
+    return command;
+  }
+
+  public ServerPreparedStatement prep() {
+    return prep;
+  }
+
+  public boolean binaryProtocol() {
+    return true;
+  }
+
+  public String description() {
+    return command;
+  }
+
+  public void setPrepareResult(PrepareResultPacket prepareResult) {
+    this.prepareResult = prepareResult;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/client/ChangeDbPacket.java b/src/main/java/org/mariadb/jdbc/message/client/ChangeDbPacket.java
new file mode 100644
index 0000000..c0c1e20
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/client/ChangeDbPacket.java
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.client;
+
+import java.io.IOException;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.socket.Writer;
+
+/** change database. See https://mariadb.com/kb/en/com_init_db/ protocol */
+public final class ChangeDbPacket implements RedoableClientMessage {
+
+  private final String database;
+
+  /**
+   * Constructor to encode COM_INIT_DB packet
+   *
+   * @param database database
+   */
+  public ChangeDbPacket(String database) {
+    this.database = database;
+  }
+
+  @Override
+  public int encode(Writer writer, Context context) throws IOException {
+    writer.initPacket();
+    writer.writeByte(0x02);
+    writer.writeString(this.database);
+    writer.flush();
+    return 1;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/client/ClosePreparePacket.java b/src/main/java/org/mariadb/jdbc/message/client/ClosePreparePacket.java
new file mode 100644
index 0000000..ff41762
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/client/ClosePreparePacket.java
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.client;
+
+import java.io.IOException;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.message.ClientMessage;
+
+/**
+ * Client mysql COM_STMT_CLOSE packet COM_STMT_CLOSE packet. See
+ * https://mariadb.com/kb/en/3-binary-protocol-prepared-statements-com_stmt_close/
+ */
+public final class ClosePreparePacket implements ClientMessage {
+
+  private final int statementId;
+
+  /**
+   * Constructor for a prepare statement id
+   *
+   * @param statementId statement identifier
+   */
+  public ClosePreparePacket(int statementId) {
+    this.statementId = statementId;
+  }
+
+  /** send packet to socket */
+  @Override
+  public int encode(Writer writer, Context context) throws IOException {
+    writer.initPacket();
+    writer.writeByte(0x19);
+    writer.writeInt(statementId);
+    writer.flush();
+    return 0;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/client/ExecutePacket.java b/src/main/java/org/mariadb/jdbc/message/client/ExecutePacket.java
new file mode 100644
index 0000000..2975591
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/client/ExecutePacket.java
@@ -0,0 +1,165 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.client;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.sql.SQLException;
+import org.mariadb.jdbc.ServerPreparedStatement;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.client.util.Parameter;
+import org.mariadb.jdbc.client.util.Parameters;
+import org.mariadb.jdbc.export.Prepare;
+import org.mariadb.jdbc.message.ClientMessage;
+import org.mariadb.jdbc.message.server.PrepareResultPacket;
+import org.mariadb.jdbc.plugin.codec.ByteArrayCodec;
+
+/**
+ * Execute command (COM_STMT_EXECUTE) See https://mariadb.com/kb/en/com_stmt_execute/ for
+ * documentation
+ */
+public final class ExecutePacket implements RedoableWithPrepareClientMessage {
+  private Parameters parameters;
+  private final String command;
+  private final ServerPreparedStatement prep;
+  private Prepare prepareResult;
+  private InputStream localInfileInputStream;
+
+  /**
+   * Constructor
+   *
+   * @param prepareResult prepare result
+   * @param parameters parameter
+   * @param command sql command
+   * @param prep prepared statement
+   * @param localInfileInputStream local infile input stream
+   */
+  public ExecutePacket(
+      Prepare prepareResult,
+      Parameters parameters,
+      String command,
+      ServerPreparedStatement prep,
+      InputStream localInfileInputStream) {
+    this.parameters = parameters;
+    this.prepareResult = prepareResult;
+    this.command = command;
+    this.prep = prep;
+    this.localInfileInputStream = localInfileInputStream;
+  }
+
+  public void saveParameters() {
+    this.parameters = this.parameters.clone();
+  }
+
+  @Override
+  public void ensureReplayable(Context context) throws IOException, SQLException {
+    int parameterCount = parameters.size();
+    for (int i = 0; i < parameterCount; i++) {
+      Parameter p = parameters.get(i);
+      if (!p.isNull() && p.canEncodeLongData()) {
+        this.parameters.set(
+            i, new org.mariadb.jdbc.codec.Parameter<>(ByteArrayCodec.INSTANCE, p.encodeData()));
+      }
+    }
+  }
+
+  public int encode(Writer writer, Context context, Prepare newPrepareResult)
+      throws IOException, SQLException {
+
+    int statementId =
+        (newPrepareResult != null && newPrepareResult.getStatementId() != -1)
+            ? newPrepareResult.getStatementId()
+            : (this.prepareResult != null ? this.prepareResult.getStatementId() : -1);
+
+    int parameterCount = parameters.size();
+
+    // send long data value in separate packet
+    for (int i = 0; i < parameterCount; i++) {
+      Parameter p = parameters.get(i);
+      if (!p.isNull() && p.canEncodeLongData()) {
+        new LongDataPacket(statementId, p, i).encode(writer, context);
+      }
+    }
+
+    writer.initPacket();
+    writer.writeByte(0x17);
+    writer.writeInt(statementId);
+    writer.writeByte(0x00); // NO CURSOR
+    writer.writeInt(1); // Iteration pos
+
+    if (parameterCount > 0) {
+
+      // create null bitmap and reserve place in writer
+      int nullCount = (parameterCount + 7) / 8;
+      byte[] nullBitsBuffer = new byte[nullCount];
+      int initialPos = writer.pos();
+      writer.pos(initialPos + nullCount);
+
+      // Send Parameter type flag
+      writer.writeByte(0x01);
+
+      // Store types of parameters in first package that is sent to the server.
+      for (int i = 0; i < parameterCount; i++) {
+        Parameter p = parameters.get(i);
+        writer.writeByte(p.getBinaryEncodeType());
+        writer.writeByte(0);
+        if (p.isNull()) {
+          nullBitsBuffer[i / 8] |= (1 << (i % 8));
+        }
+      }
+
+      // write nullBitsBuffer in reserved place
+      writer.writeBytesAtPos(nullBitsBuffer, initialPos);
+
+      // send not null parameter, not long data
+      for (int i = 0; i < parameterCount; i++) {
+        Parameter p = parameters.get(i);
+        if (!p.isNull() && !p.canEncodeLongData()) {
+          p.encodeBinary(writer);
+        }
+      }
+    }
+
+    writer.flush();
+    return 1;
+  }
+
+  public boolean canSkipMeta() {
+    return true;
+  }
+
+  public int batchUpdateLength() {
+    return 1;
+  }
+
+  public String getCommand() {
+    return command;
+  }
+
+  public InputStream getLocalInfileInputStream() {
+    return localInfileInputStream;
+  }
+
+  public ServerPreparedStatement prep() {
+    return prep;
+  }
+
+  public boolean binaryProtocol() {
+    return true;
+  }
+
+  public String description() {
+    return "EXECUTE " + command;
+  }
+
+  public boolean validateLocalFileName(String fileName, Context context) {
+    return ClientMessage.validateLocalFileName(command, parameters, fileName, context);
+  }
+
+  public void setPrepareResult(PrepareResultPacket prepareResult) {
+    this.prepareResult = prepareResult;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/client/HandshakeResponse.java b/src/main/java/org/mariadb/jdbc/message/client/HandshakeResponse.java
new file mode 100644
index 0000000..d0a406e
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/client/HandshakeResponse.java
@@ -0,0 +1,184 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.client;
+
+import static org.mariadb.jdbc.util.constants.Capabilities.*;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.StringTokenizer;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.client.socket.impl.PacketWriter;
+import org.mariadb.jdbc.message.ClientMessage;
+import org.mariadb.jdbc.plugin.Credential;
+import org.mariadb.jdbc.plugin.authentication.standard.NativePasswordPlugin;
+import org.mariadb.jdbc.util.VersionFactory;
+
+/**
+ * Server handshake response builder. see
+ * https://mariadb.com/kb/en/connection/#client-handshake-response
+ */
+public final class HandshakeResponse implements ClientMessage {
+
+  private static final String _CLIENT_NAME = "_client_name";
+  private static final String _CLIENT_VERSION = "_client_version";
+  private static final String _SERVER_HOST = "_server_host";
+  private static final String _OS = "_os";
+  private static final String _THREAD = "_thread";
+  private static final String _JAVA_VENDOR = "_java_vendor";
+  private static final String _JAVA_VERSION = "_java_version";
+
+  private final String username;
+  private final CharSequence password;
+  private final String database;
+  private final String connectionAttributes;
+  private final String host;
+  private final long clientCapabilities;
+  private final byte exchangeCharset;
+  private final byte[] seed;
+  private String authenticationPluginType;
+
+  /**
+   * Object with parsed results
+   *
+   * @param credential credential
+   * @param authenticationPluginType authentication plugin to use
+   * @param seed server seed
+   * @param conf configuration
+   * @param host current host
+   * @param clientCapabilities client capabilities
+   * @param exchangeCharset connection charset
+   */
+  public HandshakeResponse(
+      Credential credential,
+      String authenticationPluginType,
+      byte[] seed,
+      Configuration conf,
+      String host,
+      long clientCapabilities,
+      byte exchangeCharset) {
+    this.authenticationPluginType = authenticationPluginType;
+    this.seed = seed;
+    this.username = credential.getUser();
+    this.password = credential.getPassword();
+    this.database = conf.database();
+    this.connectionAttributes = conf.connectionAttributes();
+    this.host = host;
+    this.clientCapabilities = clientCapabilities;
+    this.exchangeCharset = exchangeCharset;
+  }
+
+  private static void writeStringLengthAscii(Writer encoder, String value) throws IOException {
+    byte[] valBytes = value.getBytes(StandardCharsets.US_ASCII);
+    encoder.writeLength(valBytes.length);
+    encoder.writeBytes(valBytes);
+  }
+
+  private static void writeStringLength(Writer encoder, String value) throws IOException {
+    byte[] valBytes = value.getBytes(StandardCharsets.UTF_8);
+    encoder.writeLength(valBytes.length);
+    encoder.writeBytes(valBytes);
+  }
+
+  private static void writeConnectAttributes(
+      Writer writer, String connectionAttributes, String host) throws IOException {
+
+    PacketWriter tmpWriter = new PacketWriter(null, 0, 0, null, null);
+    tmpWriter.pos(0);
+    writeStringLengthAscii(tmpWriter, _CLIENT_NAME);
+    writeStringLength(tmpWriter, "MariaDB Connector/J");
+
+    writeStringLengthAscii(tmpWriter, _CLIENT_VERSION);
+    writeStringLength(tmpWriter, VersionFactory.getInstance().getVersion());
+
+    writeStringLengthAscii(tmpWriter, _SERVER_HOST);
+    writeStringLength(tmpWriter, (host != null) ? host : "");
+
+    writeStringLengthAscii(tmpWriter, _OS);
+    writeStringLength(tmpWriter, System.getProperty("os.name"));
+
+    writeStringLengthAscii(tmpWriter, _THREAD);
+    writeStringLength(tmpWriter, Long.toString(Thread.currentThread().getId()));
+
+    writeStringLengthAscii(tmpWriter, _JAVA_VENDOR);
+    writeStringLength(tmpWriter, System.getProperty("java.vendor"));
+
+    writeStringLengthAscii(tmpWriter, _JAVA_VERSION);
+    writeStringLength(tmpWriter, System.getProperty("java.version"));
+
+    if (connectionAttributes != null) {
+      StringTokenizer tokenizer = new StringTokenizer(connectionAttributes, ",");
+      while (tokenizer.hasMoreTokens()) {
+        String token = tokenizer.nextToken();
+        int separator = token.indexOf(":");
+        if (separator != -1) {
+          writeStringLength(tmpWriter, token.substring(0, separator));
+          writeStringLength(tmpWriter, token.substring(separator + 1));
+        } else {
+          writeStringLength(tmpWriter, token);
+          writeStringLength(tmpWriter, "");
+        }
+      }
+    }
+
+    writer.writeLength(tmpWriter.pos());
+    writer.writeBytes(tmpWriter.buf(), 0, tmpWriter.pos());
+  }
+
+  @Override
+  public int encode(Writer writer, Context context) throws IOException {
+
+    final byte[] authData;
+    if ("mysql_clear_password".equals(authenticationPluginType)) {
+      if (!context.hasClientCapability(SSL)) {
+        throw new IllegalStateException("Cannot send password in clear if SSL is not enabled.");
+      }
+      authData =
+          (password == null) ? new byte[0] : password.toString().getBytes(StandardCharsets.UTF_8);
+    } else {
+      authenticationPluginType = "mysql_native_password";
+      authData = NativePasswordPlugin.encryptPassword(password, seed);
+    }
+
+    writer.writeInt((int) clientCapabilities);
+    writer.writeInt(1024 * 1024 * 1024);
+    writer.writeByte(exchangeCharset); // 1
+
+    writer.writeBytes(new byte[19]); // 19
+    writer.writeInt((int) (clientCapabilities >> 32)); // Maria extended flag
+
+    writer.writeString(username != null ? username : System.getProperty("user.name"));
+    writer.writeByte(0x00);
+
+    if (context.hasServerCapability(PLUGIN_AUTH_LENENC_CLIENT_DATA)) {
+      writer.writeLength(authData.length);
+      writer.writeBytes(authData);
+    } else if (context.hasServerCapability(SECURE_CONNECTION)) {
+      writer.writeByte((byte) authData.length);
+      writer.writeBytes(authData);
+    } else {
+      writer.writeBytes(authData);
+      writer.writeByte(0x00);
+    }
+
+    if (context.hasClientCapability(CONNECT_WITH_DB)) {
+      writer.writeString(database);
+      writer.writeByte(0x00);
+    }
+
+    if (context.hasServerCapability(PLUGIN_AUTH)) {
+      writer.writeString(authenticationPluginType);
+      writer.writeByte(0x00);
+    }
+
+    if (context.hasServerCapability(CONNECT_ATTRS)) {
+      writeConnectAttributes(writer, connectionAttributes, host);
+    }
+    writer.flush();
+    return 1;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/client/LongDataPacket.java b/src/main/java/org/mariadb/jdbc/message/client/LongDataPacket.java
new file mode 100644
index 0000000..2caee04
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/client/LongDataPacket.java
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.client;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.client.util.Parameter;
+import org.mariadb.jdbc.message.ClientMessage;
+
+/**
+ * COM_STMT_SEND_LONG_DATA
+ *
+ * <p>Permit to send ONE value in a dedicated packet. The advantage is when length is unknown, to
+ * stream easily data to socket
+ *
+ * <p>https://mariadb.com/kb/en/com_stmt_send_long_data/
+ */
+public final class LongDataPacket implements ClientMessage {
+
+  private final int statementId;
+  private final Parameter parameter;
+  private final int index;
+
+  /**
+   * Constructor
+   *
+   * @param statementId statement identifier
+   * @param parameter parameter
+   * @param index index
+   */
+  public LongDataPacket(int statementId, Parameter parameter, int index) {
+    this.statementId = statementId;
+    this.parameter = parameter;
+    this.index = index;
+  }
+
+  @Override
+  public int encode(Writer writer, Context context) throws IOException, SQLException {
+    writer.initPacket();
+    writer.writeByte(0x18);
+    writer.writeInt(statementId);
+    writer.writeShort((short) index);
+    parameter.encodeLongData(writer);
+    writer.flush();
+    return 0;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/client/PingPacket.java b/src/main/java/org/mariadb/jdbc/message/client/PingPacket.java
new file mode 100644
index 0000000..9504c5b
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/client/PingPacket.java
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.client;
+
+import java.io.IOException;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.message.ClientMessage;
+
+/** Ping packet see COM_PING (https://mariadb.com/kb/en/com_ping/) */
+public final class PingPacket implements ClientMessage {
+
+  /** default instance */
+  public static final PingPacket INSTANCE = new PingPacket();
+
+  @Override
+  public int encode(Writer writer, Context context) throws IOException {
+    writer.initPacket();
+    writer.writeByte(0x0e);
+    writer.flush();
+    return 1;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/client/PrepareExecutePacket.java b/src/main/java/org/mariadb/jdbc/message/client/PrepareExecutePacket.java
new file mode 100644
index 0000000..9b8d6df
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/client/PrepareExecutePacket.java
@@ -0,0 +1,253 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.client;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.sql.SQLException;
+import java.util.concurrent.locks.ReentrantLock;
+import org.mariadb.jdbc.BasePreparedStatement;
+import org.mariadb.jdbc.ServerPreparedStatement;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.Completion;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.client.socket.Reader;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.client.util.Parameter;
+import org.mariadb.jdbc.client.util.Parameters;
+import org.mariadb.jdbc.export.ExceptionFactory;
+import org.mariadb.jdbc.export.Prepare;
+import org.mariadb.jdbc.message.ClientMessage;
+import org.mariadb.jdbc.message.server.CachedPrepareResultPacket;
+import org.mariadb.jdbc.message.server.ErrorPacket;
+import org.mariadb.jdbc.message.server.PrepareResultPacket;
+import org.mariadb.jdbc.plugin.codec.ByteArrayCodec;
+
+/**
+ * Send a client COM_STMT_PREPARE + COM_STMT_EXECUTE packets see
+ * https://mariadb.com/kb/en/com_stmt_prepare/
+ */
+public final class PrepareExecutePacket implements RedoableWithPrepareClientMessage {
+  private final String sql;
+  private Parameters parameters;
+  private final ServerPreparedStatement prep;
+  private PrepareResultPacket prepareResult;
+  private InputStream localInfileInputStream;
+  /**
+   * Construct prepare packet
+   *
+   * @param sql sql
+   * @param parameters parameter
+   * @param prep prepare
+   * @param localInfileInputStream local infile input stream
+   */
+  public PrepareExecutePacket(
+      String sql,
+      Parameters parameters,
+      ServerPreparedStatement prep,
+      InputStream localInfileInputStream) {
+    this.sql = sql;
+    this.parameters = parameters;
+    this.prep = prep;
+    this.localInfileInputStream = localInfileInputStream;
+    this.prepareResult = null;
+  }
+
+  @Override
+  public int encode(Writer writer, Context context, Prepare newPrepareResult)
+      throws IOException, SQLException {
+    int statementId = -1;
+    if (newPrepareResult == null) {
+
+      writer.initPacket();
+      writer.writeByte(0x16);
+      writer.writeString(this.sql);
+      writer.flushPipeline();
+    } else {
+      statementId = newPrepareResult.getStatementId();
+    }
+    int parameterCount = parameters.size();
+
+    // send long data value in separate packet
+    for (int i = 0; i < parameterCount; i++) {
+      Parameter p = parameters.get(i);
+      if (!p.isNull() && p.canEncodeLongData()) {
+        new LongDataPacket(statementId, p, i).encode(writer, context);
+      }
+    }
+
+    writer.initPacket();
+    writer.writeByte(0x17);
+    writer.writeInt(statementId);
+    writer.writeByte(0x00); // NO CURSOR
+    writer.writeInt(1); // Iteration pos
+
+    if (parameterCount > 0) {
+
+      // create null bitmap and reserve place in writer
+      int nullCount = (parameterCount + 7) / 8;
+      byte[] nullBitsBuffer = new byte[nullCount];
+      int initialPos = writer.pos();
+      writer.pos(initialPos + nullCount);
+
+      // Send Parameter type flag
+      writer.writeByte(0x01);
+
+      // Store types of parameters in first package that is sent to the server.
+      for (int i = 0; i < parameterCount; i++) {
+        Parameter p = parameters.get(i);
+        writer.writeByte(p.getBinaryEncodeType());
+        writer.writeByte(0);
+        if (p.isNull()) {
+          nullBitsBuffer[i / 8] |= (1 << (i % 8));
+        }
+      }
+
+      // write nullBitsBuffer in reserved place
+      writer.writeBytesAtPos(nullBitsBuffer, initialPos);
+
+      // send not null parameter, not long data
+      for (int i = 0; i < parameterCount; i++) {
+        Parameter p = parameters.get(i);
+        if (!p.isNull() && !p.canEncodeLongData()) {
+          p.encodeBinary(writer);
+        }
+      }
+    }
+
+    writer.flush();
+    return (newPrepareResult == null) ? 2 : 1;
+  }
+
+  @Override
+  public Completion readPacket(
+      Statement stmt,
+      int fetchSize,
+      long maxRows,
+      int resultSetConcurrency,
+      int resultSetType,
+      boolean closeOnCompletion,
+      Reader reader,
+      Writer writer,
+      Context context,
+      ExceptionFactory exceptionFactory,
+      ReentrantLock lock,
+      boolean traceEnable,
+      ClientMessage message)
+      throws IOException, SQLException {
+    if (this.prepareResult == null) {
+      ReadableByteBuf buf = reader.readReusablePacket(traceEnable);
+      // *********************************************************************************************************
+      // * ERROR response
+      // *********************************************************************************************************
+      if (buf.getUnsignedByte()
+          == 0xff) { // force current status to in transaction to ensure rollback/commit, since
+        // command may
+        // have issue a transaction
+        ErrorPacket errorPacket = new ErrorPacket(buf, context);
+        throw exceptionFactory
+            .withSql(this.description())
+            .create(
+                errorPacket.getMessage(), errorPacket.getSqlState(), errorPacket.getErrorCode());
+      }
+      if (context.getConf().useServerPrepStmts()
+          && context.getConf().cachePrepStmts()
+          && sql.length() < 8192) {
+        PrepareResultPacket prepare = new CachedPrepareResultPacket(buf, reader, context);
+        PrepareResultPacket previousCached =
+            (PrepareResultPacket)
+                context
+                    .getPrepareCache()
+                    .put(
+                        sql,
+                        prepare,
+                        stmt instanceof ServerPreparedStatement
+                            ? (ServerPreparedStatement) stmt
+                            : null);
+        if (stmt != null) {
+          ((BasePreparedStatement) stmt)
+              .setPrepareResult(previousCached != null ? previousCached : prepare);
+        }
+        this.prepareResult = previousCached != null ? previousCached : prepare;
+        return this.prepareResult;
+      }
+      PrepareResultPacket prepareResult = new PrepareResultPacket(buf, reader, context);
+      if (stmt != null) {
+        ((BasePreparedStatement) stmt).setPrepareResult(prepareResult);
+      }
+      this.prepareResult = prepareResult;
+      return prepareResult;
+    } else {
+      return RedoableWithPrepareClientMessage.super.readPacket(
+          stmt,
+          fetchSize,
+          maxRows,
+          resultSetConcurrency,
+          resultSetType,
+          closeOnCompletion,
+          reader,
+          writer,
+          context,
+          exceptionFactory,
+          lock,
+          traceEnable,
+          message);
+    }
+  }
+
+  public void saveParameters() {
+    this.parameters = this.parameters.clone();
+  }
+
+  @Override
+  public void ensureReplayable(Context context) throws IOException, SQLException {
+    int parameterCount = parameters.size();
+    for (int i = 0; i < parameterCount; i++) {
+      Parameter p = parameters.get(i);
+      if (!p.isNull() && p.canEncodeLongData()) {
+        this.parameters.set(
+            i, new org.mariadb.jdbc.codec.Parameter<>(ByteArrayCodec.INSTANCE, p.encodeData()));
+      }
+    }
+  }
+
+  public boolean canSkipMeta() {
+    return true;
+  }
+
+  @Override
+  public String description() {
+    return "PREPARE + EXECUTE " + sql;
+  }
+
+  public int batchUpdateLength() {
+    return 1;
+  }
+
+  public String getCommand() {
+    return sql;
+  }
+
+  public InputStream getLocalInfileInputStream() {
+    return localInfileInputStream;
+  }
+
+  public ServerPreparedStatement prep() {
+    return prep;
+  }
+
+  public boolean binaryProtocol() {
+    return true;
+  }
+
+  public boolean validateLocalFileName(String fileName, Context context) {
+    return ClientMessage.validateLocalFileName(sql, parameters, fileName, context);
+  }
+
+  public void setPrepareResult(PrepareResultPacket prepareResult) {
+    this.prepareResult = prepareResult;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/client/PreparePacket.java b/src/main/java/org/mariadb/jdbc/message/client/PreparePacket.java
new file mode 100644
index 0000000..1112419
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/client/PreparePacket.java
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.client;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.concurrent.locks.ReentrantLock;
+import org.mariadb.jdbc.BasePreparedStatement;
+import org.mariadb.jdbc.ServerPreparedStatement;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.Completion;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.client.socket.Reader;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.export.ExceptionFactory;
+import org.mariadb.jdbc.message.ClientMessage;
+import org.mariadb.jdbc.message.server.CachedPrepareResultPacket;
+import org.mariadb.jdbc.message.server.ErrorPacket;
+import org.mariadb.jdbc.message.server.PrepareResultPacket;
+
+/** Send a client COM_STMT_PREPARE packet see https://mariadb.com/kb/en/com_stmt_prepare/ */
+public final class PreparePacket implements ClientMessage {
+  private final String sql;
+
+  /**
+   * Construct prepare packet
+   *
+   * @param sql sql command
+   */
+  public PreparePacket(String sql) {
+    this.sql = sql;
+  }
+
+  @Override
+  public int encode(Writer writer, Context context) throws IOException {
+    writer.initPacket();
+    writer.writeByte(0x16);
+    writer.writeString(this.sql);
+    writer.flush();
+    return 1;
+  }
+
+  @Override
+  public Completion readPacket(
+      Statement stmt,
+      int fetchSize,
+      long maxRows,
+      int resultSetConcurrency,
+      int resultSetType,
+      boolean closeOnCompletion,
+      Reader reader,
+      Writer writer,
+      Context context,
+      ExceptionFactory exceptionFactory,
+      ReentrantLock lock,
+      boolean traceEnable,
+      ClientMessage message)
+      throws IOException, SQLException {
+
+    ReadableByteBuf buf = reader.readReusablePacket(traceEnable);
+    // *********************************************************************************************************
+    // * ERROR response
+    // *********************************************************************************************************
+    if (buf.getUnsignedByte()
+        == 0xff) { // force current status to in transaction to ensure rollback/commit, since
+      // command may
+      // have issue a transaction
+      ErrorPacket errorPacket = new ErrorPacket(buf, context);
+      throw exceptionFactory
+          .withSql(this.description())
+          .create(errorPacket.getMessage(), errorPacket.getSqlState(), errorPacket.getErrorCode());
+    }
+    if (context.getConf().useServerPrepStmts()
+        && context.getConf().cachePrepStmts()
+        && sql.length() < 8192) {
+      PrepareResultPacket prepare = new CachedPrepareResultPacket(buf, reader, context);
+      PrepareResultPacket previousCached =
+          (PrepareResultPacket)
+              context
+                  .getPrepareCache()
+                  .put(
+                      sql,
+                      prepare,
+                      stmt instanceof ServerPreparedStatement
+                          ? (ServerPreparedStatement) stmt
+                          : null);
+      if (stmt != null) {
+        ((BasePreparedStatement) stmt)
+            .setPrepareResult(previousCached != null ? previousCached : prepare);
+      }
+      return previousCached != null ? previousCached : prepare;
+    }
+    PrepareResultPacket prepareResult = new PrepareResultPacket(buf, reader, context);
+    if (stmt != null) {
+      ((BasePreparedStatement) stmt).setPrepareResult(prepareResult);
+    }
+    return prepareResult;
+  }
+
+  @Override
+  public String description() {
+    return "PREPARE " + sql;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/client/QueryPacket.java b/src/main/java/org/mariadb/jdbc/message/client/QueryPacket.java
new file mode 100644
index 0000000..98d2ee7
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/client/QueryPacket.java
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.client;
+
+import java.io.IOException;
+import java.io.InputStream;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.message.ClientMessage;
+
+/** Query client packet COM_QUERY see https://mariadb.com/kb/en/com_query/ */
+public final class QueryPacket implements RedoableClientMessage {
+
+  private final String sql;
+  private final InputStream localInfileInputStream;
+
+  /**
+   * Constructor
+   *
+   * @param sql sql command
+   */
+  public QueryPacket(String sql) {
+    this.sql = sql;
+    this.localInfileInputStream = null;
+  }
+
+  /**
+   * Constructor with local infile input stream
+   *
+   * @param sql sql
+   * @param localInfileInputStream local infile input stream
+   */
+  public QueryPacket(String sql, InputStream localInfileInputStream) {
+    this.sql = sql;
+    this.localInfileInputStream = localInfileInputStream;
+  }
+
+  public int batchUpdateLength() {
+    return 1;
+  }
+
+  @Override
+  public int encode(Writer writer, Context context) throws IOException {
+    writer.initPacket();
+    writer.writeByte(0x03);
+    writer.writeString(this.sql);
+    writer.flush();
+    return 1;
+  }
+
+  /**
+   * Check that command is a COMMIT command
+   *
+   * @return true if a commit command
+   */
+  public boolean isCommit() {
+    return "COMMIT".equalsIgnoreCase(sql);
+  }
+
+  public boolean validateLocalFileName(String fileName, Context context) {
+    return ClientMessage.validateLocalFileName(sql, null, fileName, context);
+  }
+
+  public InputStream getLocalInfileInputStream() {
+    return localInfileInputStream;
+  }
+
+  public String description() {
+    return sql;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/client/QueryWithParametersPacket.java b/src/main/java/org/mariadb/jdbc/message/client/QueryWithParametersPacket.java
new file mode 100644
index 0000000..7c76a6e
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/client/QueryWithParametersPacket.java
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.client;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.sql.SQLException;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.client.util.Parameter;
+import org.mariadb.jdbc.client.util.Parameters;
+import org.mariadb.jdbc.message.ClientMessage;
+import org.mariadb.jdbc.plugin.codec.ByteArrayCodec;
+import org.mariadb.jdbc.util.ClientParser;
+
+/**
+ * Query client packet COM_QUERY see https://mariadb.com/kb/en/com_query/ same than QueryPacket, but
+ * with parameters that will be escaped
+ */
+public final class QueryWithParametersPacket implements RedoableClientMessage {
+
+  private final String preSqlCmd;
+  private final ClientParser parser;
+  private Parameters parameters;
+  private InputStream localInfileInputStream;
+
+  /**
+   * Constructor
+   *
+   * @param preSqlCmd additional pre command
+   * @param parser command parser result
+   * @param parameters parameters
+   * @param localInfileInputStream local infile input stream
+   */
+  public QueryWithParametersPacket(
+      String preSqlCmd,
+      ClientParser parser,
+      Parameters parameters,
+      InputStream localInfileInputStream) {
+    this.preSqlCmd = preSqlCmd;
+    this.parser = parser;
+    this.parameters = parameters;
+    this.localInfileInputStream = localInfileInputStream;
+  }
+
+  @Override
+  public void ensureReplayable(Context context) throws IOException, SQLException {
+    int parameterCount = parameters.size();
+    for (int i = 0; i < parameterCount; i++) {
+      Parameter p = parameters.get(i);
+      if (!p.isNull() && p.canEncodeLongData()) {
+        this.parameters.set(
+            i, new org.mariadb.jdbc.codec.Parameter<>(ByteArrayCodec.INSTANCE, p.encodeData()));
+      }
+    }
+  }
+
+  public void saveParameters() {
+    this.parameters = this.parameters.clone();
+  }
+
+  @Override
+  public int encode(Writer encoder, Context context) throws IOException, SQLException {
+    encoder.initPacket();
+    encoder.writeByte(0x03);
+    if (preSqlCmd != null) encoder.writeAscii(preSqlCmd);
+    if (parser.getParamPositions().size() == 0) {
+      encoder.writeBytes(parser.getQuery());
+    } else {
+      int pos = 0;
+      int paramPos;
+      for (int i = 0; i < parser.getParamPositions().size(); i++) {
+        paramPos = parser.getParamPositions().get(i);
+        encoder.writeBytes(parser.getQuery(), pos, paramPos - pos);
+        pos = paramPos + 1;
+        parameters.get(i).encodeText(encoder, context);
+      }
+      encoder.writeBytes(parser.getQuery(), pos, parser.getQuery().length - pos);
+    }
+    encoder.flush();
+    return 1;
+  }
+
+  public int batchUpdateLength() {
+    return 1;
+  }
+
+  public boolean validateLocalFileName(String fileName, Context context) {
+    return ClientMessage.validateLocalFileName(parser.getSql(), parameters, fileName, context);
+  }
+
+  public InputStream getLocalInfileInputStream() {
+    return localInfileInputStream;
+  }
+
+  @Override
+  public String description() {
+    return parser.getSql();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/client/QuitPacket.java b/src/main/java/org/mariadb/jdbc/message/client/QuitPacket.java
new file mode 100644
index 0000000..001f59c
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/client/QuitPacket.java
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.client;
+
+import java.io.IOException;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.message.ClientMessage;
+
+/**
+ * ending connection packet COM_QUIT proper end of a connection. see
+ * https://mariadb.com/kb/en/com_quit/
+ */
+public final class QuitPacket implements ClientMessage {
+
+  /** default instance to encode packet */
+  public static final QuitPacket INSTANCE = new QuitPacket();
+
+  @Override
+  public int encode(Writer writer, Context context) throws IOException {
+    writer.initPacket();
+    writer.writeByte(0x01);
+    writer.flush();
+    return 0;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/client/RedoableClientMessage.java b/src/main/java/org/mariadb/jdbc/message/client/RedoableClientMessage.java
new file mode 100644
index 0000000..fed79a8
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/client/RedoableClientMessage.java
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.client;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.export.Prepare;
+import org.mariadb.jdbc.message.ClientMessage;
+
+/** Client message that can be replayed */
+public interface RedoableClientMessage extends ClientMessage {
+
+  /** Save parameters of command that can be re-executed */
+  default void saveParameters() {}
+
+  /**
+   * Ensure that command can be replayed
+   *
+   * @param context connection context
+   * @throws IOException If socket error occurs
+   * @throws SQLException for other type of issue
+   */
+  default void ensureReplayable(Context context) throws IOException, SQLException {}
+
+  /**
+   * Encode command to packet
+   *
+   * @param writer socket writer
+   * @param context connection context
+   * @param newPrepareResult new prepare result if prepare has been changed
+   * @return number of send command
+   * @throws IOException if any socket error is issued
+   * @throws SQLException if any other kind of error occurs during encoding
+   */
+  default int encode(Writer writer, Context context, Prepare newPrepareResult)
+      throws IOException, SQLException {
+    return encode(writer, context);
+  }
+
+  /**
+   * re-encode command to packet
+   *
+   * @param writer socket writer
+   * @param context connection context
+   * @param newPrepareResult new prepare result if prepare has been changed
+   * @return number of send command
+   * @throws IOException if any socket error is issued
+   * @throws SQLException if any other kind of error occurs during encoding
+   */
+  default int reEncode(Writer writer, Context context, Prepare newPrepareResult)
+      throws IOException, SQLException {
+    return encode(writer, context, newPrepareResult);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/client/RedoableWithPrepareClientMessage.java b/src/main/java/org/mariadb/jdbc/message/client/RedoableWithPrepareClientMessage.java
new file mode 100644
index 0000000..1891e62
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/client/RedoableWithPrepareClientMessage.java
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.client;
+
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import org.mariadb.jdbc.ServerPreparedStatement;
+import org.mariadb.jdbc.client.Client;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.export.Prepare;
+import org.mariadb.jdbc.message.server.PrepareResultPacket;
+
+/** Client message that can be replayed with parameter */
+public interface RedoableWithPrepareClientMessage extends RedoableClientMessage {
+
+  /**
+   * sql command value
+   *
+   * @return sql command
+   */
+  String getCommand();
+
+  /**
+   * Server prepare statement caller
+   *
+   * @return caller
+   */
+  ServerPreparedStatement prep();
+
+  /**
+   * Default encoder caller
+   *
+   * @param writer socket writer
+   * @param context connection context
+   * @return number of command sent
+   * @throws IOException if a socket exception occurs
+   * @throws SQLException for any other kind of error
+   */
+  default int encode(Writer writer, Context context) throws IOException, SQLException {
+    return encode(writer, context, null);
+  }
+
+  /**
+   * encoder method in case of failover, passing new prepared object
+   *
+   * @param writer socket writer
+   * @param context connection context
+   * @param newPrepareResult new prepare result
+   * @return number of command sent
+   * @throws IOException if a socket exception occurs
+   * @throws SQLException for any other kind of error
+   */
+  int encode(Writer writer, Context context, Prepare newPrepareResult)
+      throws IOException, SQLException;
+
+  /**
+   * re-encoder method in case of failover, passing new prepared object
+   *
+   * @param writer socket writer
+   * @param context connection context
+   * @param newPrepareResult new prepare result
+   * @return number of command sent
+   * @throws IOException if a socket exception occurs
+   * @throws SQLException for any other kind of error
+   */
+  @Override
+  default int reEncode(Writer writer, Context context, Prepare newPrepareResult)
+      throws IOException, SQLException {
+    return encode(writer, context, newPrepareResult);
+  }
+
+  /**
+   * Set prepare result, if pipelining prepare
+   *
+   * @param prepareResult prepare results
+   */
+  void setPrepareResult(PrepareResultPacket prepareResult);
+
+  /**
+   * Force re-prepare command
+   *
+   * @param client client
+   * @throws SQLException if any error occurs
+   */
+  default void rePrepare(Client client) throws SQLException {
+    PreparePacket preparePacket = new PreparePacket(getCommand());
+    setPrepareResult(
+        (PrepareResultPacket)
+            client
+                .execute(
+                    preparePacket,
+                    prep(),
+                    0,
+                    0L,
+                    ResultSet.CONCUR_READ_ONLY,
+                    ResultSet.TYPE_FORWARD_ONLY,
+                    false,
+                    true)
+                .get(0));
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/client/ResetPacket.java b/src/main/java/org/mariadb/jdbc/message/client/ResetPacket.java
new file mode 100644
index 0000000..372b280
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/client/ResetPacket.java
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.client;
+
+import java.io.IOException;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.message.ClientMessage;
+
+/** Reset packet COM_RESET_CONNECTION see https://mariadb.com/kb/en/com_reset_connection/ */
+public final class ResetPacket implements ClientMessage {
+
+  /** default instance */
+  public static final ResetPacket INSTANCE = new ResetPacket();
+
+  @Override
+  public int encode(Writer writer, Context context) throws IOException {
+    writer.initPacket();
+    writer.writeByte(0x1f);
+    writer.flush();
+    return 1;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/client/SslRequestPacket.java b/src/main/java/org/mariadb/jdbc/message/client/SslRequestPacket.java
new file mode 100644
index 0000000..642992a
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/client/SslRequestPacket.java
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.client;
+
+import java.io.IOException;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.message.ClientMessage;
+
+/** SSL request packet see https://mariadb.com/kb/en/connection/#sslrequest-packet */
+public final class SslRequestPacket implements ClientMessage {
+
+  private final long clientCapabilities;
+  private final byte exchangeCharset;
+
+  /**
+   * Constructor
+   *
+   * @param clientCapabilities client capabilities
+   * @param exchangeCharset connection charset to set
+   */
+  private SslRequestPacket(long clientCapabilities, byte exchangeCharset) {
+    this.clientCapabilities = clientCapabilities;
+    this.exchangeCharset = exchangeCharset;
+  }
+
+  /**
+   * Create ssl request packet
+   *
+   * @param clientCapabilities client capabilities
+   * @param exchangeCharset connection charset
+   * @return ssl request packet
+   */
+  public static SslRequestPacket create(long clientCapabilities, byte exchangeCharset) {
+    return new SslRequestPacket(clientCapabilities, exchangeCharset);
+  }
+
+  @Override
+  public int encode(Writer writer, Context context) throws IOException {
+    writer.writeInt((int) clientCapabilities);
+    writer.writeInt(1024 * 1024 * 1024);
+    writer.writeByte(exchangeCharset); // 1 byte
+    writer.writeBytes(new byte[19]); // 19  bytes
+    writer.writeInt((int) (clientCapabilities >> 32)); // Maria extended flag
+    writer.flush();
+    return 0;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/server/AuthSwitchPacket.java b/src/main/java/org/mariadb/jdbc/message/server/AuthSwitchPacket.java
new file mode 100644
index 0000000..f12cb49
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/server/AuthSwitchPacket.java
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.server;
+
+import java.util.Arrays;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.ServerMessage;
+
+/**
+ * Authentication switch packet. See
+ * https://mariadb.com/kb/en/connection/#authentication-switch-request
+ */
+public class AuthSwitchPacket implements ServerMessage {
+
+  private final String plugin;
+  private final byte[] seed;
+
+  /**
+   * Authentication switch constructor
+   *
+   * @param plugin plugin requested
+   * @param seed plugin seed
+   */
+  public AuthSwitchPacket(String plugin, byte[] seed) {
+    this.plugin = plugin;
+    this.seed = seed;
+  }
+
+  /**
+   * Decode an AUTH_SWITCH_PACKET from a MYSQL packet.
+   *
+   * @param buf packet
+   * @return Authentication switch packet.
+   */
+  public static AuthSwitchPacket decode(ReadableByteBuf buf) {
+    buf.skip(1);
+    String plugin = buf.readStringNullEnd();
+
+    byte[] seed = new byte[buf.readableBytes()];
+    buf.readBytes(seed);
+    return new AuthSwitchPacket(plugin, seed);
+  }
+
+  /**
+   * Get authentication switch plugin information
+   *
+   * @return plugin
+   */
+  public String getPlugin() {
+    return plugin;
+  }
+
+  /**
+   * Get authentication switch seed information
+   *
+   * @return seed
+   */
+  public byte[] getSeed() {
+    return seed;
+  }
+
+  /**
+   * Get truncated seed (seed without ending 0x00 byte)
+   *
+   * @param seed connection seed
+   * @return truncated seed
+   */
+  public static byte[] getTruncatedSeed(byte[] seed) {
+    return (seed.length > 0) ? Arrays.copyOfRange(seed, 0, seed.length - 1) : new byte[0];
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/server/CachedPrepareResultPacket.java b/src/main/java/org/mariadb/jdbc/message/server/CachedPrepareResultPacket.java
new file mode 100644
index 0000000..edf5219
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/server/CachedPrepareResultPacket.java
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.server;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.mariadb.jdbc.ServerPreparedStatement;
+import org.mariadb.jdbc.client.Client;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.client.socket.Reader;
+
+/** Prepare packet result with flag indicating use */
+public final class CachedPrepareResultPacket extends PrepareResultPacket {
+
+  private final AtomicBoolean closing = new AtomicBoolean();
+  private final AtomicBoolean cached = new AtomicBoolean();
+  private final List<ServerPreparedStatement> statements = new ArrayList<>();
+
+  /**
+   * Cache prepare result with flag indicating use
+   *
+   * @param buffer prepare packet buffer
+   * @param reader packet reader
+   * @param context connection context
+   * @throws IOException if any socket error occurs
+   */
+  public CachedPrepareResultPacket(ReadableByteBuf buffer, Reader reader, Context context)
+      throws IOException {
+    super(buffer, reader, context);
+  }
+
+  /**
+   * Indicate that a prepare statement must be closed (if not in LRU cache)
+   *
+   * @param con current connection
+   * @throws SQLException if SQL
+   */
+  public void close(Client con) throws SQLException {
+    if (!cached.get() && closing.compareAndSet(false, true)) {
+      con.closePrepare(this);
+    }
+  }
+
+  public void decrementUse(Client con, ServerPreparedStatement preparedStatement)
+      throws SQLException {
+    statements.remove(preparedStatement);
+    if (statements.size() == 0 && !cached.get()) {
+      close(con);
+    }
+  }
+
+  /**
+   * Increment use of prepare statement.
+   *
+   * @param preparedStatement new statement using prepare result
+   */
+  public void incrementUse(ServerPreparedStatement preparedStatement) {
+    if (closing.get()) {
+      return;
+    }
+    if (preparedStatement != null) statements.add(preparedStatement);
+  }
+
+  /**
+   * Indicate that Prepare command is not on LRU cache anymore. closing prepare command if not used
+   *
+   * @param con current connection
+   */
+  public void unCache(Client con) {
+    cached.set(false);
+    if (statements.size() <= 0) {
+      try {
+        close(con);
+      } catch (SQLException e) {
+        // eat
+      }
+    }
+  }
+
+  /**
+   * indicate that result is in LRU cache
+   *
+   * @return true if cached
+   */
+  public boolean cache() {
+    if (closing.get()) {
+      return false;
+    }
+    return cached.compareAndSet(false, true);
+  }
+
+  /**
+   * Return prepare statement id.
+   *
+   * @return statement id
+   */
+  public int getStatementId() {
+    return statementId;
+  }
+
+  /** Resetting cache in case of failover */
+  public void reset() {
+    statementId = -1;
+    for (ServerPreparedStatement stmt : statements) {
+      stmt.reset();
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/server/ColumnDefinitionPacket.java b/src/main/java/org/mariadb/jdbc/message/server/ColumnDefinitionPacket.java
new file mode 100644
index 0000000..51bc56d
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/server/ColumnDefinitionPacket.java
@@ -0,0 +1,177 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.server;
+
+import java.util.Objects;
+import org.mariadb.jdbc.client.Column;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.ServerMessage;
+import org.mariadb.jdbc.util.CharsetEncodingLength;
+import org.mariadb.jdbc.util.constants.ColumnFlags;
+
+/** Column metadata definition */
+public class ColumnDefinitionPacket implements Column, ServerMessage {
+
+  private final ReadableByteBuf buf;
+  /** charset */
+  protected final int charset;
+  /** column maximum length */
+  protected final long columnLength;
+  /** data type see https://mariadb.com/kb/en/result-set-packets/#field-types */
+  protected final DataType dataType;
+  /** number of decimal */
+  protected final byte decimals;
+  /** flags see https://mariadb.com/kb/en/result-set-packets/#field-details-flag */
+  private final int flags;
+  /** string offset position in buffer */
+  private final int[] stringPos;
+  /** extended type name */
+  protected final String extTypeName;
+  /** extended type format */
+  protected final String extTypeFormat;
+  /** configuration: use alias as name */
+  private boolean useAliasAsName;
+
+  /**
+   * Column definition constructor
+   *
+   * @param buf buffer
+   * @param charset charset
+   * @param columnLength maxium column length
+   * @param dataType data type
+   * @param decimals decimal length
+   * @param flags flags
+   * @param stringPos string position indexes
+   * @param extTypeName extended type name
+   * @param extTypeFormat extended type format
+   */
+  public ColumnDefinitionPacket(
+      ReadableByteBuf buf,
+      int charset,
+      long columnLength,
+      DataType dataType,
+      byte decimals,
+      int flags,
+      int[] stringPos,
+      String extTypeName,
+      String extTypeFormat) {
+    this.buf = buf;
+    this.charset = charset;
+    this.columnLength = columnLength;
+    this.dataType = dataType;
+    this.decimals = decimals;
+    this.flags = flags;
+    this.stringPos = stringPos;
+    this.extTypeName = extTypeName;
+    this.extTypeFormat = extTypeFormat;
+  }
+
+  public String getSchema() {
+    buf.pos(stringPos[0]);
+    return buf.readString(buf.readIntLengthEncodedNotNull());
+  }
+
+  public String getTableAlias() {
+    buf.pos(stringPos[1]);
+    return buf.readString(buf.readIntLengthEncodedNotNull());
+  }
+
+  public String getTable() {
+    buf.pos(stringPos[useAliasAsName ? 1 : 2]);
+    return buf.readString(buf.readIntLengthEncodedNotNull());
+  }
+
+  public String getColumnAlias() {
+    buf.pos(stringPos[3]);
+    return buf.readString(buf.readIntLengthEncodedNotNull());
+  }
+
+  public String getColumnName() {
+    buf.pos(stringPos[4]);
+    return buf.readString(buf.readIntLengthEncodedNotNull());
+  }
+
+  public long getColumnLength() {
+    return columnLength;
+  }
+
+  public DataType getType() {
+    return dataType;
+  }
+
+  public byte getDecimals() {
+    return decimals;
+  }
+
+  public boolean isSigned() {
+    return (flags & ColumnFlags.UNSIGNED) == 0;
+  }
+
+  public int getDisplaySize() {
+    if (!isBinary()
+        && (dataType == DataType.VARCHAR
+            || dataType == DataType.JSON
+            || dataType == DataType.ENUM
+            || dataType == DataType.SET
+            || dataType == DataType.VARSTRING
+            || dataType == DataType.STRING
+            || dataType == DataType.BLOB
+            || dataType == DataType.TINYBLOB
+            || dataType == DataType.MEDIUMBLOB
+            || dataType == DataType.LONGBLOB)) {
+      Integer maxWidth = CharsetEncodingLength.maxCharlen.get(charset);
+      if (maxWidth != null) return (int) (columnLength / maxWidth);
+    }
+    return (int) columnLength;
+  }
+
+  public boolean isPrimaryKey() {
+    return (this.flags & ColumnFlags.PRIMARY_KEY) > 0;
+  }
+
+  public boolean isAutoIncrement() {
+    return (this.flags & ColumnFlags.AUTO_INCREMENT) > 0;
+  }
+
+  public boolean hasDefault() {
+    return (this.flags & ColumnFlags.NO_DEFAULT_VALUE_FLAG) == 0;
+  }
+
+  // doesn't use & 128 bit filter, because char binary and varchar binary are not binary (handle
+  // like string), but have the binary flag
+  public boolean isBinary() {
+    return charset == 63;
+  }
+
+  public int getFlags() {
+    return flags;
+  }
+
+  public String getExtTypeName() {
+    return extTypeName;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    ColumnDefinitionPacket that = (ColumnDefinitionPacket) o;
+    return charset == that.charset
+        && columnLength == that.columnLength
+        && dataType == that.dataType
+        && decimals == that.decimals
+        && flags == that.flags;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(charset, columnLength, dataType, decimals, flags);
+  }
+
+  public void useAliasAsName() {
+    useAliasAsName = true;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/server/ErrorPacket.java b/src/main/java/org/mariadb/jdbc/message/server/ErrorPacket.java
new file mode 100644
index 0000000..965e202
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/server/ErrorPacket.java
@@ -0,0 +1,82 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.server;
+
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.message.ServerMessage;
+import org.mariadb.jdbc.util.constants.ServerStatus;
+import org.mariadb.jdbc.util.log.Logger;
+import org.mariadb.jdbc.util.log.Loggers;
+
+/** Server Error Packet see https://mariadb.com/kb/en/err_packet/ */
+public final class ErrorPacket implements ServerMessage {
+  private static final Logger logger = Loggers.getLogger(ErrorPacket.class);
+  private final short errorCode;
+  private final String message;
+  private final String sqlState;
+
+  /**
+   * +--------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+   * +--------------------------------------------------+------------------+ | 17 00 00 01 FF 10 04
+   * 54 6F 6F 20 6D 61 6E 79 20 | .......Too many | | 63 6F 6E 6E 65 63 74 69 6F 6E 73 | connections
+   * | +--------------------------------------------------+------------------+
+   *
+   * @param buf error packet buffer
+   * @param context current context
+   */
+  public ErrorPacket(ReadableByteBuf buf, Context context) {
+    buf.skip();
+    this.errorCode = buf.readShort();
+    byte next = buf.getByte(buf.pos());
+    if (next == (byte) '#') {
+      buf.skip(); // skip '#'
+      this.sqlState = buf.readAscii(5);
+      this.message = buf.readStringEof();
+    } else {
+      // Pre-4.1 message, still can be output in newer versions (e.g. with 'Too many connections')
+      this.message = buf.readStringEof();
+      this.sqlState = "HY000";
+    }
+    if (logger.isWarnEnabled()) {
+      logger.warn("Error: {}-{}: {}", errorCode, sqlState, message);
+    }
+
+    // force current status to in transaction to ensure rollback/commit, since command may have
+    // issue a transaction
+    if (context != null) {
+      int serverStatus = context.getServerStatus();
+      serverStatus |= ServerStatus.IN_TRANSACTION;
+      context.setServerStatus(serverStatus);
+    }
+  }
+
+  /**
+   * Error code
+   *
+   * @return error code
+   */
+  public short getErrorCode() {
+    return errorCode;
+  }
+
+  /**
+   * Error message
+   *
+   * @return error message
+   */
+  public String getMessage() {
+    return message;
+  }
+
+  /**
+   * SQL State
+   *
+   * @return SQLState
+   */
+  public String getSqlState() {
+    return sqlState;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/server/InitialHandshakePacket.java b/src/main/java/org/mariadb/jdbc/message/server/InitialHandshakePacket.java
new file mode 100644
index 0000000..863a366
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/server/InitialHandshakePacket.java
@@ -0,0 +1,211 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.server;
+
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.client.ServerVersion;
+import org.mariadb.jdbc.message.ServerMessage;
+import org.mariadb.jdbc.message.server.util.ServerVersionUtility;
+import org.mariadb.jdbc.util.constants.Capabilities;
+
+/**
+ * Server initial handshake parser. see
+ * https://mariadb.com/kb/en/connection/#initial-handshake-packet
+ */
+public final class InitialHandshakePacket implements ServerMessage {
+
+  private static final String MARIADB_RPL_HACK_PREFIX = "5.5.5-";
+
+  private final long threadId;
+  private final byte[] seed;
+  private final long capabilities;
+  private final short defaultCollation;
+  private final short serverStatus;
+  private final String authenticationPluginType;
+  private final ServerVersion version;
+
+  /**
+   * parse result
+   *
+   * @param serverVersion server version
+   * @param threadId server thread id
+   * @param seed seed
+   * @param capabilities server capabilities
+   * @param defaultCollation default server collation
+   * @param serverStatus server status flags
+   * @param mariaDBServer is a mariadb server
+   * @param authenticationPluginType default authentication plugin type
+   */
+  private InitialHandshakePacket(
+      String serverVersion,
+      long threadId,
+      byte[] seed,
+      long capabilities,
+      short defaultCollation,
+      short serverStatus,
+      boolean mariaDBServer,
+      String authenticationPluginType) {
+
+    this.threadId = threadId;
+    this.seed = seed;
+    this.capabilities = capabilities;
+    this.defaultCollation = defaultCollation;
+    this.serverStatus = serverStatus;
+    this.authenticationPluginType = authenticationPluginType;
+    this.version = new ServerVersionUtility(serverVersion, mariaDBServer);
+  }
+
+  /**
+   * parsing packet
+   *
+   * @param reader packet reader
+   * @return Parsed packet
+   */
+  public static InitialHandshakePacket decode(ReadableByteBuf reader) {
+    byte protocolVersion = reader.readByte();
+    if (protocolVersion != 0x0a) {
+      throw new IllegalArgumentException(
+          String.format("Unexpected initial handshake protocol value [%s]", protocolVersion));
+    }
+
+    String serverVersion = reader.readStringNullEnd();
+    long threadId = reader.readInt();
+    final byte[] seed1 = new byte[8];
+    reader.readBytes(seed1);
+    reader.skip();
+    int serverCapabilities2FirstBytes = reader.readUnsignedShort();
+    short defaultCollation = reader.readUnsignedByte();
+    short serverStatus = reader.readShort();
+    int serverCapabilities4FirstBytes = serverCapabilities2FirstBytes + (reader.readShort() << 16);
+    int saltLength = 0;
+
+    if ((serverCapabilities4FirstBytes & Capabilities.PLUGIN_AUTH) != 0) {
+      saltLength = Math.max(12, reader.readByte() - 9);
+    } else {
+      reader.skip();
+    }
+    reader.skip(6);
+
+    // MariaDB additional capabilities.
+    // Filled only if MariaDB server 10.2+
+    long mariaDbAdditionalCapacities = reader.readInt();
+    byte[] seed;
+    if ((serverCapabilities4FirstBytes & Capabilities.SECURE_CONNECTION) != 0) {
+      final byte[] seed2;
+      if (saltLength > 0) {
+        seed2 = new byte[saltLength];
+        reader.readBytes(seed2);
+      } else {
+        seed2 = reader.readBytesNullEnd();
+      }
+      seed = new byte[seed1.length + seed2.length];
+      System.arraycopy(seed1, 0, seed, 0, seed1.length);
+      System.arraycopy(seed2, 0, seed, seed1.length, seed2.length);
+    } else {
+      seed = seed1;
+    }
+    reader.skip();
+
+    /*
+     * check for MariaDB 10.x replication hack , remove fake prefix if needed
+     *  (see comments about MARIADB_RPL_HACK_PREFIX)
+     */
+    boolean serverMariaDb;
+    if (serverVersion.startsWith(MARIADB_RPL_HACK_PREFIX)) {
+      serverMariaDb = true;
+      serverVersion = serverVersion.substring(MARIADB_RPL_HACK_PREFIX.length());
+    } else {
+      serverMariaDb = serverVersion.contains("MariaDB");
+    }
+
+    // since MariaDB 10.2
+    long serverCapabilities;
+    if ((serverCapabilities4FirstBytes & Capabilities.CLIENT_MYSQL) == 0) {
+      serverCapabilities =
+          (serverCapabilities4FirstBytes & 0xffffffffL) + (mariaDbAdditionalCapacities << 32);
+      serverMariaDb = true;
+    } else {
+      serverCapabilities = serverCapabilities4FirstBytes & 0xffffffffL;
+    }
+
+    String authenticationPluginType = null;
+    if ((serverCapabilities4FirstBytes & Capabilities.PLUGIN_AUTH) != 0) {
+      authenticationPluginType = reader.readStringNullEnd();
+    }
+
+    return new InitialHandshakePacket(
+        serverVersion,
+        threadId,
+        seed,
+        serverCapabilities,
+        defaultCollation,
+        serverStatus,
+        serverMariaDb,
+        authenticationPluginType);
+  }
+
+  /**
+   * Server Version object
+   *
+   * @return server version
+   */
+  public ServerVersion getVersion() {
+    return version;
+  }
+
+  /**
+   * Server thread id
+   *
+   * @return thread id
+   */
+  public long getThreadId() {
+    return threadId;
+  }
+
+  /**
+   * Seed for authentication plugin encryption
+   *
+   * @return seed
+   */
+  public byte[] getSeed() {
+    return seed;
+  }
+
+  /**
+   * Server capabilities
+   *
+   * @return server capabilities
+   */
+  public long getCapabilities() {
+    return capabilities;
+  }
+
+  /**
+   * Server default collation
+   *
+   * @return server default collation
+   */
+  public short getDefaultCollation() {
+    return defaultCollation;
+  }
+
+  /**
+   * Server status flags
+   *
+   * @return server status
+   */
+  public short getServerStatus() {
+    return serverStatus;
+  }
+
+  /**
+   * return authentication plugin type
+   *
+   * @return authentication plugin type
+   */
+  public String getAuthenticationPluginType() {
+    return authenticationPluginType;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/server/OkPacket.java b/src/main/java/org/mariadb/jdbc/message/server/OkPacket.java
new file mode 100644
index 0000000..1eff4c4
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/server/OkPacket.java
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.server;
+
+import org.mariadb.jdbc.client.Completion;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.util.constants.Capabilities;
+import org.mariadb.jdbc.util.constants.StateChange;
+import org.mariadb.jdbc.util.log.Logger;
+import org.mariadb.jdbc.util.log.Loggers;
+
+/** Ok packet parser see https://mariadb.com/kb/en/ok_packet/ */
+public class OkPacket implements Completion {
+  private static final Logger logger = Loggers.getLogger(OkPacket.class);
+
+  private final long affectedRows;
+  private final long lastInsertId;
+
+  /**
+   * Parser
+   *
+   * @param buf packet buffer
+   * @param context connection context
+   */
+  public OkPacket(ReadableByteBuf buf, Context context) {
+    buf.skip(); // ok header
+    this.affectedRows = buf.readLongLengthEncodedNotNull();
+    this.lastInsertId = buf.readLongLengthEncodedNotNull();
+    context.setServerStatus(buf.readUnsignedShort());
+    context.setWarning(buf.readUnsignedShort());
+
+    if (buf.readableBytes() > 0 && context.hasClientCapability(Capabilities.CLIENT_SESSION_TRACK)) {
+      buf.skip(buf.readIntLengthEncodedNotNull()); // skip info
+      while (buf.readableBytes() > 0) {
+        if (buf.readIntLengthEncodedNotNull() > 0) {
+          switch (buf.readByte()) {
+            case StateChange.SESSION_TRACK_SYSTEM_VARIABLES:
+              buf.readIntLengthEncodedNotNull();
+              String variable = buf.readString(buf.readIntLengthEncodedNotNull());
+              Integer len = buf.readLength();
+              String value = len == null ? null : buf.readString(len);
+              logger.debug("System variable change:  {} = {}", variable, value);
+              break;
+
+            case StateChange.SESSION_TRACK_SCHEMA:
+              buf.readIntLengthEncodedNotNull();
+              Integer dbLen = buf.readLength();
+              String database = dbLen == null ? null : buf.readString(dbLen);
+              context.setDatabase(database.isEmpty() ? null : database);
+              logger.debug("Database change: is '{}'", database);
+              break;
+
+            default:
+              buf.skip(buf.readIntLengthEncodedNotNull());
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * get affected rows
+   *
+   * @return affected rows
+   */
+  public long getAffectedRows() {
+    return affectedRows;
+  }
+
+  /**
+   * Get last auto generated insert id
+   *
+   * @return last insert id
+   */
+  public long getLastInsertId() {
+    return lastInsertId;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/server/PrepareResultPacket.java b/src/main/java/org/mariadb/jdbc/message/server/PrepareResultPacket.java
new file mode 100644
index 0000000..1e1b579
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/server/PrepareResultPacket.java
@@ -0,0 +1,145 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.server;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import org.mariadb.jdbc.ServerPreparedStatement;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.impl.StandardReadableByteBuf;
+import org.mariadb.jdbc.client.socket.Reader;
+import org.mariadb.jdbc.export.Prepare;
+import org.mariadb.jdbc.util.constants.Capabilities;
+import org.mariadb.jdbc.util.log.Logger;
+import org.mariadb.jdbc.util.log.Loggers;
+
+/** Prepare result packet See https://mariadb.com/kb/en/com_stmt_prepare/#COM_STMT_PREPARE_OK */
+public class PrepareResultPacket implements Completion, Prepare {
+  static final ColumnDecoder CONSTANT_PARAMETER;
+
+  static {
+    byte[] bytes =
+        new byte[] {
+          0x03,
+          0x64,
+          0x65,
+          0x66,
+          0x00,
+          0x00,
+          0x00,
+          0x01,
+          0x3F,
+          0x00,
+          0x00,
+          0x0C,
+          0x3F,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x06,
+          (byte) 0x80,
+          0x00,
+          0x00,
+          0x00,
+          0x00
+        };
+    CONSTANT_PARAMETER =
+        ColumnDecoder.decode(new StandardReadableByteBuf(bytes, bytes.length), true);
+  }
+
+  private static final Logger logger = Loggers.getLogger(PrepareResultPacket.class);
+  private final ColumnDecoder[] parameters;
+  private ColumnDecoder[] columns;
+
+  /** prepare statement id */
+  protected int statementId;
+
+  /**
+   * Prepare packet constructor (parsing)
+   *
+   * @param buffer packet buffer
+   * @param reader packet reader
+   * @param context connection context
+   * @throws IOException if socket exception occurs
+   */
+  public PrepareResultPacket(ReadableByteBuf buffer, Reader reader, Context context)
+      throws IOException {
+    boolean trace = logger.isTraceEnabled();
+    buffer.readByte(); /* skip COM_STMT_PREPARE_OK */
+    this.statementId = buffer.readInt();
+    final int numColumns = buffer.readUnsignedShort();
+    final int numParams = buffer.readUnsignedShort();
+    this.parameters = new ColumnDecoder[numParams];
+    this.columns = new ColumnDecoder[numColumns];
+
+    if (numParams > 0) {
+      for (int i = 0; i < numParams; i++) {
+        // skipping packet, since there is no metadata information.
+        // might change when https://jira.mariadb.org/browse/MDEV-15031 is done
+        parameters[i] = CONSTANT_PARAMETER;
+        reader.skipPacket();
+      }
+      if (!context.isEofDeprecated()) {
+        reader.skipPacket();
+      }
+    }
+    if (numColumns > 0) {
+      for (int i = 0; i < numColumns; i++) {
+        columns[i] =
+            ColumnDecoder.decode(
+                new StandardReadableByteBuf(reader.readPacket(trace)),
+                context.hasClientCapability(Capabilities.EXTENDED_TYPE_INFO));
+      }
+      if (!context.isEofDeprecated()) {
+        reader.skipPacket();
+      }
+    }
+  }
+
+  /**
+   * Close prepare packet
+   *
+   * @param con current connection
+   * @throws SQLException if exception occurs
+   */
+  public void close(Client con) throws SQLException {
+    con.closePrepare(this);
+  }
+
+  /**
+   * Decrement use of prepare packet, so closing it if last used
+   *
+   * @param con connection
+   * @param preparedStatement current prepared statement that was using prepare object
+   * @throws SQLException if exception occurs
+   */
+  public void decrementUse(Client con, ServerPreparedStatement preparedStatement)
+      throws SQLException {
+    close(con);
+  }
+
+  /**
+   * Get statement id
+   *
+   * @return statement id
+   */
+  public int getStatementId() {
+    return statementId;
+  }
+
+  public ColumnDecoder[] getParameters() {
+    return parameters;
+  }
+
+  public ColumnDecoder[] getColumns() {
+    return columns;
+  }
+
+  public void setColumns(ColumnDecoder[] columns) {
+    this.columns = columns;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/message/server/util/ServerVersionUtility.java b/src/main/java/org/mariadb/jdbc/message/server/util/ServerVersionUtility.java
new file mode 100644
index 0000000..8e601b0
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/message/server/util/ServerVersionUtility.java
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.message.server.util;
+
+import org.mariadb.jdbc.client.ServerVersion;
+import org.mariadb.jdbc.util.Version;
+
+/** Server version utility */
+public final class ServerVersionUtility extends Version implements ServerVersion {
+
+  private final boolean mariaDBServer;
+
+  /**
+   * Constructor
+   *
+   * @param serverVersion server version string
+   * @param mariaDBServer is server mariadb
+   */
+  public ServerVersionUtility(String serverVersion, boolean mariaDBServer) {
+    super(serverVersion);
+    this.mariaDBServer = mariaDBServer;
+  }
+
+  /**
+   * Is server mariadb
+   *
+   * @return true if server is a MariaDB server
+   */
+  public boolean isMariaDBServer() {
+    return mariaDBServer;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/AuthenticationPlugin.java b/src/main/java/org/mariadb/jdbc/plugin/AuthenticationPlugin.java
new file mode 100644
index 0000000..a870016
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/AuthenticationPlugin.java
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.client.socket.Reader;
+import org.mariadb.jdbc.client.socket.Writer;
+
+/** Authentication plugin descriptor */
+public interface AuthenticationPlugin {
+
+  /**
+   * Authentication plugin type.
+   *
+   * @return authentication plugin type. ex: mysql_native_password
+   */
+  String type();
+
+  /**
+   * Plugin initialization.
+   *
+   * @param authenticationData authentication data (password/token)
+   * @param seed server provided seed
+   * @param conf Connection options
+   */
+  void initialize(String authenticationData, byte[] seed, Configuration conf);
+
+  /**
+   * Process plugin authentication.
+   *
+   * @param encoder out stream
+   * @param decoder in stream
+   * @param context connection context
+   * @return response packet
+   * @throws IOException if socket error
+   * @throws SQLException if plugin exception
+   */
+  ReadableByteBuf process(Writer encoder, Reader decoder, Context context)
+      throws IOException, SQLException;
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/Codec.java b/src/main/java/org/mariadb/jdbc/plugin/Codec.java
new file mode 100644
index 0000000..8e991d8
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/Codec.java
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.sql.SQLException;
+import java.util.Calendar;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.client.socket.Writer;
+
+/**
+ * Codec interface, to describe how a certain type of data must be encoded / decoded
+ *
+ * @param <T> java type supported
+ */
+public interface Codec<T> {
+
+  /**
+   * Codec native type
+   *
+   * @return code native return type
+   */
+  String className();
+
+  /**
+   * If codec can decode this a server datatype to a java class type
+   *
+   * @param column server datatype
+   * @param type java return class
+   * @return true if codec can decode it
+   */
+  boolean canDecode(ColumnDecoder column, Class<?> type);
+
+  /**
+   * Can Codec encode the java object type
+   *
+   * @param value java object type
+   * @return true if codec can encode java type
+   */
+  boolean canEncode(Object value);
+
+  /**
+   * Decode from a mysql packet text encoded a value to codec java type
+   *
+   * @param buffer mysql packet buffer
+   * @param length encoded value length
+   * @param column server column metadata
+   * @param cal calendar
+   * @return decoded value
+   * @throws SQLDataException if unexpected error occurs during decoding
+   */
+  T decodeText(
+      final ReadableByteBuf buffer,
+      final int length,
+      final ColumnDecoder column,
+      final Calendar cal)
+      throws SQLDataException;
+
+  /**
+   * Decode from a mysql packet binary encoded a value to codec java type
+   *
+   * @param buffer mysql packet buffer
+   * @param length encoded value length
+   * @param column server column metadata
+   * @param cal calendar
+   * @return decoded value
+   * @throws SQLDataException if unexpected error occurs during decoding
+   */
+  T decodeBinary(
+      final ReadableByteBuf buffer,
+      final int length,
+      final ColumnDecoder column,
+      final Calendar cal)
+      throws SQLDataException;
+
+  /**
+   * Text encode value to writer
+   *
+   * @param encoder writer
+   * @param context connection context
+   * @param value value to encode
+   * @param cal calendar
+   * @param length maximum value length
+   * @throws IOException if any socket error occurs
+   * @throws SQLException if encoding error occurs
+   */
+  void encodeText(Writer encoder, Context context, Object value, Calendar cal, Long length)
+      throws IOException, SQLException;
+
+  /**
+   * Binary encode value to writer
+   *
+   * @param encoder writer
+   * @param value value to encode
+   * @param cal calendar
+   * @param length maximum value length
+   * @throws IOException if any socket error occurs
+   * @throws SQLException if encoding error occurs
+   */
+  void encodeBinary(Writer encoder, Object value, Calendar cal, Long length)
+      throws IOException, SQLException;
+
+  /**
+   * Indicate if can encode long data
+   *
+   * @return true if possible
+   */
+  default boolean canEncodeLongData() {
+    return false;
+  }
+
+  /**
+   * binary encoding value to a long data packet
+   *
+   * @param encoder writer
+   * @param value value to encode
+   * @param length maximum length value
+   * @throws IOException if any socket error occurs
+   * @throws SQLException if encoding error occurs
+   */
+  default void encodeLongData(Writer encoder, T value, Long length)
+      throws IOException, SQLException {
+    throw new SQLException("Data is not supposed to be send in COM_STMT_LONG_DATA");
+  }
+
+  /**
+   * binary encoding value to a byte[]
+   *
+   * @param value value to encode
+   * @param length maximum length value
+   * @return encoded value
+   * @throws IOException if any socket error occurs
+   * @throws SQLException if encoding error occurs
+   */
+  default byte[] encodeData(T value, Long length) throws IOException, SQLException {
+    throw new SQLException("Data is not supposed to be send in COM_STMT_LONG_DATA");
+  }
+
+  /**
+   * Return server encoding data type
+   *
+   * @return server encoding data type
+   */
+  int getBinaryEncodeType();
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/Credential.java b/src/main/java/org/mariadb/jdbc/plugin/Credential.java
new file mode 100644
index 0000000..9e07e9a
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/Credential.java
@@ -0,0 +1,40 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin;
+
+/** basic Credential (couple of user/password) */
+public class Credential {
+  private final String password;
+  private final String user;
+
+  /**
+   * Constructor of basic credential
+   *
+   * @param user user
+   * @param password password
+   */
+  public Credential(String user, String password) {
+    this.user = user;
+    this.password = password;
+  }
+
+  /**
+   * Get user
+   *
+   * @return user
+   */
+  public String getUser() {
+    return user;
+  }
+
+  /**
+   * Get password
+   *
+   * @return password
+   */
+  public String getPassword() {
+    return password;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/CredentialPlugin.java b/src/main/java/org/mariadb/jdbc/plugin/CredentialPlugin.java
new file mode 100644
index 0000000..e80d20b
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/CredentialPlugin.java
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin;
+
+import java.sql.SQLException;
+import java.util.function.Supplier;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.HostAddress;
+
+/** Credential plugin definition, to permit providing Credential to server */
+public interface CredentialPlugin extends Supplier<Credential> {
+  /**
+   * credential identifier
+   *
+   * @return type
+   */
+  String type();
+
+  /**
+   * Indicate if plugin must throw an error if SSL is not enabled
+   *
+   * @return if ssl is required
+   */
+  default boolean mustUseSsl() {
+    return false;
+  }
+
+  /**
+   * Indicate authentication plugin type to use for authentication
+   *
+   * @return plugin type to use for authentication, or null for default
+   */
+  default String defaultAuthenticationPluginType() {
+    return null;
+  }
+
+  /**
+   * Permit initializing plugin if overridden
+   *
+   * @param conf configuration
+   * @param userName user
+   * @param hostAddress host information
+   * @return credential plugin
+   * @throws SQLException if any error occurs
+   */
+  default CredentialPlugin initialize(Configuration conf, String userName, HostAddress hostAddress)
+      throws SQLException {
+    return this;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/TlsSocketPlugin.java b/src/main/java/org/mariadb/jdbc/plugin/TlsSocketPlugin.java
new file mode 100644
index 0000000..6b5abaa
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/TlsSocketPlugin.java
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.sql.SQLException;
+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.export.ExceptionFactory;
+
+/** TLS Socket interface plugin */
+public interface TlsSocketPlugin {
+
+  /**
+   * plugin type.
+   *
+   * @return plugin type
+   */
+  String type();
+
+  /**
+   * Get socket factory.
+   *
+   * @param conf connection string option. Non-standard option are stored in `nonMappedOptions` if
+   *     any specific option is needed.
+   * @param exceptionFactory exception handler
+   * @return custom SSL socket factory
+   * @throws SQLException if socket factory configuration failed.
+   */
+  SSLSocketFactory getSocketFactory(Configuration conf, ExceptionFactory exceptionFactory)
+      throws SQLException;
+
+  /**
+   * Returns a socket layered over an existing socket negotiating the use of SSL over an existing
+   * socket.
+   *
+   * @param socket existing socket
+   * @param sslSocketFactory SSL socket factory
+   * @return SSL socket
+   * @throws IOException if any socket error occurs.
+   */
+  default SSLSocket createSocket(Socket socket, SSLSocketFactory sslSocketFactory)
+      throws IOException {
+    return (SSLSocket)
+        sslSocketFactory.createSocket(
+            socket,
+            socket.getInetAddress() == null ? null : socket.getInetAddress().getHostAddress(),
+            socket.getPort(),
+            true);
+  }
+
+  /**
+   * Host name verifier implementation.
+   *
+   * @param host hostname
+   * @param sslSession ssl session
+   * @param serverThreadId current server threadId
+   * @throws SSLException if verification fail
+   */
+  void verify(String host, SSLSession sslSession, long serverThreadId) throws SSLException;
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/AuthenticationPluginLoader.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/AuthenticationPluginLoader.java
new file mode 100644
index 0000000..0ac349a
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/AuthenticationPluginLoader.java
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.authentication;
+
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.ServiceLoader;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.Driver;
+import org.mariadb.jdbc.plugin.AuthenticationPlugin;
+
+/** permit loading authentication plugins */
+public final class AuthenticationPluginLoader {
+
+  /**
+   * Get authentication plugin from type String. Customs authentication plugin can be added
+   * implementing AuthenticationPlugin and registering new type in resources services.
+   *
+   * @param type authentication plugin type
+   * @param conf current configuration
+   * @return Authentication plugin corresponding to type
+   * @throws SQLException if no authentication plugin in classpath have indicated type
+   */
+  public static AuthenticationPlugin get(String type, Configuration conf) throws SQLException {
+
+    ServiceLoader<AuthenticationPlugin> loader =
+        ServiceLoader.load(AuthenticationPlugin.class, Driver.class.getClassLoader());
+
+    String[] authList = (conf.restrictedAuth() != null) ? conf.restrictedAuth().split(",") : null;
+
+    for (AuthenticationPlugin implClass : loader) {
+      if (type.equals(implClass.type())) {
+        if (conf.restrictedAuth() == null || Arrays.stream(authList).anyMatch(type::contains)) {
+          return implClass;
+        } else {
+          throw new SQLException(
+              String.format(
+                  "Client restrict authentication plugin to a limited set of authentication plugin"
+                      + " and doesn't permit requested plugin ('%s'). Current list is"
+                      + " `restrictedAuth=%s`",
+                  type, conf.restrictedAuth()),
+              "08004",
+              1251);
+        }
+      }
+    }
+    throw new SQLException(
+        "Client does not support authentication protocol requested by server. "
+            + "plugin type was = '"
+            + type
+            + "'",
+        "08004",
+        1251);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/addon/ClearPasswordPlugin.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/addon/ClearPasswordPlugin.java
new file mode 100644
index 0000000..f24048b
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/addon/ClearPasswordPlugin.java
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.authentication.addon;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.client.socket.Reader;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.AuthenticationPlugin;
+
+/** Clear password plugin. */
+public class ClearPasswordPlugin implements AuthenticationPlugin {
+  /** plugin name */
+  public static final String TYPE = "mysql_clear_password";
+
+  private String authenticationData;
+
+  @Override
+  public String type() {
+    return TYPE;
+  }
+
+  public void initialize(String authenticationData, byte[] authData, Configuration conf) {
+    this.authenticationData = authenticationData;
+  }
+
+  /**
+   * Send password in clear text to server.
+   *
+   * @param out out stream
+   * @param in in stream
+   * @param context context
+   * @return response packet
+   * @throws IOException if socket error
+   */
+  public ReadableByteBuf process(Writer out, Reader in, Context context) throws IOException {
+    if (authenticationData == null) {
+      out.writeEmptyPacket();
+    } else {
+      byte[] bytePwd = authenticationData.getBytes(StandardCharsets.UTF_8);
+      out.writeBytes(bytePwd);
+      out.writeByte(0);
+      out.flush();
+    }
+
+    return in.readReusablePacket();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/addon/SendGssApiAuthPacket.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/addon/SendGssApiAuthPacket.java
new file mode 100644
index 0000000..ac44788
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/addon/SendGssApiAuthPacket.java
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.authentication.addon;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.client.impl.StandardReadableByteBuf;
+import org.mariadb.jdbc.client.socket.Reader;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.AuthenticationPlugin;
+import org.mariadb.jdbc.plugin.authentication.addon.gssapi.GssUtility;
+import org.mariadb.jdbc.plugin.authentication.addon.gssapi.GssapiAuth;
+import org.mariadb.jdbc.plugin.authentication.addon.gssapi.StandardGssapiAuthentication;
+
+/** GSSAPI plugin */
+public class SendGssApiAuthPacket implements AuthenticationPlugin {
+
+  private static final GssapiAuth gssapiAuth;
+
+  static {
+    GssapiAuth init;
+    try {
+      init = GssUtility.getAuthenticationMethod();
+    } catch (Throwable t) {
+      init = new StandardGssapiAuthentication();
+    }
+    gssapiAuth = init;
+  }
+
+  private byte[] seed;
+  private String optionServicePrincipalName;
+
+  @Override
+  public String type() {
+    return "auth_gssapi_client";
+  }
+
+  /**
+   * Initialization.
+   *
+   * @param authenticationData authentication data (password/token)
+   * @param seed server provided seed
+   * @param conf Connection string options
+   */
+  public void initialize(String authenticationData, byte[] seed, Configuration conf) {
+    this.seed = seed;
+    this.optionServicePrincipalName = conf.servicePrincipalName();
+  }
+
+  /**
+   * Process gssapi plugin authentication. see
+   * https://mariadb.com/kb/en/library/authentication-plugin-gssapi/
+   *
+   * @param out out stream
+   * @param in in stream
+   * @param context context
+   * @return response packet
+   * @throws IOException if socket error
+   * @throws SQLException if plugin exception
+   */
+  public ReadableByteBuf process(Writer out, Reader in, Context context)
+      throws IOException, SQLException {
+    ReadableByteBuf buf = new StandardReadableByteBuf(seed, seed.length);
+
+    final String serverSpn = buf.readStringNullEnd();
+    // using provided connection string SPN if set, or if not, using to server information
+    final String servicePrincipalName =
+        (optionServicePrincipalName != null) ? optionServicePrincipalName : serverSpn;
+    String mechanisms = buf.readStringNullEnd();
+    if (mechanisms.isEmpty()) {
+      mechanisms = "Kerberos";
+    }
+
+    gssapiAuth.authenticate(out, in, servicePrincipalName, mechanisms);
+
+    return in.readReusablePacket();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/addon/gssapi/GssUtility.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/addon/gssapi/GssUtility.java
new file mode 100644
index 0000000..1b6d3f5
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/addon/gssapi/GssUtility.java
@@ -0,0 +1,35 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.authentication.addon.gssapi;
+
+import com.sun.jna.Platform;
+
+/** GSSAPI utility */
+public class GssUtility {
+
+  /**
+   * Get authentication method according to classpath. Windows native authentication is using
+   * Waffle-jna.
+   *
+   * @return authentication method
+   */
+  public static GssapiAuth getAuthenticationMethod() {
+    try {
+      // Waffle-jna has jna as dependency, so if not available on classpath, just use standard
+      // authentication
+      if (Platform.isWindows()) {
+        try {
+          Class.forName("waffle.windows.auth.impl.WindowsAuthProviderImpl");
+          return new WindowsNativeSspiAuthentication();
+        } catch (ClassNotFoundException cle) {
+          // waffle not in the classpath
+        }
+      }
+    } catch (Throwable cle) {
+      // jna jar's are not in classpath
+    }
+    return new StandardGssapiAuthentication();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/addon/gssapi/GssapiAuth.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/addon/gssapi/GssapiAuth.java
new file mode 100644
index 0000000..e6e906c
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/addon/gssapi/GssapiAuth.java
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.authentication.addon.gssapi;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import org.mariadb.jdbc.client.socket.Reader;
+import org.mariadb.jdbc.client.socket.Writer;
+
+/** GSSAPI interface */
+public interface GssapiAuth {
+
+  /**
+   * Authenticate
+   *
+   * @param writer socket writer
+   * @param in socket reader
+   * @param servicePrincipalName SPN
+   * @param mechanisms mechanisms
+   * @throws IOException if any socket error occurs
+   * @throws SQLException for any other type of errors
+   */
+  void authenticate(Writer writer, Reader in, String servicePrincipalName, String mechanisms)
+      throws IOException, SQLException;
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/addon/gssapi/StandardGssapiAuthentication.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/addon/gssapi/StandardGssapiAuthentication.java
new file mode 100644
index 0000000..0b32a1a
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/addon/gssapi/StandardGssapiAuthentication.java
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.authentication.addon.gssapi;
+
+import java.io.*;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.sql.SQLException;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import org.ietf.jgss.*;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.client.socket.Reader;
+import org.mariadb.jdbc.client.socket.Writer;
+
+/** Basic GSSAPI implementation if waffle is not on classpath */
+public class StandardGssapiAuthentication implements GssapiAuth {
+
+  /**
+   * Process default GSS plugin authentication.
+   *
+   * @param out out stream
+   * @param in in stream
+   * @param servicePrincipalName service principal name
+   * @param mechanisms gssapi mechanism
+   * @throws IOException if socket error
+   * @throws SQLException in any Exception occur
+   */
+  public void authenticate(
+      final Writer out, final Reader in, final String servicePrincipalName, String mechanisms)
+      throws SQLException, IOException {
+
+    if ("".equals(servicePrincipalName)) {
+      throw new SQLException(
+          "No principal name defined on server. Please set server variable"
+              + " \"gssapi-principal-name\" or set option \"servicePrincipalName\"",
+          "28000");
+    }
+
+    if (System.getProperty("java.security.auth.login.config") == null) {
+      final File jaasConfFile;
+      try {
+        jaasConfFile = File.createTempFile("jaas.conf", null);
+        try (PrintStream bos = new PrintStream(new FileOutputStream(jaasConfFile))) {
+          bos.print(
+              "Krb5ConnectorContext {\n"
+                  + "com.sun.security.auth.module.Krb5LoginModule required "
+                  + "useTicketCache=true "
+                  + "debug=true "
+                  + "renewTGT=true "
+                  + "doNotPrompt=true; };");
+        }
+        jaasConfFile.deleteOnExit();
+      } catch (final IOException ex) {
+        throw new UncheckedIOException(ex);
+      }
+
+      System.setProperty("java.security.auth.login.config", jaasConfFile.getCanonicalPath());
+    }
+    try {
+      LoginContext loginContext = new LoginContext("Krb5ConnectorContext");
+      // attempt authentication
+      loginContext.login();
+      final Subject mySubject = loginContext.getSubject();
+      if (!mySubject.getPrincipals().isEmpty()) {
+        try {
+          PrivilegedExceptionAction<Void> action =
+              () -> {
+                try {
+                  Oid krb5Mechanism = new Oid("1.2.840.113554.1.2.2");
+
+                  GSSManager manager = GSSManager.getInstance();
+                  GSSName peerName = manager.createName(servicePrincipalName, GSSName.NT_USER_NAME);
+                  GSSContext context =
+                      manager.createContext(
+                          peerName, krb5Mechanism, null, GSSContext.DEFAULT_LIFETIME);
+                  context.requestMutualAuth(true);
+
+                  byte[] inToken = new byte[0];
+                  byte[] outToken;
+
+                  while (true) {
+                    outToken = context.initSecContext(inToken, 0, inToken.length);
+
+                    // Send a token to the peer if one was generated by acceptSecContext
+                    if (outToken != null) {
+                      out.writeBytes(outToken);
+                      out.flush();
+                    }
+                    if (context.isEstablished()) {
+                      break;
+                    }
+                    ReadableByteBuf buf = in.readReusablePacket();
+                    inToken = new byte[buf.readableBytes()];
+                    buf.readBytes(inToken);
+                  }
+
+                } catch (GSSException le) {
+                  throw new SQLException("GSS-API authentication exception", "28000", 1045, le);
+                }
+                return null;
+              };
+          Subject.doAs(mySubject, action);
+        } catch (PrivilegedActionException exception) {
+          throw new SQLException("GSS-API authentication exception", "28000", 1045, exception);
+        }
+      } else {
+        throw new SQLException(
+            "GSS-API authentication exception : no credential cache not found.", "28000", 1045);
+      }
+
+    } catch (LoginException le) {
+      throw new SQLException("GSS-API authentication exception", "28000", 1045, le);
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/addon/gssapi/WindowsNativeSspiAuthentication.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/addon/gssapi/WindowsNativeSspiAuthentication.java
new file mode 100644
index 0000000..6d9c8d8
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/addon/gssapi/WindowsNativeSspiAuthentication.java
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.authentication.addon.gssapi;
+
+import com.sun.jna.platform.win32.Sspi;
+import com.sun.jna.platform.win32.SspiUtil;
+import java.io.IOException;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.client.socket.Reader;
+import org.mariadb.jdbc.client.socket.Writer;
+import waffle.windows.auth.IWindowsSecurityContext;
+import waffle.windows.auth.impl.WindowsSecurityContextImpl;
+
+/** Waffle windows GSSAPI implementation */
+public class WindowsNativeSspiAuthentication implements GssapiAuth {
+
+  /**
+   * Process native windows GSS plugin authentication.
+   *
+   * @param out out stream
+   * @param in in stream
+   * @param servicePrincipalName principal name
+   * @param mechanisms gssapi mechanism
+   * @throws IOException if socket error
+   */
+  public void authenticate(
+      final Writer out, final Reader in, final String servicePrincipalName, final String mechanisms)
+      throws IOException {
+
+    // initialize a security context on the client
+    IWindowsSecurityContext clientContext =
+        WindowsSecurityContextImpl.getCurrent(mechanisms, servicePrincipalName);
+
+    do {
+
+      // Step 1: send token to server
+      byte[] tokenForTheServerOnTheClient = clientContext.getToken();
+      if (tokenForTheServerOnTheClient != null && tokenForTheServerOnTheClient.length > 0) {
+        out.writeBytes(tokenForTheServerOnTheClient);
+        out.flush();
+      }
+      if (!clientContext.isContinue()) {
+        break;
+      }
+
+      // Step 2: read server response token
+      ReadableByteBuf buf = in.readReusablePacket();
+      byte[] tokenForTheClientOnTheServer = new byte[buf.readableBytes()];
+      buf.readBytes(tokenForTheClientOnTheServer);
+      Sspi.SecBufferDesc continueToken =
+          new SspiUtil.ManagedSecBufferDesc(Sspi.SECBUFFER_TOKEN, tokenForTheClientOnTheServer);
+      clientContext.initialize(clientContext.getHandle(), continueToken, servicePrincipalName);
+
+    } while (true);
+
+    clientContext.dispose();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/CachingSha2PasswordPlugin.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/CachingSha2PasswordPlugin.java
new file mode 100644
index 0000000..75fc123
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/CachingSha2PasswordPlugin.java
@@ -0,0 +1,261 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.authentication.standard;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.KeyFactory;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.spec.X509EncodedKeySpec;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.Base64;
+import javax.crypto.Cipher;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.client.socket.Reader;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.export.SslMode;
+import org.mariadb.jdbc.message.client.AuthMoreRawPacket;
+import org.mariadb.jdbc.message.server.AuthSwitchPacket;
+import org.mariadb.jdbc.plugin.AuthenticationPlugin;
+
+/** Mysql caching sha2 password plugin */
+public class CachingSha2PasswordPlugin implements AuthenticationPlugin {
+
+  /** plugin name */
+  public static final String TYPE = "caching_sha2_password";
+
+  private String authenticationData;
+  private byte[] seed;
+  private Configuration conf;
+
+  /**
+   * Send an SHA-2 encrypted password. encryption XOR(SHA256(password), SHA256(seed,
+   * SHA256(SHA256(password))))
+   *
+   * @param password password
+   * @param seed seed
+   * @return encrypted pwd
+   */
+  public static byte[] sha256encryptPassword(final CharSequence password, final byte[] seed) {
+    if (password == null) return new byte[0];
+    byte[] truncatedSeed = AuthSwitchPacket.getTruncatedSeed(seed);
+    try {
+      final MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
+      byte[] bytePwd = password.toString().getBytes(StandardCharsets.UTF_8);
+
+      final byte[] stage1 = messageDigest.digest(bytePwd);
+      messageDigest.reset();
+
+      final byte[] stage2 = messageDigest.digest(stage1);
+      messageDigest.reset();
+
+      messageDigest.update(stage2);
+      messageDigest.update(truncatedSeed);
+
+      final byte[] digest = messageDigest.digest();
+      final byte[] returnBytes = new byte[digest.length];
+      for (int i = 0; i < digest.length; i++) {
+        returnBytes[i] = (byte) (stage1[i] ^ digest[i]);
+      }
+      return returnBytes;
+    } catch (NoSuchAlgorithmException e) {
+      throw new RuntimeException("Could not use SHA-256, failing", e);
+    }
+  }
+
+  @Override
+  public String type() {
+    return TYPE;
+  }
+
+  /**
+   * Initialized data.
+   *
+   * @param authenticationData authentication data (password/token)
+   * @param seed server provided seed
+   * @param conf Connection string options
+   */
+  public void initialize(String authenticationData, byte[] seed, Configuration conf) {
+    this.seed = seed;
+    this.authenticationData = authenticationData;
+    this.conf = conf;
+  }
+
+  /**
+   * Process native password plugin authentication. see
+   * https://mariadb.com/kb/en/library/authentication-plugin-mysql_native_password/
+   *
+   * @param out out stream
+   * @param in in stream
+   * @param context connection context
+   * @return response packet
+   * @throws IOException if socket error
+   */
+  public ReadableByteBuf process(Writer out, Reader in, Context context)
+      throws IOException, SQLException {
+    byte[] fastCryptPwd = sha256encryptPassword(authenticationData, seed);
+    new AuthMoreRawPacket(fastCryptPwd).encode(out, context);
+
+    ReadableByteBuf buf = in.readReusablePacket();
+
+    switch (buf.getByte()) {
+      case (byte) 0x00:
+      case (byte) 0xFF:
+        // success or error
+        return buf;
+
+      default:
+        // fast authentication result
+        byte[] authResult = new byte[buf.readIntLengthEncodedNotNull()];
+        buf.readBytes(authResult);
+        switch (authResult[0]) {
+          case 3:
+            return in.readReusablePacket();
+          case 4:
+            if (conf.sslMode() != SslMode.DISABLE) {
+              // send clear password
+
+              byte[] bytePwd = authenticationData.getBytes();
+              byte[] nullEndedValue = new byte[bytePwd.length + 1];
+              System.arraycopy(bytePwd, 0, nullEndedValue, 0, bytePwd.length);
+              new AuthMoreRawPacket(nullEndedValue).encode(out, context);
+              out.flush();
+
+            } else {
+              // retrieve public key from configuration or from server
+              PublicKey publicKey;
+              if (conf.serverRsaPublicKeyFile() != null) {
+                if (conf.serverRsaPublicKeyFile().contains("BEGIN PUBLIC KEY")) {
+                  publicKey = generatePublicKey(conf.serverRsaPublicKeyFile().getBytes());
+                } else {
+                  publicKey = readPublicKeyFromFile(conf.serverRsaPublicKeyFile());
+                }
+              } else {
+                // read public key from socket
+                if (!conf.allowPublicKeyRetrieval()) {
+                  throw new SQLException(
+                      "RSA public key is not available client side (option serverRsaPublicKeyFile"
+                          + " not set)",
+                      "S1009");
+                }
+
+                // ask public Key Retrieval
+                out.writeByte(2);
+                out.flush();
+
+                buf = in.readReusablePacket();
+                switch (buf.getByte(0)) {
+                  case (byte) 0xFF:
+                  case (byte) 0xFE:
+                    return buf;
+
+                  default:
+                    // AuthMoreData packet
+                    buf.skip();
+                    byte[] authMoreData = new byte[buf.readableBytes()];
+                    buf.readBytes(authMoreData);
+                    publicKey = generatePublicKey(authMoreData);
+                }
+              }
+
+              byte[] cipherBytes = encrypt(publicKey, authenticationData, seed);
+              out.writeBytes(cipherBytes);
+              out.flush();
+            }
+
+            return in.readReusablePacket();
+
+          default:
+            throw new SQLException(
+                "Protocol exchange error. Expect login success or RSA login request message",
+                "S1009");
+        }
+    }
+  }
+
+  /**
+   * Read public Key from file.
+   *
+   * @param serverRsaPublicKeyFile RSA public key file
+   * @return public key
+   * @throws SQLException if having an error reading file or file content is not a public key.
+   */
+  public static PublicKey readPublicKeyFromFile(String serverRsaPublicKeyFile) throws SQLException {
+    byte[] keyBytes;
+    try {
+      keyBytes = Files.readAllBytes(Paths.get(serverRsaPublicKeyFile));
+    } catch (IOException ex) {
+      throw new SQLException(
+          "Could not read server RSA public key from file : "
+              + "serverRsaPublicKeyFile="
+              + serverRsaPublicKeyFile,
+          "S1009",
+          ex);
+    }
+    return generatePublicKey(keyBytes);
+  }
+
+  /**
+   * Read public pem key from String.
+   *
+   * @param publicKeyBytes public key bytes value
+   * @return public key
+   * @throws SQLException if key cannot be parsed
+   */
+  public static PublicKey generatePublicKey(byte[] publicKeyBytes) throws SQLException {
+    try {
+      String publicKey =
+          new String(publicKeyBytes, StandardCharsets.US_ASCII)
+              .replaceAll("(-+BEGIN PUBLIC KEY-+\\r?\\n|\\n?-+END PUBLIC KEY-+\\r?\\n?)", "");
+
+      byte[] keyBytes = Base64.getMimeDecoder().decode(publicKey);
+      X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
+      KeyFactory kf = KeyFactory.getInstance("RSA");
+      return kf.generatePublic(spec);
+    } catch (Exception ex) {
+      throw new SQLException("Could read server RSA public key: " + ex.getMessage(), "S1009", ex);
+    }
+  }
+
+  /**
+   * Encode password with seed and public key.
+   *
+   * @param publicKey public key
+   * @param password password
+   * @param seed seed
+   * @return encoded password
+   * @throws SQLException if cannot encode password
+   */
+  public static byte[] encrypt(PublicKey publicKey, String password, byte[] seed)
+      throws SQLException {
+
+    byte[] correctedSeed = Arrays.copyOfRange(seed, 0, seed.length - 1);
+    byte[] bytePwd = password.getBytes(StandardCharsets.UTF_8);
+
+    byte[] nullFinishedPwd = Arrays.copyOf(bytePwd, bytePwd.length + 1);
+    byte[] xorBytes = new byte[nullFinishedPwd.length];
+    int seedLength = correctedSeed.length;
+
+    for (int i = 0; i < xorBytes.length; i++) {
+      xorBytes[i] = (byte) (nullFinishedPwd[i] ^ correctedSeed[i % seedLength]);
+    }
+
+    try {
+      Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
+      cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+      return cipher.doFinal(xorBytes);
+    } catch (Exception ex) {
+      throw new SQLException(
+          "Error encoding password with public key : " + ex.getMessage(), "S1009", ex);
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/Ed25519PasswordPlugin.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/Ed25519PasswordPlugin.java
new file mode 100644
index 0000000..735921e
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/Ed25519PasswordPlugin.java
@@ -0,0 +1,124 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.authentication.standard;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.sql.SQLException;
+import java.util.Arrays;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.client.socket.Reader;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.AuthenticationPlugin;
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.math.GroupElement;
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.math.ed25519.Ed25519ScalarOps;
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.spec.EdDSANamedCurveTable;
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.spec.EdDSAParameterSpec;
+
+/** ED25519 password plugin */
+public class Ed25519PasswordPlugin implements AuthenticationPlugin {
+
+  private String authenticationData;
+  private byte[] seed;
+
+  /**
+   * Sign password
+   *
+   * @param password password
+   * @param seed server seed
+   * @return encrypted value
+   * @throws SQLException if any error occurs
+   */
+  private static byte[] ed25519SignWithPassword(final String password, final byte[] seed)
+      throws SQLException {
+
+    try {
+      byte[] bytePwd = password.getBytes(StandardCharsets.UTF_8);
+
+      MessageDigest hash = MessageDigest.getInstance("SHA-512");
+
+      int mlen = seed.length;
+      final byte[] sm = new byte[64 + mlen];
+
+      byte[] az = hash.digest(bytePwd);
+      az[0] &= 248;
+      az[31] &= 63;
+      az[31] |= 64;
+
+      System.arraycopy(seed, 0, sm, 64, mlen);
+      System.arraycopy(az, 32, sm, 32, 32);
+
+      byte[] buff = Arrays.copyOfRange(sm, 32, 96);
+      hash.reset();
+      byte[] nonce = hash.digest(buff);
+
+      Ed25519ScalarOps scalar = new Ed25519ScalarOps();
+
+      EdDSAParameterSpec spec = EdDSANamedCurveTable.getByName("Ed25519");
+      GroupElement elementAvalue = spec.getB().scalarMultiply(az);
+      byte[] elementAarray = elementAvalue.toByteArray();
+      System.arraycopy(elementAarray, 0, sm, 32, elementAarray.length);
+
+      nonce = scalar.reduce(nonce);
+      GroupElement elementRvalue = spec.getB().scalarMultiply(nonce);
+      byte[] elementRarray = elementRvalue.toByteArray();
+      System.arraycopy(elementRarray, 0, sm, 0, elementRarray.length);
+
+      hash.reset();
+      byte[] hram = hash.digest(sm);
+      hram = scalar.reduce(hram);
+      byte[] tt = scalar.multiplyAndAdd(hram, az, nonce);
+      System.arraycopy(tt, 0, sm, 32, tt.length);
+
+      return Arrays.copyOfRange(sm, 0, 64);
+
+    } catch (NoSuchAlgorithmException e) {
+      throw new SQLException("Could not use SHA-512, failing", e);
+    }
+  }
+
+  @Override
+  public String type() {
+    return "client_ed25519";
+  }
+
+  /**
+   * Initialization.
+   *
+   * @param authenticationData authentication data (password/token)
+   * @param seed server provided seed
+   * @param conf Connection string options
+   */
+  public void initialize(String authenticationData, byte[] seed, Configuration conf) {
+    this.seed = seed;
+    this.authenticationData = authenticationData;
+  }
+
+  /**
+   * Process Ed25519 password plugin authentication. see
+   * https://mariadb.com/kb/en/library/authentication-plugin-ed25519/
+   *
+   * @param out out stream
+   * @param in in stream
+   * @param context connection context
+   * @return response packet
+   * @throws IOException if socket error
+   */
+  public ReadableByteBuf process(Writer out, Reader in, Context context)
+      throws SQLException, IOException {
+    if (authenticationData == null) {
+      out.writeEmptyPacket();
+    } else {
+      out.writeBytes(ed25519SignWithPassword(authenticationData, seed));
+      out.flush();
+    }
+
+    return in.readReusablePacket();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/NativePasswordPlugin.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/NativePasswordPlugin.java
new file mode 100644
index 0000000..75cfa04
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/NativePasswordPlugin.java
@@ -0,0 +1,113 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.authentication.standard;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.client.socket.Reader;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.message.server.AuthSwitchPacket;
+import org.mariadb.jdbc.plugin.AuthenticationPlugin;
+
+/** Native password implementation */
+public class NativePasswordPlugin implements AuthenticationPlugin {
+
+  /** plugin name */
+  public static final String TYPE = "mysql_native_password";
+
+  private String authenticationData;
+  private byte[] seed;
+
+  /**
+   * Encrypts a password.
+   *
+   * <p>protocol for authentication is like this:
+   *
+   * <ul>
+   *   <li>Server sends a random array of bytes (the seed)
+   *   <li>client makes a sha1 digest of the password
+   *   <li>client hashes the output of 2
+   *   <li>client digests the seed
+   *   <li>client updates the digest with the output from 3
+   *   <li>an xor of the output of 5 and 2 is sent to server
+   *   <li>server does the same thing and verifies that the scrambled passwords match
+   * </ul>
+   *
+   * @param password the password to encrypt
+   * @param seed the seed to use
+   * @return a scrambled password
+   */
+  public static byte[] encryptPassword(final CharSequence password, final byte[] seed) {
+    try {
+      if (password == null) return new byte[0];
+
+      final MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
+      byte[] bytePwd = password.toString().getBytes(StandardCharsets.UTF_8);
+
+      final byte[] stage1 = messageDigest.digest(bytePwd);
+      messageDigest.reset();
+
+      final byte[] stage2 = messageDigest.digest(stage1);
+      messageDigest.reset();
+
+      messageDigest.update(seed);
+      messageDigest.update(stage2);
+
+      final byte[] digest = messageDigest.digest();
+      final byte[] returnBytes = new byte[digest.length];
+      for (int i = 0; i < digest.length; i++) {
+        returnBytes[i] = (byte) (stage1[i] ^ digest[i]);
+      }
+      return returnBytes;
+
+    } catch (NoSuchAlgorithmException e) {
+      throw new RuntimeException("Could not use SHA-1, failing", e);
+    }
+  }
+
+  @Override
+  public String type() {
+    return TYPE;
+  }
+
+  /**
+   * Initialized data.
+   *
+   * @param authenticationData authentication data (password/token)
+   * @param seed server provided seed
+   * @param conf Connection string options
+   */
+  public void initialize(String authenticationData, byte[] seed, Configuration conf) {
+    this.seed = seed;
+    this.authenticationData = authenticationData;
+  }
+
+  /**
+   * Process native password plugin authentication. see
+   * https://mariadb.com/kb/en/library/authentication-plugin-mysql_native_password/
+   *
+   * @param out out stream
+   * @param in in stream
+   * @param context connection context
+   * @return response packet
+   * @throws IOException if socket error
+   */
+  public ReadableByteBuf process(Writer out, Reader in, Context context) throws IOException {
+    if (authenticationData == null) {
+      out.writeEmptyPacket();
+    } else {
+      byte[] truncatedSeed = AuthSwitchPacket.getTruncatedSeed(seed);
+      out.writeBytes(encryptPassword(authenticationData, truncatedSeed));
+      out.flush();
+    }
+
+    return in.readReusablePacket();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/SendPamAuthPacket.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/SendPamAuthPacket.java
new file mode 100644
index 0000000..e965fee
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/SendPamAuthPacket.java
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.authentication.standard;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.sql.SQLException;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.client.socket.Reader;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.AuthenticationPlugin;
+
+/**
+ * PAM (dialog) authentication plugin. This is a multi-step exchange password. If more than one
+ * step, passwordX (password2, password3, ...) options must be set.
+ */
+public class SendPamAuthPacket implements AuthenticationPlugin {
+
+  private String authenticationData;
+  private Configuration conf;
+  private int counter = 0;
+
+  @Override
+  public String type() {
+    return "dialog";
+  }
+
+  /**
+   * Initialization.
+   *
+   * @param authenticationData authentication data (password/token)
+   * @param seed server provided seed
+   * @param conf Connection string options
+   */
+  public void initialize(String authenticationData, byte[] seed, Configuration conf) {
+    this.authenticationData = authenticationData;
+    this.conf = conf;
+  }
+
+  /**
+   * Process PAM plugin authentication. see
+   * https://mariadb.com/kb/en/library/authentication-plugin-pam/
+   *
+   * @param out out stream
+   * @param in in stream
+   * @param context connection context
+   * @return response packet
+   * @throws IOException if socket error
+   */
+  public ReadableByteBuf process(Writer out, Reader in, Context context)
+      throws SQLException, IOException {
+
+    while (true) {
+      counter++;
+      String password;
+      if (counter == 1) {
+        password = authenticationData;
+      } else {
+        if (!conf.nonMappedOptions().containsKey("password" + counter)) {
+          throw new SQLException(
+              "PAM authentication request multiple passwords, but "
+                  + "'password"
+                  + counter
+                  + "' is not set");
+        }
+        password = (String) conf.nonMappedOptions().get("password" + counter);
+      }
+
+      byte[] bytePwd = password != null ? password.getBytes(StandardCharsets.UTF_8) : new byte[0];
+      out.writeBytes(bytePwd, 0, bytePwd.length);
+      out.writeByte(0);
+      out.flush();
+
+      ReadableByteBuf buf = in.readReusablePacket();
+
+      int type = buf.getUnsignedByte();
+
+      // PAM continue until finish.
+      if (type == 0xfe // Switch Request
+          || type == 0x00 // OK_Packet
+          || type == 0xff) { // ERR_Packet
+        return buf;
+      }
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/README b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/README
new file mode 100644
index 0000000..531e22d
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/README
@@ -0,0 +1,7 @@
+This plugin uses public domain ed25519 code by str4d (https://github.com/str4d/ed25519-java).
+It is "ref10" java implementation from the SUPERCOP ed25519:
+
+==============================
+MariaDB changes:
+- doesn't register ed25519 to global java providers
+- remove unused classes
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/Utils.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/Utils.java
new file mode 100644
index 0000000..650e925
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/Utils.java
@@ -0,0 +1,96 @@
+package org.mariadb.jdbc.plugin.authentication.standard.ed25519;
+
+/**
+ * Basic utilities for EdDSA. Not for external use, not maintained as a public API.
+ *
+ * @author str4d
+ */
+public class Utils {
+
+  /**
+   * Constant-time byte comparison.
+   *
+   * @param b a byte
+   * @param c a byte
+   * @return 1 if b and c are equal, 0 otherwise.
+   */
+  public static int equal(int b, int c) {
+    int result = 0;
+    int xor = b ^ c;
+    for (int i = 0; i < 8; i++) {
+      result |= xor >> i;
+    }
+    return (result ^ 0x01) & 0x01;
+  }
+
+  /**
+   * Constant-time byte[] comparison.
+   *
+   * @param b a byte[]
+   * @param c a byte[]
+   * @return 1 if b and c are equal, 0 otherwise.
+   */
+  public static int equal(byte[] b, byte[] c) {
+    int result = 0;
+    for (int i = 0; i < 32; i++) {
+      result |= b[i] ^ c[i];
+    }
+
+    return equal(result, 0);
+  }
+
+  /**
+   * Constant-time determine if byte is negative.
+   *
+   * @param b the byte to check.
+   * @return 1 if the byte is negative, 0 otherwise.
+   */
+  public static int negative(int b) {
+    return (b >> 8) & 1;
+  }
+
+  /**
+   * Get the i'th bit of a byte array.
+   *
+   * @param h the byte array.
+   * @param i the bit index.
+   * @return 0 or 1, the value of the i'th bit in h
+   */
+  public static int bit(byte[] h, int i) {
+    return (h[i >> 3] >> (i & 7)) & 1;
+  }
+
+  /**
+   * Converts a hex string to bytes.
+   *
+   * @param s the hex string to be converted.
+   * @return the byte[]
+   */
+  public static byte[] hexToBytes(String s) {
+    int len = s.length();
+    byte[] data = new byte[len / 2];
+    for (int i = 0; i < len; i += 2) {
+      data[i / 2] =
+          (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
+    }
+    return data;
+  }
+
+  /**
+   * Converts bytes to a hex string.
+   *
+   * @param raw the byte[] to be converted.
+   * @return the hex representation as a string.
+   */
+  public static String bytesToHex(byte[] raw) {
+    if (raw == null) {
+      return null;
+    }
+    final StringBuilder hex = new StringBuilder(2 * raw.length);
+    for (final byte b : raw) {
+      hex.append(Character.forDigit((b & 0xF0) >> 4, 16))
+          .append(Character.forDigit((b & 0x0F), 16));
+    }
+    return hex.toString();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/Constants.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/Constants.java
new file mode 100644
index 0000000..0b22794
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/Constants.java
@@ -0,0 +1,18 @@
+package org.mariadb.jdbc.plugin.authentication.standard.ed25519.math;
+
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.Utils;
+
+final class Constants {
+  public static final byte[] ZERO =
+      Utils.hexToBytes("0000000000000000000000000000000000000000000000000000000000000000");
+  public static final byte[] ONE =
+      Utils.hexToBytes("0100000000000000000000000000000000000000000000000000000000000000");
+  public static final byte[] TWO =
+      Utils.hexToBytes("0200000000000000000000000000000000000000000000000000000000000000");
+  public static final byte[] FOUR =
+      Utils.hexToBytes("0400000000000000000000000000000000000000000000000000000000000000");
+  public static final byte[] FIVE =
+      Utils.hexToBytes("0500000000000000000000000000000000000000000000000000000000000000");
+  public static final byte[] EIGHT =
+      Utils.hexToBytes("0800000000000000000000000000000000000000000000000000000000000000");
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/Curve.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/Curve.java
new file mode 100644
index 0000000..049f123
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/Curve.java
@@ -0,0 +1,83 @@
+package org.mariadb.jdbc.plugin.authentication.standard.ed25519.math;
+
+import java.io.Serializable;
+
+/**
+ * A twisted Edwards curve. Points on the curve satisfy $-x^2 + y^2 = 1 + d x^2y^2$
+ *
+ * @author str4d
+ */
+public class Curve implements Serializable {
+  private static final long serialVersionUID = 4578920872509827L;
+  private final Field f;
+  private final FieldElement d;
+  private final FieldElement d2;
+  private final FieldElement I;
+
+  private final GroupElement zeroP2;
+  private final GroupElement zeroP3;
+  private final GroupElement zeroP3PrecomputedDouble;
+  private final GroupElement zeroPrecomp;
+
+  public Curve(Field f, byte[] d, FieldElement I) {
+    this.f = f;
+    this.d = f.fromByteArray(d);
+    this.d2 = this.d.add(this.d);
+    this.I = I;
+
+    FieldElement zero = f.ZERO;
+    FieldElement one = f.ONE;
+    zeroP2 = GroupElement.p2(this, zero, one, one);
+    zeroP3 = GroupElement.p3(this, zero, one, one, zero, false);
+    zeroP3PrecomputedDouble = GroupElement.p3(this, zero, one, one, zero, true);
+    zeroPrecomp = GroupElement.precomp(this, one, one, zero);
+  }
+
+  public Field getField() {
+    return f;
+  }
+
+  public FieldElement getD() {
+    return d;
+  }
+
+  public FieldElement get2D() {
+    return d2;
+  }
+
+  public FieldElement getI() {
+    return I;
+  }
+
+  public GroupElement getZero(GroupElement.Representation repr) {
+    switch (repr) {
+      case P2:
+        return zeroP2;
+      case P3:
+        return zeroP3;
+      case P3PrecomputedDouble:
+        return zeroP3PrecomputedDouble;
+      case PRECOMP:
+        return zeroPrecomp;
+      default:
+        return null;
+    }
+  }
+
+  public GroupElement createPoint(byte[] P, boolean precompute) {
+    return new GroupElement(this, P, precompute);
+  }
+
+  @Override
+  public int hashCode() {
+    return f.hashCode() ^ d.hashCode() ^ I.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o == this) return true;
+    if (!(o instanceof Curve)) return false;
+    Curve c = (Curve) o;
+    return f.equals(c.getField()) && d.equals(c.getD()) && I.equals(c.getI());
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/Encoding.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/Encoding.java
new file mode 100644
index 0000000..b6182f4
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/Encoding.java
@@ -0,0 +1,43 @@
+package org.mariadb.jdbc.plugin.authentication.standard.ed25519.math;
+
+/**
+ * Common interface for all $(b-1)$-bit encodings of elements of EdDSA finite fields.
+ *
+ * @author str4d
+ */
+public abstract class Encoding {
+  protected Field f;
+
+  public synchronized void setField(Field f) {
+    if (this.f != null) throw new IllegalStateException("already set");
+    this.f = f;
+  }
+
+  /**
+   * Encode a FieldElement in its $(b-1)$-bit encoding.
+   *
+   * @param x the FieldElement to encode
+   * @return the $(b-1)$-bit encoding of this FieldElement.
+   */
+  public abstract byte[] encode(FieldElement x);
+
+  /**
+   * Decode a FieldElement from its $(b-1)$-bit encoding. The highest bit is masked out.
+   *
+   * @param in the $(b-1)$-bit encoding of a FieldElement.
+   * @return the FieldElement represented by 'val'.
+   */
+  public abstract FieldElement decode(byte[] in);
+
+  /**
+   * From the Ed25519 paper:<br>
+   * $x$ is negative if the $(b-1)$-bit encoding of $x$ is lexicographically larger than the
+   * $(b-1)$-bit encoding of -x. If $q$ is an odd prime and the encoding is the little-endian
+   * representation of $\{0, 1,\dots, q-1\}$ then the negative elements of $F_q$ are $\{1, 3,
+   * 5,\dots, q-2\}$.
+   *
+   * @param x the FieldElement to check
+   * @return true if negative
+   */
+  public abstract boolean isNegative(FieldElement x);
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/Field.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/Field.java
new file mode 100644
index 0000000..2eecde6
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/Field.java
@@ -0,0 +1,72 @@
+package org.mariadb.jdbc.plugin.authentication.standard.ed25519.math;
+
+import java.io.Serializable;
+
+/**
+ * An EdDSA finite field. Includes several pre-computed values.
+ *
+ * @author str4d
+ */
+public class Field implements Serializable {
+  private static final long serialVersionUID = 8746587465875676L;
+
+  public final FieldElement ZERO;
+  public final FieldElement ONE;
+  public final FieldElement TWO;
+  public final FieldElement FOUR;
+  public final FieldElement FIVE;
+  public final FieldElement EIGHT;
+
+  private final int b;
+  private final FieldElement q;
+  /** q-2 */
+  private final FieldElement qm2;
+  /** (q-5) / 8 */
+  private final FieldElement qm5d8;
+
+  private final Encoding enc;
+
+  public Field(int b, byte[] q, Encoding enc) {
+    this.b = b;
+    this.enc = enc;
+    this.enc.setField(this);
+
+    this.q = fromByteArray(q);
+
+    // Set up constants
+    ZERO = fromByteArray(Constants.ZERO);
+    ONE = fromByteArray(Constants.ONE);
+    TWO = fromByteArray(Constants.TWO);
+    FOUR = fromByteArray(Constants.FOUR);
+    FIVE = fromByteArray(Constants.FIVE);
+    EIGHT = fromByteArray(Constants.EIGHT);
+
+    // Precompute values
+    qm2 = this.q.subtract(TWO);
+    qm5d8 = this.q.subtract(FIVE).divide(EIGHT);
+  }
+
+  public FieldElement fromByteArray(byte[] x) {
+    return enc.decode(x);
+  }
+
+  public int getb() {
+    return b;
+  }
+
+  public Encoding getEncoding() {
+    return enc;
+  }
+
+  @Override
+  public int hashCode() {
+    return q.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (!(obj instanceof Field)) return false;
+    Field f = (Field) obj;
+    return b == f.b && q.equals(f.q);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/FieldElement.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/FieldElement.java
new file mode 100644
index 0000000..3f2262e
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/FieldElement.java
@@ -0,0 +1,64 @@
+package org.mariadb.jdbc.plugin.authentication.standard.ed25519.math;
+
+import java.io.Serializable;
+
+/** Note: concrete subclasses must implement hashCode() and equals() */
+public abstract class FieldElement implements Serializable {
+  private static final long serialVersionUID = 1239527465875676L;
+
+  protected final Field f;
+
+  public FieldElement(Field f) {
+    if (null == f) {
+      throw new IllegalArgumentException("field cannot be null");
+    }
+    this.f = f;
+  }
+
+  /**
+   * Encode a FieldElement in its $(b-1)$-bit encoding.
+   *
+   * @return the $(b-1)$-bit encoding of this FieldElement.
+   */
+  public byte[] toByteArray() {
+    return f.getEncoding().encode(this);
+  }
+
+  public abstract boolean isNonZero();
+
+  public boolean isNegative() {
+    return f.getEncoding().isNegative(this);
+  }
+
+  public abstract FieldElement add(FieldElement val);
+
+  public FieldElement addOne() {
+    return add(f.ONE);
+  }
+
+  public abstract FieldElement subtract(FieldElement val);
+
+  public FieldElement subtractOne() {
+    return subtract(f.ONE);
+  }
+
+  public abstract FieldElement negate();
+
+  public FieldElement divide(FieldElement val) {
+    return multiply(val.invert());
+  }
+
+  public abstract FieldElement multiply(FieldElement val);
+
+  public abstract FieldElement square();
+
+  public abstract FieldElement squareAndDouble();
+
+  public abstract FieldElement invert();
+
+  public abstract FieldElement pow22523();
+
+  public abstract FieldElement cmov(FieldElement val, final int b);
+
+  // Note: concrete subclasses must implement hashCode() and equals()
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/GroupElement.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/GroupElement.java
new file mode 100644
index 0000000..feb9ffa
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/GroupElement.java
@@ -0,0 +1,1003 @@
+package org.mariadb.jdbc.plugin.authentication.standard.ed25519.math;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.Utils;
+
+/**
+ * A point $(x,y)$ on an EdDSA curve.
+ *
+ * <p>Reviewed/commented by Bloody Rookie (nemproject@gmx.de)
+ *
+ * <p>Literature:<br>
+ * [1] Daniel J. Bernstein, Niels Duif, Tanja Lange, Peter Schwabe and Bo-Yin Yang : High-speed
+ * high-security signatures<br>
+ * [2] Huseyin Hisil, Kenneth Koon-Ho Wong, Gary Carter, Ed Dawson: Twisted Edwards Curves Revisited
+ * <br>
+ * [3] Daniel J. Bernsteina, Tanja Lange: A complete set of addition laws for incomplete Edwards
+ * curves<br>
+ * [4] Daniel J. Bernstein, Peter Birkner, Marc Joye, Tanja Lange and Christiane Peters: Twisted
+ * Edwards Curves<br>
+ * [5] Christiane Pascale Peters: Curves, Codes, and Cryptography (PhD thesis)<br>
+ * [6] Daniel J. Bernstein, Peter Birkner, Tanja Lange and Christiane Peters: Optimizing double-base
+ * elliptic-curve single-scalar multiplication<br>
+ *
+ * @author str4d
+ */
+public class GroupElement implements Serializable {
+  private static final long serialVersionUID = 2395879087349587L;
+
+  /**
+   * Available representations for a group element.
+   *
+   * <ul>
+   *   <li>P2: Projective representation $(X:Y:Z)$ satisfying $x=X/Z, y=Y/Z$.
+   *   <li>P3: Extended projective representation $(X:Y:Z:T)$ satisfying $x=X/Z, y=Y/Z, XY=ZT$.
+   *   <li>P3PrecomputedDouble: P3 but with dblPrecmp populated.
+   *   <li>P1P1: Completed representation $((X:Z), (Y:T))$ satisfying $x=X/Z, y=Y/T$.
+   *   <li>PRECOMP: Precomputed representation $(y+x, y-x, 2dxy)$.
+   *   <li>CACHED: Cached representation $(Y+X, Y-X, Z, 2dT)$
+   * </ul>
+   */
+  public enum Representation {
+    /** Projective ($P^2$): $(X:Y:Z)$ satisfying $x=X/Z, y=Y/Z$ */
+    P2,
+    /** Extended ($P^3$): $(X:Y:Z:T)$ satisfying $x=X/Z, y=Y/Z, XY=ZT$ */
+    P3,
+    /** Can only be requested. Results in P3 representation but also populates dblPrecmp. */
+    P3PrecomputedDouble,
+    /** Completed ($P \times P$): $((X:Z),(Y:T))$ satisfying $x=X/Z, y=Y/T$ */
+    P1P1,
+    /** Precomputed (Duif): $(y+x,y-x,2dxy)$ */
+    PRECOMP,
+    /** Cached: $(Y+X,Y-X,Z,2dT)$ */
+    CACHED
+  }
+
+  /**
+   * Creates a new group element in P2 representation.
+   *
+   * @param curve The curve.
+   * @param X The $X$ coordinate.
+   * @param Y The $Y$ coordinate.
+   * @param Z The $Z$ coordinate.
+   * @return The group element in P2 representation.
+   */
+  public static GroupElement p2(
+      final Curve curve, final FieldElement X, final FieldElement Y, final FieldElement Z) {
+    return new GroupElement(curve, Representation.P2, X, Y, Z, null);
+  }
+
+  /**
+   * Creates a new group element in P3 representation, without pre-computation.
+   *
+   * @param curve The curve.
+   * @param X The $X$ coordinate.
+   * @param Y The $Y$ coordinate.
+   * @param Z The $Z$ coordinate.
+   * @param T The $T$ coordinate.
+   * @return The group element in P3 representation.
+   */
+  public static GroupElement p3(
+      final Curve curve,
+      final FieldElement X,
+      final FieldElement Y,
+      final FieldElement Z,
+      final FieldElement T) {
+    return p3(curve, X, Y, Z, T, false);
+  }
+
+  /**
+   * Creates a new group element in P3 representation, potentially with pre-computation.
+   *
+   * @param curve The curve.
+   * @param X The $X$ coordinate.
+   * @param Y The $Y$ coordinate.
+   * @param Z The $Z$ coordinate.
+   * @param T The $T$ coordinate.
+   * @param precomputeDoubleOnly If true, populate dblPrecmp, else set to null.
+   * @return The group element in P3 representation.
+   */
+  public static GroupElement p3(
+      final Curve curve,
+      final FieldElement X,
+      final FieldElement Y,
+      final FieldElement Z,
+      final FieldElement T,
+      final boolean precomputeDoubleOnly) {
+    return new GroupElement(curve, Representation.P3, X, Y, Z, T, precomputeDoubleOnly);
+  }
+
+  /**
+   * Creates a new group element in P1P1 representation.
+   *
+   * @param curve The curve.
+   * @param X The $X$ coordinate.
+   * @param Y The $Y$ coordinate.
+   * @param Z The $Z$ coordinate.
+   * @param T The $T$ coordinate.
+   * @return The group element in P1P1 representation.
+   */
+  public static GroupElement p1p1(
+      final Curve curve,
+      final FieldElement X,
+      final FieldElement Y,
+      final FieldElement Z,
+      final FieldElement T) {
+    return new GroupElement(curve, Representation.P1P1, X, Y, Z, T);
+  }
+
+  /**
+   * Creates a new group element in PRECOMP representation.
+   *
+   * @param curve The curve.
+   * @param ypx The $y + x$ value.
+   * @param ymx The $y - x$ value.
+   * @param xy2d The $2 * d * x * y$ value.
+   * @return The group element in PRECOMP representation.
+   */
+  public static GroupElement precomp(
+      final Curve curve, final FieldElement ypx, final FieldElement ymx, final FieldElement xy2d) {
+    return new GroupElement(curve, Representation.PRECOMP, ypx, ymx, xy2d, null);
+  }
+
+  /**
+   * Creates a new group element in CACHED representation.
+   *
+   * @param curve The curve.
+   * @param YpX The $Y + X$ value.
+   * @param YmX The $Y - X$ value.
+   * @param Z The $Z$ coordinate.
+   * @param T2d The $2 * d * T$ value.
+   * @return The group element in CACHED representation.
+   */
+  public static GroupElement cached(
+      final Curve curve,
+      final FieldElement YpX,
+      final FieldElement YmX,
+      final FieldElement Z,
+      final FieldElement T2d) {
+    return new GroupElement(curve, Representation.CACHED, YpX, YmX, Z, T2d);
+  }
+
+  /** Variable is package private only so that tests run. */
+  final Curve curve;
+
+  /** Variable is package private only so that tests run. */
+  final Representation repr;
+
+  /** Variable is package private only so that tests run. */
+  final FieldElement X;
+
+  /** Variable is package private only so that tests run. */
+  final FieldElement Y;
+
+  /** Variable is package private only so that tests run. */
+  final FieldElement Z;
+
+  /** Variable is package private only so that tests run. */
+  final FieldElement T;
+
+  /**
+   * Precomputed table for {@link #scalarMultiply(byte[])}, filled if necessary.
+   *
+   * <p>Variable is package private only so that tests run.
+   */
+  final GroupElement[][] precmp;
+
+  /**
+   * Precomputed table for , filled if necessary.
+   *
+   * <p>Variable is package private only so that tests run.
+   */
+  final GroupElement[] dblPrecmp;
+
+  /**
+   * Creates a group element for a curve, without any pre-computation.
+   *
+   * @param curve The curve.
+   * @param repr The representation used to represent the group element.
+   * @param X The $X$ coordinate.
+   * @param Y The $Y$ coordinate.
+   * @param Z The $Z$ coordinate.
+   * @param T The $T$ coordinate.
+   */
+  public GroupElement(
+      final Curve curve,
+      final Representation repr,
+      final FieldElement X,
+      final FieldElement Y,
+      final FieldElement Z,
+      final FieldElement T) {
+    this(curve, repr, X, Y, Z, T, false);
+  }
+
+  /**
+   * Creates a group element for a curve, with optional pre-computation.
+   *
+   * @param curve The curve.
+   * @param repr The representation used to represent the group element.
+   * @param X The $X$ coordinate.
+   * @param Y The $Y$ coordinate.
+   * @param Z The $Z$ coordinate.
+   * @param T The $T$ coordinate.
+   * @param precomputeDouble If true, populate dblPrecmp, else set to null.
+   */
+  public GroupElement(
+      final Curve curve,
+      final Representation repr,
+      final FieldElement X,
+      final FieldElement Y,
+      final FieldElement Z,
+      final FieldElement T,
+      final boolean precomputeDouble) {
+    this.curve = curve;
+    this.repr = repr;
+    this.X = X;
+    this.Y = Y;
+    this.Z = Z;
+    this.T = T;
+    this.precmp = null;
+    this.dblPrecmp = precomputeDouble ? precomputeDouble() : null;
+  }
+
+  /**
+   * Creates a group element for a curve from a given encoded point. No pre-computation.
+   *
+   * <p>A point $(x,y)$ is encoded by storing $y$ in bit 0 to bit 254 and the sign of $x$ in bit
+   * 255. $x$ is recovered in the following way:
+   *
+   * <ul>
+   *   <li>$x = sign(x) * \sqrt{(y^2 - 1) / (d * y^2 + 1)} = sign(x) * \sqrt{u / v}$ with $u = y^2 -
+   *       1$ and $v = d * y^2 + 1$.
+   *   <li>Setting $β = (u * v^3) * (u * v^7)^{((q - 5) / 8)}$ one has $β^2 = \pm(u / v)$.
+   *   <li>If $v * β = -u$ multiply $β$ with $i=\sqrt{-1}$.
+   *   <li>Set $x := β$.
+   *   <li>If $sign(x) \ne$ bit 255 of $s$ then negate $x$.
+   * </ul>
+   *
+   * @param curve The curve.
+   * @param s The encoded point.
+   */
+  public GroupElement(final Curve curve, final byte[] s) {
+    this(curve, s, false);
+  }
+
+  /**
+   * Creates a group element for a curve from a given encoded point. With optional pre-computation.
+   *
+   * <p>A point $(x,y)$ is encoded by storing $y$ in bit 0 to bit 254 and the sign of $x$ in bit
+   * 255. $x$ is recovered in the following way:
+   *
+   * <ul>
+   *   <li>$x = sign(x) * \sqrt{(y^2 - 1) / (d * y^2 + 1)} = sign(x) * \sqrt{u / v}$ with $u = y^2 -
+   *       1$ and $v = d * y^2 + 1$.
+   *   <li>Setting $β = (u * v^3) * (u * v^7)^{((q - 5) / 8)}$ one has $β^2 = \pm(u / v)$.
+   *   <li>If $v * β = -u$ multiply $β$ with $i=\sqrt{-1}$.
+   *   <li>Set $x := β$.
+   *   <li>If $sign(x) \ne$ bit 255 of $s$ then negate $x$.
+   * </ul>
+   *
+   * @param curve The curve.
+   * @param s The encoded point.
+   * @param precomputeSingleAndDouble If true, populate both precmp and dblPrecmp, else set both to
+   *     null.
+   */
+  public GroupElement(final Curve curve, final byte[] s, boolean precomputeSingleAndDouble) {
+    FieldElement x, y, yy, u, v, v3, vxx, check;
+    y = curve.getField().fromByteArray(s);
+    yy = y.square();
+
+    // u = y^2-1
+    u = yy.subtractOne();
+
+    // v = dy^2+1
+    v = yy.multiply(curve.getD()).addOne();
+
+    // v3 = v^3
+    v3 = v.square().multiply(v);
+
+    // x = (v3^2)vu, aka x = uv^7
+    x = v3.square().multiply(v).multiply(u);
+
+    //  x = (uv^7)^((q-5)/8)
+    x = x.pow22523();
+
+    // x = uv^3(uv^7)^((q-5)/8)
+    x = v3.multiply(u).multiply(x);
+
+    vxx = x.square().multiply(v);
+    check = vxx.subtract(u); // vx^2-u
+    if (check.isNonZero()) {
+      check = vxx.add(u); // vx^2+u
+
+      if (check.isNonZero()) throw new IllegalArgumentException("not a valid GroupElement");
+      x = x.multiply(curve.getI());
+    }
+
+    if ((x.isNegative() ? 1 : 0) != Utils.bit(s, curve.getField().getb() - 1)) {
+      x = x.negate();
+    }
+
+    this.curve = curve;
+    this.repr = Representation.P3;
+    this.X = x;
+    this.Y = y;
+    this.Z = curve.getField().ONE;
+    this.T = this.X.multiply(this.Y);
+    if (precomputeSingleAndDouble) {
+      precmp = precomputeSingle();
+      dblPrecmp = precomputeDouble();
+    } else {
+      precmp = null;
+      dblPrecmp = null;
+    }
+  }
+
+  /**
+   * Gets the representation of the group element.
+   *
+   * @return The representation.
+   */
+  public Representation getRepresentation() {
+    return this.repr;
+  }
+
+  /**
+   * Converts the group element to an encoded point on the curve.
+   *
+   * @return The encoded point as byte array.
+   */
+  public byte[] toByteArray() {
+    switch (this.repr) {
+      case P2:
+      case P3:
+        FieldElement recip = Z.invert();
+        FieldElement x = X.multiply(recip);
+        FieldElement y = Y.multiply(recip);
+        byte[] s = y.toByteArray();
+        s[s.length - 1] |= (x.isNegative() ? (byte) 0x80 : 0);
+        return s;
+      default:
+        return toP2().toByteArray();
+    }
+  }
+
+  /**
+   * Converts the group element to the P2 representation.
+   *
+   * @return The group element in the P2 representation.
+   */
+  public GroupElement toP2() {
+    return toRep(Representation.P2);
+  }
+
+  /**
+   * Converts the group element to the P3 representation.
+   *
+   * @return The group element in the P3 representation.
+   */
+  public GroupElement toP3() {
+    return toRep(Representation.P3);
+  }
+
+  /**
+   * Converts the group element to the P3 representation, with dblPrecmp populated.
+   *
+   * @return The group element in the P3 representation.
+   */
+  public GroupElement toP3PrecomputeDouble() {
+    return toRep(Representation.P3PrecomputedDouble);
+  }
+
+  /**
+   * Converts the group element to the CACHED representation.
+   *
+   * @return The group element in the CACHED representation.
+   */
+  public GroupElement toCached() {
+    return toRep(Representation.CACHED);
+  }
+
+  /**
+   * Convert a GroupElement from one Representation to another. TODO-CR: Add additional conversion?
+   * $r = p$
+   *
+   * <p>Supported conversions:
+   *
+   * <p>
+   *
+   * <ul>
+   *   <li>P3 $\rightarrow$ P2
+   *   <li>P3 $\rightarrow$ CACHED (1 multiply, 1 add, 1 subtract)
+   *   <li>P1P1 $\rightarrow$ P2 (3 multiply)
+   *   <li>P1P1 $\rightarrow$ P3 (4 multiply)
+   *
+   * @param repr The representation to convert to.
+   * @return A new group element in the given representation.
+   */
+  private GroupElement toRep(final Representation repr) {
+    switch (this.repr) {
+      case P2:
+        if (repr == Representation.P2) {
+          return p2(this.curve, this.X, this.Y, this.Z);
+        }
+        throw new IllegalArgumentException();
+      case P3:
+        switch (repr) {
+          case P2:
+            return p2(this.curve, this.X, this.Y, this.Z);
+          case P3:
+            return p3(this.curve, this.X, this.Y, this.Z, this.T);
+          case CACHED:
+            return cached(
+                this.curve,
+                this.Y.add(this.X),
+                this.Y.subtract(this.X),
+                this.Z,
+                this.T.multiply(this.curve.get2D()));
+          default:
+            throw new IllegalArgumentException();
+        }
+      case P1P1:
+        switch (repr) {
+          case P2:
+            return p2(
+                this.curve, this.X.multiply(this.T), Y.multiply(this.Z), this.Z.multiply(this.T));
+          case P3:
+            return p3(
+                this.curve,
+                this.X.multiply(this.T),
+                Y.multiply(this.Z),
+                this.Z.multiply(this.T),
+                this.X.multiply(this.Y),
+                false);
+          case P3PrecomputedDouble:
+            return p3(
+                this.curve,
+                this.X.multiply(this.T),
+                Y.multiply(this.Z),
+                this.Z.multiply(this.T),
+                this.X.multiply(this.Y),
+                true);
+          case P1P1:
+            return p1p1(this.curve, this.X, this.Y, this.Z, this.T);
+          default:
+            throw new IllegalArgumentException();
+        }
+      case PRECOMP:
+        if (repr == Representation.PRECOMP) {
+          return precomp(this.curve, this.X, this.Y, this.Z);
+        }
+        throw new IllegalArgumentException();
+      case CACHED:
+        if (repr == Representation.CACHED) {
+          return cached(this.curve, this.X, this.Y, this.Z, this.T);
+        }
+        throw new IllegalArgumentException();
+      default:
+        throw new UnsupportedOperationException();
+    }
+  }
+
+  /** Precomputes table for {@link #scalarMultiply(byte[])}. */
+  private GroupElement[][] precomputeSingle() {
+    // Precomputation for single scalar multiplication.
+    GroupElement[][] precmp = new GroupElement[32][8];
+    // TODO-CR BR: check that this == base point when the method is called.
+    GroupElement Bi = this;
+    for (int i = 0; i < 32; i++) {
+      GroupElement Bij = Bi;
+      for (int j = 0; j < 8; j++) {
+        final FieldElement recip = Bij.Z.invert();
+        final FieldElement x = Bij.X.multiply(recip);
+        final FieldElement y = Bij.Y.multiply(recip);
+        precmp[i][j] =
+            precomp(
+                this.curve, y.add(x), y.subtract(x), x.multiply(y).multiply(this.curve.get2D()));
+        Bij = Bij.add(Bi.toCached()).toP3();
+      }
+      // Only every second summand is precomputed (16^2 = 256)
+      for (int k = 0; k < 8; k++) {
+        Bi = Bi.add(Bi.toCached()).toP3();
+      }
+    }
+    return precmp;
+  }
+
+  /** Precomputes table for . */
+  private GroupElement[] precomputeDouble() {
+    // Precomputation for double scalar multiplication.
+    // P,3P,5P,7P,9P,11P,13P,15P
+    GroupElement[] dblPrecmp = new GroupElement[8];
+    GroupElement Bi = this;
+    for (int i = 0; i < 8; i++) {
+      final FieldElement recip = Bi.Z.invert();
+      final FieldElement x = Bi.X.multiply(recip);
+      final FieldElement y = Bi.Y.multiply(recip);
+      dblPrecmp[i] =
+          precomp(this.curve, y.add(x), y.subtract(x), x.multiply(y).multiply(this.curve.get2D()));
+      // Bi = edwards(B,edwards(B,Bi))
+      Bi = this.add(this.add(Bi.toCached()).toP3().toCached()).toP3();
+    }
+    return dblPrecmp;
+  }
+
+  /**
+   * Doubles a given group element $p$ in $P^2$ or $P^3$ representation and returns the result in $P
+   * \times P$ representation. $r = 2 * p$ where $p = (X : Y : Z)$ or $p = (X : Y : Z : T)$
+   *
+   * <p>$r$ in $P \times P$ representation:
+   *
+   * <p>$r = ((X' : Z'), (Y' : T'))$ where
+   *
+   * <ul>
+   *   <li>$X' = (X + Y)^2 - (Y^2 + X^2)$
+   *   <li>$Y' = Y^2 + X^2$
+   *   <li>$Z' = y^2 - X^2$
+   *   <li>$T' = 2 * Z^2 - (y^2 - X^2)$
+   * </ul>
+   *
+   * <p>$r$ converted from $P \times P$ to $P^2$ representation:
+   *
+   * <p>$r = (X'' : Y'' : Z'')$ where
+   *
+   * <ul>
+   *   <li>$X'' = X' * Z' = ((X + Y)^2 - Y^2 - X^2) * (2 * Z^2 - (y^2 - X^2))$
+   *   <li>$Y'' = Y' * T' = (Y^2 + X^2) * (2 * Z^2 - (y^2 - X^2))$
+   *   <li>$Z'' = Z' * T' = (y^2 - X^2) * (2 * Z^2 - (y^2 - X^2))$
+   * </ul>
+   *
+   * <p>Formula for the $P^2$ representation is in agreement with the formula given in [4] page 12
+   * (with $a = -1$) up to a common factor -1 which does not matter:
+   *
+   * <p>$$ B = (X + Y)^2; C = X^2; D = Y^2; E = -C = -X^2; F := E + D = Y^2 - X^2; H = Z^2; J = F −
+   * 2 * H; \\ X3 = (B − C − D) · J = X' * (-T'); \\ Y3 = F · (E − D) = Z' * (-Y'); \\ Z3 = F · J =
+   * Z' * (-T'). $$
+   *
+   * @return The P1P1 representation
+   */
+  public GroupElement dbl() {
+    switch (this.repr) {
+      case P2:
+      case P3: // Ignore T for P3 representation
+        FieldElement XX, YY, B, A, AA, Yn, Zn;
+        XX = this.X.square();
+        YY = this.Y.square();
+        B = this.Z.squareAndDouble();
+        A = this.X.add(this.Y);
+        AA = A.square();
+        Yn = YY.add(XX);
+        Zn = YY.subtract(XX);
+        return p1p1(this.curve, AA.subtract(Yn), Yn, Zn, B.subtract(Zn));
+      default:
+        throw new UnsupportedOperationException();
+    }
+  }
+
+  /**
+   * GroupElement addition using the twisted Edwards addition law with extended coordinates
+   * (Hisil2008).
+   *
+   * <p>this must be in $P^3$ representation and $q$ in PRECOMP representation. $r = p + q$ where $p
+   * = this = (X1 : Y1 : Z1 : T1), q = (q.X, q.Y, q.Z) = (Y2/Z2 + X2/Z2, Y2/Z2 - X2/Z2, 2 * d *
+   * X2/Z2 * Y2/Z2)$
+   *
+   * <p>$r$ in $P \times P$ representation:
+   *
+   * <p>$r = ((X' : Z'), (Y' : T'))$ where
+   *
+   * <p>
+   *
+   * <ul>
+   *   <li>$X' = (Y1 + X1) * q.X - (Y1 - X1) * q.Y = ((Y1 + X1) * (Y2 + X2) - (Y1 - X1) * (Y2 - X2))
+   *       * 1/Z2$
+   *   <li>$Y' = (Y1 + X1) * q.X + (Y1 - X1) * q.Y = ((Y1 + X1) * (Y2 + X2) + (Y1 - X1) * (Y2 - X2))
+   *       * 1/Z2$
+   *   <li>$Z' = 2 * Z1 + T1 * q.Z = 2 * Z1 + T1 * 2 * d * X2 * Y2 * 1/Z2^2 = (2 * Z1 * Z2 + 2 * d *
+   *       T1 * T2) * 1/Z2$
+   *   <li>$T' = 2 * Z1 - T1 * q.Z = 2 * Z1 - T1 * 2 * d * X2 * Y2 * 1/Z2^2 = (2 * Z1 * Z2 - 2 * d *
+   *       T1 * T2) * 1/Z2$
+   * </ul>
+   *
+   * <p>Setting $A = (Y1 - X1) * (Y2 - X2), B = (Y1 + X1) * (Y2 + X2), C = 2 * d * T1 * T2, D = 2 *
+   * Z1 * Z2$ we get
+   *
+   * <p>
+   *
+   * <ul>
+   *   <li>$X' = (B - A) * 1/Z2$
+   *   <li>$Y' = (B + A) * 1/Z2$
+   *   <li>$Z' = (D + C) * 1/Z2$
+   *   <li>$T' = (D - C) * 1/Z2$
+   * </ul>
+   *
+   * <p>$r$ converted from $P \times P$ to $P^2$ representation:
+   *
+   * <p>$r = (X'' : Y'' : Z'' : T'')$ where
+   *
+   * <p>
+   *
+   * <ul>
+   *   <li>$X'' = X' * Z' = (B - A) * (D + C) * 1/Z2^2$
+   *   <li>$Y'' = Y' * T' = (B + A) * (D - C) * 1/Z2^2$
+   *   <li>$Z'' = Z' * T' = (D + C) * (D - C) * 1/Z2^2$
+   *   <li>$T'' = X' * Y' = (B - A) * (B + A) * 1/Z2^2$
+   * </ul>
+   *
+   * <p>TODO-CR BR: Formula for the $P^2$ representation is not in agreement with the formula given
+   * in [2] page 6<br>
+   * TODO-CR BR: (the common factor $1/Z2^2$ does not matter):<br>
+   * $$ E = B - A, F = D - C, G = D + C, H = B + A \\ X3 = E * F = (B - A) * (D - C); \\ Y3 = G * H
+   * = (D + C) * (B + A); \\ Z3 = F * G = (D - C) * (D + C); \\ T3 = E * H = (B - A) * (B + A); $$
+   *
+   * @param q the PRECOMP representation of the GroupElement to add.
+   * @return the P1P1 representation of the result.
+   */
+  private GroupElement madd(GroupElement q) {
+    if (this.repr != Representation.P3) throw new UnsupportedOperationException();
+    if (q.repr != Representation.PRECOMP) throw new IllegalArgumentException();
+
+    FieldElement YpX, YmX, A, B, C, D;
+    YpX = this.Y.add(this.X);
+    YmX = this.Y.subtract(this.X);
+    A = YpX.multiply(q.X); // q->y+x
+    B = YmX.multiply(q.Y); // q->y-x
+    C = q.Z.multiply(this.T); // q->2dxy
+    D = this.Z.add(this.Z);
+    return p1p1(this.curve, A.subtract(B), A.add(B), D.add(C), D.subtract(C));
+  }
+
+  /**
+   * GroupElement subtraction using the twisted Edwards addition law with extended coordinates
+   * (Hisil2008).
+   *
+   * <p>this must be in $P^3$ representation and $q$ in PRECOMP representation. $r = p - q$ where $p
+   * = this = (X1 : Y1 : Z1 : T1), q = (q.X, q.Y, q.Z) = (Y2/Z2 + X2/Z2, Y2/Z2 - X2/Z2, 2 * d *
+   * X2/Z2 * Y2/Z2)$
+   *
+   * <p>Negating $q$ means negating the value of $X2$ and $T2$ (the latter is irrelevant here). The
+   * formula is in accordance to {@link #madd the above addition}.
+   *
+   * @param q the PRECOMP representation of the GroupElement to subtract.
+   * @return the P1P1 representation of the result.
+   */
+  private GroupElement msub(GroupElement q) {
+    if (this.repr != Representation.P3) throw new UnsupportedOperationException();
+    if (q.repr != Representation.PRECOMP) throw new IllegalArgumentException();
+
+    FieldElement YpX, YmX, A, B, C, D;
+    YpX = this.Y.add(this.X);
+    YmX = this.Y.subtract(this.X);
+    A = YpX.multiply(q.Y); // q->y-x
+    B = YmX.multiply(q.X); // q->y+x
+    C = q.Z.multiply(this.T); // q->2dxy
+    D = this.Z.add(this.Z);
+    return p1p1(this.curve, A.subtract(B), A.add(B), D.subtract(C), D.add(C));
+  }
+
+  /**
+   * GroupElement addition using the twisted Edwards addition law with extended coordinates
+   * (Hisil2008).
+   *
+   * <p>this must be in $P^3$ representation and $q$ in CACHED representation. $r = p + q$ where $p
+   * = this = (X1 : Y1 : Z1 : T1), q = (q.X, q.Y, q.Z, q.T) = (Y2 + X2, Y2 - X2, Z2, 2 * d * T2)$
+   *
+   * <p>$r$ in $P \times P$ representation:
+   *
+   * <ul>
+   *   <li>$X' = (Y1 + X1) * (Y2 + X2) - (Y1 - X1) * (Y2 - X2)$
+   *   <li>$Y' = (Y1 + X1) * (Y2 + X2) + (Y1 - X1) * (Y2 - X2)$
+   *   <li>$Z' = 2 * Z1 * Z2 + 2 * d * T1 * T2$
+   *   <li>$T' = 2 * Z1 * T2 - 2 * d * T1 * T2$
+   * </ul>
+   *
+   * <p>Setting $A = (Y1 - X1) * (Y2 - X2), B = (Y1 + X1) * (Y2 + X2), C = 2 * d * T1 * T2, D = 2 *
+   * Z1 * Z2$ we get
+   *
+   * <ul>
+   *   <li>$X' = (B - A)$
+   *   <li>$Y' = (B + A)$
+   *   <li>$Z' = (D + C)$
+   *   <li>$T' = (D - C)$
+   * </ul>
+   *
+   * <p>Same result as in {@link #madd} (up to a common factor which does not matter).
+   *
+   * @param q the CACHED representation of the GroupElement to add.
+   * @return the P1P1 representation of the result.
+   */
+  public GroupElement add(GroupElement q) {
+    if (this.repr != Representation.P3) throw new UnsupportedOperationException();
+    if (q.repr != Representation.CACHED) throw new IllegalArgumentException();
+
+    FieldElement YpX, YmX, A, B, C, ZZ, D;
+    YpX = this.Y.add(this.X);
+    YmX = this.Y.subtract(this.X);
+    A = YpX.multiply(q.X); // q->Y+X
+    B = YmX.multiply(q.Y); // q->Y-X
+    C = q.T.multiply(this.T); // q->2dT
+    ZZ = this.Z.multiply(q.Z);
+    D = ZZ.add(ZZ);
+    return p1p1(this.curve, A.subtract(B), A.add(B), D.add(C), D.subtract(C));
+  }
+
+  /**
+   * GroupElement subtraction using the twisted Edwards addition law with extended coordinates
+   * (Hisil2008).
+   *
+   * <p>$r = p - q$
+   *
+   * <p>Negating $q$ means negating the value of the coordinate $X2$ and $T2$. The formula is in
+   * accordance to {@link #add the above addition}.
+   *
+   * @param q the PRECOMP representation of the GroupElement to subtract.
+   * @return the P1P1 representation of the result.
+   */
+  public GroupElement sub(GroupElement q) {
+    if (this.repr != Representation.P3) throw new UnsupportedOperationException();
+    if (q.repr != Representation.CACHED) throw new IllegalArgumentException();
+
+    FieldElement YpX, YmX, A, B, C, ZZ, D;
+    YpX = Y.add(X);
+    YmX = Y.subtract(X);
+    A = YpX.multiply(q.Y); // q->Y-X
+    B = YmX.multiply(q.X); // q->Y+X
+    C = q.T.multiply(T); // q->2dT
+    ZZ = Z.multiply(q.Z);
+    D = ZZ.add(ZZ);
+    return p1p1(curve, A.subtract(B), A.add(B), D.subtract(C), D.add(C));
+  }
+
+  /**
+   * Negates this group element by subtracting it from the neutral group element.
+   *
+   * <p>TODO-CR BR: why not simply negate the coordinates $X$ and $T$?
+   *
+   * @return The negative of this group element.
+   */
+  public GroupElement negate() {
+    if (this.repr != Representation.P3) throw new UnsupportedOperationException();
+    return this.curve.getZero(Representation.P3).sub(toCached()).toP3PrecomputeDouble();
+  }
+
+  @Override
+  public int hashCode() {
+    return Arrays.hashCode(this.toByteArray());
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj == this) return true;
+    if (!(obj instanceof GroupElement)) return false;
+    GroupElement ge = (GroupElement) obj;
+    if (!this.repr.equals(ge.repr)) {
+      try {
+        ge = ge.toRep(this.repr);
+      } catch (RuntimeException e) {
+        return false;
+      }
+    }
+    switch (this.repr) {
+      case P2:
+      case P3:
+        // Try easy way first
+        if (this.Z.equals(ge.Z)) return this.X.equals(ge.X) && this.Y.equals(ge.Y);
+        // X1/Z1 = X2/Z2 --> X1*Z2 = X2*Z1
+        final FieldElement x1 = this.X.multiply(ge.Z);
+        final FieldElement y1 = this.Y.multiply(ge.Z);
+        final FieldElement x2 = ge.X.multiply(this.Z);
+        final FieldElement y2 = ge.Y.multiply(this.Z);
+        return x1.equals(x2) && y1.equals(y2);
+      case P1P1:
+        return toP2().equals(ge);
+      case PRECOMP:
+        // Compare directly, PRECOMP is derived directly from x and y
+        return this.X.equals(ge.X) && this.Y.equals(ge.Y) && this.Z.equals(ge.Z);
+      case CACHED:
+        // Try easy way first
+        if (this.Z.equals(ge.Z))
+          return this.X.equals(ge.X) && this.Y.equals(ge.Y) && this.T.equals(ge.T);
+        // (Y+X)/Z = y+x etc.
+        final FieldElement x3 = this.X.multiply(ge.Z);
+        final FieldElement y3 = this.Y.multiply(ge.Z);
+        final FieldElement t3 = this.T.multiply(ge.Z);
+        final FieldElement x4 = ge.X.multiply(this.Z);
+        final FieldElement y4 = ge.Y.multiply(this.Z);
+        final FieldElement t4 = ge.T.multiply(this.Z);
+        return x3.equals(x4) && y3.equals(y4) && t3.equals(t4);
+      default:
+        return false;
+    }
+  }
+
+  /**
+   * Convert a to radix 16.
+   *
+   * <p>Method is package private only so that tests run.
+   *
+   * @param a $= a[0]+256*a[1]+...+256^{31} a[31]$
+   * @return 64 bytes, each between -8 and 7
+   */
+  static byte[] toRadix16(final byte[] a) {
+    final byte[] e = new byte[64];
+    int i;
+    // Radix 16 notation
+    for (i = 0; i < 32; i++) {
+      e[2 * i] = (byte) (a[i] & 15);
+      e[2 * i + 1] = (byte) ((a[i] >> 4) & 15);
+    }
+    /* each e[i] is between 0 and 15 */
+    /* e[63] is between 0 and 7 */
+    int carry = 0;
+    for (i = 0; i < 63; i++) {
+      e[i] += carry;
+      carry = e[i] + 8;
+      carry >>= 4;
+      e[i] -= carry << 4;
+    }
+    e[63] += carry;
+    /* each e[i] is between -8 and 7 */
+    return e;
+  }
+
+  /**
+   * Constant-time conditional move.
+   *
+   * <p>Replaces this with $u$ if $b == 1$.<br>
+   * Replaces this with this if $b == 0$.
+   *
+   * <p>Method is package private only so that tests run.
+   *
+   * @param u The group element to return if $b == 1$.
+   * @param b in $\{0, 1\}$
+   * @return $u$ if $b == 1$; this if $b == 0$. Results undefined if $b$ is not in $\{0, 1\}$.
+   */
+  GroupElement cmov(final GroupElement u, final int b) {
+    return precomp(curve, X.cmov(u.X, b), Y.cmov(u.Y, b), Z.cmov(u.Z, b));
+  }
+
+  /**
+   * Look up $16^i r_i B$ in the precomputed table.
+   *
+   * <p>No secret array indices, no secret branching. Constant time.
+   *
+   * <p>Must have previously precomputed.
+   *
+   * <p>Method is package private only so that tests run.
+   *
+   * @param pos $= i/2$ for $i$ in $\{0, 2, 4,..., 62\}$
+   * @param b $= r_i$
+   * @return the GroupElement
+   */
+  GroupElement select(final int pos, final int b) {
+    // Is r_i negative?
+    final int bnegative = Utils.negative(b);
+    // |r_i|
+    final int babs = b - (((-bnegative) & b) << 1);
+
+    // 16^i |r_i| B
+    final GroupElement t =
+        this.curve
+            .getZero(Representation.PRECOMP)
+            .cmov(this.precmp[pos][0], Utils.equal(babs, 1))
+            .cmov(this.precmp[pos][1], Utils.equal(babs, 2))
+            .cmov(this.precmp[pos][2], Utils.equal(babs, 3))
+            .cmov(this.precmp[pos][3], Utils.equal(babs, 4))
+            .cmov(this.precmp[pos][4], Utils.equal(babs, 5))
+            .cmov(this.precmp[pos][5], Utils.equal(babs, 6))
+            .cmov(this.precmp[pos][6], Utils.equal(babs, 7))
+            .cmov(this.precmp[pos][7], Utils.equal(babs, 8));
+    // -16^i |r_i| B
+    final GroupElement tminus = precomp(curve, t.Y, t.X, t.Z.negate());
+    // 16^i r_i B
+    return t.cmov(tminus, bnegative);
+  }
+
+  /**
+   * $h = a * B$ where $a = a[0]+256*a[1]+\dots+256^{31} a[31]$ and $B$ is this point. If its lookup
+   * table has not been precomputed, it will be at the start of the method (and cached for later
+   * calls). Constant time.
+   *
+   * <p>Preconditions: (TODO: Check this applies here) $a[31] \le 127$
+   *
+   * @param a $= a[0]+256*a[1]+\dots+256^{31} a[31]$
+   * @return the GroupElement
+   */
+  public GroupElement scalarMultiply(final byte[] a) {
+    GroupElement t;
+    int i;
+
+    final byte[] e = toRadix16(a);
+
+    GroupElement h = this.curve.getZero(Representation.P3);
+    for (i = 1; i < 64; i += 2) {
+      t = select(i / 2, e[i]);
+      h = h.madd(t).toP3();
+    }
+
+    h = h.dbl().toP2().dbl().toP2().dbl().toP2().dbl().toP3();
+
+    for (i = 0; i < 64; i += 2) {
+      t = select(i / 2, e[i]);
+      h = h.madd(t).toP3();
+    }
+
+    return h;
+  }
+
+  /**
+   * Calculates a sliding-windows base 2 representation for a given value $a$. To learn more about
+   * it see [6] page 8.
+   *
+   * <p>Output: $r$ which satisfies $a = r0 * 2^0 + r1 * 2^1 + \dots + r255 * 2^{255}$ with $ri$ in
+   * $\{-15, -13, -11, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 11, 13, 15\}$
+   *
+   * <p>Method is package private only so that tests run.
+   *
+   * @param a $= a[0]+256*a[1]+\dots+256^{31} a[31]$.
+   * @return The byte array $r$ in the above described form.
+   */
+  static byte[] slide(final byte[] a) {
+    byte[] r = new byte[256];
+
+    // Put each bit of 'a' into a separate byte, 0 or 1
+    for (int i = 0; i < 256; ++i) {
+      r[i] = (byte) (1 & (a[i >> 3] >> (i & 7)));
+    }
+
+    // Note: r[i] will always be odd.
+    for (int i = 0; i < 256; ++i) {
+      if (r[i] != 0) {
+        for (int b = 1; b <= 6 && i + b < 256; ++b) {
+          // Accumulate bits if possible
+          if (r[i + b] != 0) {
+            if (r[i] + (r[i + b] << b) <= 15) {
+              r[i] += r[i + b] << b;
+              r[i + b] = 0;
+            } else if (r[i] - (r[i + b] << b) >= -15) {
+              r[i] -= r[i + b] << b;
+              for (int k = i + b; k < 256; ++k) {
+                if (r[k] == 0) {
+                  r[k] = 1;
+                  break;
+                }
+                r[k] = 0;
+              }
+            } else break;
+          }
+        }
+      }
+    }
+
+    return r;
+  }
+
+  /**
+   * Verify that a point is on the curve.
+   *
+   * @param curve The curve to check.
+   * @return true if the point lies on the curve.
+   */
+  public boolean isOnCurve(Curve curve) {
+    switch (repr) {
+      case P2:
+      case P3:
+        FieldElement recip = Z.invert();
+        FieldElement x = X.multiply(recip);
+        FieldElement y = Y.multiply(recip);
+        FieldElement xx = x.square();
+        FieldElement yy = y.square();
+        FieldElement dxxyy = curve.getD().multiply(xx).multiply(yy);
+        return curve.getField().ONE.add(dxxyy).add(xx).equals(yy);
+
+      default:
+        return toP2().isOnCurve(curve);
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "[GroupElement\nX=" + X + "\nY=" + Y + "\nZ=" + Z + "\nT=" + T + "\n]";
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/ScalarOps.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/ScalarOps.java
new file mode 100644
index 0000000..c52b7f2
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/ScalarOps.java
@@ -0,0 +1,25 @@
+package org.mariadb.jdbc.plugin.authentication.standard.ed25519.math;
+
+public interface ScalarOps {
+  /**
+   * Reduce the given scalar mod $l$.
+   *
+   * <p>From the Ed25519 paper:<br>
+   * Here we interpret $2b$-bit strings in little-endian form as integers in $\{0, 1,...,
+   * 2^{(2b)}-1\}$.
+   *
+   * @param s the scalar to reduce
+   * @return $s \bmod l$
+   */
+  byte[] reduce(byte[] s);
+
+  /**
+   * $r = (a * b + c) \bmod l$
+   *
+   * @param a a scalar
+   * @param b a scalar
+   * @param c a scalar
+   * @return $(a*b + c) \bmod l$
+   */
+  byte[] multiplyAndAdd(byte[] a, byte[] b, byte[] c);
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/ed25519/Ed25519FieldElement.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/ed25519/Ed25519FieldElement.java
new file mode 100644
index 0000000..37fb78b
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/ed25519/Ed25519FieldElement.java
@@ -0,0 +1,1050 @@
+package org.mariadb.jdbc.plugin.authentication.standard.ed25519.math.ed25519;
+
+import java.util.Arrays;
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.Utils;
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.math.Field;
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.math.FieldElement;
+
+/**
+ * Class to represent a field element of the finite field $p = 2^{255} - 19$ elements.
+ *
+ * <p>An element $t$, entries $t[0] \dots t[9]$, represents the integer $t[0]+2^{26} t[1]+2^{51}
+ * t[2]+2^{77} t[3]+2^{102} t[4]+\dots+2^{230} t[9]$. Bounds on each $t[i]$ vary depending on
+ * context.
+ *
+ * <p>Reviewed/commented by Bloody Rookie (nemproject@gmx.de)
+ */
+public class Ed25519FieldElement extends FieldElement {
+  private static final long serialVersionUID = -2455098303824960263L;
+  /** Variable is package private for encoding. */
+  final int[] t;
+
+  /**
+   * Creates a field element.
+   *
+   * @param f The underlying field, must be the finite field with $p = 2^{255} - 19$ elements
+   * @param t The $2^{25.5}$ bit representation of the field element.
+   */
+  public Ed25519FieldElement(Field f, int[] t) {
+    super(f);
+    if (t.length != 10) throw new IllegalArgumentException("Invalid radix-2^51 representation");
+    this.t = t;
+  }
+
+  private static final byte[] ZERO = new byte[32];
+
+  /**
+   * Gets a value indicating whether or not the field element is non-zero.
+   *
+   * @return 1 if it is non-zero, 0 otherwise.
+   */
+  public boolean isNonZero() {
+    final byte[] s = toByteArray();
+    return Utils.equal(s, ZERO) == 0;
+  }
+
+  /**
+   * $h = f + g$
+   *
+   * <p>TODO-CR BR: $h$ is allocated via new, probably not a good idea. Do we need the copying into
+   * temp variables if we do that?
+   *
+   * <p>Preconditions:
+   *
+   * <ul>
+   *   <li>$|f|$ bounded by $1.1*2^{25},1.1*2^{24},1.1*2^{25},1.1*2^{24},$ etc.
+   *   <li>$|g|$ bounded by $1.1*2^{25},1.1*2^{24},1.1*2^{25},1.1*2^{24},$ etc.
+   * </ul>
+   *
+   * <p>Postconditions:
+   *
+   * <ul>
+   *   <li>$|h|$ bounded by $1.1*2^{26},1.1*2^{25},1.1*2^{26},1.1*2^{25},$ etc.
+   * </ul>
+   *
+   * @param val The field element to add.
+   * @return The field element this + val.
+   */
+  public FieldElement add(FieldElement val) {
+    int[] g = ((Ed25519FieldElement) val).t;
+    int[] h = new int[10];
+    for (int i = 0; i < 10; i++) {
+      h[i] = t[i] + g[i];
+    }
+    return new Ed25519FieldElement(f, h);
+  }
+
+  /**
+   * $h = f - g$
+   *
+   * <p>Can overlap $h$ with $f$ or $g$.
+   *
+   * <p>TODO-CR BR: See above.
+   *
+   * <p>Preconditions:
+   *
+   * <ul>
+   *   <li>$|f|$ bounded by $1.1*2^{25},1.1*2^{24},1.1*2^{25},1.1*2^{24},$ etc.
+   *   <li>$|g|$ bounded by $1.1*2^{25},1.1*2^{24},1.1*2^{25},1.1*2^{24},$ etc.
+   * </ul>
+   *
+   * <p>Postconditions:
+   *
+   * <ul>
+   *   <li>$|h|$ bounded by $1.1*2^{26},1.1*2^{25},1.1*2^{26},1.1*2^{25},$ etc.
+   * </ul>
+   *
+   * @param val The field element to subtract.
+   * @return The field element this - val.
+   */
+  public FieldElement subtract(FieldElement val) {
+    int[] g = ((Ed25519FieldElement) val).t;
+    int[] h = new int[10];
+    for (int i = 0; i < 10; i++) {
+      h[i] = t[i] - g[i];
+    }
+    return new Ed25519FieldElement(f, h);
+  }
+
+  /**
+   * $h = -f$
+   *
+   * <p>TODO-CR BR: see above.
+   *
+   * <p>Preconditions:
+   *
+   * <ul>
+   *   <li>$|f|$ bounded by $1.1*2^{25},1.1*2^{24},1.1*2^{25},1.1*2^{24},$ etc.
+   * </ul>
+   *
+   * <p>Postconditions:
+   *
+   * <ul>
+   *   <li>$|h|$ bounded by $1.1*2^{25},1.1*2^{24},1.1*2^{25},1.1*2^{24},$ etc.
+   * </ul>
+   *
+   * @return The field element (-1) * this.
+   */
+  public FieldElement negate() {
+    int[] h = new int[10];
+    for (int i = 0; i < 10; i++) {
+      h[i] = -t[i];
+    }
+    return new Ed25519FieldElement(f, h);
+  }
+
+  /**
+   * $h = f * g$
+   *
+   * <p>Can overlap $h$ with $f$ or $g$.
+   *
+   * <p>Preconditions:
+   *
+   * <ul>
+   *   <li>$|f|$ bounded by $1.65*2^{26},1.65*2^{25},1.65*2^{26},1.65*2^{25},$ etc.
+   *   <li>$|g|$ bounded by $1.65*2^{26},1.65*2^{25},1.65*2^{26},1.65*2^{25},$ etc.
+   * </ul>
+   *
+   * <p>Postconditions:
+   *
+   * <ul>
+   *   <li>$|h|$ bounded by $1.01*2^{25},1.01*2^{24},1.01*2^{25},1.01*2^{24},$ etc.
+   * </ul>
+   *
+   * <p>Notes on implementation strategy:
+   *
+   * <p>Using schoolbook multiplication. Karatsuba would save a little in some cost models.
+   *
+   * <p>Most multiplications by 2 and 19 are 32-bit precomputations; cheaper than 64-bit
+   * postcomputations.
+   *
+   * <p>There is one remaining multiplication by 19 in the carry chain; one *19 precomputation can
+   * be merged into this, but the resulting data flow is considerably less clean.
+   *
+   * <p>There are 12 carries below. 10 of them are 2-way parallelizable and vectorizable. Can get
+   * away with 11 carries, but then data flow is much deeper.
+   *
+   * <p>With tighter constraints on inputs can squeeze carries into int32.
+   *
+   * @param val The field element to multiply.
+   * @return The (reasonably reduced) field element this * val.
+   */
+  public FieldElement multiply(FieldElement val) {
+    int[] g = ((Ed25519FieldElement) val).t;
+    int g1_19 = 19 * g[1]; /* 1.959375*2^29 */
+    int g2_19 = 19 * g[2]; /* 1.959375*2^30; still ok */
+    int g3_19 = 19 * g[3];
+    int g4_19 = 19 * g[4];
+    int g5_19 = 19 * g[5];
+    int g6_19 = 19 * g[6];
+    int g7_19 = 19 * g[7];
+    int g8_19 = 19 * g[8];
+    int g9_19 = 19 * g[9];
+    int f1_2 = 2 * t[1];
+    int f3_2 = 2 * t[3];
+    int f5_2 = 2 * t[5];
+    int f7_2 = 2 * t[7];
+    int f9_2 = 2 * t[9];
+    long f0g0 = t[0] * (long) g[0];
+    long f0g1 = t[0] * (long) g[1];
+    long f0g2 = t[0] * (long) g[2];
+    long f0g3 = t[0] * (long) g[3];
+    long f0g4 = t[0] * (long) g[4];
+    long f0g5 = t[0] * (long) g[5];
+    long f0g6 = t[0] * (long) g[6];
+    long f0g7 = t[0] * (long) g[7];
+    long f0g8 = t[0] * (long) g[8];
+    long f0g9 = t[0] * (long) g[9];
+    long f1g0 = t[1] * (long) g[0];
+    long f1g1_2 = f1_2 * (long) g[1];
+    long f1g2 = t[1] * (long) g[2];
+    long f1g3_2 = f1_2 * (long) g[3];
+    long f1g4 = t[1] * (long) g[4];
+    long f1g5_2 = f1_2 * (long) g[5];
+    long f1g6 = t[1] * (long) g[6];
+    long f1g7_2 = f1_2 * (long) g[7];
+    long f1g8 = t[1] * (long) g[8];
+    long f1g9_38 = f1_2 * (long) g9_19;
+    long f2g0 = t[2] * (long) g[0];
+    long f2g1 = t[2] * (long) g[1];
+    long f2g2 = t[2] * (long) g[2];
+    long f2g3 = t[2] * (long) g[3];
+    long f2g4 = t[2] * (long) g[4];
+    long f2g5 = t[2] * (long) g[5];
+    long f2g6 = t[2] * (long) g[6];
+    long f2g7 = t[2] * (long) g[7];
+    long f2g8_19 = t[2] * (long) g8_19;
+    long f2g9_19 = t[2] * (long) g9_19;
+    long f3g0 = t[3] * (long) g[0];
+    long f3g1_2 = f3_2 * (long) g[1];
+    long f3g2 = t[3] * (long) g[2];
+    long f3g3_2 = f3_2 * (long) g[3];
+    long f3g4 = t[3] * (long) g[4];
+    long f3g5_2 = f3_2 * (long) g[5];
+    long f3g6 = t[3] * (long) g[6];
+    long f3g7_38 = f3_2 * (long) g7_19;
+    long f3g8_19 = t[3] * (long) g8_19;
+    long f3g9_38 = f3_2 * (long) g9_19;
+    long f4g0 = t[4] * (long) g[0];
+    long f4g1 = t[4] * (long) g[1];
+    long f4g2 = t[4] * (long) g[2];
+    long f4g3 = t[4] * (long) g[3];
+    long f4g4 = t[4] * (long) g[4];
+    long f4g5 = t[4] * (long) g[5];
+    long f4g6_19 = t[4] * (long) g6_19;
+    long f4g7_19 = t[4] * (long) g7_19;
+    long f4g8_19 = t[4] * (long) g8_19;
+    long f4g9_19 = t[4] * (long) g9_19;
+    long f5g0 = t[5] * (long) g[0];
+    long f5g1_2 = f5_2 * (long) g[1];
+    long f5g2 = t[5] * (long) g[2];
+    long f5g3_2 = f5_2 * (long) g[3];
+    long f5g4 = t[5] * (long) g[4];
+    long f5g5_38 = f5_2 * (long) g5_19;
+    long f5g6_19 = t[5] * (long) g6_19;
+    long f5g7_38 = f5_2 * (long) g7_19;
+    long f5g8_19 = t[5] * (long) g8_19;
+    long f5g9_38 = f5_2 * (long) g9_19;
+    long f6g0 = t[6] * (long) g[0];
+    long f6g1 = t[6] * (long) g[1];
+    long f6g2 = t[6] * (long) g[2];
+    long f6g3 = t[6] * (long) g[3];
+    long f6g4_19 = t[6] * (long) g4_19;
+    long f6g5_19 = t[6] * (long) g5_19;
+    long f6g6_19 = t[6] * (long) g6_19;
+    long f6g7_19 = t[6] * (long) g7_19;
+    long f6g8_19 = t[6] * (long) g8_19;
+    long f6g9_19 = t[6] * (long) g9_19;
+    long f7g0 = t[7] * (long) g[0];
+    long f7g1_2 = f7_2 * (long) g[1];
+    long f7g2 = t[7] * (long) g[2];
+    long f7g3_38 = f7_2 * (long) g3_19;
+    long f7g4_19 = t[7] * (long) g4_19;
+    long f7g5_38 = f7_2 * (long) g5_19;
+    long f7g6_19 = t[7] * (long) g6_19;
+    long f7g7_38 = f7_2 * (long) g7_19;
+    long f7g8_19 = t[7] * (long) g8_19;
+    long f7g9_38 = f7_2 * (long) g9_19;
+    long f8g0 = t[8] * (long) g[0];
+    long f8g1 = t[8] * (long) g[1];
+    long f8g2_19 = t[8] * (long) g2_19;
+    long f8g3_19 = t[8] * (long) g3_19;
+    long f8g4_19 = t[8] * (long) g4_19;
+    long f8g5_19 = t[8] * (long) g5_19;
+    long f8g6_19 = t[8] * (long) g6_19;
+    long f8g7_19 = t[8] * (long) g7_19;
+    long f8g8_19 = t[8] * (long) g8_19;
+    long f8g9_19 = t[8] * (long) g9_19;
+    long f9g0 = t[9] * (long) g[0];
+    long f9g1_38 = f9_2 * (long) g1_19;
+    long f9g2_19 = t[9] * (long) g2_19;
+    long f9g3_38 = f9_2 * (long) g3_19;
+    long f9g4_19 = t[9] * (long) g4_19;
+    long f9g5_38 = f9_2 * (long) g5_19;
+    long f9g6_19 = t[9] * (long) g6_19;
+    long f9g7_38 = f9_2 * (long) g7_19;
+    long f9g8_19 = t[9] * (long) g8_19;
+    long f9g9_38 = f9_2 * (long) g9_19;
+
+    long h0 =
+        f0g0 + f1g9_38 + f2g8_19 + f3g7_38 + f4g6_19 + f5g5_38 + f6g4_19 + f7g3_38 + f8g2_19
+            + f9g1_38;
+    long h1 =
+        f0g1 + f1g0 + f2g9_19 + f3g8_19 + f4g7_19 + f5g6_19 + f6g5_19 + f7g4_19 + f8g3_19 + f9g2_19;
+    long h2 =
+        f0g2 + f1g1_2 + f2g0 + f3g9_38 + f4g8_19 + f5g7_38 + f6g6_19 + f7g5_38 + f8g4_19 + f9g3_38;
+    long h3 = f0g3 + f1g2 + f2g1 + f3g0 + f4g9_19 + f5g8_19 + f6g7_19 + f7g6_19 + f8g5_19 + f9g4_19;
+    long h4 =
+        f0g4 + f1g3_2 + f2g2 + f3g1_2 + f4g0 + f5g9_38 + f6g8_19 + f7g7_38 + f8g6_19 + f9g5_38;
+    long h5 = f0g5 + f1g4 + f2g3 + f3g2 + f4g1 + f5g0 + f6g9_19 + f7g8_19 + f8g7_19 + f9g6_19;
+    long h6 = f0g6 + f1g5_2 + f2g4 + f3g3_2 + f4g2 + f5g1_2 + f6g0 + f7g9_38 + f8g8_19 + f9g7_38;
+    long h7 = f0g7 + f1g6 + f2g5 + f3g4 + f4g3 + f5g2 + f6g1 + f7g0 + f8g9_19 + f9g8_19;
+    long h8 = f0g8 + f1g7_2 + f2g6 + f3g5_2 + f4g4 + f5g3_2 + f6g2 + f7g1_2 + f8g0 + f9g9_38;
+    long h9 = f0g9 + f1g8 + f2g7 + f3g6 + f4g5 + f5g4 + f6g3 + f7g2 + f8g1 + f9g0;
+    long carry0;
+    long carry1;
+    long carry2;
+    long carry3;
+    long carry4;
+    long carry5;
+    long carry6;
+    long carry7;
+    long carry8;
+    long carry9;
+
+    /*
+    |h0| <= (1.65*1.65*2^52*(1+19+19+19+19)+1.65*1.65*2^50*(38+38+38+38+38))
+      i.e. |h0| <= 1.4*2^60; narrower ranges for h2, h4, h6, h8
+    |h1| <= (1.65*1.65*2^51*(1+1+19+19+19+19+19+19+19+19))
+      i.e. |h1| <= 1.7*2^59; narrower ranges for h3, h5, h7, h9
+    */
+
+    carry0 = (h0 + (long) (1 << 25)) >> 26;
+    h1 += carry0;
+    h0 -= carry0 << 26;
+    carry4 = (h4 + (long) (1 << 25)) >> 26;
+    h5 += carry4;
+    h4 -= carry4 << 26;
+    /* |h0| <= 2^25 */
+    /* |h4| <= 2^25 */
+    /* |h1| <= 1.71*2^59 */
+    /* |h5| <= 1.71*2^59 */
+
+    carry1 = (h1 + (long) (1 << 24)) >> 25;
+    h2 += carry1;
+    h1 -= carry1 << 25;
+    carry5 = (h5 + (long) (1 << 24)) >> 25;
+    h6 += carry5;
+    h5 -= carry5 << 25;
+    /* |h1| <= 2^24; from now on fits into int32 */
+    /* |h5| <= 2^24; from now on fits into int32 */
+    /* |h2| <= 1.41*2^60 */
+    /* |h6| <= 1.41*2^60 */
+
+    carry2 = (h2 + (long) (1 << 25)) >> 26;
+    h3 += carry2;
+    h2 -= carry2 << 26;
+    carry6 = (h6 + (long) (1 << 25)) >> 26;
+    h7 += carry6;
+    h6 -= carry6 << 26;
+    /* |h2| <= 2^25; from now on fits into int32 unchanged */
+    /* |h6| <= 2^25; from now on fits into int32 unchanged */
+    /* |h3| <= 1.71*2^59 */
+    /* |h7| <= 1.71*2^59 */
+
+    carry3 = (h3 + (long) (1 << 24)) >> 25;
+    h4 += carry3;
+    h3 -= carry3 << 25;
+    carry7 = (h7 + (long) (1 << 24)) >> 25;
+    h8 += carry7;
+    h7 -= carry7 << 25;
+    /* |h3| <= 2^24; from now on fits into int32 unchanged */
+    /* |h7| <= 2^24; from now on fits into int32 unchanged */
+    /* |h4| <= 1.72*2^34 */
+    /* |h8| <= 1.41*2^60 */
+
+    carry4 = (h4 + (long) (1 << 25)) >> 26;
+    h5 += carry4;
+    h4 -= carry4 << 26;
+    carry8 = (h8 + (long) (1 << 25)) >> 26;
+    h9 += carry8;
+    h8 -= carry8 << 26;
+    /* |h4| <= 2^25; from now on fits into int32 unchanged */
+    /* |h8| <= 2^25; from now on fits into int32 unchanged */
+    /* |h5| <= 1.01*2^24 */
+    /* |h9| <= 1.71*2^59 */
+
+    carry9 = (h9 + (long) (1 << 24)) >> 25;
+    h0 += carry9 * 19;
+    h9 -= carry9 << 25;
+    /* |h9| <= 2^24; from now on fits into int32 unchanged */
+    /* |h0| <= 1.1*2^39 */
+
+    carry0 = (h0 + (long) (1 << 25)) >> 26;
+    h1 += carry0;
+    h0 -= carry0 << 26;
+    /* |h0| <= 2^25; from now on fits into int32 unchanged */
+    /* |h1| <= 1.01*2^24 */
+
+    int[] h = new int[10];
+    h[0] = (int) h0;
+    h[1] = (int) h1;
+    h[2] = (int) h2;
+    h[3] = (int) h3;
+    h[4] = (int) h4;
+    h[5] = (int) h5;
+    h[6] = (int) h6;
+    h[7] = (int) h7;
+    h[8] = (int) h8;
+    h[9] = (int) h9;
+    return new Ed25519FieldElement(f, h);
+  }
+
+  /**
+   * $h = f * f$
+   *
+   * <p>Can overlap $h$ with $f$.
+   *
+   * <p>Preconditions:
+   *
+   * <ul>
+   *   <li>$|f|$ bounded by $1.65*2^{26},1.65*2^{25},1.65*2^{26},1.65*2^{25},$ etc.
+   * </ul>
+   *
+   * <p>Postconditions:
+   *
+   * <ul>
+   *   <li>$|h|$ bounded by $1.01*2^{25},1.01*2^{24},1.01*2^{25},1.01*2^{24},$ etc.
+   * </ul>
+   *
+   * <p>See {@link #multiply(FieldElement)} for discussion of implementation strategy.
+   *
+   * @return The (reasonably reduced) square of this field element.
+   */
+  public FieldElement square() {
+    int f0 = t[0];
+    int f1 = t[1];
+    int f2 = t[2];
+    int f3 = t[3];
+    int f4 = t[4];
+    int f5 = t[5];
+    int f6 = t[6];
+    int f7 = t[7];
+    int f8 = t[8];
+    int f9 = t[9];
+    int f0_2 = 2 * f0;
+    int f1_2 = 2 * f1;
+    int f2_2 = 2 * f2;
+    int f3_2 = 2 * f3;
+    int f4_2 = 2 * f4;
+    int f5_2 = 2 * f5;
+    int f6_2 = 2 * f6;
+    int f7_2 = 2 * f7;
+    int f5_38 = 38 * f5; /* 1.959375*2^30 */
+    int f6_19 = 19 * f6; /* 1.959375*2^30 */
+    int f7_38 = 38 * f7; /* 1.959375*2^30 */
+    int f8_19 = 19 * f8; /* 1.959375*2^30 */
+    int f9_38 = 38 * f9; /* 1.959375*2^30 */
+    long f0f0 = f0 * (long) f0;
+    long f0f1_2 = f0_2 * (long) f1;
+    long f0f2_2 = f0_2 * (long) f2;
+    long f0f3_2 = f0_2 * (long) f3;
+    long f0f4_2 = f0_2 * (long) f4;
+    long f0f5_2 = f0_2 * (long) f5;
+    long f0f6_2 = f0_2 * (long) f6;
+    long f0f7_2 = f0_2 * (long) f7;
+    long f0f8_2 = f0_2 * (long) f8;
+    long f0f9_2 = f0_2 * (long) f9;
+    long f1f1_2 = f1_2 * (long) f1;
+    long f1f2_2 = f1_2 * (long) f2;
+    long f1f3_4 = f1_2 * (long) f3_2;
+    long f1f4_2 = f1_2 * (long) f4;
+    long f1f5_4 = f1_2 * (long) f5_2;
+    long f1f6_2 = f1_2 * (long) f6;
+    long f1f7_4 = f1_2 * (long) f7_2;
+    long f1f8_2 = f1_2 * (long) f8;
+    long f1f9_76 = f1_2 * (long) f9_38;
+    long f2f2 = f2 * (long) f2;
+    long f2f3_2 = f2_2 * (long) f3;
+    long f2f4_2 = f2_2 * (long) f4;
+    long f2f5_2 = f2_2 * (long) f5;
+    long f2f6_2 = f2_2 * (long) f6;
+    long f2f7_2 = f2_2 * (long) f7;
+    long f2f8_38 = f2_2 * (long) f8_19;
+    long f2f9_38 = f2 * (long) f9_38;
+    long f3f3_2 = f3_2 * (long) f3;
+    long f3f4_2 = f3_2 * (long) f4;
+    long f3f5_4 = f3_2 * (long) f5_2;
+    long f3f6_2 = f3_2 * (long) f6;
+    long f3f7_76 = f3_2 * (long) f7_38;
+    long f3f8_38 = f3_2 * (long) f8_19;
+    long f3f9_76 = f3_2 * (long) f9_38;
+    long f4f4 = f4 * (long) f4;
+    long f4f5_2 = f4_2 * (long) f5;
+    long f4f6_38 = f4_2 * (long) f6_19;
+    long f4f7_38 = f4 * (long) f7_38;
+    long f4f8_38 = f4_2 * (long) f8_19;
+    long f4f9_38 = f4 * (long) f9_38;
+    long f5f5_38 = f5 * (long) f5_38;
+    long f5f6_38 = f5_2 * (long) f6_19;
+    long f5f7_76 = f5_2 * (long) f7_38;
+    long f5f8_38 = f5_2 * (long) f8_19;
+    long f5f9_76 = f5_2 * (long) f9_38;
+    long f6f6_19 = f6 * (long) f6_19;
+    long f6f7_38 = f6 * (long) f7_38;
+    long f6f8_38 = f6_2 * (long) f8_19;
+    long f6f9_38 = f6 * (long) f9_38;
+    long f7f7_38 = f7 * (long) f7_38;
+    long f7f8_38 = f7_2 * (long) f8_19;
+    long f7f9_76 = f7_2 * (long) f9_38;
+    long f8f8_19 = f8 * (long) f8_19;
+    long f8f9_38 = f8 * (long) f9_38;
+    long f9f9_38 = f9 * (long) f9_38;
+
+    long h0 = f0f0 + f1f9_76 + f2f8_38 + f3f7_76 + f4f6_38 + f5f5_38;
+    long h1 = f0f1_2 + f2f9_38 + f3f8_38 + f4f7_38 + f5f6_38;
+    long h2 = f0f2_2 + f1f1_2 + f3f9_76 + f4f8_38 + f5f7_76 + f6f6_19;
+    long h3 = f0f3_2 + f1f2_2 + f4f9_38 + f5f8_38 + f6f7_38;
+    long h4 = f0f4_2 + f1f3_4 + f2f2 + f5f9_76 + f6f8_38 + f7f7_38;
+    long h5 = f0f5_2 + f1f4_2 + f2f3_2 + f6f9_38 + f7f8_38;
+    long h6 = f0f6_2 + f1f5_4 + f2f4_2 + f3f3_2 + f7f9_76 + f8f8_19;
+    long h7 = f0f7_2 + f1f6_2 + f2f5_2 + f3f4_2 + f8f9_38;
+    long h8 = f0f8_2 + f1f7_4 + f2f6_2 + f3f5_4 + f4f4 + f9f9_38;
+    long h9 = f0f9_2 + f1f8_2 + f2f7_2 + f3f6_2 + f4f5_2;
+    long carry0;
+    long carry1;
+    long carry2;
+    long carry3;
+    long carry4;
+    long carry5;
+    long carry6;
+    long carry7;
+    long carry8;
+    long carry9;
+
+    carry0 = (h0 + (long) (1 << 25)) >> 26;
+    h1 += carry0;
+    h0 -= carry0 << 26;
+    carry4 = (h4 + (long) (1 << 25)) >> 26;
+    h5 += carry4;
+    h4 -= carry4 << 26;
+
+    carry1 = (h1 + (long) (1 << 24)) >> 25;
+    h2 += carry1;
+    h1 -= carry1 << 25;
+    carry5 = (h5 + (long) (1 << 24)) >> 25;
+    h6 += carry5;
+    h5 -= carry5 << 25;
+
+    carry2 = (h2 + (long) (1 << 25)) >> 26;
+    h3 += carry2;
+    h2 -= carry2 << 26;
+    carry6 = (h6 + (long) (1 << 25)) >> 26;
+    h7 += carry6;
+    h6 -= carry6 << 26;
+
+    carry3 = (h3 + (long) (1 << 24)) >> 25;
+    h4 += carry3;
+    h3 -= carry3 << 25;
+    carry7 = (h7 + (long) (1 << 24)) >> 25;
+    h8 += carry7;
+    h7 -= carry7 << 25;
+
+    carry4 = (h4 + (long) (1 << 25)) >> 26;
+    h5 += carry4;
+    h4 -= carry4 << 26;
+    carry8 = (h8 + (long) (1 << 25)) >> 26;
+    h9 += carry8;
+    h8 -= carry8 << 26;
+
+    carry9 = (h9 + (long) (1 << 24)) >> 25;
+    h0 += carry9 * 19;
+    h9 -= carry9 << 25;
+
+    carry0 = (h0 + (long) (1 << 25)) >> 26;
+    h1 += carry0;
+    h0 -= carry0 << 26;
+
+    int[] h = new int[10];
+    h[0] = (int) h0;
+    h[1] = (int) h1;
+    h[2] = (int) h2;
+    h[3] = (int) h3;
+    h[4] = (int) h4;
+    h[5] = (int) h5;
+    h[6] = (int) h6;
+    h[7] = (int) h7;
+    h[8] = (int) h8;
+    h[9] = (int) h9;
+    return new Ed25519FieldElement(f, h);
+  }
+
+  /**
+   * $h = 2 * f * f$
+   *
+   * <p>Can overlap $h$ with $f$.
+   *
+   * <p>Preconditions:
+   *
+   * <ul>
+   *   <li>$|f|$ bounded by $1.65*2^{26},1.65*2^{25},1.65*2^{26},1.65*2^{25},$ etc.
+   * </ul>
+   *
+   * <p>Postconditions:
+   *
+   * <ul>
+   *   <li>$|h|$ bounded by $1.01*2^{25},1.01*2^{24},1.01*2^{25},1.01*2^{24},$ etc.
+   * </ul>
+   *
+   * <p>See {@link #multiply(FieldElement)} for discussion of implementation strategy.
+   *
+   * @return The (reasonably reduced) square of this field element times 2.
+   */
+  public FieldElement squareAndDouble() {
+    int f0 = t[0];
+    int f1 = t[1];
+    int f2 = t[2];
+    int f3 = t[3];
+    int f4 = t[4];
+    int f5 = t[5];
+    int f6 = t[6];
+    int f7 = t[7];
+    int f8 = t[8];
+    int f9 = t[9];
+    int f0_2 = 2 * f0;
+    int f1_2 = 2 * f1;
+    int f2_2 = 2 * f2;
+    int f3_2 = 2 * f3;
+    int f4_2 = 2 * f4;
+    int f5_2 = 2 * f5;
+    int f6_2 = 2 * f6;
+    int f7_2 = 2 * f7;
+    int f5_38 = 38 * f5; /* 1.959375*2^30 */
+    int f6_19 = 19 * f6; /* 1.959375*2^30 */
+    int f7_38 = 38 * f7; /* 1.959375*2^30 */
+    int f8_19 = 19 * f8; /* 1.959375*2^30 */
+    int f9_38 = 38 * f9; /* 1.959375*2^30 */
+    long f0f0 = f0 * (long) f0;
+    long f0f1_2 = f0_2 * (long) f1;
+    long f0f2_2 = f0_2 * (long) f2;
+    long f0f3_2 = f0_2 * (long) f3;
+    long f0f4_2 = f0_2 * (long) f4;
+    long f0f5_2 = f0_2 * (long) f5;
+    long f0f6_2 = f0_2 * (long) f6;
+    long f0f7_2 = f0_2 * (long) f7;
+    long f0f8_2 = f0_2 * (long) f8;
+    long f0f9_2 = f0_2 * (long) f9;
+    long f1f1_2 = f1_2 * (long) f1;
+    long f1f2_2 = f1_2 * (long) f2;
+    long f1f3_4 = f1_2 * (long) f3_2;
+    long f1f4_2 = f1_2 * (long) f4;
+    long f1f5_4 = f1_2 * (long) f5_2;
+    long f1f6_2 = f1_2 * (long) f6;
+    long f1f7_4 = f1_2 * (long) f7_2;
+    long f1f8_2 = f1_2 * (long) f8;
+    long f1f9_76 = f1_2 * (long) f9_38;
+    long f2f2 = f2 * (long) f2;
+    long f2f3_2 = f2_2 * (long) f3;
+    long f2f4_2 = f2_2 * (long) f4;
+    long f2f5_2 = f2_2 * (long) f5;
+    long f2f6_2 = f2_2 * (long) f6;
+    long f2f7_2 = f2_2 * (long) f7;
+    long f2f8_38 = f2_2 * (long) f8_19;
+    long f2f9_38 = f2 * (long) f9_38;
+    long f3f3_2 = f3_2 * (long) f3;
+    long f3f4_2 = f3_2 * (long) f4;
+    long f3f5_4 = f3_2 * (long) f5_2;
+    long f3f6_2 = f3_2 * (long) f6;
+    long f3f7_76 = f3_2 * (long) f7_38;
+    long f3f8_38 = f3_2 * (long) f8_19;
+    long f3f9_76 = f3_2 * (long) f9_38;
+    long f4f4 = f4 * (long) f4;
+    long f4f5_2 = f4_2 * (long) f5;
+    long f4f6_38 = f4_2 * (long) f6_19;
+    long f4f7_38 = f4 * (long) f7_38;
+    long f4f8_38 = f4_2 * (long) f8_19;
+    long f4f9_38 = f4 * (long) f9_38;
+    long f5f5_38 = f5 * (long) f5_38;
+    long f5f6_38 = f5_2 * (long) f6_19;
+    long f5f7_76 = f5_2 * (long) f7_38;
+    long f5f8_38 = f5_2 * (long) f8_19;
+    long f5f9_76 = f5_2 * (long) f9_38;
+    long f6f6_19 = f6 * (long) f6_19;
+    long f6f7_38 = f6 * (long) f7_38;
+    long f6f8_38 = f6_2 * (long) f8_19;
+    long f6f9_38 = f6 * (long) f9_38;
+    long f7f7_38 = f7 * (long) f7_38;
+    long f7f8_38 = f7_2 * (long) f8_19;
+    long f7f9_76 = f7_2 * (long) f9_38;
+    long f8f8_19 = f8 * (long) f8_19;
+    long f8f9_38 = f8 * (long) f9_38;
+    long f9f9_38 = f9 * (long) f9_38;
+    long h0 = f0f0 + f1f9_76 + f2f8_38 + f3f7_76 + f4f6_38 + f5f5_38;
+    long h1 = f0f1_2 + f2f9_38 + f3f8_38 + f4f7_38 + f5f6_38;
+    long h2 = f0f2_2 + f1f1_2 + f3f9_76 + f4f8_38 + f5f7_76 + f6f6_19;
+    long h3 = f0f3_2 + f1f2_2 + f4f9_38 + f5f8_38 + f6f7_38;
+    long h4 = f0f4_2 + f1f3_4 + f2f2 + f5f9_76 + f6f8_38 + f7f7_38;
+    long h5 = f0f5_2 + f1f4_2 + f2f3_2 + f6f9_38 + f7f8_38;
+    long h6 = f0f6_2 + f1f5_4 + f2f4_2 + f3f3_2 + f7f9_76 + f8f8_19;
+    long h7 = f0f7_2 + f1f6_2 + f2f5_2 + f3f4_2 + f8f9_38;
+    long h8 = f0f8_2 + f1f7_4 + f2f6_2 + f3f5_4 + f4f4 + f9f9_38;
+    long h9 = f0f9_2 + f1f8_2 + f2f7_2 + f3f6_2 + f4f5_2;
+    long carry0;
+    long carry1;
+    long carry2;
+    long carry3;
+    long carry4;
+    long carry5;
+    long carry6;
+    long carry7;
+    long carry8;
+    long carry9;
+
+    h0 += h0;
+    h1 += h1;
+    h2 += h2;
+    h3 += h3;
+    h4 += h4;
+    h5 += h5;
+    h6 += h6;
+    h7 += h7;
+    h8 += h8;
+    h9 += h9;
+
+    carry0 = (h0 + (long) (1 << 25)) >> 26;
+    h1 += carry0;
+    h0 -= carry0 << 26;
+    carry4 = (h4 + (long) (1 << 25)) >> 26;
+    h5 += carry4;
+    h4 -= carry4 << 26;
+
+    carry1 = (h1 + (long) (1 << 24)) >> 25;
+    h2 += carry1;
+    h1 -= carry1 << 25;
+    carry5 = (h5 + (long) (1 << 24)) >> 25;
+    h6 += carry5;
+    h5 -= carry5 << 25;
+
+    carry2 = (h2 + (long) (1 << 25)) >> 26;
+    h3 += carry2;
+    h2 -= carry2 << 26;
+    carry6 = (h6 + (long) (1 << 25)) >> 26;
+    h7 += carry6;
+    h6 -= carry6 << 26;
+
+    carry3 = (h3 + (long) (1 << 24)) >> 25;
+    h4 += carry3;
+    h3 -= carry3 << 25;
+    carry7 = (h7 + (long) (1 << 24)) >> 25;
+    h8 += carry7;
+    h7 -= carry7 << 25;
+
+    carry4 = (h4 + (long) (1 << 25)) >> 26;
+    h5 += carry4;
+    h4 -= carry4 << 26;
+    carry8 = (h8 + (long) (1 << 25)) >> 26;
+    h9 += carry8;
+    h8 -= carry8 << 26;
+
+    carry9 = (h9 + (long) (1 << 24)) >> 25;
+    h0 += carry9 * 19;
+    h9 -= carry9 << 25;
+
+    carry0 = (h0 + (long) (1 << 25)) >> 26;
+    h1 += carry0;
+    h0 -= carry0 << 26;
+
+    int[] h = new int[10];
+    h[0] = (int) h0;
+    h[1] = (int) h1;
+    h[2] = (int) h2;
+    h[3] = (int) h3;
+    h[4] = (int) h4;
+    h[5] = (int) h5;
+    h[6] = (int) h6;
+    h[7] = (int) h7;
+    h[8] = (int) h8;
+    h[9] = (int) h9;
+    return new Ed25519FieldElement(f, h);
+  }
+
+  /**
+   * Invert this field element.
+   *
+   * <p>The inverse is found via Fermat's little theorem:<br>
+   * $a^p \cong a \mod p$ and therefore $a^{(p-2)} \cong a^{-1} \mod p$
+   *
+   * @return The inverse of this field element.
+   */
+  public FieldElement invert() {
+    FieldElement t0, t1, t2, t3;
+
+    // 2 == 2 * 1
+    t0 = square();
+
+    // 4 == 2 * 2
+    t1 = t0.square();
+
+    // 8 == 2 * 4
+    t1 = t1.square();
+
+    // 9 == 8 + 1
+    t1 = multiply(t1);
+
+    // 11 == 9 + 2
+    t0 = t0.multiply(t1);
+
+    // 22 == 2 * 11
+    t2 = t0.square();
+
+    // 31 == 22 + 9
+    t1 = t1.multiply(t2);
+
+    // 2^6 - 2^1
+    t2 = t1.square();
+
+    // 2^10 - 2^5
+    for (int i = 1; i < 5; ++i) {
+      t2 = t2.square();
+    }
+
+    // 2^10 - 2^0
+    t1 = t2.multiply(t1);
+
+    // 2^11 - 2^1
+    t2 = t1.square();
+
+    // 2^20 - 2^10
+    for (int i = 1; i < 10; ++i) {
+      t2 = t2.square();
+    }
+
+    // 2^20 - 2^0
+    t2 = t2.multiply(t1);
+
+    // 2^21 - 2^1
+    t3 = t2.square();
+
+    // 2^40 - 2^20
+    for (int i = 1; i < 20; ++i) {
+      t3 = t3.square();
+    }
+
+    // 2^40 - 2^0
+    t2 = t3.multiply(t2);
+
+    // 2^41 - 2^1
+    t2 = t2.square();
+
+    // 2^50 - 2^10
+    for (int i = 1; i < 10; ++i) {
+      t2 = t2.square();
+    }
+
+    // 2^50 - 2^0
+    t1 = t2.multiply(t1);
+
+    // 2^51 - 2^1
+    t2 = t1.square();
+
+    // 2^100 - 2^50
+    for (int i = 1; i < 50; ++i) {
+      t2 = t2.square();
+    }
+
+    // 2^100 - 2^0
+    t2 = t2.multiply(t1);
+
+    // 2^101 - 2^1
+    t3 = t2.square();
+
+    // 2^200 - 2^100
+    for (int i = 1; i < 100; ++i) {
+      t3 = t3.square();
+    }
+
+    // 2^200 - 2^0
+    t2 = t3.multiply(t2);
+
+    // 2^201 - 2^1
+    t2 = t2.square();
+
+    // 2^250 - 2^50
+    for (int i = 1; i < 50; ++i) {
+      t2 = t2.square();
+    }
+
+    // 2^250 - 2^0
+    t1 = t2.multiply(t1);
+
+    // 2^251 - 2^1
+    t1 = t1.square();
+
+    // 2^255 - 2^5
+    for (int i = 1; i < 5; ++i) {
+      t1 = t1.square();
+    }
+
+    // 2^255 - 21
+    return t1.multiply(t0);
+  }
+
+  /**
+   * Gets this field element to the power of $(2^{252} - 3)$. This is a helper function for
+   * calculating the square root.
+   *
+   * <p>TODO-CR BR: I think it makes sense to have a sqrt function.
+   *
+   * @return This field element to the power of $(2^{252} - 3)$.
+   */
+  public FieldElement pow22523() {
+    FieldElement t0, t1, t2;
+
+    // 2 == 2 * 1
+    t0 = square();
+
+    // 4 == 2 * 2
+    t1 = t0.square();
+
+    // 8 == 2 * 4
+    t1 = t1.square();
+
+    // z9 = z1*z8
+    t1 = multiply(t1);
+
+    // 11 == 9 + 2
+    t0 = t0.multiply(t1);
+
+    // 22 == 2 * 11
+    t0 = t0.square();
+
+    // 31 == 22 + 9
+    t0 = t1.multiply(t0);
+
+    // 2^6 - 2^1
+    t1 = t0.square();
+
+    // 2^10 - 2^5
+    for (int i = 1; i < 5; ++i) {
+      t1 = t1.square();
+    }
+
+    // 2^10 - 2^0
+    t0 = t1.multiply(t0);
+
+    // 2^11 - 2^1
+    t1 = t0.square();
+
+    // 2^20 - 2^10
+    for (int i = 1; i < 10; ++i) {
+      t1 = t1.square();
+    }
+
+    // 2^20 - 2^0
+    t1 = t1.multiply(t0);
+
+    // 2^21 - 2^1
+    t2 = t1.square();
+
+    // 2^40 - 2^20
+    for (int i = 1; i < 20; ++i) {
+      t2 = t2.square();
+    }
+
+    // 2^40 - 2^0
+    t1 = t2.multiply(t1);
+
+    // 2^41 - 2^1
+    t1 = t1.square();
+
+    // 2^50 - 2^10
+    for (int i = 1; i < 10; ++i) {
+      t1 = t1.square();
+    }
+
+    // 2^50 - 2^0
+    t0 = t1.multiply(t0);
+
+    // 2^51 - 2^1
+    t1 = t0.square();
+
+    // 2^100 - 2^50
+    for (int i = 1; i < 50; ++i) {
+      t1 = t1.square();
+    }
+
+    // 2^100 - 2^0
+    t1 = t1.multiply(t0);
+
+    // 2^101 - 2^1
+    t2 = t1.square();
+
+    // 2^200 - 2^100
+    for (int i = 1; i < 100; ++i) {
+      t2 = t2.square();
+    }
+
+    // 2^200 - 2^0
+    t1 = t2.multiply(t1);
+
+    // 2^201 - 2^1
+    t1 = t1.square();
+
+    // 2^250 - 2^50
+    for (int i = 1; i < 50; ++i) {
+      t1 = t1.square();
+    }
+
+    // 2^250 - 2^0
+    t0 = t1.multiply(t0);
+
+    // 2^251 - 2^1
+    t0 = t0.square();
+
+    // 2^252 - 2^2
+    t0 = t0.square();
+
+    // 2^252 - 3
+    return multiply(t0);
+  }
+
+  /**
+   * Constant-time conditional move. Well, actually it is a conditional copy. Logic is inspired by
+   * the SUPERCOP implementation at:
+   * https://github.com/floodyberry/supercop/blob/master/crypto_sign/ed25519/ref10/fe_cmov.c
+   *
+   * @param val the other field element.
+   * @param b must be 0 or 1, otherwise results are undefined.
+   * @return a copy of this if $b == 0$, or a copy of val if $b == 1$.
+   */
+  @Override
+  public FieldElement cmov(FieldElement val, int b) {
+    Ed25519FieldElement that = (Ed25519FieldElement) val;
+    b = -b;
+    int[] result = new int[10];
+    for (int i = 0; i < 10; i++) {
+      result[i] = this.t[i];
+      int x = this.t[i] ^ that.t[i];
+      x &= b;
+      result[i] ^= x;
+    }
+    return new Ed25519FieldElement(this.f, result);
+  }
+
+  @Override
+  public int hashCode() {
+    return Arrays.hashCode(t);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (!(obj instanceof Ed25519FieldElement)) return false;
+    Ed25519FieldElement fe = (Ed25519FieldElement) obj;
+    return 1 == Utils.equal(toByteArray(), fe.toByteArray());
+  }
+
+  @Override
+  public String toString() {
+    return "[Ed25519FieldElement val=" + Utils.bytesToHex(toByteArray()) + "]";
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/ed25519/Ed25519LittleEndianEncoding.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/ed25519/Ed25519LittleEndianEncoding.java
new file mode 100644
index 0000000..7c17169
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/ed25519/Ed25519LittleEndianEncoding.java
@@ -0,0 +1,278 @@
+package org.mariadb.jdbc.plugin.authentication.standard.ed25519.math.ed25519;
+
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.math.Encoding;
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.math.FieldElement;
+
+/**
+ * Helper class for encoding/decoding from/to the 32 byte representation.
+ *
+ * <p>Reviewed/commented by Bloody Rookie (nemproject@gmx.de)
+ */
+public class Ed25519LittleEndianEncoding extends Encoding {
+  /**
+   * Encodes a given field element in its 32 byte representation. This is done in two steps:
+   *
+   * <ol>
+   *   <li>Reduce the value of the field element modulo $p$.
+   *   <li>Convert the field element to the 32 byte representation.
+   * </ol>
+   *
+   * <p>The idea for the modulo $p$ reduction algorithm is as follows:
+   *
+   * <ul>
+   *   <li>$p = 2^{255} - 19$
+   *   <li>$h = h_0 + 2^{25} * h_1 + 2^{(26+25)} * h_2 + \dots + 2^{230} * h_9$ where $0 \le |h_i|
+   *       \lt 2^{27}$ for all $i=0,\dots,9$.
+   *   <li>$h \cong r \mod p$, i.e. $h = r + q * p$ for some suitable $0 \le r \lt p$ and an integer
+   *       $q$.
+   * </ul>
+   *
+   * <p>Then $q = [2^{-255} * (h + 19 * 2^{-25} * h_9 + 1/2)]$ where $[x] = floor(x)$.
+   *
+   * <p>Proof:
+   *
+   * <p>We begin with some very raw estimation for the bounds of some expressions:
+   *
+   * <p>$$ \begin{equation} |h| \lt 2^{230} * 2^{30} = 2^{260} \Rightarrow |r + q * p| \lt 2^{260}
+   * \Rightarrow |q| \lt 2^{10}. \\ \Rightarrow -1/4 \le a := 19^2 * 2^{-255} * q \lt 1/4. \\ |h -
+   * 2^{230} * h_9| = |h_0 + \dots + 2^{204} * h_8| \lt 2^{204} * 2^{30} = 2^{234}. \\ \Rightarrow
+   * -1/4 \le b := 19 * 2^{-255} * (h - 2^{230} * h_9) \lt 1/4 \end{equation} $$
+   *
+   * <p>Therefore $0 \lt 1/2 - a - b \lt 1$.
+   *
+   * <p>Set $x := r + 19 * 2^{-255} * r + 1/2 - a - b$. Then:
+   *
+   * <p>$$ 0 \le x \lt 255 - 20 + 19 + 1 = 2^{255} \\ \Rightarrow 0 \le 2^{-255} * x \lt 1. $$
+   *
+   * <p>Since $q$ is an integer we have
+   *
+   * <p>$$ [q + 2^{-255} * x] = q \quad (1) $$
+   *
+   * <p>Have a closer look at $x$:
+   *
+   * <p>$$ \begin{align} x &amp;= h - q * (2^{255} - 19) + 19 * 2^{-255} * (h - q * (2^{255} - 19))
+   * + 1/2 - 19^2 * 2^{-255} * q - 19 * 2^{-255} * (h - 2^{230} * h_9) \\ &amp;= h - q * 2^{255} +
+   * 19 * q + 19 * 2^{-255} * h - 19 * q + 19^2 * 2^{-255} * q + 1/2 - 19^2 * 2^{-255} * q - 19 *
+   * 2^{-255} * h + 19 * 2^{-25} * h_9 \\ &amp;= h + 19 * 2^{-25} * h_9 + 1/2 - q^{255}. \end{align}
+   * $$
+   *
+   * <p>Inserting the expression for $x$ into $(1)$ we get the desired expression for $q$.
+   */
+  public byte[] encode(FieldElement x) {
+    int[] h = ((Ed25519FieldElement) x).t;
+    int h0 = h[0];
+    int h1 = h[1];
+    int h2 = h[2];
+    int h3 = h[3];
+    int h4 = h[4];
+    int h5 = h[5];
+    int h6 = h[6];
+    int h7 = h[7];
+    int h8 = h[8];
+    int h9 = h[9];
+    int q;
+    int carry0;
+    int carry1;
+    int carry2;
+    int carry3;
+    int carry4;
+    int carry5;
+    int carry6;
+    int carry7;
+    int carry8;
+    int carry9;
+
+    // Step 1:
+    // Calculate q
+    q = (19 * h9 + (1 << 24)) >> 25;
+    q = (h0 + q) >> 26;
+    q = (h1 + q) >> 25;
+    q = (h2 + q) >> 26;
+    q = (h3 + q) >> 25;
+    q = (h4 + q) >> 26;
+    q = (h5 + q) >> 25;
+    q = (h6 + q) >> 26;
+    q = (h7 + q) >> 25;
+    q = (h8 + q) >> 26;
+    q = (h9 + q) >> 25;
+
+    // r = h - q * p = h - 2^255 * q + 19 * q
+    // First add 19 * q then discard the bit 255
+    h0 += 19 * q;
+
+    carry0 = h0 >> 26;
+    h1 += carry0;
+    h0 -= carry0 << 26;
+    carry1 = h1 >> 25;
+    h2 += carry1;
+    h1 -= carry1 << 25;
+    carry2 = h2 >> 26;
+    h3 += carry2;
+    h2 -= carry2 << 26;
+    carry3 = h3 >> 25;
+    h4 += carry3;
+    h3 -= carry3 << 25;
+    carry4 = h4 >> 26;
+    h5 += carry4;
+    h4 -= carry4 << 26;
+    carry5 = h5 >> 25;
+    h6 += carry5;
+    h5 -= carry5 << 25;
+    carry6 = h6 >> 26;
+    h7 += carry6;
+    h6 -= carry6 << 26;
+    carry7 = h7 >> 25;
+    h8 += carry7;
+    h7 -= carry7 << 25;
+    carry8 = h8 >> 26;
+    h9 += carry8;
+    h8 -= carry8 << 26;
+    carry9 = h9 >> 25;
+    h9 -= carry9 << 25;
+
+    // Step 2 (straight forward conversion):
+    byte[] s = new byte[32];
+    s[0] = (byte) h0;
+    s[1] = (byte) (h0 >> 8);
+    s[2] = (byte) (h0 >> 16);
+    s[3] = (byte) ((h0 >> 24) | (h1 << 2));
+    s[4] = (byte) (h1 >> 6);
+    s[5] = (byte) (h1 >> 14);
+    s[6] = (byte) ((h1 >> 22) | (h2 << 3));
+    s[7] = (byte) (h2 >> 5);
+    s[8] = (byte) (h2 >> 13);
+    s[9] = (byte) ((h2 >> 21) | (h3 << 5));
+    s[10] = (byte) (h3 >> 3);
+    s[11] = (byte) (h3 >> 11);
+    s[12] = (byte) ((h3 >> 19) | (h4 << 6));
+    s[13] = (byte) (h4 >> 2);
+    s[14] = (byte) (h4 >> 10);
+    s[15] = (byte) (h4 >> 18);
+    s[16] = (byte) h5;
+    s[17] = (byte) (h5 >> 8);
+    s[18] = (byte) (h5 >> 16);
+    s[19] = (byte) ((h5 >> 24) | (h6 << 1));
+    s[20] = (byte) (h6 >> 7);
+    s[21] = (byte) (h6 >> 15);
+    s[22] = (byte) ((h6 >> 23) | (h7 << 3));
+    s[23] = (byte) (h7 >> 5);
+    s[24] = (byte) (h7 >> 13);
+    s[25] = (byte) ((h7 >> 21) | (h8 << 4));
+    s[26] = (byte) (h8 >> 4);
+    s[27] = (byte) (h8 >> 12);
+    s[28] = (byte) ((h8 >> 20) | (h9 << 6));
+    s[29] = (byte) (h9 >> 2);
+    s[30] = (byte) (h9 >> 10);
+    s[31] = (byte) (h9 >> 18);
+    return s;
+  }
+
+  static int load_3(byte[] in, int offset) {
+    int result = in[offset++] & 0xff;
+    result |= (in[offset++] & 0xff) << 8;
+    result |= (in[offset] & 0xff) << 16;
+    return result;
+  }
+
+  static long load_4(byte[] in, int offset) {
+    int result = in[offset++] & 0xff;
+    result |= (in[offset++] & 0xff) << 8;
+    result |= (in[offset++] & 0xff) << 16;
+    result |= in[offset] << 24;
+    return ((long) result) & 0xffffffffL;
+  }
+
+  /**
+   * Decodes a given field element in its 10 byte $2^{25.5}$ representation.
+   *
+   * @param in The 32 byte representation.
+   * @return The field element in its $2^{25.5}$ bit representation.
+   */
+  public FieldElement decode(byte[] in) {
+    long h0 = load_4(in, 0);
+    long h1 = load_3(in, 4) << 6;
+    long h2 = load_3(in, 7) << 5;
+    long h3 = load_3(in, 10) << 3;
+    long h4 = load_3(in, 13) << 2;
+    long h5 = load_4(in, 16);
+    long h6 = load_3(in, 20) << 7;
+    long h7 = load_3(in, 23) << 5;
+    long h8 = load_3(in, 26) << 4;
+    long h9 = (load_3(in, 29) & 0x7FFFFF) << 2;
+    long carry0;
+    long carry1;
+    long carry2;
+    long carry3;
+    long carry4;
+    long carry5;
+    long carry6;
+    long carry7;
+    long carry8;
+    long carry9;
+
+    // Remember: 2^255 congruent 19 modulo p
+    carry9 = (h9 + (long) (1 << 24)) >> 25;
+    h0 += carry9 * 19;
+    h9 -= carry9 << 25;
+    carry1 = (h1 + (long) (1 << 24)) >> 25;
+    h2 += carry1;
+    h1 -= carry1 << 25;
+    carry3 = (h3 + (long) (1 << 24)) >> 25;
+    h4 += carry3;
+    h3 -= carry3 << 25;
+    carry5 = (h5 + (long) (1 << 24)) >> 25;
+    h6 += carry5;
+    h5 -= carry5 << 25;
+    carry7 = (h7 + (long) (1 << 24)) >> 25;
+    h8 += carry7;
+    h7 -= carry7 << 25;
+
+    carry0 = (h0 + (long) (1 << 25)) >> 26;
+    h1 += carry0;
+    h0 -= carry0 << 26;
+    carry2 = (h2 + (long) (1 << 25)) >> 26;
+    h3 += carry2;
+    h2 -= carry2 << 26;
+    carry4 = (h4 + (long) (1 << 25)) >> 26;
+    h5 += carry4;
+    h4 -= carry4 << 26;
+    carry6 = (h6 + (long) (1 << 25)) >> 26;
+    h7 += carry6;
+    h6 -= carry6 << 26;
+    carry8 = (h8 + (long) (1 << 25)) >> 26;
+    h9 += carry8;
+    h8 -= carry8 << 26;
+
+    int[] h = new int[10];
+    h[0] = (int) h0;
+    h[1] = (int) h1;
+    h[2] = (int) h2;
+    h[3] = (int) h3;
+    h[4] = (int) h4;
+    h[5] = (int) h5;
+    h[6] = (int) h6;
+    h[7] = (int) h7;
+    h[8] = (int) h8;
+    h[9] = (int) h9;
+    return new Ed25519FieldElement(f, h);
+  }
+
+  /**
+   * Is the FieldElement negative in this encoding?
+   *
+   * <p>Return true if $x$ is in $\{1,3,5,\dots,q-2\}$<br>
+   * Return false if $x$ is in $\{0,2,4,\dots,q-1\}$
+   *
+   * <p>Preconditions:
+   *
+   * <ul>
+   *   <li>$|x|$ bounded by $1.1*2^{26},1.1*2^{25},1.1*2^{26},1.1*2^{25}$, etc.
+   * </ul>
+   *
+   * @return true if $x$ is in $\{1,3,5,\dots,q-2\}$, false otherwise.
+   */
+  public boolean isNegative(FieldElement x) {
+    byte[] s = encode(x);
+    return (s[0] & 1) != 0;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/ed25519/Ed25519ScalarOps.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/ed25519/Ed25519ScalarOps.java
new file mode 100644
index 0000000..0f9a518
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/math/ed25519/Ed25519ScalarOps.java
@@ -0,0 +1,905 @@
+/**
+ * EdDSA-Java by str4d
+ *
+ * <p>To the extent possible under law, the person who associated CC0 with EdDSA-Java has waived all
+ * copyright and related or neighboring rights to EdDSA-Java.
+ *
+ * <p>You should have received a copy of the CC0 legalcode along with this work. If not, see
+ * <https://creativecommons.org/publicdomain/zero/1.0/>.
+ */
+package org.mariadb.jdbc.plugin.authentication.standard.ed25519.math.ed25519;
+
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.math.ScalarOps;
+
+/**
+ * Class for reducing a huge integer modulo the group order q and doing a combined multiply plus add
+ * plus reduce operation.
+ *
+ * <p>$q = 2^{252} + 27742317777372353535851937790883648493$.
+ *
+ * <p>Reviewed/commented by Bloody Rookie (nemproject@gmx.de)
+ */
+public class Ed25519ScalarOps implements ScalarOps {
+
+  /**
+   * Reduction modulo the group order $q$.
+   *
+   * <p>Input: $s[0]+256*s[1]+\dots+256^{63}*s[63] = s$
+   *
+   * <p>Output: $s[0]+256*s[1]+\dots+256^{31}*s[31] = s \bmod q$ where $q = 2^{252} +
+   * 27742317777372353535851937790883648493$.
+   */
+  public byte[] reduce(byte[] s) {
+    // s0,..., s22 have 21 bits, s23 has 29 bits
+    long s0 = 0x1FFFFF & Ed25519LittleEndianEncoding.load_3(s, 0);
+    long s1 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(s, 2) >> 5);
+    long s2 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_3(s, 5) >> 2);
+    long s3 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(s, 7) >> 7);
+    long s4 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(s, 10) >> 4);
+    long s5 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_3(s, 13) >> 1);
+    long s6 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(s, 15) >> 6);
+    long s7 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_3(s, 18) >> 3);
+    long s8 = 0x1FFFFF & Ed25519LittleEndianEncoding.load_3(s, 21);
+    long s9 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(s, 23) >> 5);
+    long s10 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_3(s, 26) >> 2);
+    long s11 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(s, 28) >> 7);
+    long s12 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(s, 31) >> 4);
+    long s13 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_3(s, 34) >> 1);
+    long s14 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(s, 36) >> 6);
+    long s15 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_3(s, 39) >> 3);
+    long s16 = 0x1FFFFF & Ed25519LittleEndianEncoding.load_3(s, 42);
+    long s17 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(s, 44) >> 5);
+    long s18 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_3(s, 47) >> 2);
+    long s19 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(s, 49) >> 7);
+    long s20 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(s, 52) >> 4);
+    long s21 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_3(s, 55) >> 1);
+    long s22 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(s, 57) >> 6);
+    long s23 = (Ed25519LittleEndianEncoding.load_4(s, 60) >> 3);
+    long carry0;
+    long carry1;
+    long carry2;
+    long carry3;
+    long carry4;
+    long carry5;
+    long carry6;
+    long carry7;
+    long carry8;
+    long carry9;
+    long carry10;
+    long carry11;
+    long carry12;
+    long carry13;
+    long carry14;
+    long carry15;
+    long carry16;
+
+    s11 += s23 * 666643;
+    s12 += s23 * 470296;
+    s13 += s23 * 654183;
+    s14 -= s23 * 997805;
+    s15 += s23 * 136657;
+    s16 -= s23 * 683901;
+    // not used again
+    // s23 = 0;
+
+    s10 += s22 * 666643;
+    s11 += s22 * 470296;
+    s12 += s22 * 654183;
+    s13 -= s22 * 997805;
+    s14 += s22 * 136657;
+    s15 -= s22 * 683901;
+    // not used again
+    // s22 = 0;
+
+    s9 += s21 * 666643;
+    s10 += s21 * 470296;
+    s11 += s21 * 654183;
+    s12 -= s21 * 997805;
+    s13 += s21 * 136657;
+    s14 -= s21 * 683901;
+    // not used again
+    // s21 = 0;
+
+    s8 += s20 * 666643;
+    s9 += s20 * 470296;
+    s10 += s20 * 654183;
+    s11 -= s20 * 997805;
+    s12 += s20 * 136657;
+    s13 -= s20 * 683901;
+    // not used again
+    // s20 = 0;
+
+    s7 += s19 * 666643;
+    s8 += s19 * 470296;
+    s9 += s19 * 654183;
+    s10 -= s19 * 997805;
+    s11 += s19 * 136657;
+    s12 -= s19 * 683901;
+    // not used again
+    // s19 = 0;
+
+    s6 += s18 * 666643;
+    s7 += s18 * 470296;
+    s8 += s18 * 654183;
+    s9 -= s18 * 997805;
+    s10 += s18 * 136657;
+    s11 -= s18 * 683901;
+    // not used again
+    // s18 = 0;
+
+    carry6 = (s6 + (1 << 20)) >> 21;
+    s7 += carry6;
+    s6 -= carry6 << 21;
+    carry8 = (s8 + (1 << 20)) >> 21;
+    s9 += carry8;
+    s8 -= carry8 << 21;
+    carry10 = (s10 + (1 << 20)) >> 21;
+    s11 += carry10;
+    s10 -= carry10 << 21;
+    carry12 = (s12 + (1 << 20)) >> 21;
+    s13 += carry12;
+    s12 -= carry12 << 21;
+    carry14 = (s14 + (1 << 20)) >> 21;
+    s15 += carry14;
+    s14 -= carry14 << 21;
+    carry16 = (s16 + (1 << 20)) >> 21;
+    s17 += carry16;
+    s16 -= carry16 << 21;
+
+    carry7 = (s7 + (1 << 20)) >> 21;
+    s8 += carry7;
+    s7 -= carry7 << 21;
+    carry9 = (s9 + (1 << 20)) >> 21;
+    s10 += carry9;
+    s9 -= carry9 << 21;
+    carry11 = (s11 + (1 << 20)) >> 21;
+    s12 += carry11;
+    s11 -= carry11 << 21;
+    carry13 = (s13 + (1 << 20)) >> 21;
+    s14 += carry13;
+    s13 -= carry13 << 21;
+    carry15 = (s15 + (1 << 20)) >> 21;
+    s16 += carry15;
+    s15 -= carry15 << 21;
+
+    s5 += s17 * 666643;
+    s6 += s17 * 470296;
+    s7 += s17 * 654183;
+    s8 -= s17 * 997805;
+    s9 += s17 * 136657;
+    s10 -= s17 * 683901;
+    // not used again
+    // s17 = 0;
+
+    s4 += s16 * 666643;
+    s5 += s16 * 470296;
+    s6 += s16 * 654183;
+    s7 -= s16 * 997805;
+    s8 += s16 * 136657;
+    s9 -= s16 * 683901;
+    // not used again
+    // s16 = 0;
+
+    s3 += s15 * 666643;
+    s4 += s15 * 470296;
+    s5 += s15 * 654183;
+    s6 -= s15 * 997805;
+    s7 += s15 * 136657;
+    s8 -= s15 * 683901;
+    // not used again
+    // s15 = 0;
+
+    s2 += s14 * 666643;
+    s3 += s14 * 470296;
+    s4 += s14 * 654183;
+    s5 -= s14 * 997805;
+    s6 += s14 * 136657;
+    s7 -= s14 * 683901;
+    // not used again
+    // s14 = 0;
+
+    s1 += s13 * 666643;
+    s2 += s13 * 470296;
+    s3 += s13 * 654183;
+    s4 -= s13 * 997805;
+    s5 += s13 * 136657;
+    s6 -= s13 * 683901;
+    // not used again
+    // s13 = 0;
+
+    s0 += s12 * 666643;
+    s1 += s12 * 470296;
+    s2 += s12 * 654183;
+    s3 -= s12 * 997805;
+    s4 += s12 * 136657;
+    s5 -= s12 * 683901;
+    // set below
+    // s12 = 0;
+
+    carry0 = (s0 + (1 << 20)) >> 21;
+    s1 += carry0;
+    s0 -= carry0 << 21;
+    carry2 = (s2 + (1 << 20)) >> 21;
+    s3 += carry2;
+    s2 -= carry2 << 21;
+    carry4 = (s4 + (1 << 20)) >> 21;
+    s5 += carry4;
+    s4 -= carry4 << 21;
+    carry6 = (s6 + (1 << 20)) >> 21;
+    s7 += carry6;
+    s6 -= carry6 << 21;
+    carry8 = (s8 + (1 << 20)) >> 21;
+    s9 += carry8;
+    s8 -= carry8 << 21;
+    carry10 = (s10 + (1 << 20)) >> 21;
+    s11 += carry10;
+    s10 -= carry10 << 21;
+
+    carry1 = (s1 + (1 << 20)) >> 21;
+    s2 += carry1;
+    s1 -= carry1 << 21;
+    carry3 = (s3 + (1 << 20)) >> 21;
+    s4 += carry3;
+    s3 -= carry3 << 21;
+    carry5 = (s5 + (1 << 20)) >> 21;
+    s6 += carry5;
+    s5 -= carry5 << 21;
+    carry7 = (s7 + (1 << 20)) >> 21;
+    s8 += carry7;
+    s7 -= carry7 << 21;
+    carry9 = (s9 + (1 << 20)) >> 21;
+    s10 += carry9;
+    s9 -= carry9 << 21;
+    // carry11 = (s11 + (1<<20)) >> 21; s12 += carry11; s11 -= carry11 << 21;
+    carry11 = (s11 + (1 << 20)) >> 21;
+    s12 = carry11;
+    s11 -= carry11 << 21;
+
+    s0 += s12 * 666643;
+    s1 += s12 * 470296;
+    s2 += s12 * 654183;
+    s3 -= s12 * 997805;
+    s4 += s12 * 136657;
+    s5 -= s12 * 683901;
+    // set below
+    // s12 = 0;
+
+    carry0 = s0 >> 21;
+    s1 += carry0;
+    s0 -= carry0 << 21;
+    carry1 = s1 >> 21;
+    s2 += carry1;
+    s1 -= carry1 << 21;
+    carry2 = s2 >> 21;
+    s3 += carry2;
+    s2 -= carry2 << 21;
+    carry3 = s3 >> 21;
+    s4 += carry3;
+    s3 -= carry3 << 21;
+    carry4 = s4 >> 21;
+    s5 += carry4;
+    s4 -= carry4 << 21;
+    carry5 = s5 >> 21;
+    s6 += carry5;
+    s5 -= carry5 << 21;
+    carry6 = s6 >> 21;
+    s7 += carry6;
+    s6 -= carry6 << 21;
+    carry7 = s7 >> 21;
+    s8 += carry7;
+    s7 -= carry7 << 21;
+    carry8 = s8 >> 21;
+    s9 += carry8;
+    s8 -= carry8 << 21;
+    carry9 = s9 >> 21;
+    s10 += carry9;
+    s9 -= carry9 << 21;
+    carry10 = s10 >> 21;
+    s11 += carry10;
+    s10 -= carry10 << 21;
+    // carry11 = s11 >> 21; s12 += carry11; s11 -= carry11 << 21;
+    carry11 = s11 >> 21;
+    s12 = carry11;
+    s11 -= carry11 << 21;
+
+    // TODO-CR BR: Is it really needed to do it TWO times? (it doesn't hurt, just a question).
+    s0 += s12 * 666643;
+    s1 += s12 * 470296;
+    s2 += s12 * 654183;
+    s3 -= s12 * 997805;
+    s4 += s12 * 136657;
+    s5 -= s12 * 683901;
+    // not used again
+    // s12 = 0;
+
+    carry0 = s0 >> 21;
+    s1 += carry0;
+    s0 -= carry0 << 21;
+    carry1 = s1 >> 21;
+    s2 += carry1;
+    s1 -= carry1 << 21;
+    carry2 = s2 >> 21;
+    s3 += carry2;
+    s2 -= carry2 << 21;
+    carry3 = s3 >> 21;
+    s4 += carry3;
+    s3 -= carry3 << 21;
+    carry4 = s4 >> 21;
+    s5 += carry4;
+    s4 -= carry4 << 21;
+    carry5 = s5 >> 21;
+    s6 += carry5;
+    s5 -= carry5 << 21;
+    carry6 = s6 >> 21;
+    s7 += carry6;
+    s6 -= carry6 << 21;
+    carry7 = s7 >> 21;
+    s8 += carry7;
+    s7 -= carry7 << 21;
+    carry8 = s8 >> 21;
+    s9 += carry8;
+    s8 -= carry8 << 21;
+    carry9 = s9 >> 21;
+    s10 += carry9;
+    s9 -= carry9 << 21;
+    carry10 = s10 >> 21;
+    s11 += carry10;
+    s10 -= carry10 << 21;
+
+    // s0, ..., s11 got 21 bits each.
+    byte[] result = new byte[32];
+    result[0] = (byte) s0;
+    result[1] = (byte) (s0 >> 8);
+    result[2] = (byte) ((s0 >> 16) | (s1 << 5));
+    result[3] = (byte) (s1 >> 3);
+    result[4] = (byte) (s1 >> 11);
+    result[5] = (byte) ((s1 >> 19) | (s2 << 2));
+    result[6] = (byte) (s2 >> 6);
+    result[7] = (byte) ((s2 >> 14) | (s3 << 7));
+    result[8] = (byte) (s3 >> 1);
+    result[9] = (byte) (s3 >> 9);
+    result[10] = (byte) ((s3 >> 17) | (s4 << 4));
+    result[11] = (byte) (s4 >> 4);
+    result[12] = (byte) (s4 >> 12);
+    result[13] = (byte) ((s4 >> 20) | (s5 << 1));
+    result[14] = (byte) (s5 >> 7);
+    result[15] = (byte) ((s5 >> 15) | (s6 << 6));
+    result[16] = (byte) (s6 >> 2);
+    result[17] = (byte) (s6 >> 10);
+    result[18] = (byte) ((s6 >> 18) | (s7 << 3));
+    result[19] = (byte) (s7 >> 5);
+    result[20] = (byte) (s7 >> 13);
+    result[21] = (byte) s8;
+    result[22] = (byte) (s8 >> 8);
+    result[23] = (byte) ((s8 >> 16) | (s9 << 5));
+    result[24] = (byte) (s9 >> 3);
+    result[25] = (byte) (s9 >> 11);
+    result[26] = (byte) ((s9 >> 19) | (s10 << 2));
+    result[27] = (byte) (s10 >> 6);
+    result[28] = (byte) ((s10 >> 14) | (s11 << 7));
+    result[29] = (byte) (s11 >> 1);
+    result[30] = (byte) (s11 >> 9);
+    result[31] = (byte) (s11 >> 17);
+    return result;
+  }
+
+  /**
+   * $(ab+c) \bmod q$
+   *
+   * <p>Input:
+   *
+   * <ul>
+   *   <li>$a[0]+256*a[1]+\dots+256^{31}*a[31] = a$
+   *   <li>$b[0]+256*b[1]+\dots+256^{31}*b[31] = b$
+   *   <li>$c[0]+256*c[1]+\dots+256^{31}*c[31] = c$
+   * </ul>
+   *
+   * <p>Output: $result[0]+256*result[1]+\dots+256^{31}*result[31] = (ab+c) \bmod q$ where $q =
+   * 2^{252} + 27742317777372353535851937790883648493$.
+   *
+   * <p>See the comments in {@link #reduce(byte[])} for an explanation of the algorithm.
+   */
+  public byte[] multiplyAndAdd(byte[] a, byte[] b, byte[] c) {
+    long a0 = 0x1FFFFF & Ed25519LittleEndianEncoding.load_3(a, 0);
+    long a1 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(a, 2) >> 5);
+    long a2 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_3(a, 5) >> 2);
+    long a3 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(a, 7) >> 7);
+    long a4 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(a, 10) >> 4);
+    long a5 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_3(a, 13) >> 1);
+    long a6 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(a, 15) >> 6);
+    long a7 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_3(a, 18) >> 3);
+    long a8 = 0x1FFFFF & Ed25519LittleEndianEncoding.load_3(a, 21);
+    long a9 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(a, 23) >> 5);
+    long a10 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_3(a, 26) >> 2);
+    long a11 = (Ed25519LittleEndianEncoding.load_4(a, 28) >> 7);
+    long b0 = 0x1FFFFF & Ed25519LittleEndianEncoding.load_3(b, 0);
+    long b1 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(b, 2) >> 5);
+    long b2 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_3(b, 5) >> 2);
+    long b3 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(b, 7) >> 7);
+    long b4 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(b, 10) >> 4);
+    long b5 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_3(b, 13) >> 1);
+    long b6 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(b, 15) >> 6);
+    long b7 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_3(b, 18) >> 3);
+    long b8 = 0x1FFFFF & Ed25519LittleEndianEncoding.load_3(b, 21);
+    long b9 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(b, 23) >> 5);
+    long b10 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_3(b, 26) >> 2);
+    long b11 = (Ed25519LittleEndianEncoding.load_4(b, 28) >> 7);
+    long c0 = 0x1FFFFF & Ed25519LittleEndianEncoding.load_3(c, 0);
+    long c1 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(c, 2) >> 5);
+    long c2 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_3(c, 5) >> 2);
+    long c3 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(c, 7) >> 7);
+    long c4 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(c, 10) >> 4);
+    long c5 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_3(c, 13) >> 1);
+    long c6 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(c, 15) >> 6);
+    long c7 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_3(c, 18) >> 3);
+    long c8 = 0x1FFFFF & Ed25519LittleEndianEncoding.load_3(c, 21);
+    long c9 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_4(c, 23) >> 5);
+    long c10 = 0x1FFFFF & (Ed25519LittleEndianEncoding.load_3(c, 26) >> 2);
+    long c11 = (Ed25519LittleEndianEncoding.load_4(c, 28) >> 7);
+    long s0;
+    long s1;
+    long s2;
+    long s3;
+    long s4;
+    long s5;
+    long s6;
+    long s7;
+    long s8;
+    long s9;
+    long s10;
+    long s11;
+    long s12;
+    long s13;
+    long s14;
+    long s15;
+    long s16;
+    long s17;
+    long s18;
+    long s19;
+    long s20;
+    long s21;
+    long s22;
+    long s23;
+    long carry0;
+    long carry1;
+    long carry2;
+    long carry3;
+    long carry4;
+    long carry5;
+    long carry6;
+    long carry7;
+    long carry8;
+    long carry9;
+    long carry10;
+    long carry11;
+    long carry12;
+    long carry13;
+    long carry14;
+    long carry15;
+    long carry16;
+    long carry17;
+    long carry18;
+    long carry19;
+    long carry20;
+    long carry21;
+    long carry22;
+
+    s0 = c0 + a0 * b0;
+    s1 = c1 + a0 * b1 + a1 * b0;
+    s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0;
+    s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0;
+    s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0;
+    s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0;
+    s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0;
+    s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 + a7 * b0;
+    s8 =
+        c8 + a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + a6 * b2 + a7 * b1
+            + a8 * b0;
+    s9 =
+        c9 + a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + a6 * b3 + a7 * b2 + a8 * b1
+            + a9 * b0;
+    s10 =
+        c10 + a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + a6 * b4 + a7 * b3
+            + a8 * b2 + a9 * b1 + a10 * b0;
+    s11 =
+        c11 + a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + a6 * b5 + a7 * b4
+            + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0;
+    s12 =
+        a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + a7 * b5 + a8 * b4 + a9 * b3
+            + a10 * b2 + a11 * b1;
+    s13 =
+        a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + a8 * b5 + a9 * b4 + a10 * b3
+            + a11 * b2;
+    s14 =
+        a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + a9 * b5 + a10 * b4 + a11 * b3;
+    s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + a10 * b5 + a11 * b4;
+    s16 = a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5;
+    s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6;
+    s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7;
+    s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8;
+    s20 = a9 * b11 + a10 * b10 + a11 * b9;
+    s21 = a10 * b11 + a11 * b10;
+    s22 = a11 * b11;
+    // set below
+    // s23 = 0;
+
+    carry0 = (s0 + (1 << 20)) >> 21;
+    s1 += carry0;
+    s0 -= carry0 << 21;
+    carry2 = (s2 + (1 << 20)) >> 21;
+    s3 += carry2;
+    s2 -= carry2 << 21;
+    carry4 = (s4 + (1 << 20)) >> 21;
+    s5 += carry4;
+    s4 -= carry4 << 21;
+    carry6 = (s6 + (1 << 20)) >> 21;
+    s7 += carry6;
+    s6 -= carry6 << 21;
+    carry8 = (s8 + (1 << 20)) >> 21;
+    s9 += carry8;
+    s8 -= carry8 << 21;
+    carry10 = (s10 + (1 << 20)) >> 21;
+    s11 += carry10;
+    s10 -= carry10 << 21;
+    carry12 = (s12 + (1 << 20)) >> 21;
+    s13 += carry12;
+    s12 -= carry12 << 21;
+    carry14 = (s14 + (1 << 20)) >> 21;
+    s15 += carry14;
+    s14 -= carry14 << 21;
+    carry16 = (s16 + (1 << 20)) >> 21;
+    s17 += carry16;
+    s16 -= carry16 << 21;
+    carry18 = (s18 + (1 << 20)) >> 21;
+    s19 += carry18;
+    s18 -= carry18 << 21;
+    carry20 = (s20 + (1 << 20)) >> 21;
+    s21 += carry20;
+    s20 -= carry20 << 21;
+    // carry22 = (s22 + (1<<20)) >> 21; s23 += carry22; s22 -= carry22 << 21;
+    carry22 = (s22 + (1 << 20)) >> 21;
+    s23 = carry22;
+    s22 -= carry22 << 21;
+
+    carry1 = (s1 + (1 << 20)) >> 21;
+    s2 += carry1;
+    s1 -= carry1 << 21;
+    carry3 = (s3 + (1 << 20)) >> 21;
+    s4 += carry3;
+    s3 -= carry3 << 21;
+    carry5 = (s5 + (1 << 20)) >> 21;
+    s6 += carry5;
+    s5 -= carry5 << 21;
+    carry7 = (s7 + (1 << 20)) >> 21;
+    s8 += carry7;
+    s7 -= carry7 << 21;
+    carry9 = (s9 + (1 << 20)) >> 21;
+    s10 += carry9;
+    s9 -= carry9 << 21;
+    carry11 = (s11 + (1 << 20)) >> 21;
+    s12 += carry11;
+    s11 -= carry11 << 21;
+    carry13 = (s13 + (1 << 20)) >> 21;
+    s14 += carry13;
+    s13 -= carry13 << 21;
+    carry15 = (s15 + (1 << 20)) >> 21;
+    s16 += carry15;
+    s15 -= carry15 << 21;
+    carry17 = (s17 + (1 << 20)) >> 21;
+    s18 += carry17;
+    s17 -= carry17 << 21;
+    carry19 = (s19 + (1 << 20)) >> 21;
+    s20 += carry19;
+    s19 -= carry19 << 21;
+    carry21 = (s21 + (1 << 20)) >> 21;
+    s22 += carry21;
+    s21 -= carry21 << 21;
+
+    s11 += s23 * 666643;
+    s12 += s23 * 470296;
+    s13 += s23 * 654183;
+    s14 -= s23 * 997805;
+    s15 += s23 * 136657;
+    s16 -= s23 * 683901;
+    // not used again
+    // s23 = 0;
+
+    s10 += s22 * 666643;
+    s11 += s22 * 470296;
+    s12 += s22 * 654183;
+    s13 -= s22 * 997805;
+    s14 += s22 * 136657;
+    s15 -= s22 * 683901;
+    // not used again
+    // s22 = 0;
+
+    s9 += s21 * 666643;
+    s10 += s21 * 470296;
+    s11 += s21 * 654183;
+    s12 -= s21 * 997805;
+    s13 += s21 * 136657;
+    s14 -= s21 * 683901;
+    // not used again
+    // s21 = 0;
+
+    s8 += s20 * 666643;
+    s9 += s20 * 470296;
+    s10 += s20 * 654183;
+    s11 -= s20 * 997805;
+    s12 += s20 * 136657;
+    s13 -= s20 * 683901;
+    // not used again
+    // s20 = 0;
+
+    s7 += s19 * 666643;
+    s8 += s19 * 470296;
+    s9 += s19 * 654183;
+    s10 -= s19 * 997805;
+    s11 += s19 * 136657;
+    s12 -= s19 * 683901;
+    // not used again
+    // s19 = 0;
+
+    s6 += s18 * 666643;
+    s7 += s18 * 470296;
+    s8 += s18 * 654183;
+    s9 -= s18 * 997805;
+    s10 += s18 * 136657;
+    s11 -= s18 * 683901;
+    // not used again
+    // s18 = 0;
+
+    carry6 = (s6 + (1 << 20)) >> 21;
+    s7 += carry6;
+    s6 -= carry6 << 21;
+    carry8 = (s8 + (1 << 20)) >> 21;
+    s9 += carry8;
+    s8 -= carry8 << 21;
+    carry10 = (s10 + (1 << 20)) >> 21;
+    s11 += carry10;
+    s10 -= carry10 << 21;
+    carry12 = (s12 + (1 << 20)) >> 21;
+    s13 += carry12;
+    s12 -= carry12 << 21;
+    carry14 = (s14 + (1 << 20)) >> 21;
+    s15 += carry14;
+    s14 -= carry14 << 21;
+    carry16 = (s16 + (1 << 20)) >> 21;
+    s17 += carry16;
+    s16 -= carry16 << 21;
+
+    carry7 = (s7 + (1 << 20)) >> 21;
+    s8 += carry7;
+    s7 -= carry7 << 21;
+    carry9 = (s9 + (1 << 20)) >> 21;
+    s10 += carry9;
+    s9 -= carry9 << 21;
+    carry11 = (s11 + (1 << 20)) >> 21;
+    s12 += carry11;
+    s11 -= carry11 << 21;
+    carry13 = (s13 + (1 << 20)) >> 21;
+    s14 += carry13;
+    s13 -= carry13 << 21;
+    carry15 = (s15 + (1 << 20)) >> 21;
+    s16 += carry15;
+    s15 -= carry15 << 21;
+
+    s5 += s17 * 666643;
+    s6 += s17 * 470296;
+    s7 += s17 * 654183;
+    s8 -= s17 * 997805;
+    s9 += s17 * 136657;
+    s10 -= s17 * 683901;
+    // not used again
+    // s17 = 0;
+
+    s4 += s16 * 666643;
+    s5 += s16 * 470296;
+    s6 += s16 * 654183;
+    s7 -= s16 * 997805;
+    s8 += s16 * 136657;
+    s9 -= s16 * 683901;
+    // not used again
+    // s16 = 0;
+
+    s3 += s15 * 666643;
+    s4 += s15 * 470296;
+    s5 += s15 * 654183;
+    s6 -= s15 * 997805;
+    s7 += s15 * 136657;
+    s8 -= s15 * 683901;
+    // not used again
+    // s15 = 0;
+
+    s2 += s14 * 666643;
+    s3 += s14 * 470296;
+    s4 += s14 * 654183;
+    s5 -= s14 * 997805;
+    s6 += s14 * 136657;
+    s7 -= s14 * 683901;
+    // not used again
+    // s14 = 0;
+
+    s1 += s13 * 666643;
+    s2 += s13 * 470296;
+    s3 += s13 * 654183;
+    s4 -= s13 * 997805;
+    s5 += s13 * 136657;
+    s6 -= s13 * 683901;
+    // not used again
+    // s13 = 0;
+
+    s0 += s12 * 666643;
+    s1 += s12 * 470296;
+    s2 += s12 * 654183;
+    s3 -= s12 * 997805;
+    s4 += s12 * 136657;
+    s5 -= s12 * 683901;
+    // set below
+    // s12 = 0;
+
+    carry0 = (s0 + (1 << 20)) >> 21;
+    s1 += carry0;
+    s0 -= carry0 << 21;
+    carry2 = (s2 + (1 << 20)) >> 21;
+    s3 += carry2;
+    s2 -= carry2 << 21;
+    carry4 = (s4 + (1 << 20)) >> 21;
+    s5 += carry4;
+    s4 -= carry4 << 21;
+    carry6 = (s6 + (1 << 20)) >> 21;
+    s7 += carry6;
+    s6 -= carry6 << 21;
+    carry8 = (s8 + (1 << 20)) >> 21;
+    s9 += carry8;
+    s8 -= carry8 << 21;
+    carry10 = (s10 + (1 << 20)) >> 21;
+    s11 += carry10;
+    s10 -= carry10 << 21;
+
+    carry1 = (s1 + (1 << 20)) >> 21;
+    s2 += carry1;
+    s1 -= carry1 << 21;
+    carry3 = (s3 + (1 << 20)) >> 21;
+    s4 += carry3;
+    s3 -= carry3 << 21;
+    carry5 = (s5 + (1 << 20)) >> 21;
+    s6 += carry5;
+    s5 -= carry5 << 21;
+    carry7 = (s7 + (1 << 20)) >> 21;
+    s8 += carry7;
+    s7 -= carry7 << 21;
+    carry9 = (s9 + (1 << 20)) >> 21;
+    s10 += carry9;
+    s9 -= carry9 << 21;
+    // carry11 = (s11 + (1<<20)) >> 21; s12 += carry11; s11 -= carry11 << 21;
+    carry11 = (s11 + (1 << 20)) >> 21;
+    s12 = carry11;
+    s11 -= carry11 << 21;
+
+    s0 += s12 * 666643;
+    s1 += s12 * 470296;
+    s2 += s12 * 654183;
+    s3 -= s12 * 997805;
+    s4 += s12 * 136657;
+    s5 -= s12 * 683901;
+    // set below
+    // s12 = 0;
+
+    carry0 = s0 >> 21;
+    s1 += carry0;
+    s0 -= carry0 << 21;
+    carry1 = s1 >> 21;
+    s2 += carry1;
+    s1 -= carry1 << 21;
+    carry2 = s2 >> 21;
+    s3 += carry2;
+    s2 -= carry2 << 21;
+    carry3 = s3 >> 21;
+    s4 += carry3;
+    s3 -= carry3 << 21;
+    carry4 = s4 >> 21;
+    s5 += carry4;
+    s4 -= carry4 << 21;
+    carry5 = s5 >> 21;
+    s6 += carry5;
+    s5 -= carry5 << 21;
+    carry6 = s6 >> 21;
+    s7 += carry6;
+    s6 -= carry6 << 21;
+    carry7 = s7 >> 21;
+    s8 += carry7;
+    s7 -= carry7 << 21;
+    carry8 = s8 >> 21;
+    s9 += carry8;
+    s8 -= carry8 << 21;
+    carry9 = s9 >> 21;
+    s10 += carry9;
+    s9 -= carry9 << 21;
+    carry10 = s10 >> 21;
+    s11 += carry10;
+    s10 -= carry10 << 21;
+    // carry11 = s11 >> 21; s12 += carry11; s11 -= carry11 << 21;
+    carry11 = s11 >> 21;
+    s12 = carry11;
+    s11 -= carry11 << 21;
+
+    s0 += s12 * 666643;
+    s1 += s12 * 470296;
+    s2 += s12 * 654183;
+    s3 -= s12 * 997805;
+    s4 += s12 * 136657;
+    s5 -= s12 * 683901;
+    // not used again
+    // s12 = 0;
+
+    carry0 = s0 >> 21;
+    s1 += carry0;
+    s0 -= carry0 << 21;
+    carry1 = s1 >> 21;
+    s2 += carry1;
+    s1 -= carry1 << 21;
+    carry2 = s2 >> 21;
+    s3 += carry2;
+    s2 -= carry2 << 21;
+    carry3 = s3 >> 21;
+    s4 += carry3;
+    s3 -= carry3 << 21;
+    carry4 = s4 >> 21;
+    s5 += carry4;
+    s4 -= carry4 << 21;
+    carry5 = s5 >> 21;
+    s6 += carry5;
+    s5 -= carry5 << 21;
+    carry6 = s6 >> 21;
+    s7 += carry6;
+    s6 -= carry6 << 21;
+    carry7 = s7 >> 21;
+    s8 += carry7;
+    s7 -= carry7 << 21;
+    carry8 = s8 >> 21;
+    s9 += carry8;
+    s8 -= carry8 << 21;
+    carry9 = s9 >> 21;
+    s10 += carry9;
+    s9 -= carry9 << 21;
+    carry10 = s10 >> 21;
+    s11 += carry10;
+    s10 -= carry10 << 21;
+
+    byte[] result = new byte[32];
+    result[0] = (byte) s0;
+    result[1] = (byte) (s0 >> 8);
+    result[2] = (byte) ((s0 >> 16) | (s1 << 5));
+    result[3] = (byte) (s1 >> 3);
+    result[4] = (byte) (s1 >> 11);
+    result[5] = (byte) ((s1 >> 19) | (s2 << 2));
+    result[6] = (byte) (s2 >> 6);
+    result[7] = (byte) ((s2 >> 14) | (s3 << 7));
+    result[8] = (byte) (s3 >> 1);
+    result[9] = (byte) (s3 >> 9);
+    result[10] = (byte) ((s3 >> 17) | (s4 << 4));
+    result[11] = (byte) (s4 >> 4);
+    result[12] = (byte) (s4 >> 12);
+    result[13] = (byte) ((s4 >> 20) | (s5 << 1));
+    result[14] = (byte) (s5 >> 7);
+    result[15] = (byte) ((s5 >> 15) | (s6 << 6));
+    result[16] = (byte) (s6 >> 2);
+    result[17] = (byte) (s6 >> 10);
+    result[18] = (byte) ((s6 >> 18) | (s7 << 3));
+    result[19] = (byte) (s7 >> 5);
+    result[20] = (byte) (s7 >> 13);
+    result[21] = (byte) s8;
+    result[22] = (byte) (s8 >> 8);
+    result[23] = (byte) ((s8 >> 16) | (s9 << 5));
+    result[24] = (byte) (s9 >> 3);
+    result[25] = (byte) (s9 >> 11);
+    result[26] = (byte) ((s9 >> 19) | (s10 << 2));
+    result[27] = (byte) (s10 >> 6);
+    result[28] = (byte) ((s10 >> 14) | (s11 << 7));
+    result[29] = (byte) (s11 >> 1);
+    result[30] = (byte) (s11 >> 9);
+    result[31] = (byte) (s11 >> 17);
+    return result;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/spec/EdDSANamedCurveSpec.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/spec/EdDSANamedCurveSpec.java
new file mode 100644
index 0000000..69af3b6
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/spec/EdDSANamedCurveSpec.java
@@ -0,0 +1,25 @@
+package org.mariadb.jdbc.plugin.authentication.standard.ed25519.spec;
+
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.math.Curve;
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.math.GroupElement;
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.math.ScalarOps;
+
+/**
+ * EdDSA Curve specification that can also be referred to by name.
+ *
+ * @author str4d
+ */
+public class EdDSANamedCurveSpec extends EdDSAParameterSpec {
+  private static final long serialVersionUID = -4080022735829727073L;
+  private final String name;
+
+  public EdDSANamedCurveSpec(
+      String name, Curve curve, String hashAlgo, ScalarOps sc, GroupElement B) {
+    super(curve, hashAlgo, sc, B);
+    this.name = name;
+  }
+
+  public String getName() {
+    return name;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/spec/EdDSANamedCurveTable.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/spec/EdDSANamedCurveTable.java
new file mode 100644
index 0000000..f15c338
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/spec/EdDSANamedCurveTable.java
@@ -0,0 +1,63 @@
+package org.mariadb.jdbc.plugin.authentication.standard.ed25519.spec;
+
+import java.util.HashMap;
+import java.util.Locale;
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.Utils;
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.math.Curve;
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.math.Field;
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.math.ed25519.Ed25519LittleEndianEncoding;
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.math.ed25519.Ed25519ScalarOps;
+
+/**
+ * The named EdDSA curves.
+ *
+ * @author str4d
+ */
+public class EdDSANamedCurveTable {
+  public static final String ED_25519 = "Ed25519";
+
+  private static final Field ed25519field =
+      new Field(
+          256, // b
+          Utils.hexToBytes("edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), // q
+          new Ed25519LittleEndianEncoding());
+
+  private static final Curve ed25519curve =
+      new Curve(
+          ed25519field,
+          Utils.hexToBytes("a3785913ca4deb75abd841414d0a700098e879777940c78c73fe6f2bee6c0352"), // d
+          ed25519field.fromByteArray(
+              Utils.hexToBytes(
+                  "b0a00e4a271beec478e42fad0618432fa7d7fb3d99004d2b0bdfc14f8024832b"))); // I
+
+  public static final EdDSANamedCurveSpec ED_25519_CURVE_SPEC =
+      new EdDSANamedCurveSpec(
+          ED_25519,
+          ed25519curve,
+          "SHA-512", // H
+          new Ed25519ScalarOps(), // l
+          ed25519curve.createPoint( // B
+              Utils.hexToBytes("5866666666666666666666666666666666666666666666666666666666666666"),
+              true)); // Precompute tables for B
+
+  private static volatile HashMap<String, EdDSANamedCurveSpec> curves = new HashMap<>();
+
+  private static synchronized void putCurve(String name, EdDSANamedCurveSpec curve) {
+    HashMap<String, EdDSANamedCurveSpec> newCurves = new HashMap<>(curves);
+    newCurves.put(name, curve);
+    curves = newCurves;
+  }
+
+  public static void defineCurve(EdDSANamedCurveSpec curve) {
+    putCurve(curve.getName().toLowerCase(Locale.ENGLISH), curve);
+  }
+
+  static {
+    // RFC 8032
+    defineCurve(ED_25519_CURVE_SPEC);
+  }
+
+  public static EdDSANamedCurveSpec getByName(String name) {
+    return curves.get(name.toLowerCase(Locale.ENGLISH));
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/spec/EdDSAParameterSpec.java b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/spec/EdDSAParameterSpec.java
new file mode 100644
index 0000000..0fa7df9
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ed25519/spec/EdDSAParameterSpec.java
@@ -0,0 +1,84 @@
+/**
+ * EdDSA-Java by str4d
+ *
+ * <p>To the extent possible under law, the person who associated CC0 with EdDSA-Java has waived all
+ * copyright and related or neighboring rights to EdDSA-Java.
+ *
+ * <p>You should have received a copy of the CC0 legalcode along with this work. If not, see
+ * <https://creativecommons.org/publicdomain/zero/1.0/>.
+ */
+package org.mariadb.jdbc.plugin.authentication.standard.ed25519.spec;
+
+import java.io.Serializable;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.AlgorithmParameterSpec;
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.math.Curve;
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.math.GroupElement;
+import org.mariadb.jdbc.plugin.authentication.standard.ed25519.math.ScalarOps;
+
+/**
+ * Parameter specification for an EdDSA algorithm.
+ *
+ * @author str4d
+ */
+public class EdDSAParameterSpec implements AlgorithmParameterSpec, Serializable {
+  private static final long serialVersionUID = 8274987108472012L;
+  private final Curve curve;
+  private final String hashAlgo;
+  private final ScalarOps sc;
+  private final GroupElement B;
+
+  /**
+   * @param curve the curve
+   * @param hashAlgo the JCA string for the hash algorithm
+   * @param sc the parameter L represented as ScalarOps
+   * @param B the parameter B
+   * @throws IllegalArgumentException if hash algorithm is unsupported or length is wrong
+   */
+  public EdDSAParameterSpec(Curve curve, String hashAlgo, ScalarOps sc, GroupElement B) {
+    try {
+      MessageDigest hash = MessageDigest.getInstance(hashAlgo);
+      // EdDSA hash function must produce 2b-bit output
+      if (curve.getField().getb() / 4 != hash.getDigestLength())
+        throw new IllegalArgumentException("Hash output is not 2b-bit");
+    } catch (NoSuchAlgorithmException e) {
+      throw new IllegalArgumentException("Unsupported hash algorithm");
+    }
+
+    this.curve = curve;
+    this.hashAlgo = hashAlgo;
+    this.sc = sc;
+    this.B = B;
+  }
+
+  public Curve getCurve() {
+    return curve;
+  }
+
+  public String getHashAlgorithm() {
+    return hashAlgo;
+  }
+
+  /**
+   * @return the base (generator)
+   */
+  public GroupElement getB() {
+    return B;
+  }
+
+  @Override
+  public int hashCode() {
+    return hashAlgo.hashCode() ^ curve.hashCode() ^ B.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o == this) return true;
+    if (!(o instanceof EdDSAParameterSpec)) return false;
+    EdDSAParameterSpec s = (EdDSAParameterSpec) o;
+    return hashAlgo.equals(s.getHashAlgorithm())
+        && curve.equals(s.getCurve())
+        && B.equals(s.getB());
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/BigDecimalCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/BigDecimalCodec.java
new file mode 100644
index 0000000..41664f2
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/BigDecimalCodec.java
@@ -0,0 +1,228 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+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.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** Big decimal codec */
+public class BigDecimalCodec implements Codec<BigDecimal> {
+
+  /** default instance */
+  public static final BigDecimalCodec INSTANCE = new BigDecimalCodec();
+
+  private static final EnumSet<DataType> 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.VARCHAR,
+          DataType.VARSTRING,
+          DataType.STRING,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  public String className() {
+    return BigDecimal.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder column, Class<?> type) {
+    return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(BigDecimal.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof BigDecimal;
+  }
+
+  @Override
+  @SuppressWarnings("fallthrough")
+  public BigDecimal decodeText(ReadableByteBuf buf, int length, ColumnDecoder 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:
+      case YEAR:
+        return new BigDecimal(buf.readAscii(length));
+
+      case BLOB:
+      case TINYBLOB:
+      case MEDIUMBLOB:
+      case LONGBLOB:
+        if (column.isBinary()) {
+          buf.skip(length);
+          throw new SQLDataException(
+              String.format("Data type %s cannot be decoded as BigDecimal", column.getType()));
+        }
+        // expected fallthrough
+        // BLOB is considered as String if it has a collation (this is TEXT column)
+
+      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 < length; i++) {
+          byte b = buf.readByte();
+          result = (result << 8) + (b & 0xff);
+        }
+        return BigDecimal.valueOf(result);
+
+      default:
+        buf.skip(length);
+        throw new SQLDataException(
+            String.format("Data type %s cannot be decoded as BigDecimal", column.getType()));
+    }
+  }
+
+  @Override
+  @SuppressWarnings("fallthrough")
+  public BigDecimal decodeBinary(
+      ReadableByteBuf buf, int length, ColumnDecoder 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 = getBigInteger(buf, column);
+        return new BigDecimal(String.valueOf(val))
+            .setScale(column.getDecimals(), RoundingMode.CEILING);
+
+      case FLOAT:
+        return BigDecimal.valueOf(buf.readFloat());
+
+      case DOUBLE:
+        return BigDecimal.valueOf(buf.readDouble());
+
+      case BIT:
+        long result = 0;
+        for (int i = 0; i < length; i++) {
+          byte b = buf.readByte();
+          result = (result << 8) + (b & 0xff);
+        }
+        return BigDecimal.valueOf(result);
+
+      case BLOB:
+      case TINYBLOB:
+      case MEDIUMBLOB:
+      case LONGBLOB:
+        if (column.isBinary()) {
+          buf.skip(length);
+          throw new SQLDataException(
+              String.format("Data type %s cannot be decoded as BigDecimal", column.getType()));
+        }
+        // expected fallthrough
+        // BLOB is considered as String if has a collation (this is TEXT column)
+
+      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()));
+    }
+  }
+
+  static BigInteger getBigInteger(ReadableByteBuf buf, ColumnDecoder column) {
+    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 val;
+  }
+
+  @Override
+  public void encodeText(Writer encoder, Context context, Object value, Calendar cal, Long length)
+      throws IOException {
+    encoder.writeAscii(((BigDecimal) value).toPlainString());
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    String asciiFormat = ((BigDecimal) value).toPlainString();
+    encoder.writeLength(asciiFormat.length());
+    encoder.writeAscii(asciiFormat);
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.DECIMAL.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/BigIntegerCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/BigIntegerCodec.java
new file mode 100644
index 0000000..1f59e26
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/BigIntegerCodec.java
@@ -0,0 +1,220 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+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.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** BigInteger codec */
+public class BigIntegerCodec implements Codec<BigInteger> {
+
+  /** default instance */
+  public static final BigIntegerCodec INSTANCE = new BigIntegerCodec();
+
+  private static final EnumSet<DataType> COMPATIBLE_TYPES =
+      EnumSet.of(
+          DataType.TINYINT,
+          DataType.SMALLINT,
+          DataType.MEDIUMINT,
+          DataType.INTEGER,
+          DataType.BIGINT,
+          DataType.YEAR,
+          DataType.DOUBLE,
+          DataType.DECIMAL,
+          DataType.OLDDECIMAL,
+          DataType.FLOAT,
+          DataType.BIT,
+          DataType.VARCHAR,
+          DataType.VARSTRING,
+          DataType.STRING,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  public String className() {
+    return BigInteger.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder column, Class<?> type) {
+    return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(BigInteger.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof BigInteger;
+  }
+
+  @Override
+  @SuppressWarnings("fallthrough")
+  public BigInteger decodeText(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+
+    switch (column.getType()) {
+      case FLOAT:
+      case DOUBLE:
+      case DECIMAL:
+      case OLDDECIMAL:
+        return new BigDecimal(buf.readAscii(length)).toBigInteger();
+
+      case BLOB:
+      case TINYBLOB:
+      case MEDIUMBLOB:
+      case LONGBLOB:
+        if (column.isBinary()) {
+          buf.skip(length);
+          throw new SQLDataException(
+              String.format("Data type %s cannot be decoded as BigInteger", column.getType()));
+        }
+        // expected fallthrough
+        // BLOB is considered as String if it has a collation (this is TEXT column)
+
+      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 < length; i++) {
+          byte b = buf.readByte();
+          result = (result << 8) + (b & 0xff);
+        }
+        return BigInteger.valueOf(result);
+
+      case TINYINT:
+      case SMALLINT:
+      case MEDIUMINT:
+      case INTEGER:
+      case BIGINT:
+      case YEAR:
+        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
+  @SuppressWarnings("fallthrough")
+  public BigInteger decodeBinary(
+      ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal) throws SQLDataException {
+
+    switch (column.getType()) {
+      case BIT:
+        long result = 0;
+        for (int i = 0; i < length; i++) {
+          byte b = buf.readByte();
+          result = (result << 8) + (b & 0xff);
+        }
+        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()) {
+          int val = buf.readUnsignedMedium();
+          buf.skip();
+          return BigInteger.valueOf(val);
+        }
+        return BigInteger.valueOf(buf.readInt());
+
+      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 BLOB:
+      case TINYBLOB:
+      case MEDIUMBLOB:
+      case LONGBLOB:
+        if (column.isBinary()) {
+          buf.skip(length);
+          throw new SQLDataException(
+              String.format("Data type %s cannot be decoded as BigInteger", column.getType()));
+        }
+        // expected fallthrough
+        // BLOB is considered as String if it has a collation (this is TEXT column)
+
+      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(Writer encoder, Context context, Object value, Calendar cal, Long length)
+      throws IOException {
+    encoder.writeAscii(value.toString());
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    String asciiFormat = value.toString();
+    encoder.writeLength(asciiFormat.length());
+    encoder.writeAscii(asciiFormat);
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.DECIMAL.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/BitSetCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/BitSetCodec.java
new file mode 100644
index 0000000..6999f5a
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/BitSetCodec.java
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.util.BitSet;
+import java.util.Calendar;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** BitSet Codec */
+public class BitSetCodec implements Codec<BitSet> {
+
+  /** default instance */
+  public static final BitSetCodec INSTANCE = new BitSetCodec();
+
+  /**
+   * decode from mysql packet value to BitSet
+   *
+   * @param buf mysql packet buffer
+   * @param length encoded length
+   * @return BitSet value
+   */
+  public static BitSet parseBit(ReadableByteBuf buf, int length) {
+    byte[] arr = new byte[length];
+    buf.readBytes(arr);
+    revertOrder(arr);
+    return BitSet.valueOf(arr);
+  }
+
+  /**
+   * Revert byte array order
+   *
+   * @param array array to revert
+   */
+  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(ColumnDecoder column, Class<?> type) {
+    return column.getType() == DataType.BIT && type.isAssignableFrom(BitSet.class);
+  }
+
+  @Override
+  public BitSet decodeText(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal) {
+    return parseBit(buf, length);
+  }
+
+  @Override
+  public BitSet decodeBinary(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal) {
+    return parseBit(buf, length);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof BitSet;
+  }
+
+  @Override
+  public void encodeText(Writer encoder, Context context, Object value, Calendar cal, Long length)
+      throws IOException {
+    byte[] bytes = ((BitSet) 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(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    byte[] bytes = ((BitSet) value).toByteArray();
+    revertOrder(bytes);
+    encoder.writeLength(bytes.length);
+    encoder.writeBytes(bytes);
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.BLOB.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/BlobCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/BlobCodec.java
new file mode 100644
index 0000000..65b7b8b
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/BlobCodec.java
@@ -0,0 +1,228 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.sql.Blob;
+import java.sql.Clob;
+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.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+import org.mariadb.jdbc.util.constants.ServerStatus;
+
+/** Blob codec */
+public class BlobCodec implements Codec<Blob> {
+
+  /** default instance */
+  public static final BlobCodec INSTANCE = new BlobCodec();
+
+  private static final EnumSet<DataType> 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(ColumnDecoder column, Class<?> type) {
+    return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(Blob.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof Blob && !(value instanceof Clob);
+  }
+
+  @Override
+  @SuppressWarnings("fallthrough")
+  public Blob decodeText(ReadableByteBuf buf, int length, ColumnDecoder 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:
+        if (!column.isBinary()) {
+          buf.skip(length);
+          throw new SQLDataException(
+              String.format(
+                  "Data type %s (not binary) cannot be decoded as Blob", column.getType()));
+        }
+        return buf.readBlob(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, ColumnDecoder 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:
+        if (!column.isBinary()) {
+          buf.skip(length);
+          throw new SQLDataException(
+              String.format(
+                  "Data type %s (not binary) cannot be decoded as Blob", column.getType()));
+        }
+        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
+  public void encodeText(
+      Writer encoder, Context context, Object value, Calendar cal, Long maxLength)
+      throws IOException, SQLException {
+    encoder.writeBytes(ByteArrayCodec.BINARY_PREFIX);
+    byte[] array = new byte[4096];
+    InputStream is = ((Blob) 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 (maxLen > 0 && (len = is.read(array)) > 0) {
+        encoder.writeBytesEscaped(
+            array,
+            Math.min(len, (int) maxLen),
+            (context.getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES) != 0);
+        maxLen -= len;
+      }
+    }
+    encoder.writeByte('\'');
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException, SQLException {
+    long length;
+    InputStream is = ((Blob) value).getBinaryStream();
+    try {
+      length = ((Blob) value).length();
+      if (maxLength != null) length = Math.min(maxLength, length);
+
+      // if not have thrown an error
+      encoder.writeLength(length);
+      byte[] array = new byte[4096];
+      int len;
+      long remainingLen = length;
+      while ((len = is.read(array)) > 0) {
+        encoder.writeBytes(array, 0, Math.min((int) remainingLen, len));
+        remainingLen -= len;
+        if (remainingLen < 0) break;
+      }
+
+    } catch (SQLException sqle) {
+      byte[] val = encode(is, maxLength);
+      encoder.writeLength(val.length);
+      encoder.writeBytes(val, 0, val.length);
+    }
+  }
+
+  @Override
+  public void encodeLongData(Writer encoder, Blob value, Long maxLength)
+      throws IOException, SQLException {
+    byte[] array = new byte[4096];
+    InputStream is = value.getBinaryStream();
+
+    if (maxLength == null) {
+      int len;
+      while ((len = is.read(array)) > 0) {
+        encoder.writeBytes(array, 0, len);
+      }
+    } else {
+      long maxLen = maxLength;
+      int len;
+      while (maxLen > 0 && (len = is.read(array)) > 0) {
+        encoder.writeBytes(array, 0, Math.min(len, (int) maxLen));
+        maxLen -= len;
+      }
+    }
+  }
+
+  @Override
+  public byte[] encodeData(Blob value, Long maxLength) throws IOException, SQLException {
+    return encode(value.getBinaryStream(), maxLength);
+  }
+
+  private byte[] encode(InputStream is, Long maxLength) throws IOException {
+    ByteArrayOutputStream bb = new ByteArrayOutputStream();
+    byte[] array = new byte[4096];
+    if (maxLength == null) {
+      int len;
+      while ((len = is.read(array)) > 0) {
+        bb.write(array, 0, len);
+      }
+    } else {
+      long maxLen = maxLength;
+      int len;
+      while (maxLen > 0 && (len = is.read(array)) > 0) {
+        bb.write(array, 0, Math.min(len, (int) maxLen));
+        maxLen -= len;
+      }
+    }
+    return bb.toByteArray();
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.BLOB.get();
+  }
+
+  public boolean canEncodeLongData() {
+    return true;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/BooleanCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/BooleanCodec.java
new file mode 100644
index 0000000..5eaede1
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/BooleanCodec.java
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.util.Calendar;
+import java.util.EnumSet;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** Boolean codec */
+public class BooleanCodec implements Codec<Boolean> {
+
+  /** default instance */
+  public static final BooleanCodec INSTANCE = new BooleanCodec();
+
+  private static final EnumSet<DataType> COMPATIBLE_TYPES =
+      EnumSet.of(
+          DataType.VARCHAR,
+          DataType.VARSTRING,
+          DataType.STRING,
+          DataType.BIGINT,
+          DataType.INTEGER,
+          DataType.MEDIUMINT,
+          DataType.SMALLINT,
+          DataType.YEAR,
+          DataType.TINYINT,
+          DataType.DECIMAL,
+          DataType.OLDDECIMAL,
+          DataType.FLOAT,
+          DataType.DOUBLE,
+          DataType.BIT,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  public String className() {
+    return Boolean.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder 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;
+  }
+
+  public Boolean decodeText(
+      final ReadableByteBuf buffer,
+      final int length,
+      final ColumnDecoder column,
+      final Calendar cal)
+      throws SQLDataException {
+    return column.decodeBooleanText(buffer, length);
+  }
+
+  public Boolean decodeBinary(
+      final ReadableByteBuf buffer,
+      final int length,
+      final ColumnDecoder column,
+      final Calendar cal)
+      throws SQLDataException {
+    return column.decodeBooleanBinary(buffer, length);
+  }
+
+  @Override
+  public void encodeText(
+      Writer encoder, Context context, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    encoder.writeAscii(((Boolean) value) ? "1" : "0");
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    encoder.writeByte(((Boolean) value) ? 1 : 0);
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.TINYINT.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/ByteArrayCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/ByteArrayCodec.java
new file mode 100644
index 0000000..afde4b7
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/ByteArrayCodec.java
@@ -0,0 +1,112 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.util.Calendar;
+import java.util.EnumSet;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+import org.mariadb.jdbc.util.constants.ServerStatus;
+
+/** Byte array codec. */
+public class ByteArrayCodec implements Codec<byte[]> {
+
+  /** binary prefix */
+  public static final byte[] BINARY_PREFIX = {'_', 'b', 'i', 'n', 'a', 'r', 'y', ' ', '\''};
+
+  /** default instance */
+  public static final ByteArrayCodec INSTANCE = new ByteArrayCodec();
+
+  private static final EnumSet<DataType> 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[]";
+  }
+
+  public boolean canDecode(ColumnDecoder column, Class<?> type) {
+    return COMPATIBLE_TYPES.contains(column.getType())
+        && ((type.isPrimitive() && type == Byte.TYPE && type.isArray())
+            || type.isAssignableFrom(byte[].class));
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof byte[];
+  }
+
+  @Override
+  public byte[] decodeText(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+    return getBytes(buf, length, column);
+  }
+
+  private byte[] getBytes(ReadableByteBuf buf, int length, ColumnDecoder column)
+      throws SQLDataException {
+    switch (column.getType()) {
+      case BIT:
+      case BLOB:
+      case TINYBLOB:
+      case MEDIUMBLOB:
+      case LONGBLOB:
+      case STRING:
+      case VARSTRING:
+      case VARCHAR:
+      case GEOMETRY:
+        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, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+    return getBytes(buf, length, column);
+  }
+
+  @Override
+  public void encodeText(
+      Writer encoder, Context context, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    int length = ((byte[]) value).length;
+
+    encoder.writeBytes(BINARY_PREFIX);
+    encoder.writeBytesEscaped(
+        ((byte[]) value),
+        maxLength == null ? length : Math.min(length, maxLength.intValue()),
+        (context.getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES) != 0);
+    encoder.writeByte('\'');
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    int length = ((byte[]) value).length;
+    if (maxLength != null) length = Math.min(length, maxLength.intValue());
+    encoder.writeLength(length);
+    encoder.writeBytes(((byte[]) value), 0, length);
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.BLOB.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/ByteCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/ByteCodec.java
new file mode 100644
index 0000000..ff8eefa
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/ByteCodec.java
@@ -0,0 +1,112 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.util.Calendar;
+import java.util.EnumSet;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** Byte codec */
+public class ByteCodec implements Codec<Byte> {
+
+  /** default instance */
+  public static final ByteCodec INSTANCE = new ByteCodec();
+
+  private static final EnumSet<DataType> 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);
+
+  /**
+   * Parse Bits value to long value
+   *
+   * @param buf packet buffer
+   * @param length encoded length
+   * @return long value
+   */
+  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(ColumnDecoder 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(
+      final ReadableByteBuf buffer,
+      final int length,
+      final ColumnDecoder column,
+      final Calendar cal)
+      throws SQLDataException {
+    return column.decodeByteText(buffer, length);
+  }
+
+  @Override
+  public Byte decodeBinary(
+      final ReadableByteBuf buffer,
+      final int length,
+      final ColumnDecoder column,
+      final Calendar cal)
+      throws SQLDataException {
+    return column.decodeByteBinary(buffer, length);
+  }
+
+  @Override
+  public void encodeText(
+      Writer encoder, Context context, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    encoder.writeAscii(Integer.toString((Byte) value));
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    encoder.writeByte((byte) value);
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.TINYINT.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/ClobCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/ClobCodec.java
new file mode 100644
index 0000000..a7e33a8
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/ClobCodec.java
@@ -0,0 +1,174 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.sql.*;
+import java.util.Calendar;
+import java.util.EnumSet;
+import org.mariadb.jdbc.MariaDbClob;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+import org.mariadb.jdbc.util.constants.ServerStatus;
+
+/** Clob codec */
+public class ClobCodec implements Codec<Clob> {
+
+  /** Default instance */
+  public static final ClobCodec INSTANCE = new ClobCodec();
+
+  private static final EnumSet<DataType> COMPATIBLE_TYPES =
+      EnumSet.of(
+          DataType.VARCHAR,
+          DataType.VARSTRING,
+          DataType.STRING,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  public String className() {
+    return Clob.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder 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, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+    return getClob(buf, length, column);
+  }
+
+  @SuppressWarnings("fallthrough")
+  private Clob getClob(ReadableByteBuf buf, int length, ColumnDecoder column)
+      throws SQLDataException {
+    switch (column.getType()) {
+      case BLOB:
+      case TINYBLOB:
+      case MEDIUMBLOB:
+      case LONGBLOB:
+        if (column.isBinary()) {
+          buf.skip(length);
+          throw new SQLDataException(
+              String.format("Data type %s cannot be decoded as Clob", column.getType()));
+        }
+        // expected fallthrough
+        // BLOB is considered as String if it has a collation (this is TEXT column)
+
+      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, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+    return getClob(buf, length, column);
+  }
+
+  @Override
+  public void encodeText(
+      Writer encoder, Context context, Object value, Calendar cal, Long maxLength)
+      throws IOException, SQLException {
+    Reader reader = ((Clob) value).getCharacterStream();
+    char[] buf = new char[4096];
+    int len;
+    long remainingLen = maxLength == null ? Long.MAX_VALUE : maxLength;
+    encoder.writeByte('\'');
+    while (remainingLen > 0 && (len = reader.read(buf)) >= 0) {
+      byte[] data =
+          new String(buf, 0, (int) Math.min(len, remainingLen)).getBytes(StandardCharsets.UTF_8);
+      encoder.writeBytesEscaped(
+          data, data.length, (context.getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES) != 0);
+      remainingLen -= len;
+    }
+    encoder.writeByte('\'');
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException, SQLException {
+    // prefer use of encodeLongData, because length is unknown
+    Reader reader = ((Clob) value).getCharacterStream();
+    byte[] clobBytes = new byte[4096];
+    int pos = 0;
+    char[] buf = new char[4096];
+    long remainingLen = maxLength == null ? Long.MAX_VALUE : maxLength;
+    int len;
+    while (remainingLen > 0 && (len = reader.read(buf)) > 0) {
+      byte[] data =
+          new String(buf, 0, (int) Math.min(len, remainingLen)).getBytes(StandardCharsets.UTF_8);
+      if (clobBytes.length - pos < data.length) {
+        byte[] newBlobBytes = new byte[clobBytes.length + 65536];
+        System.arraycopy(clobBytes, 0, newBlobBytes, 0, pos);
+        clobBytes = newBlobBytes;
+      }
+      System.arraycopy(data, 0, clobBytes, pos, data.length);
+      pos += data.length;
+      remainingLen -= len;
+    }
+    encoder.writeLength(pos);
+    encoder.writeBytes(clobBytes, 0, pos);
+  }
+
+  @Override
+  public void encodeLongData(Writer encoder, Clob value, Long maxLength)
+      throws IOException, SQLException {
+    Reader reader = value.getCharacterStream();
+    char[] buf = new char[4096];
+    int len;
+    long remainingLen = maxLength == null ? Long.MAX_VALUE : maxLength;
+    while (remainingLen > 0 && (len = reader.read(buf)) > 0) {
+      byte[] data =
+          new String(buf, 0, (int) Math.min(len, remainingLen)).getBytes(StandardCharsets.UTF_8);
+      encoder.writeBytes(data, 0, data.length);
+      remainingLen -= len;
+    }
+  }
+
+  @Override
+  public byte[] encodeData(Clob value, Long maxLength) throws IOException, SQLException {
+    ByteArrayOutputStream bb = new ByteArrayOutputStream();
+    Reader reader = value.getCharacterStream();
+    char[] buf = new char[4096];
+    int len;
+    long remainingLen = maxLength == null ? Long.MAX_VALUE : maxLength;
+    while (remainingLen > 0 && (len = reader.read(buf)) > 0) {
+      byte[] data =
+          new String(buf, 0, (int) Math.min(len, remainingLen)).getBytes(StandardCharsets.UTF_8);
+      bb.write(data, 0, data.length);
+      remainingLen -= len;
+    }
+    return bb.toByteArray();
+  }
+
+  public boolean canEncodeLongData() {
+    return true;
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.VARSTRING.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/DateCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/DateCodec.java
new file mode 100644
index 0000000..4e83e63
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/DateCodec.java
@@ -0,0 +1,95 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.Date;
+import java.sql.SQLDataException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.EnumSet;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** Date codec */
+public class DateCodec implements Codec<Date> {
+
+  /** default instance */
+  public static final DateCodec INSTANCE = new DateCodec();
+
+  private static final EnumSet<DataType> COMPATIBLE_TYPES =
+      EnumSet.of(
+          DataType.DATE,
+          DataType.NEWDATE,
+          DataType.DATETIME,
+          DataType.TIMESTAMP,
+          DataType.YEAR,
+          DataType.VARSTRING,
+          DataType.VARCHAR,
+          DataType.STRING,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  public String className() {
+    return Date.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder column, Class<?> type) {
+    return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(Date.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof Date || java.util.Date.class.equals(value.getClass());
+  }
+
+  @Override
+  @SuppressWarnings("fallthrough")
+  public Date decodeText(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+    return column.decodeDateText(buf, length, cal);
+  }
+
+  @Override
+  @SuppressWarnings("fallthrough")
+  public Date decodeBinary(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+    return column.decodeDateBinary(buf, length, cal);
+  }
+
+  @Override
+  public void encodeText(
+      Writer encoder, Context context, Object 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(Writer encoder, Object value, Calendar providedCal, Long maxLength)
+      throws IOException {
+    Calendar cal = providedCal == null ? Calendar.getInstance() : providedCal;
+    synchronized (cal) {
+      cal.clear();
+      cal.setTimeInMillis(((java.util.Date) 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 int getBinaryEncodeType() {
+    return DataType.DATE.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/DoubleCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/DoubleCodec.java
new file mode 100644
index 0000000..47da185
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/DoubleCodec.java
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.util.Calendar;
+import java.util.EnumSet;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** Double codec */
+public class DoubleCodec implements Codec<Double> {
+
+  /** default instance */
+  public static final DoubleCodec INSTANCE = new DoubleCodec();
+
+  private static final EnumSet<DataType> 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,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  public String className() {
+    return Double.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder 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
+  @SuppressWarnings("fallthrough")
+  public Double decodeText(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+    return column.decodeDoubleText(buf, length);
+  }
+
+  @Override
+  @SuppressWarnings("fallthrough")
+  public Double decodeBinary(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+    return column.decodeDoubleBinary(buf, length);
+  }
+
+  @Override
+  public void encodeText(Writer encoder, Context context, Object value, Calendar cal, Long maxLen)
+      throws IOException {
+    encoder.writeAscii(value.toString());
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    encoder.writeDouble((Double) value);
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.DOUBLE.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/DurationCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/DurationCodec.java
new file mode 100644
index 0000000..d0f235f
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/DurationCodec.java
@@ -0,0 +1,223 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+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.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** Duration codec */
+public class DurationCodec implements Codec<Duration> {
+
+  /** default instance */
+  public static final DurationCodec INSTANCE = new DurationCodec();
+
+  private static final EnumSet<DataType> COMPATIBLE_TYPES =
+      EnumSet.of(
+          DataType.TIME,
+          DataType.DATETIME,
+          DataType.TIMESTAMP,
+          DataType.VARSTRING,
+          DataType.VARCHAR,
+          DataType.STRING,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  public String className() {
+    return Duration.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder column, Class<?> type) {
+    return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(Duration.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof Duration;
+  }
+
+  @Override
+  @SuppressWarnings("fallthrough")
+  public Duration decodeText(ReadableByteBuf buf, int length, ColumnDecoder 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 BLOB:
+      case TINYBLOB:
+      case MEDIUMBLOB:
+      case LONGBLOB:
+        if (column.isBinary()) {
+          buf.skip(length);
+          throw new SQLDataException(
+              String.format("Data type %s cannot be decoded as Duration", column.getType()));
+        }
+        // expected fallthrough
+        // BLOB is considered as String if it has a collation (this is TEXT column)
+
+      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
+  @SuppressWarnings("fallthrough")
+  public Duration decodeBinary(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+
+    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;
+
+      case TIMESTAMP:
+      case DATETIME:
+        if (length == 0) return null;
+        int year = buf.readUnsignedShort();
+        int month = buf.readByte();
+        days = buf.readByte();
+        if (length > 4) {
+          hours = buf.readByte();
+          minutes = buf.readByte();
+          seconds = buf.readByte();
+
+          if (length > 7) {
+            microseconds = buf.readUnsignedInt();
+          }
+        }
+
+        // xpand workaround https://jira.mariadb.org/browse/XPT-274
+        if (year == 0 && month == 0 && days == 0 && hours == 0 && minutes == 0 && seconds == 0)
+          return null;
+
+        return Duration.ZERO
+            .plusDays(days - 1)
+            .plusHours(hours)
+            .plusMinutes(minutes)
+            .plusSeconds(seconds)
+            .plusNanos(microseconds * 1000);
+
+      case VARCHAR:
+      case VARSTRING:
+      case STRING:
+        int[] 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 void encodeText(Writer encoder, Context context, Object val, Calendar cal, Long maxLen)
+      throws IOException {
+    long s = ((Duration) val).getSeconds();
+    long microSecond = ((Duration) 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(Writer encoder, Object val, Calendar cal, Long maxLength)
+      throws IOException {
+    int nano = ((Duration) val).getNano();
+    if (nano > 0) {
+      encoder.writeByte((byte) 12);
+      encodeDuration(encoder, ((Duration) val));
+      encoder.writeInt(nano / 1000);
+    } else {
+      encoder.writeByte((byte) 8);
+      encodeDuration(encoder, ((Duration) val));
+    }
+  }
+
+  private void encodeDuration(Writer encoder, Duration value) throws IOException {
+    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 int getBinaryEncodeType() {
+    return DataType.TIME.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/FloatCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/FloatCodec.java
new file mode 100644
index 0000000..87256c4
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/FloatCodec.java
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.util.Calendar;
+import java.util.EnumSet;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** Float codec */
+public class FloatCodec implements Codec<Float> {
+
+  /** default instance */
+  public static final FloatCodec INSTANCE = new FloatCodec();
+
+  private static final EnumSet<DataType> 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,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  public String className() {
+    return Float.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder 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(
+      final ReadableByteBuf buffer,
+      final int length,
+      final ColumnDecoder column,
+      final Calendar cal)
+      throws SQLDataException {
+    return column.decodeFloatText(buffer, length);
+  }
+
+  @Override
+  public Float decodeBinary(
+      final ReadableByteBuf buffer,
+      final int length,
+      final ColumnDecoder column,
+      final Calendar cal)
+      throws SQLDataException {
+    return column.decodeFloatBinary(buffer, length);
+  }
+
+  @Override
+  public void encodeText(Writer encoder, Context context, Object value, Calendar cal, Long maxLen)
+      throws IOException {
+    encoder.writeAscii(value.toString());
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    encoder.writeFloat((Float) value);
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.FLOAT.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/GeometryCollectionCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/GeometryCollectionCodec.java
new file mode 100644
index 0000000..a72bdca
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/GeometryCollectionCodec.java
@@ -0,0 +1,180 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.util.Calendar;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+import org.mariadb.jdbc.type.*;
+
+/** GeometryCollection codec */
+public class GeometryCollectionCodec implements Codec<GeometryCollection> {
+
+  /** default instance */
+  public static final GeometryCollectionCodec INSTANCE = new GeometryCollectionCodec();
+
+  public String className() {
+    return GeometryCollection.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder column, Class<?> type) {
+    return column.getType() == DataType.GEOMETRY && type.isAssignableFrom(GeometryCollection.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof GeometryCollection;
+  }
+
+  @Override
+  public GeometryCollection decodeText(
+      ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal) throws SQLDataException {
+    return decodeBinary(buf, length, column, cal);
+  }
+
+  @Override
+  public GeometryCollection decodeBinary(
+      ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal) throws SQLDataException {
+    if (column.getType() == DataType.GEOMETRY) {
+      buf.skip(4); // SRID
+      Geometry geo = Geometry.getGeometry(buf, length - 4, column);
+      if (geo instanceof GeometryCollection) return (GeometryCollection) geo;
+      throw new SQLDataException(
+          String.format(
+              "Geometric type %s cannot be decoded as GeometryCollection",
+              geo.getClass().getName()));
+    }
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as GeometryCollection", column.getType()));
+  }
+
+  @Override
+  public void encodeText(
+      Writer encoder, Context context, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    encoder.writeBytes(("ST_GeomCollFromText('" + value.toString() + "')").getBytes());
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    GeometryCollection geometryCollection = (GeometryCollection) value;
+
+    int length = 13;
+    for (Geometry geo : geometryCollection.getGeometries()) {
+      if (geo instanceof Point) {
+        length += 21;
+      } else if (geo instanceof LineString) {
+        length += 9 + ((LineString) geo).getPoints().length * 16;
+      } else if (geo instanceof Polygon) {
+        length += 9;
+        for (LineString ls : ((Polygon) geo).getLines()) {
+          length += 4 + ls.getPoints().length * 16;
+        }
+      } else if (geo instanceof MultiPoint) {
+        length += 9 + ((MultiPoint) geo).getPoints().length * 21;
+      } else if (geo instanceof MultiLineString) {
+        length += 9;
+        for (LineString ls : ((MultiLineString) geo).getLines()) {
+          length += 9 + ls.getPoints().length * 16;
+        }
+      } else if (geo instanceof MultiPolygon) {
+        length += 9;
+        for (Polygon poly : ((MultiPolygon) geo).getPolygons()) {
+          length += 9;
+          for (LineString ls : poly.getLines()) {
+            length += 4 + ls.getPoints().length * 16;
+          }
+        }
+      }
+    }
+
+    encoder.writeLength(length);
+    encoder.writeInt(0); // SRID
+    encoder.writeByte(0x01); // LITTLE ENDIAN
+    encoder.writeInt(7); // wkbGeometryCollection
+    encoder.writeInt(geometryCollection.getGeometries().length);
+    for (Geometry geo : geometryCollection.getGeometries()) {
+      if (geo instanceof Point) {
+        Point pt = (Point) geo;
+        encoder.writeByte(0x01); // LITTLE ENDIAN
+        encoder.writeInt(1); // wkbPoint
+        encoder.writeDouble(pt.getX());
+        encoder.writeDouble(pt.getY());
+      } else if (geo instanceof LineString) {
+        LineString ls = (LineString) geo;
+        encoder.writeByte(0x01); // LITTLE ENDIAN
+        encoder.writeInt(2); // wkbLineString
+        encoder.writeInt(ls.getPoints().length);
+        for (Point pt : ls.getPoints()) {
+          encoder.writeDouble(pt.getX());
+          encoder.writeDouble(pt.getY());
+        }
+      } else if (geo instanceof Polygon) {
+        Polygon poly = (Polygon) geo;
+        encoder.writeByte(0x01); // LITTLE ENDIAN
+        encoder.writeInt(3); // wkbPolygon
+        encoder.writeInt(poly.getLines().length);
+        for (LineString ls : poly.getLines()) {
+          encoder.writeInt(ls.getPoints().length);
+          for (Point pt : ls.getPoints()) {
+            encoder.writeDouble(pt.getX());
+            encoder.writeDouble(pt.getY());
+          }
+        }
+      } else if (geo instanceof MultiPoint) {
+        MultiPoint mp = (MultiPoint) geo;
+        encoder.writeByte(0x01); // LITTLE ENDIAN
+        encoder.writeInt(4); // wkbMultiPoint
+        encoder.writeInt(mp.getPoints().length);
+        for (Point pt : mp.getPoints()) {
+          encoder.writeByte(0x01); // LITTLE ENDIAN
+          encoder.writeInt(1); // wkbPoint
+          encoder.writeDouble(pt.getX());
+          encoder.writeDouble(pt.getY());
+        }
+      } else if (geo instanceof MultiLineString) {
+        MultiLineString mlines = (MultiLineString) geo;
+        encoder.writeByte(0x01); // LITTLE ENDIAN
+        encoder.writeInt(5); // wkbMultiLineString
+        encoder.writeInt(mlines.getLines().length);
+        for (LineString ls : mlines.getLines()) {
+          encoder.writeByte(0x01); // LITTLE ENDIAN
+          encoder.writeInt(2); // wkbLineString
+          encoder.writeInt(ls.getPoints().length); // nb points
+          for (Point pt : ls.getPoints()) {
+            encoder.writeDouble(pt.getX());
+            encoder.writeDouble(pt.getY());
+          }
+        }
+      } else if (geo instanceof MultiPolygon) {
+        MultiPolygon multiPolygon = (MultiPolygon) geo;
+        encoder.writeByte(0x01); // LITTLE ENDIAN
+        encoder.writeInt(6); // wkbMultiPolygon
+        encoder.writeInt(multiPolygon.getPolygons().length); // nb polygon
+
+        for (Polygon poly : multiPolygon.getPolygons()) {
+          encoder.writeByte(0x01); // LITTLE ENDIAN
+          encoder.writeInt(3); // wkbPolygon
+          encoder.writeInt(poly.getLines().length);
+          for (LineString ls : poly.getLines()) {
+            encoder.writeInt(ls.getPoints().length);
+            for (Point pt : ls.getPoints()) {
+              encoder.writeDouble(pt.getX());
+              encoder.writeDouble(pt.getY());
+            }
+          }
+        }
+      }
+    }
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.BLOB.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/InstantCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/InstantCodec.java
new file mode 100644
index 0000000..ee6f344
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/InstantCodec.java
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.time.*;
+import java.time.temporal.ChronoField;
+import java.util.Calendar;
+import java.util.EnumSet;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** Instant codec */
+public class InstantCodec implements Codec<Instant> {
+
+  /** default instance */
+  public static final InstantCodec INSTANCE = new InstantCodec();
+
+  private static final EnumSet<DataType> COMPATIBLE_TYPES =
+      EnumSet.of(
+          DataType.DATETIME,
+          DataType.DATE,
+          DataType.YEAR,
+          DataType.TIMESTAMP,
+          DataType.VARSTRING,
+          DataType.VARCHAR,
+          DataType.STRING,
+          DataType.TIME,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  public String className() {
+    return Instant.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder column, Class<?> type) {
+    return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(Instant.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof Instant;
+  }
+
+  @Override
+  public Instant decodeText(
+      ReadableByteBuf buf, int length, ColumnDecoder column, Calendar calParam)
+      throws SQLDataException {
+    LocalDateTime localDateTime =
+        LocalDateTimeCodec.INSTANCE.decodeText(buf, length, column, calParam);
+    if (localDateTime == null) return null;
+    return localDateTime.atZone(ZoneId.systemDefault()).toInstant();
+  }
+
+  @Override
+  public Instant decodeBinary(
+      ReadableByteBuf buf, int length, ColumnDecoder column, Calendar calParam)
+      throws SQLDataException {
+    LocalDateTime localDateTime =
+        LocalDateTimeCodec.INSTANCE.decodeBinary(buf, length, column, calParam);
+    if (localDateTime == null) return null;
+    return localDateTime.atZone(ZoneId.systemDefault()).toInstant();
+  }
+
+  @Override
+  public void encodeText(
+      Writer encoder, Context context, Object val, Calendar calParam, Long maxLen)
+      throws IOException {
+    Instant instant = (Instant) val;
+
+    encoder.writeByte('\'');
+    ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.systemDefault());
+    if (calParam != null) {
+      zonedDateTime = zonedDateTime.withZoneSameInstant(calParam.getTimeZone().toZoneId());
+    }
+    encoder.writeAscii(
+        zonedDateTime.format(
+            instant.getNano() != 0
+                ? LocalDateTimeCodec.TIMESTAMP_FORMAT
+                : LocalDateTimeCodec.TIMESTAMP_FORMAT_NO_FRACTIONAL));
+    encoder.writeByte('\'');
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar calParam, Long maxLength)
+      throws IOException {
+    Instant instant = (Instant) value;
+    ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.systemDefault());
+    if (calParam != null) {
+      zonedDateTime = zonedDateTime.withZoneSameInstant(calParam.getTimeZone().toZoneId());
+    }
+
+    int nano = zonedDateTime.getNano();
+    if (nano > 0) {
+      encoder.writeByte((byte) 11);
+      encoder.writeShort((short) zonedDateTime.get(ChronoField.YEAR));
+      encoder.writeByte(zonedDateTime.get(ChronoField.MONTH_OF_YEAR));
+      encoder.writeByte(zonedDateTime.get(ChronoField.DAY_OF_MONTH));
+      encoder.writeByte(zonedDateTime.get(ChronoField.HOUR_OF_DAY));
+      encoder.writeByte(zonedDateTime.get(ChronoField.MINUTE_OF_HOUR));
+      encoder.writeByte(zonedDateTime.get(ChronoField.SECOND_OF_MINUTE));
+      encoder.writeInt(nano / 1000);
+    } else {
+      encoder.writeByte((byte) 7);
+      encoder.writeShort((short) zonedDateTime.get(ChronoField.YEAR));
+      encoder.writeByte(zonedDateTime.get(ChronoField.MONTH_OF_YEAR));
+      encoder.writeByte(zonedDateTime.get(ChronoField.DAY_OF_MONTH));
+      encoder.writeByte(zonedDateTime.get(ChronoField.HOUR_OF_DAY));
+      encoder.writeByte(zonedDateTime.get(ChronoField.MINUTE_OF_HOUR));
+      encoder.writeByte(zonedDateTime.get(ChronoField.SECOND_OF_MINUTE));
+    }
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.DATETIME.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/IntCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/IntCodec.java
new file mode 100644
index 0000000..f1d9e16
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/IntCodec.java
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.util.Calendar;
+import java.util.EnumSet;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** Integer codec */
+public class IntCodec implements Codec<Integer> {
+
+  /** default instance */
+  public static final IntCodec INSTANCE = new IntCodec();
+
+  private static final EnumSet<DataType> 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,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  public String className() {
+    return Integer.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder 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(
+      final ReadableByteBuf buffer,
+      final int length,
+      final ColumnDecoder column,
+      final Calendar cal)
+      throws SQLDataException {
+    return column.decodeIntText(buffer, length);
+  }
+
+  @Override
+  public Integer decodeBinary(
+      final ReadableByteBuf buffer,
+      final int length,
+      final ColumnDecoder column,
+      final Calendar cal)
+      throws SQLDataException {
+    return column.decodeIntBinary(buffer, length);
+  }
+
+  @Override
+  public void encodeText(Writer encoder, Context context, Object value, Calendar cal, Long maxLen)
+      throws IOException {
+    encoder.writeAscii(value.toString());
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    encoder.writeInt((Integer) value);
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.INTEGER.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/LineStringCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/LineStringCodec.java
new file mode 100644
index 0000000..8ca68f4
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/LineStringCodec.java
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.util.Calendar;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+import org.mariadb.jdbc.type.*;
+
+/** LineString codec */
+public class LineStringCodec implements Codec<LineString> {
+
+  /** default instance */
+  public static final LineStringCodec INSTANCE = new LineStringCodec();
+
+  public String className() {
+    return LineString.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder column, Class<?> type) {
+    return column.getType() == DataType.GEOMETRY && type.isAssignableFrom(LineString.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof LineString;
+  }
+
+  @Override
+  public LineString decodeText(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+    return decodeBinary(buf, length, column, cal);
+  }
+
+  @Override
+  public LineString decodeBinary(
+      ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal) throws SQLDataException {
+    if (column.getType() == DataType.GEOMETRY) {
+      buf.skip(4); // SRID
+      Geometry geo = Geometry.getGeometry(buf, length - 4, column);
+      if (geo instanceof LineString) return (LineString) geo;
+      throw new SQLDataException(
+          String.format(
+              "Geometric type %s cannot be decoded as LineString", geo.getClass().getName()));
+    }
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as LineString", column.getType()));
+  }
+
+  @Override
+  public void encodeText(
+      Writer encoder, Context context, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    encoder.writeBytes(("ST_LineFromText('" + value.toString() + "')").getBytes());
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    LineString line = (LineString) value;
+
+    encoder.writeLength(13 + line.getPoints().length * 16L);
+    encoder.writeInt(0); // SRID
+    encoder.writeByte(0x01); // LITTLE ENDIAN
+    encoder.writeInt(2); // wkbLineString
+    encoder.writeInt(line.getPoints().length);
+    for (Point pt : line.getPoints()) {
+      encoder.writeDouble(pt.getX());
+      encoder.writeDouble(pt.getY());
+    }
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.BLOB.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/LocalDateCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/LocalDateCodec.java
new file mode 100644
index 0000000..0bb65bf
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/LocalDateCodec.java
@@ -0,0 +1,261 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+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.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** LocalDate codec */
+public class LocalDateCodec implements Codec<LocalDate> {
+
+  /** default instance */
+  public static final LocalDateCodec INSTANCE = new LocalDateCodec();
+
+  private static final EnumSet<DataType> COMPATIBLE_TYPES =
+      EnumSet.of(
+          DataType.DATE,
+          DataType.NEWDATE,
+          DataType.DATETIME,
+          DataType.TIMESTAMP,
+          DataType.YEAR,
+          DataType.VARSTRING,
+          DataType.VARCHAR,
+          DataType.STRING,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  /**
+   * Parse text encoded Date
+   *
+   * @param buf packet buffer
+   * @param length data length
+   * @return date/month/year array
+   */
+  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(ColumnDecoder column, Class<?> type) {
+    return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(LocalDate.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof LocalDate;
+  }
+
+  @Override
+  @SuppressWarnings("fallthrough")
+  public LocalDate decodeText(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+
+    int[] parts;
+    switch (column.getType()) {
+      case YEAR:
+        short y = (short) buf.atoull(length);
+
+        if (length == 2 && column.getColumnLength() == 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 BLOB:
+      case TINYBLOB:
+      case MEDIUMBLOB:
+      case LONGBLOB:
+        if (column.isBinary()) {
+          buf.skip(length);
+          throw new SQLDataException(
+              String.format("Data type %s cannot be decoded as Date", column.getType()));
+        }
+        // expected fallthrough
+        // BLOB is considered as String if it has a collation (this is TEXT column)
+
+      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.parseInt(stDatePart[0]);
+          int month = Integer.parseInt(stDatePart[1]);
+          int dayOfMonth = Integer.parseInt(stDatePart[2]);
+          if (year == 0 && month == 0 && dayOfMonth == 0) return null;
+          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
+  @SuppressWarnings("fallthrough")
+  public LocalDate decodeBinary(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+
+    int year;
+    int month = 1;
+    int dayOfMonth = 1;
+
+    switch (column.getType()) {
+      case TIMESTAMP:
+      case DATETIME:
+        if (length == 0) return null;
+        year = buf.readUnsignedShort();
+        month = buf.readByte();
+        dayOfMonth = buf.readByte();
+
+        if (length > 4) {
+          buf.skip(length - 4);
+        }
+
+        // xpand workaround https://jira.mariadb.org/browse/XPT-274
+        if (year == 0 && month == 0 && dayOfMonth == 0) return null;
+        return LocalDate.of(year, month, dayOfMonth);
+
+      case BLOB:
+      case TINYBLOB:
+      case MEDIUMBLOB:
+      case LONGBLOB:
+        if (column.isBinary()) {
+          buf.skip(length);
+          throw new SQLDataException(
+              String.format("Data type %s cannot be decoded as Date", column.getType()));
+        }
+        // expected fallthrough
+        // BLOB is considered as String if it has a collation (this is TEXT column)
+
+      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.parseInt(stDatePart[0]);
+          month = Integer.parseInt(stDatePart[1]);
+          dayOfMonth = Integer.parseInt(stDatePart[2]);
+          if (year == 0 && month == 0 && dayOfMonth == 0) return null;
+          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:
+        if (length == 0) return null;
+        year = buf.readUnsignedShort();
+
+        if (column.getColumnLength() == 2) {
+          // YEAR(2) - deprecated
+          if (year <= 69) {
+            year += 2000;
+          } else {
+            year += 1900;
+          }
+        }
+
+        if (length >= 4) {
+          month = buf.readByte();
+          dayOfMonth = buf.readByte();
+        }
+
+        // xpand workaround https://jira.mariadb.org/browse/XPT-274
+        if (year == 0 && month == 0 && dayOfMonth == 0) return null;
+
+        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(Writer encoder, Context context, Object val, Calendar cal, Long maxLen)
+      throws IOException {
+    encoder.writeByte('\'');
+    encoder.writeAscii(((LocalDate) val).format(DateTimeFormatter.ISO_LOCAL_DATE));
+    encoder.writeByte('\'');
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar providedCal, Long maxLength)
+      throws IOException {
+    LocalDate val = (LocalDate) value;
+    encoder.writeByte(7); // length
+    encoder.writeShort((short) val.get(ChronoField.YEAR));
+    encoder.writeByte(val.get(ChronoField.MONTH_OF_YEAR));
+    encoder.writeByte(val.get(ChronoField.DAY_OF_MONTH));
+    encoder.writeBytes(new byte[] {0, 0, 0});
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.DATE.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/LocalDateTimeCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/LocalDateTimeCodec.java
new file mode 100644
index 0000000..ad559ac
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/LocalDateTimeCodec.java
@@ -0,0 +1,325 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+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.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** LocalDateTime codec */
+public class LocalDateTimeCodec implements Codec<LocalDateTime> {
+
+  /** default instance */
+  public static final LocalDateTimeCodec INSTANCE = new LocalDateTimeCodec();
+
+  /** timestamp with fractional part formatter */
+  public static final DateTimeFormatter TIMESTAMP_FORMAT =
+      DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS");
+
+  /** timestamp without fractional part formatter */
+  public static final DateTimeFormatter TIMESTAMP_FORMAT_NO_FRACTIONAL =
+      DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+  /** formatter */
+  public static final DateTimeFormatter MARIADB_LOCAL_DATE_TIME;
+
+  private static final EnumSet<DataType> COMPATIBLE_TYPES =
+      EnumSet.of(
+          DataType.DATETIME,
+          DataType.TIMESTAMP,
+          DataType.VARSTRING,
+          DataType.VARCHAR,
+          DataType.STRING,
+          DataType.TIME,
+          DataType.YEAR,
+          DataType.DATE,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  static {
+    MARIADB_LOCAL_DATE_TIME =
+        new DateTimeFormatterBuilder()
+            .parseCaseInsensitive()
+            .append(DateTimeFormatter.ISO_LOCAL_DATE)
+            .appendLiteral(' ')
+            .append(DateTimeFormatter.ISO_LOCAL_TIME)
+            .toFormatter();
+  }
+
+  /**
+   * Parse timestamp to date/month/year int array
+   *
+   * @param raw string data
+   * @return date/month/year int array
+   * @throws DateTimeException if wrong format
+   */
+  public static int[] parseTimestamp(String raw) throws DateTimeException {
+    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 (partIdx < 2) throw new DateTimeException("Wrong timestamp format");
+    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(ColumnDecoder column, Class<?> type) {
+    return COMPATIBLE_TYPES.contains(column.getType())
+        && type.isAssignableFrom(LocalDateTime.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof LocalDateTime;
+  }
+
+  @Override
+  @SuppressWarnings("fallthrough")
+  public LocalDateTime decodeText(
+      ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal) throws SQLDataException {
+    int[] parts;
+    switch (column.getType()) {
+      case BLOB:
+      case TINYBLOB:
+      case MEDIUMBLOB:
+      case LONGBLOB:
+        if (column.isBinary()) {
+          buf.skip(length);
+          throw new SQLDataException(
+              String.format("Data type %s cannot be decoded as LocalDateTime", column.getType()));
+        }
+        // expected fallthrough
+        // BLOB is considered as String if it has a collation (this is TEXT column)
+
+      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);
+        if (parts[0] == -1) {
+          return LocalDateTime.of(1970, 1, 1, 0, 0)
+              .minusHours(parts[1] % 24)
+              .minusMinutes(parts[2])
+              .minusSeconds(parts[3])
+              .minusNanos(parts[4]);
+        }
+        return LocalDateTime.of(1970, 1, 1, parts[1] % 24, parts[2], parts[3]).plusNanos(parts[4]);
+
+      case YEAR:
+        int year = Integer.parseInt(buf.readAscii(length));
+        if (column.getColumnLength() <= 2) year += year >= 70 ? 1900 : 2000;
+        return LocalDateTime.of(year, 1, 1, 0, 0);
+
+      default:
+        buf.skip(length);
+        throw new SQLDataException(
+            String.format("Data type %s cannot be decoded as LocalDateTime", column.getType()));
+    }
+  }
+
+  @Override
+  @SuppressWarnings("fallthrough")
+  public LocalDateTime decodeBinary(
+      ReadableByteBuf buf, int length, ColumnDecoder 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
+        boolean negate = buf.readByte() == 1;
+        int day = buf.readInt();
+        hour = buf.readByte();
+        minutes = buf.readByte();
+        seconds = buf.readByte();
+        if (length > 8) {
+          microseconds = buf.readUnsignedInt();
+        }
+
+        if (negate) {
+          return LocalDateTime.of(1970, 1, 1, 0, 0)
+              .minusDays(day)
+              .minusHours(hour)
+              .minusMinutes(minutes)
+              .minusSeconds(seconds)
+              .minusNanos(microseconds * 1000);
+        }
+        break;
+
+      case BLOB:
+      case TINYBLOB:
+      case MEDIUMBLOB:
+      case LONGBLOB:
+        if (column.isBinary()) {
+          buf.skip(length);
+          throw new SQLDataException(
+              String.format("Data type %s cannot be decoded as LocalDateTime", column.getType()));
+        }
+        // expected fallthrough
+        // BLOB is considered as String if it has a collation (this is TEXT column)
+
+      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:
+        if (length == 0) return null;
+        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();
+          }
+        }
+
+        // xpand workaround https://jira.mariadb.org/browse/XPT-274
+        if (year == 0 && month == 0 && dayOfMonth == 0 && hour == 0 && minutes == 0 && seconds == 0)
+          return null;
+
+        break;
+
+      case YEAR:
+        year = buf.readUnsignedShort();
+        if (column.getColumnLength() <= 2) year += year >= 70 ? 1900 : 2000;
+        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(Writer encoder, Context context, Object value, Calendar cal, Long maxLen)
+      throws IOException {
+    LocalDateTime val = (LocalDateTime) value;
+    encoder.writeByte('\'');
+    encoder.writeAscii(
+        val.format(val.getNano() != 0 ? TIMESTAMP_FORMAT : TIMESTAMP_FORMAT_NO_FRACTIONAL));
+    encoder.writeByte('\'');
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    LocalDateTime val = (LocalDateTime) value;
+    int nano = val.getNano();
+    if (nano > 0) {
+      encoder.writeByte((byte) 11);
+      encoder.writeShort((short) val.get(ChronoField.YEAR));
+      encoder.writeByte(val.get(ChronoField.MONTH_OF_YEAR));
+      encoder.writeByte(val.get(ChronoField.DAY_OF_MONTH));
+      encoder.writeByte(val.get(ChronoField.HOUR_OF_DAY));
+      encoder.writeByte(val.get(ChronoField.MINUTE_OF_HOUR));
+      encoder.writeByte(val.get(ChronoField.SECOND_OF_MINUTE));
+      encoder.writeInt(nano / 1000);
+    } else {
+      encoder.writeByte((byte) 7);
+      encoder.writeShort((short) val.get(ChronoField.YEAR));
+      encoder.writeByte(val.get(ChronoField.MONTH_OF_YEAR));
+      encoder.writeByte(val.get(ChronoField.DAY_OF_MONTH));
+      encoder.writeByte(val.get(ChronoField.HOUR_OF_DAY));
+      encoder.writeByte(val.get(ChronoField.MINUTE_OF_HOUR));
+      encoder.writeByte(val.get(ChronoField.SECOND_OF_MINUTE));
+    }
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.DATETIME.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/LocalTimeCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/LocalTimeCodec.java
new file mode 100644
index 0000000..6d48e76
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/LocalTimeCodec.java
@@ -0,0 +1,316 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoField;
+import java.util.Calendar;
+import java.util.EnumSet;
+import java.util.TimeZone;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** LocalTime codec */
+public class LocalTimeCodec implements Codec<LocalTime> {
+
+  /** default instance */
+  public static final LocalTimeCodec INSTANCE = new LocalTimeCodec();
+
+  private static final EnumSet<DataType> COMPATIBLE_TYPES =
+      EnumSet.of(
+          DataType.TIME,
+          DataType.DATETIME,
+          DataType.TIMESTAMP,
+          DataType.VARSTRING,
+          DataType.VARCHAR,
+          DataType.STRING,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  /**
+   * Parse text value into hour/minutes/seconds/microseconds array
+   *
+   * @param buf packet buffer
+   * @param length data length
+   * @param column column metadata
+   * @return hour/minutes/seconds/microseconds array
+   * @throws SQLDataException if parsing error occurs
+   */
+  public static int[] parseTime(ReadableByteBuf buf, int length, ColumnDecoder 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(ColumnDecoder column, Class<?> type) {
+    return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(LocalTime.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof LocalTime;
+  }
+
+  @Override
+  @SuppressWarnings("fallthrough")
+  public LocalTime decodeText(ReadableByteBuf buf, int length, ColumnDecoder 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] * 60L + 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 BLOB:
+      case TINYBLOB:
+      case MEDIUMBLOB:
+      case LONGBLOB:
+        if (column.isBinary()) {
+          buf.skip(length);
+          throw new SQLDataException(
+              String.format("Data type %s cannot be decoded as LocalTime", column.getType()));
+        }
+        // expected fallthrough
+        // BLOB is considered as String if it has a collation (this is TEXT column)
+
+      case VARSTRING:
+      case VARCHAR:
+      case STRING:
+        String val = buf.readString(length);
+        try {
+          if (val.contains(" ")) {
+            ZoneId tz =
+                cal != null ? cal.getTimeZone().toZoneId() : TimeZone.getDefault().toZoneId();
+            return LocalDateTime.parse(val, LocalDateTimeCodec.MARIADB_LOCAL_DATE_TIME.withZone(tz))
+                .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
+  @SuppressWarnings("fallthrough")
+  public LocalTime decodeBinary(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+
+    int hour = 0;
+    int minutes = 0;
+    int seconds = 0;
+    long microseconds = 0;
+    switch (column.getType()) {
+      case TIMESTAMP:
+      case DATETIME:
+        if (length == 0) return null;
+
+        int year = buf.readUnsignedShort();
+        int month = buf.readByte();
+        int dayOfMonth = buf.readByte();
+
+        if (length > 4) {
+          hour = buf.readByte();
+          minutes = buf.readByte();
+          seconds = buf.readByte();
+
+          if (length > 7) {
+            microseconds = buf.readInt();
+          }
+        }
+
+        // xpand workaround https://jira.mariadb.org/browse/XPT-274
+        if (year == 0 && month == 0 && dayOfMonth == 0 && hour == 0 && minutes == 0 && seconds == 0)
+          return null;
+
+        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 BLOB:
+      case TINYBLOB:
+      case MEDIUMBLOB:
+      case LONGBLOB:
+        if (column.isBinary()) {
+          buf.skip(length);
+          throw new SQLDataException(
+              String.format("Data type %s cannot be decoded as LocalTime", column.getType()));
+        }
+        // expected fallthrough
+        // BLOB is considered as String if it has a collation (this is TEXT column)
+
+      case VARSTRING:
+      case VARCHAR:
+      case STRING:
+        String val = buf.readString(length);
+        try {
+          if (val.contains(" ")) {
+            ZoneId tz =
+                cal != null ? cal.getTimeZone().toZoneId() : TimeZone.getDefault().toZoneId();
+            return LocalDateTime.parse(val, LocalDateTimeCodec.MARIADB_LOCAL_DATE_TIME.withZone(tz))
+                .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(Writer encoder, Context context, Object value, Calendar cal, Long maxLen)
+      throws IOException {
+    LocalTime val = (LocalTime) value;
+    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(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    LocalTime val = (LocalTime) value;
+    int nano = val.getNano();
+    if (nano > 0) {
+      encoder.writeByte((byte) 12);
+      encoder.writeByte((byte) 0);
+      encoder.writeInt(0);
+      encoder.writeByte((byte) val.get(ChronoField.HOUR_OF_DAY));
+      encoder.writeByte((byte) val.get(ChronoField.MINUTE_OF_HOUR));
+      encoder.writeByte((byte) val.get(ChronoField.SECOND_OF_MINUTE));
+      encoder.writeInt(nano / 1000);
+    } else {
+      encoder.writeByte((byte) 8);
+      encoder.writeByte((byte) 0);
+      encoder.writeInt(0);
+      encoder.writeByte((byte) val.get(ChronoField.HOUR_OF_DAY));
+      encoder.writeByte((byte) val.get(ChronoField.MINUTE_OF_HOUR));
+      encoder.writeByte((byte) val.get(ChronoField.SECOND_OF_MINUTE));
+    }
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.TIME.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/LongCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/LongCodec.java
new file mode 100644
index 0000000..22f05d4
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/LongCodec.java
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.util.Calendar;
+import java.util.EnumSet;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** LongCodec codec */
+public class LongCodec implements Codec<Long> {
+
+  /** default instance */
+  public static final LongCodec INSTANCE = new LongCodec();
+
+  private static final EnumSet<DataType> 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,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  public String className() {
+    return Long.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder 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(
+      final ReadableByteBuf buffer,
+      final int length,
+      final ColumnDecoder column,
+      final Calendar cal)
+      throws SQLDataException {
+    return column.decodeLongText(buffer, length);
+  }
+
+  @Override
+  public Long decodeBinary(
+      final ReadableByteBuf buffer,
+      final int length,
+      final ColumnDecoder column,
+      final Calendar cal)
+      throws SQLDataException {
+    return column.decodeLongBinary(buffer, length);
+  }
+
+  @Override
+  public void encodeText(Writer encoder, Context context, Object value, Calendar cal, Long maxLen)
+      throws IOException {
+    encoder.writeAscii(value.toString());
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    encoder.writeLong((Long) value);
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.BIGINT.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/MultiLinestringCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/MultiLinestringCodec.java
new file mode 100644
index 0000000..8c1867f
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/MultiLinestringCodec.java
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.util.Calendar;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+import org.mariadb.jdbc.type.Geometry;
+import org.mariadb.jdbc.type.LineString;
+import org.mariadb.jdbc.type.MultiLineString;
+import org.mariadb.jdbc.type.Point;
+
+/** MultiLineString codec */
+public class MultiLinestringCodec implements Codec<MultiLineString> {
+
+  /** default instance */
+  public static final MultiLinestringCodec INSTANCE = new MultiLinestringCodec();
+
+  public String className() {
+    return MultiLineString.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder column, Class<?> type) {
+    return column.getType() == DataType.GEOMETRY && type.isAssignableFrom(MultiLineString.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof MultiLineString;
+  }
+
+  @Override
+  public MultiLineString decodeText(
+      ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal) throws SQLDataException {
+    return decodeBinary(buf, length, column, cal);
+  }
+
+  @Override
+  public MultiLineString decodeBinary(
+      ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal) throws SQLDataException {
+    if (column.getType() == DataType.GEOMETRY) {
+      buf.skip(4); // SRID
+      Geometry geo = Geometry.getGeometry(buf, length - 4, column);
+      if (geo instanceof MultiLineString) return (MultiLineString) geo;
+      throw new SQLDataException(
+          String.format(
+              "Geometric type %s cannot be decoded as MultiLineString", geo.getClass().getName()));
+    }
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as MultiLineString", column.getType()));
+  }
+
+  @Override
+  public void encodeText(
+      Writer encoder, Context context, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    encoder.writeBytes(("ST_MLineFromText('" + value.toString() + "')").getBytes());
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    MultiLineString mp = (MultiLineString) value;
+
+    int length = 13;
+    for (LineString ls : mp.getLines()) {
+      length += 9 + ls.getPoints().length * 16;
+    }
+
+    encoder.writeLength(length);
+    encoder.writeInt(0); // SRID
+    encoder.writeByte(0x01); // LITTLE ENDIAN
+    encoder.writeInt(5); // wkbMultiLineString
+    encoder.writeInt(mp.getLines().length);
+    for (LineString ls : mp.getLines()) {
+      encoder.writeByte(0x01); // LITTLE ENDIAN
+      encoder.writeInt(2); // wkbLineString
+      encoder.writeInt(ls.getPoints().length); // nb points
+      for (Point pt : ls.getPoints()) {
+        encoder.writeDouble(pt.getX());
+        encoder.writeDouble(pt.getY());
+      }
+    }
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.BLOB.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/MultiPointCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/MultiPointCodec.java
new file mode 100644
index 0000000..f18de3b
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/MultiPointCodec.java
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.util.Calendar;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+import org.mariadb.jdbc.type.Geometry;
+import org.mariadb.jdbc.type.MultiPoint;
+import org.mariadb.jdbc.type.Point;
+
+/** MultiPoint codec */
+public class MultiPointCodec implements Codec<MultiPoint> {
+
+  /** default instance */
+  public static final MultiPointCodec INSTANCE = new MultiPointCodec();
+
+  public String className() {
+    return MultiPoint.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder column, Class<?> type) {
+    return column.getType() == DataType.GEOMETRY && type.isAssignableFrom(MultiPoint.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof MultiPoint;
+  }
+
+  @Override
+  public MultiPoint decodeText(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+    return decodeBinary(buf, length, column, cal);
+  }
+
+  @Override
+  public MultiPoint decodeBinary(
+      ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal) throws SQLDataException {
+    if (column.getType() == DataType.GEOMETRY) {
+      buf.skip(4); // SRID
+      Geometry geo = Geometry.getGeometry(buf, length - 4, column);
+      if (geo instanceof MultiPoint) return (MultiPoint) geo;
+      throw new SQLDataException(
+          String.format(
+              "Geometric type %s cannot be decoded as MultiPoint", geo.getClass().getName()));
+    }
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as MultiPoint", column.getType()));
+  }
+
+  @Override
+  public void encodeText(
+      Writer encoder, Context context, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    encoder.writeBytes(("ST_MPointFromText('" + value.toString() + "')").getBytes());
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    MultiPoint mp = (MultiPoint) value;
+    encoder.writeLength(13 + mp.getPoints().length * 21L);
+    encoder.writeInt(0); // SRID
+    encoder.writeByte(0x01); // LITTLE ENDIAN
+    encoder.writeInt(4); // wkbMultiPoint
+    encoder.writeInt(mp.getPoints().length);
+    for (Point pt : mp.getPoints()) {
+      encoder.writeByte(0x01); // LITTLE ENDIAN
+      encoder.writeInt(1); // wkbPoint
+      encoder.writeDouble(pt.getX());
+      encoder.writeDouble(pt.getY());
+    }
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.BLOB.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/MultiPolygonCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/MultiPolygonCodec.java
new file mode 100644
index 0000000..2f74c52
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/MultiPolygonCodec.java
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.util.Calendar;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+import org.mariadb.jdbc.type.*;
+
+/** MultiPolygon codec */
+public class MultiPolygonCodec implements Codec<MultiPolygon> {
+
+  /** default instance */
+  public static final MultiPolygonCodec INSTANCE = new MultiPolygonCodec();
+
+  public String className() {
+    return MultiPolygon.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder column, Class<?> type) {
+    return column.getType() == DataType.GEOMETRY && type.isAssignableFrom(MultiPolygon.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof MultiPolygon;
+  }
+
+  @Override
+  public MultiPolygon decodeText(
+      ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal) throws SQLDataException {
+    return decodeBinary(buf, length, column, cal);
+  }
+
+  @Override
+  public MultiPolygon decodeBinary(
+      ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal) throws SQLDataException {
+    if (column.getType() == DataType.GEOMETRY) {
+      buf.skip(4); // SRID
+      Geometry geo = Geometry.getGeometry(buf, length - 4, column);
+      if (geo instanceof MultiPolygon) return (MultiPolygon) geo;
+      throw new SQLDataException(
+          String.format(
+              "Geometric type %s cannot be decoded as MultiPolygon", geo.getClass().getName()));
+    }
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as MultiPolygon", column.getType()));
+  }
+
+  @Override
+  public void encodeText(
+      Writer encoder, Context context, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    encoder.writeBytes(("ST_MPolyFromText('" + value.toString() + "')").getBytes());
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    MultiPolygon mariadbMultiPolygon = (MultiPolygon) value;
+
+    int length = 13;
+    for (Polygon poly : mariadbMultiPolygon.getPolygons()) {
+      length += 9;
+      for (LineString ls : poly.getLines()) {
+        length += 4 + ls.getPoints().length * 16;
+      }
+    }
+
+    encoder.writeLength(length);
+    encoder.writeInt(0); // SRID
+    encoder.writeByte(0x01); // LITTLE ENDIAN
+    encoder.writeInt(6); // wkbMultiPolygon
+    encoder.writeInt(mariadbMultiPolygon.getPolygons().length); // nb polygon
+
+    for (Polygon poly : mariadbMultiPolygon.getPolygons()) {
+      encoder.writeByte(0x01); // LITTLE ENDIAN
+      encoder.writeInt(3); // wkbPolygon
+      encoder.writeInt(poly.getLines().length);
+      for (LineString ls : poly.getLines()) {
+        encoder.writeInt(ls.getPoints().length);
+        for (Point pt : ls.getPoints()) {
+          encoder.writeDouble(pt.getX());
+          encoder.writeDouble(pt.getY());
+        }
+      }
+    }
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.BLOB.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/OffsetDateTimeCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/OffsetDateTimeCodec.java
new file mode 100644
index 0000000..3e702a4
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/OffsetDateTimeCodec.java
@@ -0,0 +1,166 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoField;
+import java.util.Calendar;
+import java.util.EnumSet;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** OffsetDateTime codec */
+public class OffsetDateTimeCodec implements Codec<OffsetDateTime> {
+
+  /** default instance */
+  public static final OffsetDateTimeCodec INSTANCE = new OffsetDateTimeCodec();
+
+  private static final EnumSet<DataType> COMPATIBLE_TYPES =
+      EnumSet.of(
+          DataType.DATETIME,
+          DataType.DATE,
+          DataType.YEAR,
+          DataType.TIMESTAMP,
+          DataType.VARSTRING,
+          DataType.VARCHAR,
+          DataType.STRING,
+          DataType.TIME,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  public String className() {
+    return OffsetDateTime.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder column, Class<?> type) {
+    return COMPATIBLE_TYPES.contains(column.getType())
+        && type.isAssignableFrom(OffsetDateTime.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof OffsetDateTime;
+  }
+
+  @Override
+  public OffsetDateTime decodeText(
+      ReadableByteBuf buf, int length, ColumnDecoder column, Calendar calParam)
+      throws SQLDataException {
+
+    switch (column.getType()) {
+      case DATETIME:
+      case TIMESTAMP:
+        LocalDateTime localDateTime =
+            LocalDateTimeCodec.INSTANCE.decodeText(buf, length, column, calParam);
+        if (localDateTime == null) return null;
+        Calendar cal = calParam == null ? Calendar.getInstance() : calParam;
+        return localDateTime.atZone(cal.getTimeZone().toZoneId()).toOffsetDateTime();
+      case STRING:
+      case VARCHAR:
+      case VARSTRING:
+        String val = buf.readString(length);
+        try {
+          return OffsetDateTime.parse(val);
+        } catch (Throwable e) {
+          // eat
+        }
+        throw new SQLDataException(
+            String.format(
+                "value '%s' (%s) cannot be decoded as OffsetDateTime", val, column.getType()));
+      default:
+        buf.skip(length);
+        throw new SQLDataException(
+            String.format(
+                "value of type %s cannot be decoded as OffsetDateTime", column.getType()));
+    }
+  }
+
+  @Override
+  public OffsetDateTime decodeBinary(
+      ReadableByteBuf buf, int length, ColumnDecoder column, Calendar calParam)
+      throws SQLDataException {
+
+    switch (column.getType()) {
+      case DATETIME:
+      case TIMESTAMP:
+        LocalDateTime localDateTime =
+            LocalDateTimeCodec.INSTANCE.decodeBinary(buf, length, column, calParam);
+        if (localDateTime == null) return null;
+        Calendar cal = calParam == null ? Calendar.getInstance() : calParam;
+        return localDateTime.atZone(cal.getTimeZone().toZoneId()).toOffsetDateTime();
+      case STRING:
+      case VARCHAR:
+      case VARSTRING:
+        String val = buf.readString(length);
+        try {
+          return OffsetDateTime.parse(val);
+        } catch (Throwable e) {
+          // eat
+        }
+        throw new SQLDataException(
+            String.format(
+                "value '%s' (%s) cannot be decoded as OffsetDateTime", val, column.getType()));
+
+      default:
+        buf.skip(length);
+        throw new SQLDataException(
+            String.format(
+                "value of type %s cannot be decoded as OffsetDateTime", column.getType()));
+    }
+  }
+
+  @Override
+  public void encodeText(
+      Writer encoder, Context context, Object val, Calendar calParam, Long maxLen)
+      throws IOException {
+    OffsetDateTime zdt = (OffsetDateTime) val;
+    Calendar cal = calParam == null ? Calendar.getInstance() : calParam;
+    encoder.writeByte('\'');
+    encoder.writeAscii(
+        zdt.atZoneSameInstant(cal.getTimeZone().toZoneId())
+            .format(
+                zdt.getNano() != 0
+                    ? LocalDateTimeCodec.TIMESTAMP_FORMAT
+                    : LocalDateTimeCodec.TIMESTAMP_FORMAT_NO_FRACTIONAL));
+    encoder.writeByte('\'');
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar calParam, Long maxLength)
+      throws IOException {
+    OffsetDateTime zdt = (OffsetDateTime) value;
+    Calendar cal = calParam == null ? Calendar.getInstance() : calParam;
+    ZonedDateTime convertedZdt = zdt.atZoneSameInstant(cal.getTimeZone().toZoneId());
+    int nano = convertedZdt.getNano();
+    if (nano > 0) {
+      encoder.writeByte((byte) 11);
+      encoder.writeShort((short) convertedZdt.get(ChronoField.YEAR));
+      encoder.writeByte(convertedZdt.get(ChronoField.MONTH_OF_YEAR));
+      encoder.writeByte(convertedZdt.get(ChronoField.DAY_OF_MONTH));
+      encoder.writeByte(convertedZdt.get(ChronoField.HOUR_OF_DAY));
+      encoder.writeByte(convertedZdt.get(ChronoField.MINUTE_OF_HOUR));
+      encoder.writeByte(convertedZdt.get(ChronoField.SECOND_OF_MINUTE));
+      encoder.writeInt(nano / 1000);
+    } else {
+      encoder.writeByte((byte) 7);
+      encoder.writeShort((short) convertedZdt.get(ChronoField.YEAR));
+      encoder.writeByte(convertedZdt.get(ChronoField.MONTH_OF_YEAR));
+      encoder.writeByte(convertedZdt.get(ChronoField.DAY_OF_MONTH));
+      encoder.writeByte(convertedZdt.get(ChronoField.HOUR_OF_DAY));
+      encoder.writeByte(convertedZdt.get(ChronoField.MINUTE_OF_HOUR));
+      encoder.writeByte(convertedZdt.get(ChronoField.SECOND_OF_MINUTE));
+    }
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.DATETIME.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/PointCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/PointCodec.java
new file mode 100644
index 0000000..c5c015b
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/PointCodec.java
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.util.Calendar;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+import org.mariadb.jdbc.type.*;
+
+/** Point codec */
+public class PointCodec implements Codec<Point> {
+
+  /** default instance */
+  public static final PointCodec INSTANCE = new PointCodec();
+
+  public String className() {
+    return Point.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder column, Class<?> type) {
+    return column.getType() == DataType.GEOMETRY && type.isAssignableFrom(Point.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof Point;
+  }
+
+  @Override
+  public Point decodeText(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+    return decodeBinary(buf, length, column, cal);
+  }
+
+  @Override
+  public Point decodeBinary(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+    if (column.getType() == DataType.GEOMETRY) {
+      buf.skip(4); // SRID
+      Geometry geo = Geometry.getGeometry(buf, length - 4, column);
+      if (geo instanceof Point) return (Point) geo;
+      throw new SQLDataException(
+          String.format("Geometric type %s cannot be decoded as Point", geo.getClass().getName()));
+    }
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Point", column.getType()));
+  }
+
+  @Override
+  public void encodeText(
+      Writer encoder, Context context, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    encoder.writeBytes(("ST_PointFromText('" + value.toString() + "')").getBytes());
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    Point pt = (Point) value;
+    encoder.writeLength(25);
+    encoder.writeInt(0); // SRID
+    encoder.writeByte(0x01); // LITTLE ENDIAN
+    encoder.writeInt(1); // wkbPoint
+    encoder.writeDouble(pt.getX());
+    encoder.writeDouble(pt.getY());
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.BLOB.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/PolygonCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/PolygonCodec.java
new file mode 100644
index 0000000..e0fda8f
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/PolygonCodec.java
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.util.Calendar;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+import org.mariadb.jdbc.type.Geometry;
+import org.mariadb.jdbc.type.LineString;
+import org.mariadb.jdbc.type.Point;
+import org.mariadb.jdbc.type.Polygon;
+
+/** Polygon codec */
+public class PolygonCodec implements Codec<Polygon> {
+
+  /** default instance */
+  public static final PolygonCodec INSTANCE = new PolygonCodec();
+
+  public String className() {
+    return Polygon.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder column, Class<?> type) {
+    return column.getType() == DataType.GEOMETRY && type.isAssignableFrom(Polygon.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof Polygon;
+  }
+
+  @Override
+  public Polygon decodeText(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+    return decodeBinary(buf, length, column, cal);
+  }
+
+  @Override
+  public Polygon decodeBinary(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+    if (column.getType() == DataType.GEOMETRY) {
+      buf.skip(4); // SRID
+      Geometry geo = Geometry.getGeometry(buf, length - 4, column);
+      if (geo instanceof Polygon) return (Polygon) geo;
+      throw new SQLDataException(
+          String.format(
+              "Geometric type %s cannot be decoded as Polygon", geo.getClass().getName()));
+    }
+    buf.skip(length);
+    throw new SQLDataException(
+        String.format("Data type %s cannot be decoded as Polygon", column.getType()));
+  }
+
+  @Override
+  public void encodeText(
+      Writer encoder, Context context, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    encoder.writeBytes(("ST_PolyFromText('" + value.toString() + "')").getBytes());
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    Polygon poly = (Polygon) value;
+
+    int length = 13;
+    for (LineString ls : poly.getLines()) {
+      length += 4 + ls.getPoints().length * 16;
+    }
+
+    encoder.writeLength(length);
+    encoder.writeInt(0); // SRID
+    encoder.writeByte(0x01); // LITTLE ENDIAN
+    encoder.writeInt(3); // wkbPolygon
+    encoder.writeInt(poly.getLines().length);
+    for (LineString ls : poly.getLines()) {
+      encoder.writeInt(ls.getPoints().length);
+      for (Point pt : ls.getPoints()) {
+        encoder.writeDouble(pt.getX());
+        encoder.writeDouble(pt.getY());
+      }
+    }
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.BLOB.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/ReaderCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/ReaderCodec.java
new file mode 100644
index 0000000..2b782dc
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/ReaderCodec.java
@@ -0,0 +1,171 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.sql.SQLDataException;
+import java.util.Calendar;
+import java.util.EnumSet;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+import org.mariadb.jdbc.util.constants.ServerStatus;
+
+/** Reader codec */
+public class ReaderCodec implements Codec<Reader> {
+
+  /** default instance */
+  public static final ReaderCodec INSTANCE = new ReaderCodec();
+
+  private static final EnumSet<DataType> COMPATIBLE_TYPES =
+      EnumSet.of(
+          DataType.STRING,
+          DataType.VARCHAR,
+          DataType.VARSTRING,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  public boolean canDecode(ColumnDecoder column, Class<?> type) {
+    return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(Reader.class);
+  }
+
+  public String className() {
+    return Reader.class.getName();
+  }
+
+  @Override
+  @SuppressWarnings("fallthrough")
+  public Reader decodeText(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+    switch (column.getType()) {
+      case BLOB:
+      case TINYBLOB:
+      case MEDIUMBLOB:
+      case LONGBLOB:
+        if (column.isBinary()) {
+          buf.skip(length);
+          throw new SQLDataException(
+              String.format("Data type %s cannot be decoded as Reader", column.getType()));
+        }
+        // expected fallthrough
+        // BLOB is considered as String if it has a collation (this is TEXT column)
+
+      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, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+    return decodeText(buf, length, column, cal);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof Reader;
+  }
+
+  @Override
+  public void encodeText(Writer encoder, Context context, Object val, Calendar cal, Long maxLen)
+      throws IOException {
+    Reader reader = (Reader) val;
+    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(Writer encoder, Object val, Calendar cal, Long maxLength)
+      throws IOException {
+    // prefer use of encodeLongData, because length is unknown
+    byte[] clobBytes = new byte[4096];
+    int pos = 0;
+    char[] buf = new char[4096];
+    Reader reader = (Reader) val;
+    int len;
+
+    long maxLen = maxLength != null ? maxLength : Long.MAX_VALUE;
+    while (maxLen > 0 && (len = reader.read(buf)) > 0) {
+      byte[] data =
+          new String(buf, 0, (int) Math.min(len, maxLen)).getBytes(StandardCharsets.UTF_8);
+      if (clobBytes.length - pos < data.length) {
+        byte[] newBlobBytes = new byte[clobBytes.length + 65536];
+        System.arraycopy(clobBytes, 0, newBlobBytes, 0, pos);
+        clobBytes = newBlobBytes;
+      }
+      System.arraycopy(data, 0, clobBytes, pos, data.length);
+      pos += data.length;
+      maxLen -= len;
+    }
+    encoder.writeLength(pos);
+    encoder.writeBytes(clobBytes, 0, pos);
+  }
+
+  @Override
+  public void encodeLongData(Writer encoder, Reader reader, Long maxLength) throws IOException {
+    char[] buf = new char[4096];
+    int len;
+    long maxLen = maxLength != null ? maxLength : Long.MAX_VALUE;
+    while (maxLen > 0 && (len = reader.read(buf)) >= 0) {
+      byte[] data =
+          new String(buf, 0, (int) Math.min(len, maxLen)).getBytes(StandardCharsets.UTF_8);
+      encoder.writeBytes(data, 0, data.length);
+      maxLen -= len;
+    }
+  }
+
+  @Override
+  public byte[] encodeData(Reader reader, Long maxLength) throws IOException {
+    ByteArrayOutputStream bb = new ByteArrayOutputStream();
+    char[] buf = new char[4096];
+    int len;
+    long maxLen = maxLength != null ? maxLength : Long.MAX_VALUE;
+    while (maxLen > 0 && (len = reader.read(buf)) >= 0) {
+      byte[] data =
+          new String(buf, 0, (int) Math.min(len, maxLen)).getBytes(StandardCharsets.UTF_8);
+      bb.write(data, 0, data.length);
+      maxLen -= len;
+    }
+    return bb.toByteArray();
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.VARSTRING.get();
+  }
+
+  public boolean canEncodeLongData() {
+    return true;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/ShortCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/ShortCodec.java
new file mode 100644
index 0000000..5dfcb93
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/ShortCodec.java
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.util.Calendar;
+import java.util.EnumSet;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** Short codec */
+public class ShortCodec implements Codec<Short> {
+
+  /** default instance */
+  public static final ShortCodec INSTANCE = new ShortCodec();
+
+  private static final EnumSet<DataType> 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,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  public boolean canDecode(ColumnDecoder 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(
+      final ReadableByteBuf buffer,
+      final int length,
+      final ColumnDecoder column,
+      final Calendar cal)
+      throws SQLDataException {
+    return column.decodeShortText(buffer, length);
+  }
+
+  @Override
+  public Short decodeBinary(
+      final ReadableByteBuf buffer,
+      final int length,
+      final ColumnDecoder column,
+      final Calendar cal)
+      throws SQLDataException {
+    return column.decodeShortBinary(buffer, length);
+  }
+
+  @Override
+  public void encodeText(Writer encoder, Context context, Object value, Calendar cal, Long maxLen)
+      throws IOException {
+    encoder.writeAscii(value.toString());
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    encoder.writeShort((Short) value);
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.SMALLINT.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/StreamCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/StreamCodec.java
new file mode 100644
index 0000000..06a5a87
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/StreamCodec.java
@@ -0,0 +1,192 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.*;
+import java.sql.SQLDataException;
+import java.util.Calendar;
+import java.util.EnumSet;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+import org.mariadb.jdbc.util.constants.ServerStatus;
+
+/** InputStream codec */
+public class StreamCodec implements Codec<InputStream> {
+
+  /** default instance */
+  public static final StreamCodec INSTANCE = new StreamCodec();
+
+  private static final EnumSet<DataType> COMPATIBLE_TYPES =
+      EnumSet.of(
+          DataType.VARCHAR,
+          DataType.VARSTRING,
+          DataType.STRING,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  public String className() {
+    return InputStream.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder column, Class<?> type) {
+    return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(InputStream.class);
+  }
+
+  @Override
+  public InputStream decodeText(ReadableByteBuf buf, int length, ColumnDecoder 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, ColumnDecoder 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(Writer encoder, Context context, Object value, Calendar cal, Long maxLen)
+      throws IOException {
+    encoder.writeBytes(ByteArrayCodec.BINARY_PREFIX);
+    byte[] array = new byte[4096];
+    int len;
+    InputStream stream = (InputStream) value;
+
+    if (maxLen == null) {
+      while ((len = stream.read(array)) > 0) {
+        encoder.writeBytesEscaped(
+            array, len, (context.getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES) != 0);
+      }
+    } else {
+      while ((len = stream.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(Writer encoder, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    // length is not known
+    byte[] blobBytes = new byte[4096];
+    int pos = 0;
+    byte[] array = new byte[4096];
+    InputStream stream = (InputStream) value;
+
+    int len;
+    if (maxLength == null) {
+      while ((len = stream.read(array)) > 0) {
+        if (blobBytes.length - pos < len) {
+          byte[] newBlobBytes = new byte[blobBytes.length + 65536];
+          System.arraycopy(blobBytes, 0, newBlobBytes, 0, blobBytes.length);
+          blobBytes = newBlobBytes;
+        }
+        System.arraycopy(array, 0, blobBytes, pos, len);
+        pos += len;
+      }
+    } else {
+      long remainingLen = maxLength;
+      while ((len = stream.read(array)) > 0 && remainingLen > 0) {
+        len = Math.min((int) remainingLen, len);
+        if (blobBytes.length - pos < len) {
+          byte[] newBlobBytes = new byte[blobBytes.length + 65536];
+          System.arraycopy(blobBytes, 0, newBlobBytes, 0, blobBytes.length);
+          blobBytes = newBlobBytes;
+        }
+        System.arraycopy(array, 0, blobBytes, pos, len);
+        pos += len;
+        remainingLen -= len;
+      }
+    }
+    encoder.writeLength(pos);
+    encoder.writeBytes(blobBytes, 0, pos);
+  }
+
+  @Override
+  public void encodeLongData(Writer encoder, InputStream value, Long maxLength) throws IOException {
+    byte[] array = new byte[4096];
+    int len;
+    if (maxLength == null) {
+      while ((len = value.read(array)) > 0) {
+        encoder.writeBytes(array, 0, len);
+      }
+    } else {
+      long maxLen = maxLength;
+      while ((len = value.read(array)) > 0 && maxLen > 0) {
+        encoder.writeBytes(array, 0, Math.min(len, (int) maxLen));
+        maxLen -= len;
+      }
+    }
+  }
+
+  @Override
+  public byte[] encodeData(InputStream value, Long maxLength) throws IOException {
+    ByteArrayOutputStream bb = new ByteArrayOutputStream();
+    byte[] array = new byte[4096];
+    int len;
+    if (maxLength == null) {
+      while ((len = value.read(array)) > 0) {
+        bb.write(array, 0, len);
+      }
+    } else {
+      long maxLen = maxLength;
+      while ((len = value.read(array)) > 0 && maxLen > 0) {
+        bb.write(array, 0, Math.min(len, (int) maxLen));
+        maxLen -= len;
+      }
+    }
+    return bb.toByteArray();
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.BLOB.get();
+  }
+
+  public boolean canEncodeLongData() {
+    return true;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/StringCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/StringCodec.java
new file mode 100644
index 0000000..c09e5d0
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/StringCodec.java
@@ -0,0 +1,96 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.sql.SQLDataException;
+import java.util.Calendar;
+import java.util.EnumSet;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+import org.mariadb.jdbc.util.constants.ServerStatus;
+
+/** String codec */
+public class StringCodec implements Codec<String> {
+
+  /** default instance */
+  public static final StringCodec INSTANCE = new StringCodec();
+
+  private static final EnumSet<DataType> 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,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  public String className() {
+    return String.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder 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 ColumnDecoder column, final Calendar cal)
+      throws SQLDataException {
+    return column.decodeStringText(buf, length, cal);
+  }
+
+  public String decodeBinary(
+      final ReadableByteBuf buf, final int length, final ColumnDecoder column, final Calendar cal)
+      throws SQLDataException {
+    return column.decodeStringBinary(buf, length, cal);
+  }
+
+  public void encodeText(Writer encoder, Context context, Object value, Calendar cal, Long maxLen)
+      throws IOException {
+    encoder.writeByte('\'');
+    encoder.writeStringEscaped(
+        maxLen == null ? value.toString() : value.toString().substring(0, maxLen.intValue()),
+        (context.getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES) != 0);
+    encoder.writeByte('\'');
+  }
+
+  public void encodeBinary(Writer writer, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    byte[] b = value.toString().getBytes(StandardCharsets.UTF_8);
+    int len = maxLength != null ? Math.min(maxLength.intValue(), b.length) : b.length;
+    writer.writeLength(len);
+    writer.writeBytes(b, 0, len);
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.VARSTRING.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/TimeCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/TimeCodec.java
new file mode 100644
index 0000000..0772bd2
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/TimeCodec.java
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.sql.Time;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.util.Calendar;
+import java.util.EnumSet;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** Time codec */
+public class TimeCodec implements Codec<Time> {
+
+  /** default instance */
+  public static final TimeCodec INSTANCE = new TimeCodec();
+  /** reference local date */
+  public static final LocalDate EPOCH_DATE = LocalDate.of(1970, 1, 1);
+
+  private static final EnumSet<DataType> COMPATIBLE_TYPES =
+      EnumSet.of(
+          DataType.TIME,
+          DataType.DATETIME,
+          DataType.TIMESTAMP,
+          DataType.VARSTRING,
+          DataType.VARCHAR,
+          DataType.STRING,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  public String className() {
+    return Time.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder column, Class<?> type) {
+    return COMPATIBLE_TYPES.contains(column.getType())
+        && type.isAssignableFrom(Time.class)
+        && !type.equals(java.util.Date.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof Time;
+  }
+
+  @Override
+  @SuppressWarnings("fallthrough")
+  public Time decodeText(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+    return column.decodeTimeText(buf, length, cal);
+  }
+
+  @Override
+  @SuppressWarnings("fallthrough")
+  public Time decodeBinary(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar calParam)
+      throws SQLDataException {
+    return column.decodeTimeBinary(buf, length, calParam);
+  }
+
+  @Override
+  public void encodeText(
+      Writer encoder, Context context, Object val, Calendar providedCal, Long maxLen)
+      throws IOException {
+    Calendar cal = providedCal == null ? Calendar.getInstance() : providedCal;
+    SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
+    sdf.setTimeZone(cal.getTimeZone());
+    String dateString = sdf.format(val);
+
+    encoder.writeByte('\'');
+    encoder.writeAscii(dateString);
+    encoder.writeByte('\'');
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar providedCal, Long maxLength)
+      throws IOException {
+    Calendar cal = providedCal == null ? Calendar.getInstance() : providedCal;
+    synchronized (cal) {
+      cal.clear();
+      cal.setTime((Time) value);
+      cal.set(Calendar.DAY_OF_MONTH, 1);
+      if (cal.get(Calendar.MILLISECOND) > 0) {
+        encoder.writeByte((byte) 12);
+        encoder.writeByte((byte) 0);
+        encoder.writeInt(0);
+        encoder.writeByte((byte) cal.get(Calendar.HOUR_OF_DAY));
+        encoder.writeByte((byte) cal.get(Calendar.MINUTE));
+        encoder.writeByte((byte) cal.get(Calendar.SECOND));
+        encoder.writeInt(cal.get(Calendar.MILLISECOND) * 1000);
+      } else {
+        encoder.writeByte((byte) 8); // length
+        encoder.writeByte((byte) 0);
+        encoder.writeInt(0);
+        encoder.writeByte((byte) cal.get(Calendar.HOUR_OF_DAY));
+        encoder.writeByte((byte) cal.get(Calendar.MINUTE));
+        encoder.writeByte((byte) cal.get(Calendar.SECOND));
+      }
+    }
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.TIME.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/TimestampCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/TimestampCodec.java
new file mode 100644
index 0000000..b8389bb
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/TimestampCodec.java
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+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.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** Timestamp codec */
+public class TimestampCodec implements Codec<Timestamp> {
+
+  /** default instance */
+  public static final TimestampCodec INSTANCE = new TimestampCodec();
+
+  private static final EnumSet<DataType> COMPATIBLE_TYPES =
+      EnumSet.of(
+          DataType.DATE,
+          DataType.NEWDATE,
+          DataType.DATETIME,
+          DataType.TIMESTAMP,
+          DataType.YEAR,
+          DataType.VARSTRING,
+          DataType.VARCHAR,
+          DataType.STRING,
+          DataType.TIME,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  public String className() {
+    return Timestamp.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder column, Class<?> type) {
+    return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(Timestamp.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof Timestamp;
+  }
+
+  @Override
+  @SuppressWarnings("fallthrough")
+  public Timestamp decodeText(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+    return column.decodeTimestampText(buf, length, cal);
+  }
+
+  @Override
+  @SuppressWarnings("fallthrough")
+  public Timestamp decodeBinary(ReadableByteBuf buf, int length, ColumnDecoder column, Calendar cal)
+      throws SQLDataException {
+    return column.decodeTimestampBinary(buf, length, cal);
+  }
+
+  @Override
+  public void encodeText(
+      Writer encoder, Context context, Object val, Calendar providedCal, Long maxLen)
+      throws IOException {
+    Timestamp ts = (Timestamp) val;
+    Calendar cal = providedCal == null ? Calendar.getInstance() : providedCal;
+    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+    sdf.setTimeZone(cal.getTimeZone());
+    String dateString = sdf.format(ts);
+
+    encoder.writeByte('\'');
+    encoder.writeAscii(dateString);
+
+    int microseconds = ts.getNanos() / 1000;
+    if (microseconds > 0) {
+      if (microseconds % 1000 == 0) {
+        encoder.writeAscii("." + Integer.toString(microseconds / 1000 + 1000).substring(1));
+      } else {
+        encoder.writeAscii("." + Integer.toString(microseconds + 1000000).substring(1));
+      }
+    }
+
+    encoder.writeByte('\'');
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar providedCal, Long maxLength)
+      throws IOException {
+    Timestamp ts = (Timestamp) value;
+    Calendar cal = providedCal == null ? Calendar.getInstance() : providedCal;
+    synchronized (cal) {
+      cal.clear();
+      cal.setTimeInMillis(ts.getTime());
+      if (ts.getNanos() == 0) {
+        encoder.writeByte(7); // length
+        encoder.writeShort((short) cal.get(Calendar.YEAR));
+        encoder.writeByte((cal.get(Calendar.MONTH) + 1));
+        encoder.writeByte(cal.get(Calendar.DAY_OF_MONTH));
+        encoder.writeByte(cal.get(Calendar.HOUR_OF_DAY));
+        encoder.writeByte(cal.get(Calendar.MINUTE));
+        encoder.writeByte(cal.get(Calendar.SECOND));
+      } else {
+        encoder.writeByte(11); // length
+        encoder.writeShort((short) cal.get(Calendar.YEAR));
+        encoder.writeByte((cal.get(Calendar.MONTH) + 1));
+        encoder.writeByte(cal.get(Calendar.DAY_OF_MONTH));
+        encoder.writeByte(cal.get(Calendar.HOUR_OF_DAY));
+        encoder.writeByte(cal.get(Calendar.MINUTE));
+        encoder.writeByte(cal.get(Calendar.SECOND));
+        encoder.writeInt(ts.getNanos() / 1000);
+      }
+    }
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.DATETIME.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/UuidCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/UuidCodec.java
new file mode 100644
index 0000000..8942fef
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/UuidCodec.java
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.util.Calendar;
+import java.util.EnumSet;
+import java.util.UUID;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.Context;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** UUID codec */
+public class UuidCodec implements Codec<UUID> {
+
+  /** default instance */
+  public static final UuidCodec INSTANCE = new UuidCodec();
+
+  private static final EnumSet<DataType> COMPATIBLE_TYPES =
+      EnumSet.of(DataType.VARCHAR, DataType.VARSTRING, DataType.STRING);
+
+  public String className() {
+    return UUID.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder column, Class<?> type) {
+    return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(UUID.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof UUID;
+  }
+
+  public UUID decodeText(
+      final ReadableByteBuf buf, final int length, final ColumnDecoder column, final Calendar cal)
+      throws SQLDataException {
+    return UUID.fromString(column.decodeStringText(buf, length, cal));
+  }
+
+  public UUID decodeBinary(
+      final ReadableByteBuf buf, final int length, final ColumnDecoder column, final Calendar cal)
+      throws SQLDataException {
+    return UUID.fromString(column.decodeStringBinary(buf, length, cal));
+  }
+
+  public void encodeText(Writer encoder, Context context, Object value, Calendar cal, Long maxLen)
+      throws IOException {
+    encoder.writeByte('\'');
+    encoder.writeAscii(value.toString());
+    encoder.writeByte('\'');
+  }
+
+  public void encodeBinary(Writer writer, Object value, Calendar cal, Long maxLength)
+      throws IOException {
+    String valueSt = value.toString();
+    writer.writeLength(valueSt.length());
+    writer.writeAscii(valueSt);
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.VARSTRING.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/ZonedDateTimeCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/ZonedDateTimeCodec.java
new file mode 100644
index 0000000..df6e12b
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/codec/ZonedDateTimeCodec.java
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.codec;
+
+import java.io.IOException;
+import java.sql.SQLDataException;
+import java.time.LocalDateTime;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoField;
+import java.util.Calendar;
+import java.util.EnumSet;
+import org.mariadb.jdbc.client.*;
+import org.mariadb.jdbc.client.socket.Writer;
+import org.mariadb.jdbc.plugin.Codec;
+
+/** ZonedDateTime codec */
+public class ZonedDateTimeCodec implements Codec<ZonedDateTime> {
+
+  /** default instance */
+  public static final ZonedDateTimeCodec INSTANCE = new ZonedDateTimeCodec();
+
+  private static final EnumSet<DataType> COMPATIBLE_TYPES =
+      EnumSet.of(
+          DataType.DATETIME,
+          DataType.DATE,
+          DataType.YEAR,
+          DataType.TIMESTAMP,
+          DataType.VARSTRING,
+          DataType.VARCHAR,
+          DataType.STRING,
+          DataType.TIME,
+          DataType.BLOB,
+          DataType.TINYBLOB,
+          DataType.MEDIUMBLOB,
+          DataType.LONGBLOB);
+
+  public String className() {
+    return ZonedDateTime.class.getName();
+  }
+
+  public boolean canDecode(ColumnDecoder column, Class<?> type) {
+    return COMPATIBLE_TYPES.contains(column.getType())
+        && type.isAssignableFrom(ZonedDateTime.class);
+  }
+
+  public boolean canEncode(Object value) {
+    return value instanceof ZonedDateTime;
+  }
+
+  @Override
+  public ZonedDateTime decodeText(
+      ReadableByteBuf buf, int length, ColumnDecoder column, Calendar calParam)
+      throws SQLDataException {
+    LocalDateTime localDateTime =
+        LocalDateTimeCodec.INSTANCE.decodeText(buf, length, column, calParam);
+    if (localDateTime == null) return null;
+    Calendar cal = calParam == null ? Calendar.getInstance() : calParam;
+    return localDateTime.atZone(cal.getTimeZone().toZoneId());
+  }
+
+  @Override
+  public ZonedDateTime decodeBinary(
+      ReadableByteBuf buf, int length, ColumnDecoder column, Calendar calParam)
+      throws SQLDataException {
+    LocalDateTime localDateTime =
+        LocalDateTimeCodec.INSTANCE.decodeBinary(buf, length, column, calParam);
+    if (localDateTime == null) return null;
+    Calendar cal = calParam == null ? Calendar.getInstance() : calParam;
+    return localDateTime.atZone(cal.getTimeZone().toZoneId());
+  }
+
+  @Override
+  public void encodeText(
+      Writer encoder, Context context, Object val, Calendar calParam, Long maxLen)
+      throws IOException {
+    ZonedDateTime zdt = (ZonedDateTime) val;
+    Calendar cal = calParam == null ? Calendar.getInstance() : calParam;
+    encoder.writeByte('\'');
+    encoder.writeAscii(
+        zdt.withZoneSameInstant(cal.getTimeZone().toZoneId())
+            .format(
+                zdt.getNano() != 0
+                    ? LocalDateTimeCodec.TIMESTAMP_FORMAT
+                    : LocalDateTimeCodec.TIMESTAMP_FORMAT_NO_FRACTIONAL));
+    encoder.writeByte('\'');
+  }
+
+  @Override
+  public void encodeBinary(Writer encoder, Object value, Calendar calParam, Long maxLength)
+      throws IOException {
+    ZonedDateTime zdt = (ZonedDateTime) value;
+    Calendar cal = calParam == null ? Calendar.getInstance() : calParam;
+    ZonedDateTime convertedZdt = zdt.withZoneSameInstant(cal.getTimeZone().toZoneId());
+    int nano = convertedZdt.getNano();
+    if (nano > 0) {
+      encoder.writeByte((byte) 11);
+      encoder.writeShort((short) convertedZdt.get(ChronoField.YEAR));
+      encoder.writeByte(convertedZdt.get(ChronoField.MONTH_OF_YEAR));
+      encoder.writeByte(convertedZdt.get(ChronoField.DAY_OF_MONTH));
+      encoder.writeByte(convertedZdt.get(ChronoField.HOUR_OF_DAY));
+      encoder.writeByte(convertedZdt.get(ChronoField.MINUTE_OF_HOUR));
+      encoder.writeByte(convertedZdt.get(ChronoField.SECOND_OF_MINUTE));
+      encoder.writeInt(nano / 1000);
+    } else {
+      encoder.writeByte((byte) 7);
+      encoder.writeShort((short) convertedZdt.get(ChronoField.YEAR));
+      encoder.writeByte(convertedZdt.get(ChronoField.MONTH_OF_YEAR));
+      encoder.writeByte(convertedZdt.get(ChronoField.DAY_OF_MONTH));
+      encoder.writeByte(convertedZdt.get(ChronoField.HOUR_OF_DAY));
+      encoder.writeByte(convertedZdt.get(ChronoField.MINUTE_OF_HOUR));
+      encoder.writeByte(convertedZdt.get(ChronoField.SECOND_OF_MINUTE));
+    }
+  }
+
+  public int getBinaryEncodeType() {
+    return DataType.DATETIME.get();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/credential/CredentialPluginLoader.java b/src/main/java/org/mariadb/jdbc/plugin/credential/CredentialPluginLoader.java
new file mode 100644
index 0000000..102e92e
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/credential/CredentialPluginLoader.java
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.credential;
+
+import java.sql.SQLException;
+import java.util.ServiceLoader;
+import org.mariadb.jdbc.Driver;
+import org.mariadb.jdbc.plugin.CredentialPlugin;
+
+/**
+ * Provider to handle plugin authentication. This can allow library users to override our default
+ * Authentication provider.
+ */
+public final class CredentialPluginLoader {
+
+  private static final ServiceLoader<CredentialPlugin> loader =
+      ServiceLoader.load(CredentialPlugin.class, Driver.class.getClassLoader());
+
+  /**
+   * Get current Identity plugin according to option `identityType`.
+   *
+   * @param type identity plugin type
+   * @return identity plugin
+   * @throws SQLException if no identity plugin found with this type is in classpath
+   */
+  public static CredentialPlugin get(String type) throws SQLException {
+    if (type == null) return null;
+
+    for (CredentialPlugin implClass : loader) {
+      if (type.equals(implClass.type())) {
+        return implClass;
+      }
+    }
+    throw new SQLException(
+        "No identity plugin registered with the type \"" + type + "\".", "08004", 1251);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/credential/aws/AwsCredentialGenerator.java b/src/main/java/org/mariadb/jdbc/plugin/credential/aws/AwsCredentialGenerator.java
new file mode 100644
index 0000000..8930579
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/credential/aws/AwsCredentialGenerator.java
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.credential.aws;
+
+import java.util.Properties;
+import org.mariadb.jdbc.HostAddress;
+import org.mariadb.jdbc.plugin.Credential;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain;
+import software.amazon.awssdk.services.rds.RdsUtilities;
+
+/** AWS credential generator */
+public class AwsCredentialGenerator {
+
+  private final String authenticationToken;
+  private final String userName;
+
+  /**
+   * AWS Generator.
+   *
+   * @param nonMappedOptions non-standard options
+   * @param userName user
+   * @param hostAddress current server information
+   */
+  public AwsCredentialGenerator(
+      Properties nonMappedOptions, String userName, HostAddress hostAddress) {
+    // Build RDS IAM-auth token generator
+    this.userName = userName;
+    AwsCredentialsProvider awsCredentialsProvider;
+    String accessKeyId = nonMappedOptions.getProperty("accessKeyId");
+    String secretKey = nonMappedOptions.getProperty("secretKey");
+    String region = nonMappedOptions.getProperty("region");
+
+    if (accessKeyId != null && secretKey != null) {
+      awsCredentialsProvider =
+          StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKeyId, secretKey));
+    } else {
+      awsCredentialsProvider = DefaultCredentialsProvider.builder().build();
+    }
+
+    RdsUtilities utilities =
+        RdsUtilities.builder()
+            .credentialsProvider(awsCredentialsProvider)
+            .region(
+                region != null
+                    ? Region.of(region)
+                    : new DefaultAwsRegionProviderChain().getRegion())
+            .build();
+
+    this.authenticationToken =
+        utilities.generateAuthenticationToken(
+            builder -> {
+              builder
+                  .username(userName)
+                  .hostname(hostAddress.host)
+                  .port(hostAddress.port)
+                  .credentialsProvider(awsCredentialsProvider);
+            });
+  }
+
+  /**
+   * Generate authentication token
+   *
+   * @return token
+   */
+  public Credential getToken() {
+    return new Credential(userName, authenticationToken);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/credential/aws/AwsIamCredentialPlugin.java b/src/main/java/org/mariadb/jdbc/plugin/credential/aws/AwsIamCredentialPlugin.java
new file mode 100644
index 0000000..4524297
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/credential/aws/AwsIamCredentialPlugin.java
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.credential.aws;
+
+import static java.time.LocalDateTime.now;
+
+import java.sql.SQLException;
+import java.time.LocalDateTime;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.HostAddress;
+import org.mariadb.jdbc.plugin.Credential;
+import org.mariadb.jdbc.plugin.CredentialPlugin;
+
+/**
+ * Permit AWS database IAM authentication.
+ *
+ * <p>Token is generated using IAM credential and region.
+ *
+ * <p>Implementation use SDK DefaultAWSCredentialsProviderChain and DefaultAwsRegionProviderChain
+ * (environment variable / system properties, files, ...) or using connection string options :
+ * accessKeyId, secretKey, region.
+ *
+ * @see <a
+ *     href="https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/auth/credentials/DefaultCredentialsProvider.html">DefaultCredentialsProvider</a>
+ * @see <a
+ *     href="https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/regions/providers/DefaultAwsRegionProviderChain.html">DefaultAwsRegionProviderChain</a>
+ */
+public class AwsIamCredentialPlugin implements CredentialPlugin {
+
+  private static final int TOKEN_TTL = 10;
+
+  private static final Map<KeyCache, IdentityExpire> cache = new ConcurrentHashMap<>();
+
+  private AwsCredentialGenerator generator;
+  private KeyCache key;
+
+  @Override
+  public String type() {
+    return "AWS-IAM";
+  }
+
+  @Override
+  public boolean mustUseSsl() {
+    return true;
+  }
+
+  @Override
+  public CredentialPlugin initialize(Configuration conf, String userName, HostAddress hostAddress)
+      throws SQLException {
+    try {
+      Class.forName("software.amazon.awssdk.auth.credentials.AwsBasicCredentials");
+    } catch (ClassNotFoundException ex) {
+      throw new SQLException(
+          "Identity plugin 'AWS-IAM' is used without having AWS SDK in "
+              + "classpath. "
+              + "Please add 'software.amazon.awssdk:rds' to classpath");
+    }
+    this.generator = new AwsCredentialGenerator(conf.nonMappedOptions(), conf.user(), hostAddress);
+    this.key = new KeyCache(conf, conf.user(), hostAddress);
+    return this;
+  }
+
+  @Override
+  public Credential get() {
+    IdentityExpire val = cache.get(key);
+    if (val != null && val.isValid()) {
+      return val.getCredential();
+    }
+
+    Credential credential = generator.getToken();
+    cache.put(key, new IdentityExpire(credential));
+    return credential;
+  }
+
+  private static class IdentityExpire {
+
+    private final LocalDateTime expiration;
+    private final Credential credential;
+
+    public IdentityExpire(Credential credential) {
+      this.credential = credential;
+      expiration = now().plusMinutes(TOKEN_TTL);
+    }
+
+    public boolean isValid() {
+      return expiration.isAfter(now());
+    }
+
+    public Credential getCredential() {
+      return credential;
+    }
+  }
+
+  private static class KeyCache {
+    private final Configuration conf;
+    private final String userName;
+    private final HostAddress hostAddress;
+
+    public KeyCache(Configuration conf, String userName, HostAddress hostAddress) {
+      this.conf = conf;
+      this.userName = userName;
+      this.hostAddress = hostAddress;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      KeyCache keyCache = (KeyCache) o;
+      return conf.equals(keyCache.conf)
+          && Objects.equals(userName, keyCache.userName)
+          && hostAddress.equals(keyCache.hostAddress);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(conf, userName, hostAddress);
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/credential/env/EnvCredentialPlugin.java b/src/main/java/org/mariadb/jdbc/plugin/credential/env/EnvCredentialPlugin.java
new file mode 100644
index 0000000..883bb13
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/credential/env/EnvCredentialPlugin.java
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.credential.env;
+
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.HostAddress;
+import org.mariadb.jdbc.plugin.Credential;
+import org.mariadb.jdbc.plugin.CredentialPlugin;
+
+/**
+ * Authentication using environment variable.
+ *
+ * <p>default implementation use environment variable MARIADB_USER and MARIADB_PWD
+ *
+ * <p>example : `jdbc:mariadb://host/db?credentialType=ENV`
+ *
+ * <p>2 options `userKey` and `pwdKey` permits indicating which environment variable to use.
+ */
+public class EnvCredentialPlugin implements CredentialPlugin {
+
+  private Configuration conf;
+  private String userName;
+
+  @Override
+  public String type() {
+    return "ENV";
+  }
+
+  @Override
+  public CredentialPlugin initialize(Configuration conf, String userName, HostAddress hostAddress) {
+    this.conf = conf;
+    this.userName = userName;
+    return this;
+  }
+
+  @Override
+  public Credential get() {
+
+    String userKey = this.conf.nonMappedOptions().getProperty("userKey");
+    String pwdKey = this.conf.nonMappedOptions().getProperty("pwdKey");
+    String envUser = System.getenv(userKey != null ? userKey : "MARIADB_USER");
+    return new Credential(
+        envUser == null ? userName : envUser,
+        System.getenv(pwdKey != null ? pwdKey : "MARIADB_PWD"));
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/credential/system/PropertiesCredentialPlugin.java b/src/main/java/org/mariadb/jdbc/plugin/credential/system/PropertiesCredentialPlugin.java
new file mode 100644
index 0000000..2ef777d
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/credential/system/PropertiesCredentialPlugin.java
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.credential.system;
+
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.HostAddress;
+import org.mariadb.jdbc.plugin.Credential;
+import org.mariadb.jdbc.plugin.CredentialPlugin;
+
+/**
+ * Authentication using java system properties.
+ *
+ * <p>default implementation use system properties `mariadb.user` and `mariadb.pwd`
+ *
+ * <p>example : `jdbc:mariadb://host/db?credentialType=PROPERTY`
+ *
+ * <p>2 options `userKey` and `pwdKey` permits indicating which system properties to use .
+ */
+public class PropertiesCredentialPlugin implements CredentialPlugin {
+
+  private Configuration conf;
+  private String userName;
+
+  @Override
+  public String type() {
+    return "PROPERTY";
+  }
+
+  @Override
+  public CredentialPlugin initialize(Configuration conf, String userName, HostAddress hostAddress) {
+    this.conf = conf;
+    this.userName = userName;
+    return this;
+  }
+
+  @Override
+  public Credential get() {
+
+    String userKey = this.conf.nonMappedOptions().getProperty("userKey");
+    String pwdKey = this.conf.nonMappedOptions().getProperty("pwdKey");
+    String propUser = System.getProperty(userKey != null ? userKey : "mariadb.user");
+
+    return new Credential(
+        propUser == null ? userName : propUser,
+        System.getProperty(pwdKey != null ? pwdKey : "mariadb.pwd"));
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/tls/TlsSocketPluginLoader.java b/src/main/java/org/mariadb/jdbc/plugin/tls/TlsSocketPluginLoader.java
new file mode 100644
index 0000000..772c427
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/tls/TlsSocketPluginLoader.java
@@ -0,0 +1,35 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.tls;
+
+import java.sql.SQLException;
+import java.util.ServiceLoader;
+import org.mariadb.jdbc.plugin.TlsSocketPlugin;
+import org.mariadb.jdbc.plugin.tls.main.DefaultTlsSocketPlugin;
+
+/** TLS plugin loader */
+public final class TlsSocketPluginLoader {
+
+  /**
+   * Get authentication plugin from type String. Customs authentication plugin can be added
+   * implementing AuthenticationPlugin and registering new type in resources services.
+   *
+   * @param type authentication plugin type
+   * @return Authentication plugin corresponding to type
+   * @throws SQLException if no authentication plugin in classpath have indicated type
+   */
+  public static TlsSocketPlugin get(String type) throws SQLException {
+    if (type == null) return new DefaultTlsSocketPlugin();
+
+    ServiceLoader<TlsSocketPlugin> loader = ServiceLoader.load(TlsSocketPlugin.class);
+    for (TlsSocketPlugin implClass : loader) {
+      if (type.equals(implClass.type())) {
+        return implClass;
+      }
+    }
+    throw new SQLException(
+        "Client has not found any TLS factory plugin with name '" + type + "'.", "08004", 1251);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/plugin/tls/main/DefaultTlsSocketPlugin.java b/src/main/java/org/mariadb/jdbc/plugin/tls/main/DefaultTlsSocketPlugin.java
new file mode 100644
index 0000000..feab50f
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/plugin/tls/main/DefaultTlsSocketPlugin.java
@@ -0,0 +1,192 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.plugin.tls.main;
+
+import java.io.*;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.UUID;
+import javax.net.ssl.*;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.client.tls.HostnameVerifier;
+import org.mariadb.jdbc.client.tls.MariaDbX509KeyManager;
+import org.mariadb.jdbc.client.tls.MariaDbX509TrustingManager;
+import org.mariadb.jdbc.export.ExceptionFactory;
+import org.mariadb.jdbc.export.SslMode;
+import org.mariadb.jdbc.plugin.TlsSocketPlugin;
+import org.mariadb.jdbc.util.log.Logger;
+import org.mariadb.jdbc.util.log.Loggers;
+
+/** Default TLS socket plugin */
+public class DefaultTlsSocketPlugin implements TlsSocketPlugin {
+  private static final Logger logger = Loggers.getLogger(DefaultTlsSocketPlugin.class);
+
+  private static KeyManager loadClientCerts(
+      String keyStoreUrl,
+      String keyStorePassword,
+      String storeType,
+      ExceptionFactory exceptionFactory)
+      throws SQLException {
+
+    try {
+      try (InputStream inStream = loadFromUrl(keyStoreUrl)) {
+        char[] keyStorePasswordChars =
+            keyStorePassword == null ? null : keyStorePassword.toCharArray();
+        KeyStore ks =
+            KeyStore.getInstance(storeType != null ? storeType : KeyStore.getDefaultType());
+        ks.load(inStream, keyStorePasswordChars);
+        return new MariaDbX509KeyManager(ks, keyStorePasswordChars);
+      }
+    } catch (IOException | GeneralSecurityException ex) {
+      throw exceptionFactory.create(
+          "Failed to read keyStore file. Option keyStore=" + keyStoreUrl, "08000", ex);
+    }
+  }
+
+  private static InputStream loadFromUrl(String keyStoreUrl) throws FileNotFoundException {
+    try {
+      return new URL(keyStoreUrl).openStream();
+    } catch (IOException ioexception) {
+      return new FileInputStream(keyStoreUrl);
+    }
+  }
+
+  @Override
+  public String type() {
+    return "DEFAULT";
+  }
+
+  @Override
+  public SSLSocketFactory getSocketFactory(Configuration conf, ExceptionFactory exceptionFactory)
+      throws SQLException {
+
+    TrustManager[] trustManager = null;
+    KeyManager[] keyManager = null;
+
+    if (conf.sslMode() == SslMode.TRUST) {
+      trustManager = new X509TrustManager[] {new MariaDbX509TrustingManager()};
+    } else { // if certificate is provided, load it.
+      // if not, relying on default truststore
+      if (conf.serverSslCert() != null) {
+
+        KeyStore ks;
+        try {
+          ks = KeyStore.getInstance(KeyStore.getDefaultType());
+        } catch (GeneralSecurityException generalSecurityEx) {
+          throw exceptionFactory.create(
+              "Failed to create keystore instance", "08000", generalSecurityEx);
+        }
+
+        try (InputStream inStream = getInputStreamFromPath(conf.serverSslCert())) {
+          // generate a keyStore from the provided cert
+
+          // 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<? extends Certificate> 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 = new X509TrustManager[] {(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);
+        }
+      }
+    }
+
+    if (conf.keyStore() != null) {
+      keyManager =
+          new KeyManager[] {
+            loadClientCerts(
+                conf.keyStore(), conf.keyStorePassword(), conf.keyStoreType(), exceptionFactory)
+          };
+    } else {
+      String keyStore = System.getProperty("javax.net.ssl.keyStore");
+      String keyStorePassword =
+          System.getProperty("javax.net.ssl.keyStorePassword", conf.keyStorePassword());
+      String keyStoreType = System.getProperty("javax.net.ssl.keyStoreType", conf.keyStoreType());
+      if (keyStore != null) {
+        try {
+          keyManager =
+              new KeyManager[] {
+                loadClientCerts(keyStore, keyStorePassword, keyStoreType, exceptionFactory)
+              };
+        } catch (SQLException queryException) {
+          keyManager = null;
+          logger.error("Error loading key manager from system properties", queryException);
+        }
+      }
+    }
+
+    try {
+      SSLContext sslContext = SSLContext.getInstance("TLS");
+      sslContext.init(keyManager, trustManager, null);
+      return sslContext.getSocketFactory();
+    } catch (KeyManagementException keyManagementEx) {
+      throw exceptionFactory.create("Could not initialize SSL context", "08000", keyManagementEx);
+    } catch (NoSuchAlgorithmException noSuchAlgorithmEx) {
+      throw exceptionFactory.create(
+          "SSLContext TLS Algorithm not unknown", "08000", noSuchAlgorithmEx);
+    }
+  }
+
+  private static InputStream getInputStreamFromPath(String path) throws IOException {
+    try {
+      return new URL(path).openStream();
+    } catch (MalformedURLException e) {
+      if (path.startsWith("-----")) {
+        return new ByteArrayInputStream(path.getBytes());
+      } else {
+        File f = new File(path);
+        if (f.exists() && !f.isDirectory()) {
+          return f.toURI().toURL().openStream();
+        }
+      }
+      throw new IOException(
+          String.format("Wrong value for option `serverSslCert` (value: '%s')", path), e);
+    }
+  }
+
+  @Override
+  public void verify(String host, SSLSession session, long serverThreadId) throws SSLException {
+    try {
+      Certificate[] certs = session.getPeerCertificates();
+      X509Certificate cert = (X509Certificate) certs[0];
+      HostnameVerifier.verify(host, cert, serverThreadId);
+    } catch (SSLException ex) {
+      logger.info(ex.getMessage(), ex);
+      throw ex;
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/pool/MariaDbInnerPoolConnection.java b/src/main/java/org/mariadb/jdbc/pool/MariaDbInnerPoolConnection.java
new file mode 100644
index 0000000..ae4d996
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/pool/MariaDbInnerPoolConnection.java
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.pool;
+
+import java.util.concurrent.atomic.AtomicLong;
+import javax.sql.*;
+import org.mariadb.jdbc.Connection;
+import org.mariadb.jdbc.MariaDbPoolConnection;
+
+/**
+ * MariaDB pool connection for internal pool permit to add a last used information, to remove
+ * connection after staying in pool for long time.
+ */
+public class MariaDbInnerPoolConnection extends MariaDbPoolConnection {
+  private final AtomicLong lastUsed;
+
+  /**
+   * Constructor.
+   *
+   * @param connection connection to retrieve connection options
+   */
+  public MariaDbInnerPoolConnection(Connection connection) {
+    super(connection);
+    lastUsed = new AtomicLong(System.nanoTime());
+  }
+
+  /**
+   * 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());
+  }
+
+  /** Reset last used time, to ensure next retrieval will validate connection before borrowing */
+  public void ensureValidation() {
+    lastUsed.set(0L);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/pool/Pool.java b/src/main/java/org/mariadb/jdbc/pool/Pool.java
new file mode 100644
index 0000000..4240149
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/pool/Pool.java
@@ -0,0 +1,569 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.pool;
+
+import java.lang.management.ManagementFactory;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+import javax.sql.ConnectionEvent;
+import javax.sql.ConnectionEventListener;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.Connection;
+import org.mariadb.jdbc.Driver;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.util.log.Logger;
+import org.mariadb.jdbc.util.log.Loggers;
+
+/** MariaDB Pool */
+@SuppressWarnings({"unchecked"})
+public class Pool implements AutoCloseable, PoolMBean {
+
+  private static final Logger logger = Loggers.getLogger(Pool.class);
+
+  private static final int POOL_STATE_OK = 0;
+  private static final int POOL_STATE_CLOSING = 1;
+
+  private final AtomicInteger poolState = new AtomicInteger();
+
+  private final Configuration conf;
+  private final AtomicInteger pendingRequestNumber = new AtomicInteger();
+  private final AtomicInteger totalConnection = new AtomicInteger();
+
+  private final LinkedBlockingDeque<MariaDbInnerPoolConnection> idleConnections;
+  private final ThreadPoolExecutor connectionAppender;
+  private final BlockingQueue<Runnable> connectionAppenderQueue;
+
+  private final String poolTag;
+  private final ScheduledThreadPoolExecutor poolExecutor;
+  private final ScheduledFuture<?> scheduledFuture;
+
+  private int waitTimeout;
+
+  /**
+   * Create pool from configuration.
+   *
+   * @param conf configuration parser
+   * @param poolIndex pool index to permit distinction of thread name
+   * @param poolExecutor pools common executor
+   */
+  public Pool(Configuration conf, int poolIndex, ScheduledThreadPoolExecutor poolExecutor) {
+
+    this.conf = conf;
+    poolTag = generatePoolTag(poolIndex);
+
+    // one thread to add new connection to pool.
+    connectionAppenderQueue = new ArrayBlockingQueue<>(conf.maxPoolSize());
+    connectionAppender =
+        new ThreadPoolExecutor(
+            1,
+            1,
+            10,
+            TimeUnit.SECONDS,
+            connectionAppenderQueue,
+            new PoolThreadFactory(poolTag + "-appender"));
+    connectionAppender.allowCoreThreadTimeOut(true);
+    // create workers, since driver only interact with queue after that (i.e. not using .execute() )
+    connectionAppender.prestartCoreThread();
+
+    idleConnections = new LinkedBlockingDeque<>();
+    int minDelay =
+        Integer.parseInt(conf.nonMappedOptions().getProperty("testMinRemovalDelay", "30"));
+    int scheduleDelay = Math.min(minDelay, conf.maxIdleTime() / 2);
+    this.poolExecutor = poolExecutor;
+    scheduledFuture =
+        poolExecutor.scheduleAtFixedRate(
+            this::removeIdleTimeoutConnection, scheduleDelay, scheduleDelay, TimeUnit.SECONDS);
+
+    if (conf.registerJmxPool()) {
+      try {
+        registerJmx();
+      } catch (Exception ex) {
+        logger.error("pool " + poolTag + " not registered due to exception : " + ex.getMessage());
+      }
+    }
+
+    // create minimal connection in pool
+    try {
+      for (int i = 0; i < Math.max(1, conf.minPoolSize()); i++) {
+        addConnection();
+      }
+      waitTimeout = 28800;
+      if (!idleConnections.isEmpty()) {
+        Statement stmt = idleConnections.getFirst().getConnection().createStatement();
+        ResultSet rs = stmt.executeQuery("SELECT @@wait_timeout");
+        if (rs.next()) waitTimeout = rs.getInt(1);
+      }
+    } catch (SQLException sqle) {
+      logger.error("error initializing pool connection", sqle);
+    }
+  }
+
+  /**
+   * Add new connection if needed. Only one thread create new connection, so new connection request
+   * will wait to newly created connection or for a released connection.
+   */
+  private void addConnectionRequest() {
+    if (totalConnection.get() < conf.maxPoolSize() && poolState.get() == POOL_STATE_OK) {
+
+      // ensure to have one worker if was timeout
+      connectionAppender.prestartCoreThread();
+      boolean unused =
+          connectionAppenderQueue.offer(
+              () -> {
+                if ((totalConnection.get() < conf.minPoolSize() || pendingRequestNumber.get() > 0)
+                    && totalConnection.get() < conf.maxPoolSize()) {
+                  try {
+                    addConnection();
+                  } catch (SQLException sqle) {
+                    logger.error("error adding connection to pool", sqle);
+                  }
+                }
+              });
+    }
+  }
+
+  /**
+   * Removing idle connection. Close them and recreate connection to reach minimal number of
+   * connection.
+   */
+  private void removeIdleTimeoutConnection() {
+
+    // descending iterator since first from queue are the first to be used
+    Iterator<MariaDbInnerPoolConnection> iterator = idleConnections.descendingIterator();
+
+    MariaDbInnerPoolConnection item;
+
+    while (iterator.hasNext()) {
+      item = iterator.next();
+
+      long idleTime = System.nanoTime() - item.getLastUsed().get();
+      boolean timedOut = idleTime > TimeUnit.SECONDS.toNanos(conf.maxIdleTime());
+
+      boolean shouldBeReleased = false;
+      Connection con = item.getConnection();
+      if (waitTimeout > 0) {
+
+        // idle time is reaching server @@wait_timeout
+        if (idleTime > TimeUnit.SECONDS.toNanos(waitTimeout - 45)) {
+          shouldBeReleased = true;
+        }
+
+        //  idle has reach option maxIdleTime value and pool has more connections than minPoolSiz
+        if (timedOut && totalConnection.get() > conf.minPoolSize()) {
+          shouldBeReleased = true;
+        }
+
+      } else if (timedOut) {
+        shouldBeReleased = true;
+      }
+
+      if (shouldBeReleased && idleConnections.remove(item)) {
+
+        totalConnection.decrementAndGet();
+        silentCloseConnection(con);
+        addConnectionRequest();
+        if (logger.isDebugEnabled()) {
+          logger.debug(
+              "pool {} connection {} removed due to inactivity (total:{}, active:{}, pending:{})",
+              poolTag,
+              con.getThreadId(),
+              totalConnection.get(),
+              getActiveConnections(),
+              pendingRequestNumber.get());
+        }
+      }
+    }
+  }
+
+  /**
+   * Create new connection.
+   *
+   * @throws SQLException if connection creation failed
+   */
+  private void addConnection() throws SQLException {
+
+    // create new connection
+    Connection connection = Driver.connect(conf);
+    MariaDbInnerPoolConnection item = new MariaDbInnerPoolConnection(connection);
+    item.addConnectionEventListener(
+        new ConnectionEventListener() {
+
+          @Override
+          public void connectionClosed(ConnectionEvent event) {
+            MariaDbInnerPoolConnection item = (MariaDbInnerPoolConnection) event.getSource();
+            if (poolState.get() == POOL_STATE_OK) {
+              try {
+                if (!idleConnections.contains(item)) {
+                  item.getConnection().setPoolConnection(null);
+                  item.getConnection().reset();
+                  idleConnections.addFirst(item);
+                  item.getConnection().setPoolConnection(item);
+                }
+              } catch (SQLException sqle) {
+
+                // sql exception during reset, removing connection from pool
+                totalConnection.decrementAndGet();
+                silentCloseConnection(item.getConnection());
+                logger.debug(
+                    "connection {} removed from pool {} due to error during reset (total:{},"
+                        + " active:{}, pending:{})",
+                    item.getConnection().getThreadId(),
+                    poolTag,
+                    totalConnection.get(),
+                    getActiveConnections(),
+                    pendingRequestNumber.get());
+              }
+            } else {
+              // pool is closed, should then not be rendered to pool, but closed.
+              try {
+                item.getConnection().close();
+              } catch (SQLException sqle) {
+                // eat
+              }
+              totalConnection.decrementAndGet();
+            }
+          }
+
+          @Override
+          public void connectionErrorOccurred(ConnectionEvent event) {
+
+            MariaDbInnerPoolConnection item = ((MariaDbInnerPoolConnection) event.getSource());
+            totalConnection.decrementAndGet();
+            boolean unused = idleConnections.remove(item);
+
+            // ensure that other connection will be validated before being use
+            // since one connection failed, better to assume the other might as well
+            idleConnections.forEach(MariaDbInnerPoolConnection::ensureValidation);
+
+            silentCloseConnection(item.getConnection());
+            addConnectionRequest();
+            logger.debug(
+                "connection {} removed from pool {} due to having throw a Connection exception"
+                    + " (total:{}, active:{}, pending:{})",
+                item.getConnection().getThreadId(),
+                poolTag,
+                totalConnection.get(),
+                getActiveConnections(),
+                pendingRequestNumber.get());
+          }
+        });
+    if (poolState.get() == POOL_STATE_OK
+        && totalConnection.incrementAndGet() <= conf.maxPoolSize()) {
+      idleConnections.addFirst(item);
+
+      if (logger.isDebugEnabled()) {
+        logger.debug(
+            "pool {} new physical connection {} created (total:{}, active:{}, pending:{})",
+            poolTag,
+            connection.getThreadId(),
+            totalConnection.get(),
+            getActiveConnections(),
+            pendingRequestNumber.get());
+      }
+      return;
+    }
+
+    silentCloseConnection(connection);
+  }
+
+  /**
+   * Get an existing idle connection in pool.
+   *
+   * @return an IDLE connection.
+   */
+  private MariaDbInnerPoolConnection getIdleConnection(long timeout, TimeUnit timeUnit)
+      throws InterruptedException {
+
+    while (true) {
+      MariaDbInnerPoolConnection item =
+          (timeout == 0)
+              ? idleConnections.pollFirst()
+              : idleConnections.pollFirst(timeout, timeUnit);
+
+      if (item != null) {
+        try {
+          if (TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - item.getLastUsed().get())
+              > conf.poolValidMinDelay()) {
+
+            // validate connection
+            if (item.getConnection().isValid(10)) { // 10 seconds timeout
+              item.lastUsedToNow();
+              return item;
+            }
+
+          } else {
+
+            // connection has been retrieved recently -> skip connection validation
+            item.lastUsedToNow();
+            return item;
+          }
+
+        } catch (SQLException sqle) {
+          // eat
+        }
+
+        // validation failed
+        silentAbortConnection(item.getConnection());
+        addConnectionRequest();
+        if (logger.isDebugEnabled()) {
+          logger.debug(
+              "pool {} connection {} removed from pool due to failed validation (total:{},"
+                  + " active:{}, pending:{})",
+              poolTag,
+              item.getConnection().getThreadId(),
+              totalConnection.get(),
+              getActiveConnections(),
+              pendingRequestNumber.get());
+        }
+        continue;
+      }
+
+      return null;
+    }
+  }
+
+  private void silentCloseConnection(Connection con) {
+    con.setPoolConnection(null);
+    try {
+      con.close();
+    } catch (SQLException ex) {
+      // eat exception
+    }
+  }
+
+  private void silentAbortConnection(Connection con) {
+    con.setPoolConnection(null);
+    try {
+      con.abort(poolExecutor);
+    } catch (SQLException ex) {
+      // eat exception
+    }
+  }
+
+  /**
+   * Retrieve new connection. If possible return idle connection, if not, stack connection query,
+   * ask for a connection creation, and loop until a connection become idle / a new connection is
+   * created.
+   *
+   * @return a connection object
+   * @throws SQLException if no connection is created when reaching timeout (connectTimeout option)
+   */
+  public MariaDbInnerPoolConnection getPoolConnection() throws SQLException {
+    pendingRequestNumber.incrementAndGet();
+    MariaDbInnerPoolConnection poolConnection;
+    try {
+      // try to get Idle connection if any (with a very small timeout)
+      if ((poolConnection =
+              getIdleConnection(totalConnection.get() > 4 ? 0 : 50, TimeUnit.MICROSECONDS))
+          != null) {
+        return poolConnection;
+      }
+
+      // ask for new connection creation if max is not reached
+      addConnectionRequest();
+
+      // try to create new connection if semaphore permit it
+      if ((poolConnection =
+              getIdleConnection(
+                  TimeUnit.MILLISECONDS.toNanos(conf.connectTimeout()), TimeUnit.NANOSECONDS))
+          != null) {
+        return poolConnection;
+      }
+
+      throw new SQLException(
+          String.format(
+              "No connection available within the specified time (option 'connectTimeout': %s ms)",
+              NumberFormat.getInstance().format(conf.connectTimeout())));
+
+    } catch (InterruptedException interrupted) {
+      throw new SQLException("Thread was interrupted", "70100", interrupted);
+    } finally {
+      pendingRequestNumber.decrementAndGet();
+    }
+  }
+
+  /**
+   * Get new connection from pool if user and password correspond to pool. If username and password
+   * are different from pool, will return a dedicated connection.
+   *
+   * @param username username
+   * @param password password
+   * @return connection
+   * @throws SQLException if any error occur during connection
+   */
+  public MariaDbInnerPoolConnection getPoolConnection(String username, String password)
+      throws SQLException {
+    if (username == null
+        ? conf.user() == null
+        : username.equals(conf.user()) && (password == null || password.isEmpty())
+            ? conf.password() == null
+            : password.equals(conf.password())) {
+      return getPoolConnection();
+    }
+
+    Configuration tmpConf = conf.clone(username, password);
+    return new MariaDbInnerPoolConnection(Driver.connect(tmpConf));
+  }
+
+  private String generatePoolTag(int poolIndex) {
+    if (conf.poolName() == null) {
+      return "MariaDB-pool";
+    }
+    return conf.poolName() + "-" + poolIndex;
+  }
+
+  /**
+   * Get current configuration
+   *
+   * @return configuration
+   */
+  public Configuration getConf() {
+    return conf;
+  }
+
+  /** Close pool and underlying connections. */
+  @Override
+  public void close() {
+    try {
+      synchronized (this) {
+        Pools.remove(this);
+        poolState.set(POOL_STATE_CLOSING);
+        pendingRequestNumber.set(0);
+
+        scheduledFuture.cancel(false);
+        connectionAppender.shutdown();
+
+        try {
+          boolean unused = connectionAppender.awaitTermination(10, TimeUnit.SECONDS);
+        } catch (InterruptedException i) {
+          // eat
+        }
+
+        if (logger.isInfoEnabled()) {
+          logger.debug(
+              "closing pool {} (total:{}, active:{}, pending:{})",
+              poolTag,
+              totalConnection.get(),
+              getActiveConnections(),
+              pendingRequestNumber.get());
+        }
+
+        ExecutorService connectionRemover =
+            new ThreadPoolExecutor(
+                totalConnection.get(),
+                conf.maxPoolSize(),
+                10,
+                TimeUnit.SECONDS,
+                new LinkedBlockingQueue<>(conf.maxPoolSize()),
+                new PoolThreadFactory(poolTag + "-destroyer"));
+
+        // loop for up to 10 seconds to close not used connection
+        long start = System.nanoTime();
+        do {
+          closeAll(idleConnections);
+          if (totalConnection.get() > 0) {
+            Thread.sleep(0, 10_00);
+          }
+        } while (totalConnection.get() > 0
+            && TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start) < 10);
+
+        // after having wait for 10 seconds, force removal, even if used connections
+        if (totalConnection.get() > 0 || idleConnections.isEmpty()) {
+          closeAll(idleConnections);
+        }
+
+        connectionRemover.shutdown();
+        try {
+          unRegisterJmx();
+        } catch (Exception exception) {
+          // eat
+        }
+        boolean unused = connectionRemover.awaitTermination(10, TimeUnit.SECONDS);
+      }
+    } catch (Exception e) {
+      // eat
+    }
+  }
+
+  private void closeAll(Collection<MariaDbInnerPoolConnection> collection) {
+    synchronized (collection) { // synchronized mandatory to iterate Collections.synchronizedList()
+      for (MariaDbInnerPoolConnection item : collection) {
+        collection.remove(item);
+        totalConnection.decrementAndGet();
+        silentAbortConnection(item.getConnection());
+      }
+    }
+  }
+
+  /**
+   * return pool tag
+   *
+   * @return pool tag
+   */
+  public String getPoolTag() {
+    return poolTag;
+  }
+
+  @Override
+  public long getActiveConnections() {
+    return totalConnection.get() - idleConnections.size();
+  }
+
+  @Override
+  public long getTotalConnections() {
+    return totalConnection.get();
+  }
+
+  @Override
+  public long getIdleConnections() {
+    return idleConnections.size();
+  }
+
+  public long getConnectionRequests() {
+    return pendingRequestNumber.get();
+  }
+
+  private void registerJmx() throws Exception {
+    MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
+    String jmxName = poolTag.replace(":", "_");
+    ObjectName name = new ObjectName("org.mariadb.jdbc.pool:type=" + jmxName);
+
+    if (!mbs.isRegistered(name)) {
+      mbs.registerMBean(this, name);
+    }
+  }
+
+  private void unRegisterJmx() throws Exception {
+    MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
+    String jmxName = poolTag.replace(":", "_");
+    ObjectName name = new ObjectName("org.mariadb.jdbc.pool:type=" + jmxName);
+
+    if (mbs.isRegistered(name)) {
+      mbs.unregisterMBean(name);
+    }
+  }
+
+  /**
+   * For testing purpose only.
+   *
+   * @return current thread id's
+   */
+  public List<Long> testGetConnectionIdleThreadIds() {
+    List<Long> threadIds = new ArrayList<>();
+    for (MariaDbInnerPoolConnection pooledConnection : idleConnections) {
+      threadIds.add(pooledConnection.getConnection().getThreadId());
+    }
+    return threadIds;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/pool/PoolMBean.java b/src/main/java/org/mariadb/jdbc/pool/PoolMBean.java
new file mode 100644
index 0000000..5114e5c
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/pool/PoolMBean.java
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.pool;
+
+/** JMX pool bean */
+public interface PoolMBean {
+
+  /**
+   * get pool active connection number
+   *
+   * @return pool active connection number
+   */
+  long getActiveConnections();
+
+  /**
+   * get pool total connection
+   *
+   * @return pool total connection number
+   */
+  long getTotalConnections();
+
+  /**
+   * get idle connection number
+   *
+   * @return idle connection number
+   */
+  long getIdleConnections();
+
+  /**
+   * get connection waiting request number
+   *
+   * @return request number
+   */
+  long getConnectionRequests();
+}
diff --git a/src/main/java/org/mariadb/jdbc/pool/PoolThreadFactory.java b/src/main/java/org/mariadb/jdbc/pool/PoolThreadFactory.java
new file mode 100644
index 0000000..910c67d
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/pool/PoolThreadFactory.java
@@ -0,0 +1,35 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.pool;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/** Pool thread factory */
+public class PoolThreadFactory implements java.util.concurrent.ThreadFactory {
+
+  // start from DefaultThread factory to get security groups and what not
+  private final java.util.concurrent.ThreadFactory parentFactory = Executors.defaultThreadFactory();
+  private final AtomicInteger threadId = new AtomicInteger();
+  private final String threadName;
+
+  /**
+   * Constructor
+   *
+   * @param threadName thread name
+   */
+  public PoolThreadFactory(String threadName) {
+    this.threadName = threadName;
+  }
+
+  @Override
+  public Thread newThread(Runnable runnable) {
+    Thread result = parentFactory.newThread(runnable);
+    result.setName(threadName + "-" + threadId.incrementAndGet());
+    result.setDaemon(true); // set as daemon so that mariaDb won't hold up shutdown
+
+    return result;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/pool/Pools.java b/src/main/java/org/mariadb/jdbc/pool/Pools.java
new file mode 100644
index 0000000..809e516
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/pool/Pools.java
@@ -0,0 +1,113 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.pool;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.mariadb.jdbc.Configuration;
+
+/** Pools */
+public final class Pools {
+
+  private static final AtomicInteger poolIndex = new AtomicInteger();
+  private static final Map<Configuration, Pool> poolMap = new ConcurrentHashMap<>();
+  private static ScheduledThreadPoolExecutor poolExecutor = null;
+
+  /**
+   * Get existing pool for a configuration. Create it if it doesn't exist.
+   *
+   * @param conf configuration parser
+   * @return pool
+   */
+  public static Pool retrievePool(Configuration conf) {
+    if (!poolMap.containsKey(conf)) {
+      synchronized (poolMap) {
+        if (!poolMap.containsKey(conf)) {
+          if (poolExecutor == null) {
+            poolExecutor =
+                new ScheduledThreadPoolExecutor(
+                    1, new PoolThreadFactory("MariaDbPool-maxTimeoutIdle-checker"));
+          }
+          Pool pool = new Pool(conf, poolIndex.incrementAndGet(), poolExecutor);
+          poolMap.put(conf, pool);
+          return pool;
+        }
+      }
+    }
+    return poolMap.get(conf);
+  }
+
+  /**
+   * Remove pool.
+   *
+   * @param pool pool to remove
+   */
+  public static void remove(Pool pool) {
+    if (poolMap.containsKey(pool.getConf())) {
+      synchronized (poolMap) {
+        if (poolMap.containsKey(pool.getConf())) {
+          poolMap.remove(pool.getConf());
+
+          if (poolMap.isEmpty()) {
+            shutdownExecutor();
+          }
+        }
+      }
+    }
+  }
+
+  /** Close all pools. */
+  public static void close() {
+    synchronized (poolMap) {
+      for (Pool pool : poolMap.values()) {
+        try {
+          pool.close();
+        } catch (Exception exception) {
+          // eat
+        }
+      }
+      shutdownExecutor();
+      poolMap.clear();
+    }
+  }
+
+  /**
+   * Closing a pool with name defined in url.
+   *
+   * @param poolName the option "poolName" value
+   */
+  public static void close(String poolName) {
+    if (poolName == null) {
+      return;
+    }
+    synchronized (poolMap) {
+      for (Pool pool : poolMap.values()) {
+        if (poolName.equals(pool.getConf().poolName())) {
+          try {
+            pool.close(); // Pool.close() calls Pools.remove(), which does the rest of the cleanup
+          } catch (Exception exception) {
+            // eat
+          }
+          return;
+        }
+      }
+    }
+  }
+
+  private static void shutdownExecutor() {
+    if (poolExecutor != null) {
+      poolExecutor.shutdown();
+      try {
+        poolExecutor.awaitTermination(10, TimeUnit.SECONDS);
+      } catch (InterruptedException interrupted) {
+        // eat
+      }
+      poolExecutor = null;
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/type/Geometry.java b/src/main/java/org/mariadb/jdbc/type/Geometry.java
new file mode 100644
index 0000000..0cd27df
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/type/Geometry.java
@@ -0,0 +1,112 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.type;
+
+import java.sql.SQLDataException;
+import org.mariadb.jdbc.client.Column;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+
+/** Geometry parser */
+public interface Geometry {
+
+  /**
+   * Parse point from packet
+   *
+   * @param littleEndian encoding order
+   * @param buf packet buffer
+   * @return point value
+   */
+  static Point parsePoint(boolean littleEndian, ReadableByteBuf buf) {
+    double x = littleEndian ? buf.readDouble() : buf.readDoubleBE();
+    double y = littleEndian ? buf.readDouble() : buf.readDoubleBE();
+    return new Point(x, y);
+  }
+
+  /**
+   * parse geometry object
+   *
+   * @param buf packet buffer
+   * @param length data length
+   * @param column column meta
+   * @return geometry parsed object
+   * @throws SQLDataException if parsing exception occurs
+   */
+  static Geometry getGeometry(ReadableByteBuf buf, int length, Column column)
+      throws SQLDataException {
+    if (length == 0) return null;
+    boolean littleEndian = buf.readByte() == 0x01;
+    int dataType = littleEndian ? buf.readInt() : buf.readIntBE();
+    switch (dataType) {
+      case 1:
+        // wkbPoint
+        return parsePoint(littleEndian, buf);
+
+      case 2:
+        // wkbLineString
+        int pointNumber = littleEndian ? buf.readInt() : buf.readIntBE();
+        Point[] points = new Point[pointNumber];
+        for (int i = 0; i < pointNumber; i++) {
+          points[i] = parsePoint(littleEndian, buf);
+        }
+        return new LineString(points, true);
+
+      case 3:
+        // wkbPolygon
+        int numRings = littleEndian ? buf.readInt() : buf.readIntBE();
+        LineString[] lines = new LineString[numRings];
+        for (int i = 0; i < numRings; i++) {
+          int pointNb = littleEndian ? buf.readInt() : buf.readIntBE();
+          Point[] lsPoints = new Point[pointNb];
+          for (int j = 0; j < pointNb; j++) {
+            lsPoints[j] = parsePoint(littleEndian, buf);
+          }
+          lines[i] = new LineString(lsPoints, false);
+        }
+        return new Polygon(lines);
+
+      case 4:
+        // wkbMultiPoint
+        int pointNb = littleEndian ? buf.readInt() : buf.readIntBE();
+        Point[] pointArr = new Point[pointNb];
+        for (int i = 0; i < pointNb; i++) {
+          pointArr[i] = (Point) getGeometry(buf, length, column);
+        }
+        return new MultiPoint(pointArr);
+
+      case 5:
+        // wkbMultiLinestring
+        int multiNb = littleEndian ? buf.readInt() : buf.readIntBE();
+        LineString[] multiLines = new LineString[multiNb];
+        for (int i = 0; i < multiNb; i++) {
+          multiLines[i] = (LineString) getGeometry(buf, length, column);
+        }
+        return new MultiLineString(multiLines);
+
+      case 6:
+        // wkbMultiPolygon
+        int multiPolyNb = littleEndian ? buf.readInt() : buf.readIntBE();
+        Polygon[] multiPolygons = new Polygon[multiPolyNb];
+        for (int i = 0; i < multiPolyNb; i++) {
+          multiPolygons[i] = (Polygon) getGeometry(buf, length, column);
+        }
+        return new MultiPolygon(multiPolygons);
+
+      case 7:
+        // wkbGeometryCollection
+        int multiCollNb = littleEndian ? buf.readInt() : buf.readIntBE();
+        Geometry[] multiGeos = new Geometry[multiCollNb];
+        for (int i = 0; i < multiCollNb; i++) {
+          multiGeos[i] = getGeometry(buf, length, column);
+        }
+        return new GeometryCollection(multiGeos);
+
+      default:
+        // ERROR
+        buf.skip(length - 5);
+        throw new SQLDataException(
+            String.format("Data type %s cannot be decoded as Geometry", column.getType()));
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/type/GeometryCollection.java b/src/main/java/org/mariadb/jdbc/type/GeometryCollection.java
new file mode 100644
index 0000000..af70ffa
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/type/GeometryCollection.java
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.type;
+
+import java.util.Arrays;
+
+/** Geometry collection implementation */
+public class GeometryCollection implements Geometry {
+
+  private final Geometry[] geometries;
+
+  /**
+   * Constructor
+   *
+   * @param geometries geometry array
+   */
+  public GeometryCollection(Geometry[] geometries) {
+    this.geometries = geometries;
+  }
+
+  /**
+   * Get objects
+   *
+   * @return geometry array
+   */
+  public Geometry[] getGeometries() {
+    return geometries;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder("GEOMETRYCOLLECTION(");
+    int indexpoly = 0;
+    for (Geometry geo : geometries) {
+      if (indexpoly++ > 0) {
+        sb.append(",");
+      }
+      sb.append(geo.toString());
+    }
+    sb.append(")");
+    return sb.toString();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || !(o instanceof GeometryCollection)) return false;
+    return toString().equals(o.toString());
+  }
+
+  @Override
+  public int hashCode() {
+    return Arrays.hashCode(geometries);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/type/LineString.java b/src/main/java/org/mariadb/jdbc/type/LineString.java
new file mode 100644
index 0000000..9a4c170
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/type/LineString.java
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.type;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/** Linestring object */
+public class LineString implements Geometry {
+
+  private final Point[] points;
+  private final boolean open;
+
+  /**
+   * Constructor
+   *
+   * @param points point list
+   * @param open open linestring
+   */
+  public LineString(Point[] points, boolean open) {
+    this.points = points;
+    this.open = open;
+  }
+
+  /**
+   * get points
+   *
+   * @return points
+   */
+  public Point[] getPoints() {
+    return points;
+  }
+
+  /**
+   * Is form open
+   *
+   * @return is open
+   */
+  public boolean isOpen() {
+    return open;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder("LINESTRING(");
+    int index = 0;
+    for (Point pt : points) {
+      if (index++ > 0) {
+        sb.append(",");
+      }
+      sb.append(pt.getX()).append(" ").append(pt.getY());
+    }
+    sb.append(")");
+    return sb.toString();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || !(o instanceof LineString)) return false;
+    return open == ((LineString) o).isOpen() && toString().equals(o.toString());
+  }
+
+  @Override
+  public int hashCode() {
+    int result = Objects.hash(open);
+    result = 31 * result + Arrays.hashCode(points);
+    return result;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/type/MultiLineString.java b/src/main/java/org/mariadb/jdbc/type/MultiLineString.java
new file mode 100644
index 0000000..5bdd5f1
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/type/MultiLineString.java
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.type;
+
+import java.util.Arrays;
+
+/** Multiline */
+public class MultiLineString implements Geometry {
+
+  private final LineString[] lines;
+
+  /**
+   * Constructor
+   *
+   * @param lines lines
+   */
+  public MultiLineString(LineString[] lines) {
+    this.lines = lines;
+  }
+
+  /**
+   * get lines
+   *
+   * @return lines
+   */
+  public LineString[] getLines() {
+    return lines;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder("MULTILINESTRING(");
+    int indexLine = 0;
+    for (LineString ls : lines) {
+      if (indexLine++ > 0) {
+        sb.append(",");
+      }
+      sb.append("(");
+      int index = 0;
+      for (Point pt : ls.getPoints()) {
+        if (index++ > 0) {
+          sb.append(",");
+        }
+        sb.append(pt.getX()).append(" ").append(pt.getY());
+      }
+      sb.append(")");
+    }
+    sb.append(")");
+    return sb.toString();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || !(o instanceof MultiLineString)) return false;
+    return toString().equals(o.toString());
+  }
+
+  @Override
+  public int hashCode() {
+    return Arrays.hashCode(lines);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/type/MultiPoint.java b/src/main/java/org/mariadb/jdbc/type/MultiPoint.java
new file mode 100644
index 0000000..2795380
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/type/MultiPoint.java
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.type;
+
+import java.util.Arrays;
+
+/** Multi-point */
+public class MultiPoint implements Geometry {
+
+  private final Point[] points;
+
+  /**
+   * Constructor
+   *
+   * @param points points
+   */
+  public MultiPoint(Point[] points) {
+    this.points = points;
+  }
+
+  /**
+   * Get points
+   *
+   * @return points
+   */
+  public Point[] getPoints() {
+    return points;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder("MULTIPOINT(");
+    int index = 0;
+    for (Point pt : points) {
+      if (index++ > 0) {
+        sb.append(",");
+      }
+      sb.append(pt.getX()).append(" ").append(pt.getY());
+    }
+    sb.append(")");
+    return sb.toString();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || !(o instanceof MultiPoint)) return false;
+    return toString().equals(o.toString());
+  }
+
+  @Override
+  public int hashCode() {
+    return Arrays.hashCode(points);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/type/MultiPolygon.java b/src/main/java/org/mariadb/jdbc/type/MultiPolygon.java
new file mode 100644
index 0000000..e5cd799
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/type/MultiPolygon.java
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.type;
+
+import java.util.Arrays;
+
+/** Multi polygon */
+public class MultiPolygon implements Geometry {
+
+  private final Polygon[] polygons;
+
+  /**
+   * Constructor
+   *
+   * @param polygons polygons
+   */
+  public MultiPolygon(Polygon[] polygons) {
+    this.polygons = polygons;
+  }
+
+  /**
+   * Get polygons
+   *
+   * @return polygons
+   */
+  public Polygon[] getPolygons() {
+    return polygons;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder("MULTIPOLYGON(");
+    int indexpoly = 0;
+    for (Polygon poly : polygons) {
+      if (indexpoly++ > 0) {
+        sb.append(",");
+      }
+      sb.append("(");
+      int indexLine = 0;
+      for (LineString ls : poly.getLines()) {
+        if (indexLine++ > 0) {
+          sb.append(",");
+        }
+        sb.append("(");
+        int index = 0;
+        for (Point pt : ls.getPoints()) {
+          if (index++ > 0) {
+            sb.append(",");
+          }
+          sb.append(pt.getX()).append(" ").append(pt.getY());
+        }
+        sb.append(")");
+      }
+      sb.append(")");
+    }
+    sb.append(")");
+    return sb.toString();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || !(o instanceof MultiPolygon)) return false;
+    return toString().equals(o.toString());
+  }
+
+  @Override
+  public int hashCode() {
+    return Arrays.hashCode(polygons);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/type/Point.java b/src/main/java/org/mariadb/jdbc/type/Point.java
new file mode 100644
index 0000000..ae0e062
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/type/Point.java
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.type;
+
+import java.util.Objects;
+
+/** Point */
+public class Point implements Geometry {
+
+  private final double x;
+  private final double y;
+
+  /**
+   * Constructor
+   *
+   * @param x abscissa
+   * @param y ordinate
+   */
+  public Point(double x, double y) {
+    this.x = x;
+    this.y = y;
+  }
+
+  /**
+   * Get abscissa
+   *
+   * @return abscissa
+   */
+  public double getX() {
+    return x;
+  }
+
+  /**
+   * get ordinate
+   *
+   * @return ordinate
+   */
+  public double getY() {
+    return y;
+  }
+
+  @Override
+  public String toString() {
+    return "POINT(" + x + " " + y + ")";
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || !(o instanceof Point)) return false;
+    return toString().equals(o.toString());
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(x, y);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/type/Polygon.java b/src/main/java/org/mariadb/jdbc/type/Polygon.java
new file mode 100644
index 0000000..738c39d
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/type/Polygon.java
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.type;
+
+import java.util.Arrays;
+
+/** Polygon */
+public class Polygon implements Geometry {
+
+  private final LineString[] lines;
+
+  /**
+   * polygon
+   *
+   * @param lines lines
+   */
+  public Polygon(LineString[] lines) {
+    this.lines = lines;
+  }
+
+  /**
+   * get lines
+   *
+   * @return lines
+   */
+  public LineString[] getLines() {
+    return lines;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder("POLYGON(");
+    int indexLine = 0;
+    for (LineString ls : lines) {
+      if (indexLine++ > 0) {
+        sb.append(",");
+      }
+      sb.append("(");
+      int index = 0;
+      for (Point pt : ls.getPoints()) {
+        if (index++ > 0) {
+          sb.append(",");
+        }
+        sb.append(pt.getX()).append(" ").append(pt.getY());
+      }
+      sb.append(")");
+    }
+    sb.append(")");
+    return sb.toString();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || !(o instanceof Polygon)) return false;
+    return toString().equals(o.toString());
+  }
+
+  @Override
+  public int hashCode() {
+    return Arrays.hashCode(lines);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/CharsetEncodingLength.java b/src/main/java/org/mariadb/jdbc/util/CharsetEncodingLength.java
new file mode 100644
index 0000000..033024a
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/CharsetEncodingLength.java
@@ -0,0 +1,345 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public final class CharsetEncodingLength {
+
+  // This array stored character length for every collation
+  // query to generate:
+  //   select  id, maxlen
+  //   from information_schema.character_sets, information_schema.collations
+  //   where character_sets.character_set_name = collations.character_set_name
+  //   order by id
+  public static final Map<Integer, Integer> maxCharlen;
+
+  static {
+    maxCharlen = new HashMap<>();
+    maxCharlen.put(1, 2);
+    maxCharlen.put(2, 1);
+    maxCharlen.put(3, 1);
+    maxCharlen.put(4, 1);
+    maxCharlen.put(5, 1);
+    maxCharlen.put(6, 1);
+    maxCharlen.put(7, 1);
+    maxCharlen.put(8, 1);
+    maxCharlen.put(9, 1);
+    maxCharlen.put(10, 1);
+    maxCharlen.put(11, 1);
+    maxCharlen.put(12, 3);
+    maxCharlen.put(13, 2);
+    maxCharlen.put(14, 1);
+    maxCharlen.put(15, 1);
+    maxCharlen.put(16, 1);
+    maxCharlen.put(18, 1);
+    maxCharlen.put(19, 2);
+    maxCharlen.put(20, 1);
+    maxCharlen.put(21, 1);
+    maxCharlen.put(22, 1);
+    maxCharlen.put(23, 1);
+    maxCharlen.put(24, 2);
+    maxCharlen.put(25, 1);
+    maxCharlen.put(26, 1);
+    maxCharlen.put(27, 1);
+    maxCharlen.put(28, 2);
+    maxCharlen.put(29, 1);
+    maxCharlen.put(30, 1);
+    maxCharlen.put(31, 1);
+    maxCharlen.put(32, 1);
+    maxCharlen.put(33, 3);
+    maxCharlen.put(34, 1);
+    maxCharlen.put(35, 2);
+    maxCharlen.put(36, 1);
+    maxCharlen.put(37, 1);
+    maxCharlen.put(38, 1);
+    maxCharlen.put(39, 1);
+    maxCharlen.put(40, 1);
+    maxCharlen.put(41, 1);
+    maxCharlen.put(42, 1);
+    maxCharlen.put(43, 1);
+    maxCharlen.put(44, 1);
+    maxCharlen.put(45, 4);
+    maxCharlen.put(46, 4);
+    maxCharlen.put(47, 1);
+    maxCharlen.put(48, 1);
+    maxCharlen.put(49, 1);
+    maxCharlen.put(50, 1);
+    maxCharlen.put(51, 1);
+    maxCharlen.put(52, 1);
+    maxCharlen.put(53, 1);
+    maxCharlen.put(54, 4);
+    maxCharlen.put(55, 4);
+    maxCharlen.put(56, 4);
+    maxCharlen.put(57, 1);
+    maxCharlen.put(58, 1);
+    maxCharlen.put(59, 1);
+    maxCharlen.put(60, 4);
+    maxCharlen.put(61, 4);
+    maxCharlen.put(62, 4);
+    maxCharlen.put(63, 1);
+    maxCharlen.put(64, 1);
+    maxCharlen.put(65, 1);
+    maxCharlen.put(66, 1);
+    maxCharlen.put(67, 1);
+    maxCharlen.put(68, 1);
+    maxCharlen.put(69, 1);
+    maxCharlen.put(70, 1);
+    maxCharlen.put(71, 1);
+    maxCharlen.put(72, 1);
+    maxCharlen.put(73, 1);
+    maxCharlen.put(74, 1);
+    maxCharlen.put(75, 1);
+    maxCharlen.put(77, 1);
+    maxCharlen.put(78, 1);
+    maxCharlen.put(79, 1);
+    maxCharlen.put(80, 1);
+    maxCharlen.put(81, 1);
+    maxCharlen.put(82, 1);
+    maxCharlen.put(83, 3);
+    maxCharlen.put(84, 2);
+    maxCharlen.put(85, 2);
+    maxCharlen.put(86, 2);
+    maxCharlen.put(87, 2);
+    maxCharlen.put(88, 2);
+    maxCharlen.put(89, 1);
+    maxCharlen.put(90, 2);
+    maxCharlen.put(91, 3);
+    maxCharlen.put(92, 1);
+    maxCharlen.put(93, 1);
+    maxCharlen.put(94, 1);
+    maxCharlen.put(95, 2);
+    maxCharlen.put(96, 2);
+    maxCharlen.put(97, 3);
+    maxCharlen.put(98, 3);
+    maxCharlen.put(99, 1);
+    maxCharlen.put(101, 4);
+    maxCharlen.put(102, 4);
+    maxCharlen.put(103, 4);
+    maxCharlen.put(104, 4);
+    maxCharlen.put(105, 4);
+    maxCharlen.put(106, 4);
+    maxCharlen.put(107, 4);
+    maxCharlen.put(108, 4);
+    maxCharlen.put(109, 4);
+    maxCharlen.put(110, 4);
+    maxCharlen.put(111, 4);
+    maxCharlen.put(112, 4);
+    maxCharlen.put(113, 4);
+    maxCharlen.put(114, 4);
+    maxCharlen.put(115, 4);
+    maxCharlen.put(116, 4);
+    maxCharlen.put(117, 4);
+    maxCharlen.put(118, 4);
+    maxCharlen.put(119, 4);
+    maxCharlen.put(120, 4);
+    maxCharlen.put(121, 4);
+    maxCharlen.put(122, 4);
+    maxCharlen.put(123, 4);
+    maxCharlen.put(124, 4);
+    maxCharlen.put(128, 2);
+    maxCharlen.put(129, 2);
+    maxCharlen.put(130, 2);
+    maxCharlen.put(131, 2);
+    maxCharlen.put(132, 2);
+    maxCharlen.put(133, 2);
+    maxCharlen.put(134, 2);
+    maxCharlen.put(135, 2);
+    maxCharlen.put(136, 2);
+    maxCharlen.put(137, 2);
+    maxCharlen.put(138, 2);
+    maxCharlen.put(139, 2);
+    maxCharlen.put(140, 2);
+    maxCharlen.put(141, 2);
+    maxCharlen.put(142, 2);
+    maxCharlen.put(143, 2);
+    maxCharlen.put(144, 2);
+    maxCharlen.put(145, 2);
+    maxCharlen.put(146, 2);
+    maxCharlen.put(147, 2);
+    maxCharlen.put(148, 2);
+    maxCharlen.put(149, 2);
+    maxCharlen.put(150, 2);
+    maxCharlen.put(151, 2);
+    maxCharlen.put(159, 2);
+    maxCharlen.put(160, 4);
+    maxCharlen.put(161, 4);
+    maxCharlen.put(162, 4);
+    maxCharlen.put(163, 4);
+    maxCharlen.put(164, 4);
+    maxCharlen.put(165, 4);
+    maxCharlen.put(166, 4);
+    maxCharlen.put(167, 4);
+    maxCharlen.put(168, 4);
+    maxCharlen.put(169, 4);
+    maxCharlen.put(170, 4);
+    maxCharlen.put(171, 4);
+    maxCharlen.put(172, 4);
+    maxCharlen.put(173, 4);
+    maxCharlen.put(174, 4);
+    maxCharlen.put(175, 4);
+    maxCharlen.put(176, 4);
+    maxCharlen.put(177, 4);
+    maxCharlen.put(178, 4);
+    maxCharlen.put(179, 4);
+    maxCharlen.put(180, 4);
+    maxCharlen.put(181, 4);
+    maxCharlen.put(182, 4);
+    maxCharlen.put(183, 4);
+    maxCharlen.put(192, 3);
+    maxCharlen.put(193, 3);
+    maxCharlen.put(194, 3);
+    maxCharlen.put(195, 3);
+    maxCharlen.put(196, 3);
+    maxCharlen.put(197, 3);
+    maxCharlen.put(198, 3);
+    maxCharlen.put(199, 3);
+    maxCharlen.put(200, 3);
+    maxCharlen.put(201, 3);
+    maxCharlen.put(202, 3);
+    maxCharlen.put(203, 3);
+    maxCharlen.put(204, 3);
+    maxCharlen.put(205, 3);
+    maxCharlen.put(206, 3);
+    maxCharlen.put(207, 3);
+    maxCharlen.put(208, 3);
+    maxCharlen.put(209, 3);
+    maxCharlen.put(210, 3);
+    maxCharlen.put(211, 3);
+    maxCharlen.put(212, 3);
+    maxCharlen.put(213, 3);
+    maxCharlen.put(214, 3);
+    maxCharlen.put(215, 3);
+    maxCharlen.put(223, 3);
+    maxCharlen.put(224, 4);
+    maxCharlen.put(225, 4);
+    maxCharlen.put(226, 4);
+    maxCharlen.put(227, 4);
+    maxCharlen.put(228, 4);
+    maxCharlen.put(229, 4);
+    maxCharlen.put(230, 4);
+    maxCharlen.put(231, 4);
+    maxCharlen.put(232, 4);
+    maxCharlen.put(233, 4);
+    maxCharlen.put(234, 4);
+    maxCharlen.put(235, 4);
+    maxCharlen.put(236, 4);
+    maxCharlen.put(237, 4);
+    maxCharlen.put(238, 4);
+    maxCharlen.put(239, 4);
+    maxCharlen.put(240, 4);
+    maxCharlen.put(241, 4);
+    maxCharlen.put(242, 4);
+    maxCharlen.put(243, 4);
+    maxCharlen.put(244, 4);
+    maxCharlen.put(245, 4);
+    maxCharlen.put(246, 4);
+    maxCharlen.put(247, 4);
+    maxCharlen.put(576, 3);
+    maxCharlen.put(577, 3);
+    maxCharlen.put(578, 3);
+    maxCharlen.put(608, 4);
+    maxCharlen.put(609, 4);
+    maxCharlen.put(610, 4);
+    maxCharlen.put(640, 2);
+    maxCharlen.put(641, 2);
+    maxCharlen.put(642, 2);
+    maxCharlen.put(672, 4);
+    maxCharlen.put(673, 4);
+    maxCharlen.put(674, 4);
+    maxCharlen.put(736, 4);
+    maxCharlen.put(737, 4);
+    maxCharlen.put(738, 4);
+    maxCharlen.put(1025, 2);
+    maxCharlen.put(1027, 1);
+    maxCharlen.put(1028, 1);
+    maxCharlen.put(1030, 1);
+    maxCharlen.put(1031, 1);
+    maxCharlen.put(1032, 1);
+    maxCharlen.put(1033, 1);
+    maxCharlen.put(1034, 1);
+    maxCharlen.put(1035, 1);
+    maxCharlen.put(1036, 3);
+    maxCharlen.put(1037, 2);
+    maxCharlen.put(1040, 1);
+    maxCharlen.put(1042, 1);
+    maxCharlen.put(1043, 2);
+    maxCharlen.put(1046, 1);
+    maxCharlen.put(1048, 2);
+    maxCharlen.put(1049, 1);
+    maxCharlen.put(1050, 1);
+    maxCharlen.put(1052, 2);
+    maxCharlen.put(1054, 1);
+    maxCharlen.put(1056, 1);
+    maxCharlen.put(1057, 3);
+    maxCharlen.put(1059, 2);
+    maxCharlen.put(1060, 1);
+    maxCharlen.put(1061, 1);
+    maxCharlen.put(1062, 1);
+    maxCharlen.put(1063, 1);
+    maxCharlen.put(1064, 1);
+    maxCharlen.put(1065, 1);
+    maxCharlen.put(1067, 1);
+    maxCharlen.put(1069, 4);
+    maxCharlen.put(1070, 4);
+    maxCharlen.put(1071, 1);
+    maxCharlen.put(1074, 1);
+    maxCharlen.put(1075, 1);
+    maxCharlen.put(1077, 1);
+    maxCharlen.put(1078, 4);
+    maxCharlen.put(1079, 4);
+    maxCharlen.put(1080, 4);
+    maxCharlen.put(1081, 1);
+    maxCharlen.put(1082, 1);
+    maxCharlen.put(1083, 1);
+    maxCharlen.put(1084, 4);
+    maxCharlen.put(1085, 4);
+    maxCharlen.put(1086, 4);
+    maxCharlen.put(1088, 1);
+    maxCharlen.put(1089, 1);
+    maxCharlen.put(1090, 1);
+    maxCharlen.put(1091, 1);
+    maxCharlen.put(1092, 1);
+    maxCharlen.put(1093, 1);
+    maxCharlen.put(1094, 1);
+    maxCharlen.put(1095, 1);
+    maxCharlen.put(1096, 1);
+    maxCharlen.put(1097, 1);
+    maxCharlen.put(1098, 1);
+    maxCharlen.put(1099, 1);
+    maxCharlen.put(1101, 1);
+    maxCharlen.put(1102, 1);
+    maxCharlen.put(1103, 1);
+    maxCharlen.put(1104, 1);
+    maxCharlen.put(1105, 1);
+    maxCharlen.put(1106, 1);
+    maxCharlen.put(1107, 3);
+    maxCharlen.put(1108, 2);
+    maxCharlen.put(1109, 2);
+    maxCharlen.put(1110, 2);
+    maxCharlen.put(1111, 2);
+    maxCharlen.put(1112, 2);
+    maxCharlen.put(1113, 1);
+    maxCharlen.put(1114, 2);
+    maxCharlen.put(1115, 3);
+    maxCharlen.put(1116, 1);
+    maxCharlen.put(1117, 1);
+    maxCharlen.put(1119, 2);
+    maxCharlen.put(1120, 2);
+    maxCharlen.put(1121, 3);
+    maxCharlen.put(1122, 3);
+    maxCharlen.put(1125, 4);
+    maxCharlen.put(1147, 4);
+    maxCharlen.put(1152, 2);
+    maxCharlen.put(1174, 2);
+    maxCharlen.put(1184, 4);
+    maxCharlen.put(1206, 4);
+    maxCharlen.put(1216, 3);
+    maxCharlen.put(1238, 3);
+    maxCharlen.put(1248, 4);
+    maxCharlen.put(1270, 4);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/ClientParser.java b/src/main/java/org/mariadb/jdbc/util/ClientParser.java
new file mode 100644
index 0000000..50d2ca5
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/ClientParser.java
@@ -0,0 +1,160 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+public final class ClientParser implements PrepareResult {
+
+  private final String sql;
+  private final byte[] query;
+  private List<Integer> paramPositions;
+  private int paramCount;
+
+  private ClientParser(String sql, byte[] query, List<Integer> paramPositions) {
+    this.sql = sql;
+    this.query = query;
+    this.paramPositions = paramPositions;
+    this.paramCount = paramPositions.size();
+  }
+
+  /**
+   * Separate query in a String list and set flag isQueryMultipleRewritable. The resulting string
+   * list is separed by ? that are not in comments. isQueryMultipleRewritable flag is set if query
+   * can be rewrite in one query (all case but if using "-- comment"). example for query : "INSERT
+   * INTO tableName(id, name) VALUES (?, ?)" result list will be : {"INSERT INTO tableName(id, name)
+   * VALUES (", ", ", ")"}
+   *
+   * @param queryString query
+   * @param noBackslashEscapes escape mode
+   * @return ClientPrepareResult
+   */
+  public static ClientParser parameterParts(String queryString, boolean noBackslashEscapes) {
+
+    List<Integer> paramPositions = new ArrayList<>();
+    LexState state = LexState.Normal;
+    byte lastChar = 0x00;
+
+    boolean singleQuotes = false;
+    byte[] query = queryString.getBytes(StandardCharsets.UTF_8);
+    int queryLength = query.length;
+    for (int i = 0; i < queryLength; i++) {
+
+      byte car = query[i];
+      if (state == LexState.Escape
+          && !((car == '\'' && singleQuotes) || (car == '"' && !singleQuotes))) {
+        state = LexState.String;
+        lastChar = car;
+        continue;
+      }
+      switch (car) {
+        case (byte) '*':
+          if (state == LexState.Normal && lastChar == (byte) '/') {
+            state = LexState.SlashStarComment;
+          }
+          break;
+
+        case (byte) '/':
+          if (state == LexState.SlashStarComment && lastChar == (byte) '*') {
+            state = LexState.Normal;
+          } else if (state == LexState.Normal && lastChar == (byte) '/') {
+            state = LexState.EOLComment;
+          }
+          break;
+
+        case (byte) '#':
+          if (state == LexState.Normal) {
+            state = LexState.EOLComment;
+          }
+          break;
+
+        case (byte) '-':
+          if (state == LexState.Normal && lastChar == (byte) '-') {
+            state = LexState.EOLComment;
+          }
+          break;
+
+        case (byte) '\n':
+          if (state == LexState.EOLComment) {
+            state = LexState.Normal;
+          }
+          break;
+
+        case (byte) '"':
+          if (state == LexState.Normal) {
+            state = LexState.String;
+            singleQuotes = false;
+          } else if (state == LexState.String && !singleQuotes) {
+            state = LexState.Normal;
+          } else if (state == LexState.Escape) {
+            state = LexState.String;
+          }
+          break;
+
+        case (byte) '\'':
+          if (state == LexState.Normal) {
+            state = LexState.String;
+            singleQuotes = true;
+          } else if (state == LexState.String && singleQuotes) {
+            state = LexState.Normal;
+          } else if (state == LexState.Escape) {
+            state = LexState.String;
+          }
+          break;
+
+        case (byte) '\\':
+          if (noBackslashEscapes) {
+            break;
+          }
+          if (state == LexState.String) {
+            state = LexState.Escape;
+          }
+          break;
+        case (byte) '?':
+          if (state == LexState.Normal) {
+            paramPositions.add(i);
+          }
+          break;
+        case (byte) '`':
+          if (state == LexState.Backtick) {
+            state = LexState.Normal;
+          } else if (state == LexState.Normal) {
+            state = LexState.Backtick;
+          }
+          break;
+      }
+      lastChar = car;
+    }
+
+    return new ClientParser(queryString, query, paramPositions);
+  }
+
+  public String getSql() {
+    return sql;
+  }
+
+  public byte[] getQuery() {
+    return query;
+  }
+
+  public List<Integer> getParamPositions() {
+    return paramPositions;
+  }
+
+  public int getParamCount() {
+    return paramCount;
+  }
+
+  enum LexState {
+    Normal, /* inside  query */
+    String, /* inside string */
+    SlashStarComment, /* inside slash-star comment */
+    Escape, /* found backslash */
+    EOLComment, /* # comment, or // comment, or -- comment */
+    Backtick /* found backtick */
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/ConfigurableSocketFactory.java b/src/main/java/org/mariadb/jdbc/util/ConfigurableSocketFactory.java
new file mode 100644
index 0000000..15507d3
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/ConfigurableSocketFactory.java
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util;
+
+import javax.net.SocketFactory;
+import org.mariadb.jdbc.Configuration;
+
+public abstract class ConfigurableSocketFactory extends SocketFactory {
+  public abstract void setConfiguration(Configuration conf, String host);
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/NativeSql.java b/src/main/java/org/mariadb/jdbc/util/NativeSql.java
new file mode 100644
index 0000000..424b693
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/NativeSql.java
@@ -0,0 +1,345 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util;
+
+import java.sql.SQLException;
+import java.util.Locale;
+import org.mariadb.jdbc.client.Context;
+
+public final class NativeSql {
+
+  public static String parse(String sql, Context context) throws SQLException {
+    if (!sql.contains("{")) {
+      return sql;
+    }
+
+    ClientParser.LexState state = ClientParser.LexState.Normal;
+    char lastChar = '\0';
+    boolean singleQuotes = false;
+    int lastEscapePart = 0;
+
+    StringBuilder sb = new StringBuilder();
+    char[] query = sql.toCharArray();
+    int queryLength = query.length;
+    int escapeIdx = 0;
+    boolean inEscape = false;
+
+    for (int idx = 0; idx < queryLength; idx++) {
+
+      char car = query[idx];
+      if (state == ClientParser.LexState.Escape
+          && !((car == '\'' && singleQuotes) || (car == '"' && !singleQuotes))) {
+        state = ClientParser.LexState.String;
+        if (!inEscape) sb.append(car);
+        lastChar = car;
+        continue;
+      }
+      switch (car) {
+        case '*':
+          if (state == ClientParser.LexState.Normal && lastChar == '/') {
+            state = ClientParser.LexState.SlashStarComment;
+          }
+          break;
+
+        case '/':
+          if (state == ClientParser.LexState.SlashStarComment && lastChar == '*') {
+            state = ClientParser.LexState.Normal;
+          } else if (state == ClientParser.LexState.Normal && lastChar == '/') {
+            state = ClientParser.LexState.EOLComment;
+          }
+          break;
+
+        case '#':
+          if (state == ClientParser.LexState.Normal) {
+            state = ClientParser.LexState.EOLComment;
+          }
+          break;
+
+        case '-':
+          if (state == ClientParser.LexState.Normal && lastChar == '-') {
+            state = ClientParser.LexState.EOLComment;
+          }
+          break;
+
+        case '\n':
+          if (state == ClientParser.LexState.EOLComment) {
+            state = ClientParser.LexState.Normal;
+          }
+          break;
+
+        case '"':
+          if (state == ClientParser.LexState.Normal) {
+            state = ClientParser.LexState.String;
+            singleQuotes = false;
+          } else if (state == ClientParser.LexState.String && !singleQuotes) {
+            state = ClientParser.LexState.Normal;
+          } else if (state == ClientParser.LexState.Escape) {
+            state = ClientParser.LexState.String;
+          }
+          break;
+
+        case '\'':
+          if (state == ClientParser.LexState.Normal) {
+            state = ClientParser.LexState.String;
+            singleQuotes = true;
+          } else if (state == ClientParser.LexState.String && singleQuotes) {
+            state = ClientParser.LexState.Normal;
+          } else if (state == ClientParser.LexState.Escape) {
+            state = ClientParser.LexState.String;
+          }
+          break;
+
+        case '\\':
+          if (state == ClientParser.LexState.String) {
+            state = ClientParser.LexState.Escape;
+          }
+          break;
+        case '`':
+          if (state == ClientParser.LexState.Backtick) {
+            state = ClientParser.LexState.Normal;
+          } else if (state == ClientParser.LexState.Normal) {
+            state = ClientParser.LexState.Backtick;
+          }
+          break;
+        case '{':
+          if (state == ClientParser.LexState.Normal) {
+            if (!inEscape) {
+              inEscape = true;
+              lastEscapePart = idx;
+            }
+            escapeIdx++;
+          }
+          break;
+
+        case '}':
+          if (state == ClientParser.LexState.Normal) {
+            if (inEscape) {
+              escapeIdx--;
+
+              if (escapeIdx == 0) {
+                String str = sql.substring(lastEscapePart, idx + 1);
+                String escapedSeq = resolveEscapes(str, context);
+                sb.append(escapedSeq);
+                inEscape = false;
+                continue;
+              }
+            }
+          }
+          break;
+      }
+      if (!inEscape) sb.append(car);
+      lastChar = car;
+    }
+    if (inEscape) {
+      throw new SQLException(
+          "Invalid escape sequence , missing closing '}' character in '" + sql + "'");
+    }
+    return sb.toString();
+  }
+
+  private static String resolveEscapes(String escaped, Context context) throws SQLException {
+    int endIndex = escaped.length() - 1;
+    String escapedLower = escaped.toLowerCase(Locale.ROOT);
+    if (escaped.startsWith("{fn ")) {
+      String resolvedParams = replaceFunctionParameter(escaped.substring(4, endIndex), context);
+      return parse(resolvedParams, context);
+    } else if (escapedLower.startsWith("{oj ")) {
+      // Outer join
+      // the server supports "oj" in any case, even "oJ"
+      return parse(escaped.substring(4, endIndex), context);
+    } else if (escaped.startsWith("{d ")) {
+      // date literal
+      return escaped.substring(3, endIndex);
+    } else if (escaped.startsWith("{t ")) {
+      // time literal
+      return escaped.substring(3, endIndex);
+    } else if (escaped.startsWith("{ts ")) {
+      // timestamp literal
+      return escaped.substring(4, endIndex);
+    } else if (escaped.startsWith("{d'")) {
+      // date literal, no space
+      return escaped.substring(2, endIndex);
+    } else if (escaped.startsWith("{t'")) {
+      // time literal
+      return escaped.substring(2, endIndex);
+    } else if (escaped.startsWith("{ts'")) {
+      // timestamp literal
+      return escaped.substring(3, endIndex);
+    } else if (escaped.startsWith("{call ") || escaped.startsWith("{CALL ")) {
+      // We support uppercase "{CALL" only because Connector/J supports it. It is not in the JDBC
+      // spec.
+
+      return parse(escaped.substring(1, endIndex), context);
+    } else if (escaped.startsWith("{escape ")) {
+      return escaped.substring(1, endIndex);
+    } else if (escaped.startsWith("{?")) {
+      // likely ?=call(...)
+      return parse(escaped.substring(1, endIndex), context);
+    } else if (escaped.startsWith("{ ") || escaped.startsWith("{\n")) {
+      // Spaces and newlines before keyword, this is not JDBC compliant, however some it works in
+      // some drivers,
+      // so we support it, too
+      for (int i = 2; i < escaped.length(); i++) {
+        if (!Character.isWhitespace(escaped.charAt(i))) {
+          return resolveEscapes("{" + escaped.substring(i), context);
+        }
+      }
+    } else if (escaped.startsWith("{\r\n")) {
+      // Spaces and newlines before keyword, this is not JDBC compliant, however some it works in
+      // some drivers,
+      // so we support it, too
+      for (int i = 3; i < escaped.length(); i++) {
+        if (!Character.isWhitespace(escaped.charAt(i))) {
+          return resolveEscapes("{" + escaped.substring(i), context);
+        }
+      }
+    }
+    throw new SQLException("unknown escape sequence " + escaped);
+  }
+
+  /**
+   * Helper function to replace function parameters in escaped string. 3 functions are handles :
+   *
+   * <ul>
+   *   <li>CONVERT(value, type): replacing SQL_XXX types to convertible type, i.e SQL_BIGINT to
+   *       INTEGER
+   *   <li>TIMESTAMPDIFF(type, ...): replacing type SQL_TSI_XXX in type with XXX, i.e SQL_TSI_HOUR
+   *       with HOUR
+   *   <li>TIMESTAMPADD(type, ...): replacing type SQL_TSI_XXX in type with XXX, i.e SQL_TSI_HOUR
+   *       with HOUR
+   * </ul>
+   *
+   * <p>caution: this use MariaDB server conversion: 'SELECT CONVERT('2147483648', INTEGER)' will
+   * return a BIGINT. MySQL will throw a syntax error.
+   *
+   * @param functionString input string
+   * @return unescaped string
+   */
+  private static String replaceFunctionParameter(String functionString, Context context) {
+
+    char[] input = functionString.toCharArray();
+    StringBuilder sb = new StringBuilder();
+    int index;
+    for (index = 0; index < input.length; index++) {
+      if (input[index] != ' ') {
+        break;
+      }
+    }
+
+    for (;
+        index < input.length
+            && ((input[index] >= 'a' && input[index] <= 'z')
+                || (input[index] >= 'A' && input[index] <= 'Z'));
+        index++) {
+      sb.append(input[index]);
+    }
+    String func = sb.toString().toLowerCase(Locale.ROOT);
+    switch (func) {
+      case "convert":
+        // Handle "convert(value, type)" case
+        // extract last parameter, after the last ','
+        int lastCommaIndex = functionString.lastIndexOf(',');
+        int firstParentheses = functionString.indexOf('(');
+        String value = functionString.substring(firstParentheses + 1, lastCommaIndex);
+        for (index = lastCommaIndex + 1; index < input.length; index++) {
+          if (!Character.isWhitespace(input[index])) {
+            break;
+          }
+        }
+
+        int endParam = index + 1;
+        for (; endParam < input.length; endParam++) {
+          if ((input[endParam] < 'a' || input[endParam] > 'z')
+              && (input[endParam] < 'A' || input[endParam] > 'Z')
+              && input[endParam] != '_') {
+            break;
+          }
+        }
+        String typeParam = new String(input, index, endParam - index).toUpperCase(Locale.ROOT);
+        if (typeParam.startsWith("SQL_")) {
+          typeParam = typeParam.substring(4);
+        }
+
+        switch (typeParam) {
+          case "BOOLEAN":
+            return "1=" + value;
+
+          case "BIGINT":
+          case "SMALLINT":
+          case "TINYINT":
+            typeParam = "SIGNED INTEGER";
+            break;
+
+          case "BIT":
+            typeParam = "UNSIGNED INTEGER";
+            break;
+
+          case "BLOB":
+          case "VARBINARY":
+          case "LONGVARBINARY":
+          case "ROWID":
+            typeParam = "BINARY";
+            break;
+
+          case "NCHAR":
+          case "CLOB":
+          case "NCLOB":
+          case "DATALINK":
+          case "VARCHAR":
+          case "NVARCHAR":
+          case "LONGVARCHAR":
+          case "LONGNVARCHAR":
+          case "SQLXML":
+          case "LONGNCHAR":
+            typeParam = "CHAR";
+            break;
+
+          case "DOUBLE":
+          case "FLOAT":
+            if (context.getVersion().isMariaDBServer()
+                || context.getVersion().versionGreaterOrEqual(8, 0, 17)) {
+              typeParam = "DOUBLE";
+              break;
+            }
+            return "0.0+" + value;
+
+          case "REAL":
+          case "NUMERIC":
+            typeParam = "DECIMAL";
+            break;
+
+          case "TIMESTAMP":
+            typeParam = "DATETIME";
+            break;
+
+          default:
+            break;
+        }
+        return new String(input, 0, index)
+            + typeParam
+            + new String(input, endParam, input.length - endParam);
+
+      case "timestampdiff":
+      case "timestampadd":
+        // Skip to first parameter
+        for (; index < input.length; index++) {
+          if (!Character.isWhitespace(input[index]) && input[index] != '(') {
+            break;
+          }
+        }
+        if (index < input.length - 8) {
+          String paramPrefix = new String(input, index, 8);
+          if ("SQL_TSI_".equals(paramPrefix)) {
+            return new String(input, 0, index)
+                + new String(input, index + 8, input.length - (index + 8));
+          }
+        }
+        return functionString;
+
+      default:
+        return functionString;
+    }
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/ParameterList.java b/src/main/java/org/mariadb/jdbc/util/ParameterList.java
new file mode 100644
index 0000000..6e43e07
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/ParameterList.java
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util;
+
+import java.util.Arrays;
+import org.mariadb.jdbc.client.util.Parameter;
+import org.mariadb.jdbc.client.util.Parameters;
+
+public class ParameterList implements Parameters, Cloneable {
+  Parameter[] elementData;
+  int length;
+
+  public ParameterList(int defaultSize) {
+    elementData = new Parameter[defaultSize];
+    length = 0;
+  }
+
+  public ParameterList() {
+    elementData = new Parameter[10];
+    length = 0;
+  }
+
+  public Parameter get(int index) {
+    if (index >= length)
+      throw new ArrayIndexOutOfBoundsException("wrong index " + index + " length:" + length);
+    return elementData[index];
+  }
+
+  public boolean containsKey(int index) {
+    if (index >= 0 && length > index) {
+      return elementData[index] != null;
+    }
+    return false;
+  }
+
+  public void set(int index, Parameter element) {
+    if (elementData.length <= index) grow(index + 1);
+    elementData[index] = element;
+    if (index >= length) length = index + 1;
+  }
+
+  public int size() {
+    return length;
+  }
+
+  private void grow(int minLength) {
+    int currLength = elementData.length;
+    int newLength = Math.max(currLength + (currLength >> 1), minLength);
+    elementData = Arrays.copyOf(elementData, newLength);
+  }
+
+  @Override
+  public ParameterList clone() {
+    ParameterList param = new ParameterList(length);
+    if (length > 0) System.arraycopy(elementData, 0, param.elementData, 0, length);
+    param.length = length;
+    return param;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/PrepareResult.java b/src/main/java/org/mariadb/jdbc/util/PrepareResult.java
new file mode 100644
index 0000000..4c071ec
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/PrepareResult.java
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util;
+
+public interface PrepareResult {
+
+  String getSql();
+
+  int getParamCount();
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/Security.java b/src/main/java/org/mariadb/jdbc/util/Security.java
new file mode 100644
index 0000000..9595aa0
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/Security.java
@@ -0,0 +1,123 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util;
+
+public final class Security {
+
+  /**
+   * Parse the option "sessionVariable" to ensure having no injection. semi-column not in string
+   * will be replaced by comma.
+   *
+   * @param sessionVariable option value
+   * @return parsed String
+   */
+  public static String parseSessionVariables(String sessionVariable) {
+    StringBuilder out = new StringBuilder();
+    StringBuilder sb = new StringBuilder();
+    Parse state = Parse.Normal;
+    boolean iskey = true;
+    boolean singleQuotes = true;
+    boolean first = true;
+    String key = null;
+
+    char[] chars = sessionVariable.toCharArray();
+
+    for (char car : chars) {
+
+      if (state == Parse.Escape) {
+        sb.append(car);
+        state = Parse.String;
+        continue;
+      }
+
+      switch (car) {
+        case '"':
+          if (state == Parse.Normal) {
+            state = Parse.String;
+            singleQuotes = false;
+          } else if (!singleQuotes) {
+            state = Parse.Normal;
+          }
+          break;
+
+        case '\'':
+          if (state == Parse.Normal) {
+            state = Parse.String;
+            singleQuotes = true;
+          } else if (singleQuotes) {
+            state = Parse.Normal;
+          }
+          break;
+
+        case '\\':
+          if (state == Parse.String) {
+            state = Parse.Escape;
+          }
+          break;
+
+        case ';':
+        case ',':
+          if (state == Parse.Normal) {
+            if (!iskey) {
+              if (!first) {
+                out.append(",");
+              }
+              out.append(key);
+              out.append(sb);
+              first = false;
+            } else {
+              key = sb.toString().trim();
+              if (!key.isEmpty()) {
+                if (!first) {
+                  out.append(",");
+                }
+                out.append(key);
+                first = false;
+              }
+            }
+            iskey = true;
+            key = null;
+            sb = new StringBuilder();
+            continue;
+          }
+          break;
+
+        case '=':
+          if (state == Parse.Normal && iskey) {
+            key = sb.toString().trim();
+            iskey = false;
+            sb = new StringBuilder();
+          }
+          break;
+
+        default:
+          // nothing
+      }
+
+      sb.append(car);
+    }
+
+    if (!iskey) {
+      if (!first) {
+        out.append(",");
+      }
+      out.append(key);
+      out.append(sb);
+    } else {
+      String tmpkey = sb.toString().trim();
+      if (!tmpkey.isEmpty() && !first) {
+        out.append(",");
+      }
+      out.append(tmpkey);
+    }
+    return out.toString();
+  }
+
+  private enum Parse {
+    Normal,
+    String, /* inside string */
+    Escape /* found backslash */
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/StringUtils.java b/src/main/java/org/mariadb/jdbc/util/StringUtils.java
new file mode 100644
index 0000000..d420568
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/StringUtils.java
@@ -0,0 +1,17 @@
+package org.mariadb.jdbc.util;
+
+public final class StringUtils {
+  private static final char[] hexArray = "0123456789ABCDEF".toCharArray();
+
+  public static String byteArrayToHexString(final byte[] bytes) {
+    return (bytes != null) ? getHex(bytes) : "";
+  }
+
+  private static String getHex(final byte[] raw) {
+    final StringBuilder hex = new StringBuilder(2 * raw.length);
+    for (final byte b : raw) {
+      hex.append(hexArray[(b & 0xF0) >> 4]).append(hexArray[(b & 0x0F)]);
+    }
+    return hex.toString();
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/Version.java b/src/main/java/org/mariadb/jdbc/util/Version.java
new file mode 100644
index 0000000..38f7700
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/Version.java
@@ -0,0 +1,110 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util;
+
+public class Version {
+  private final String version;
+  private final int majorVersion;
+  private final int minorVersion;
+  private final int patchVersion;
+  private final String qualifier;
+
+  public Version(String versionString) {
+    this.version = versionString;
+    int major = 0;
+    int minor = 0;
+    int patch = 0;
+    String qualif = "";
+
+    int length = version.length();
+    char car;
+    int offset = 0;
+    int type = 0;
+    int val = 0;
+    for (; offset < length; offset++) {
+      car = version.charAt(offset);
+      if (car < '0' || car > '9') {
+        switch (type) {
+          case 0:
+            major = val;
+            break;
+          case 1:
+            minor = val;
+            break;
+          case 2:
+            patch = val;
+            qualif = version.substring(offset);
+            offset = length;
+            break;
+          default:
+            break;
+        }
+        type++;
+        val = 0;
+      } else {
+        val = val * 10 + car - 48;
+      }
+    }
+
+    if (type == 2) {
+      patch = val;
+    }
+    this.majorVersion = major;
+    this.minorVersion = minor;
+    this.patchVersion = patch;
+    this.qualifier = qualif;
+  }
+
+  public String getVersion() {
+    return version;
+  }
+
+  public int getMajorVersion() {
+    return majorVersion;
+  }
+
+  public int getMinorVersion() {
+    return minorVersion;
+  }
+
+  public int getPatchVersion() {
+    return patchVersion;
+  }
+
+  public String getQualifier() {
+    return qualifier;
+  }
+
+  /**
+   * 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;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/VersionFactory.java b/src/main/java/org/mariadb/jdbc/util/VersionFactory.java
new file mode 100644
index 0000000..496ef09
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/VersionFactory.java
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+public final class VersionFactory {
+  private static volatile Version instance = null;
+
+  // use getShape method to get object of type shape
+  public static Version getInstance() {
+    if (instance == null) {
+      synchronized (VersionFactory.class) {
+        if (instance == null) {
+          String tmpVersion = "5.5.0";
+          try (InputStream inputStream =
+              Version.class.getClassLoader().getResourceAsStream("mariadb.properties")) {
+            if (inputStream == null) {
+              System.out.println("property file 'mariadb.properties' not found in the classpath");
+            } else {
+              Properties prop = new Properties();
+              prop.load(inputStream);
+              tmpVersion = prop.getProperty("version");
+            }
+          } catch (IOException e) {
+            e.printStackTrace();
+          }
+          instance = new Version(tmpVersion);
+        }
+      }
+    }
+    return instance;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/constants/Capabilities.java b/src/main/java/org/mariadb/jdbc/util/constants/Capabilities.java
new file mode 100644
index 0000000..114bfa2
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/constants/Capabilities.java
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util.constants;
+
+/** Client / Server capabilities */
+@SuppressWarnings("unused")
+public final class Capabilities {
+
+  /** Is client mysql */
+  public static final int CLIENT_MYSQL = 1;
+
+  /** use Found rowd instead of affected rows */
+  public static final int FOUND_ROWS = 2;
+
+  /** Get all column flags */
+  public static final int LONG_FLAG = 4;
+
+  /** One can specify db on connect */
+  public static final int CONNECT_WITH_DB = 8;
+
+  /** Don't allow database.table.column */
+  public static final int NO_SCHEMA = 16;
+
+  /** use compression protocol */
+  public static final int COMPRESS = 32;
+
+  /** Odbc client */
+  public static final int ODBC = 64;
+
+  /** Can use LOAD DATA LOCAL */
+  public static final int LOCAL_FILES = 128;
+
+  /** Ignore spaces before '(' */
+  public static final int IGNORE_SPACE = 256;
+
+  /** Use 4.1 protocol */
+  public static final int CLIENT_PROTOCOL_41 = 512;
+
+  /** Is interactive client */
+  public static final int CLIENT_INTERACTIVE = 1024;
+
+  /** Switch to SSL after handshake */
+  public static final int SSL = 2048;
+
+  /** IGNORE sigpipes */
+  public static final int IGNORE_SIGPIPE = 4096;
+
+  /** transactions */
+  public static final int TRANSACTIONS = 8192;
+
+  /** reserved - not used */
+  public static final int RESERVED = 16384;
+
+  /** New 4.1 authentication */
+  public static final int SECURE_CONNECTION = 32768;
+
+  /** Enable/disable multi-stmt support */
+  public static final int MULTI_STATEMENTS = 1 << 16;
+
+  /** Enable/disable multi-results */
+  public static final int MULTI_RESULTS = 1 << 17;
+
+  /** Enable/disable multi-results for PrepareStatement */
+  public static final int PS_MULTI_RESULTS = 1 << 18;
+
+  /** Client supports plugin authentication */
+  public static final int PLUGIN_AUTH = 1 << 19;
+
+  /** Client send connection attributes */
+  public static final int CONNECT_ATTRS = 1 << 20;
+
+  /** authentication data length is a length auth integer */
+  public static final int PLUGIN_AUTH_LENENC_CLIENT_DATA = 1 << 21;
+
+  /** server send session tracking info */
+  public static final int CLIENT_SESSION_TRACK = 1 << 23;
+
+  /** EOF packet deprecated */
+  public static final int CLIENT_DEPRECATE_EOF = 1 << 24;
+
+  /** Client support progress indicator (before 10.2) */
+  public static final int PROGRESS_OLD = 1 << 29;
+
+  /* MariaDB specific capabilities */
+
+  /** Client progression */
+  public static final long PROGRESS = 1L << 32;
+
+  /** not used anymore - reserved */
+  public static final long MARIADB_RESERVED = 1L << 33;
+
+  /** permit COM_STMT_BULK commands */
+  public static final long STMT_BULK_OPERATIONS = 1L << 34;
+
+  /** metadata extended information */
+  public static final long EXTENDED_TYPE_INFO = 1L << 35;
+
+  /** permit metadata caching */
+  public static final long CACHE_METADATA = 1L << 36;
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/constants/ColumnFlags.java b/src/main/java/org/mariadb/jdbc/util/constants/ColumnFlags.java
new file mode 100644
index 0000000..7b7fad2
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/constants/ColumnFlags.java
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util.constants;
+
+/** Column flag see https://mariadb.com/kb/en/result-set-packets/#field-details-flag */
+@SuppressWarnings("unused")
+public final class ColumnFlags {
+
+  /** must a column have non-null value only */
+  public static final short NOT_NULL = 1;
+
+  /** Is column a primary key */
+  public static final short PRIMARY_KEY = 2;
+
+  /** Is this column a unique key */
+  public static final short UNIQUE_KEY = 4;
+
+  /** Is this column part of a multiple column key */
+  public static final short MULTIPLE_KEY = 8;
+
+  /** Does this column contain blob */
+  public static final short BLOB = 16;
+
+  /** Is column number value unsigned */
+  public static final short UNSIGNED = 32;
+
+  /** Must number value be filled with Zero */
+  public static final short ZEROFILL = 64;
+
+  /** Is binary value */
+  public static final short BINARY_COLLATION = 128;
+
+  /** Is column of type enum */
+  public static final short ENUM = 256;
+
+  /** Does column auto-increment */
+  public static final short AUTO_INCREMENT = 512;
+
+  /** Is column of type Timestamp */
+  public static final short TIMESTAMP = 1024;
+
+  /** Is column type set */
+  public static final short SET = 2048;
+
+  /** Does column have no default value */
+  public static final short NO_DEFAULT_VALUE_FLAG = 4096;
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/constants/ConnectionState.java b/src/main/java/org/mariadb/jdbc/util/constants/ConnectionState.java
new file mode 100644
index 0000000..3544473
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/constants/ConnectionState.java
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util.constants;
+
+/** Connection state change, to ensure what is needed to reset connection to a proper state */
+public final class ConnectionState {
+  /** flag indicating that network timeout has been changed */
+  public static final int STATE_NETWORK_TIMEOUT = 1;
+
+  /** flag indicating that default database has been changed */
+  public static final int STATE_DATABASE = 2;
+
+  /** flag indicating that connection read only has been changed */
+  public static final int STATE_READ_ONLY = 4;
+
+  /** flag indicating that autocommit has been changed */
+  public static final int STATE_AUTOCOMMIT = 8;
+
+  /** flag indicating that transaction isolation has been changed */
+  public static final int STATE_TRANSACTION_ISOLATION = 16;
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/constants/ServerStatus.java b/src/main/java/org/mariadb/jdbc/util/constants/ServerStatus.java
new file mode 100644
index 0000000..67976e8
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/constants/ServerStatus.java
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util.constants;
+
+/** Server status flags */
+@SuppressWarnings("unused")
+public class ServerStatus {
+  /** is in transaction */
+  public static final short IN_TRANSACTION = 1;
+  /** autocommit */
+  public static final short AUTOCOMMIT = 2;
+  /** more result exists (packet follows) */
+  public static final short MORE_RESULTS_EXISTS = 8;
+  /** no good index was used */
+  public static final short QUERY_NO_GOOD_INDEX_USED = 16;
+  /** no index was used */
+  public static final short QUERY_NO_INDEX_USED = 32;
+  /** cursor exists */
+  public static final short CURSOR_EXISTS = 64;
+  /** last row sent */
+  public static final short LAST_ROW_SENT = 128;
+  /** database dropped */
+  public static final short DB_DROPPED = 256;
+  /** escape type */
+  public static final short NO_BACKSLASH_ESCAPES = 512;
+  /** metadata changed */
+  public static final short METADATA_CHANGED = 1024;
+  /** query was slow */
+  public static final short QUERY_WAS_SLOW = 2048;
+  /** resultset contains output parameters */
+  public static final short PS_OUT_PARAMETERS = 4096;
+  /** session state change (OK_Packet contains additional data) */
+  public static final short SERVER_SESSION_STATE_CHANGED = 1 << 14;
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/constants/StateChange.java b/src/main/java/org/mariadb/jdbc/util/constants/StateChange.java
new file mode 100644
index 0000000..613b750
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/constants/StateChange.java
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util.constants;
+
+/** OK_PACKET state change flags */
+@SuppressWarnings("unused")
+public final class StateChange {
+  /** system variable change */
+  public static final short SESSION_TRACK_SYSTEM_VARIABLES = 0;
+  /** schema change */
+  public static final short SESSION_TRACK_SCHEMA = 1;
+  /** state change */
+  public static final short SESSION_TRACK_STATE_CHANGE = 2;
+  /** GTID change */
+  public static final short SESSION_TRACK_GTIDS = 3;
+  /** transaction characteristics change */
+  public static final short SESSION_TRACK_TRANSACTION_CHARACTERISTICS = 4;
+  /** transaction state change */
+  public static final short SESSION_TRACK_TRANSACTION_STATE = 5;
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/log/ConsoleLogger.java b/src/main/java/org/mariadb/jdbc/util/log/ConsoleLogger.java
new file mode 100644
index 0000000..75310d0
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/log/ConsoleLogger.java
@@ -0,0 +1,182 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util.log;
+
+import java.io.PrintStream;
+import java.util.regex.Matcher;
+
+/** Logger that will log to console */
+@SuppressWarnings("ALL")
+public class ConsoleLogger implements Logger {
+
+  private final String name;
+  private final PrintStream err;
+  private final PrintStream log;
+  private final boolean logDebugLvl;
+
+  /**
+   * Constructor. All logs will be send to console.
+   *
+   * @param name name to log
+   * @param log log stream
+   * @param err error stream
+   * @param logDebugLvl log level
+   */
+  public ConsoleLogger(String name, PrintStream log, PrintStream err, boolean logDebugLvl) {
+    this.name = name;
+    this.log = log;
+    this.err = err;
+    this.logDebugLvl = logDebugLvl;
+  }
+
+  ConsoleLogger(String name, boolean logDebugLvl) {
+    this(name, System.out, System.err, logDebugLvl);
+  }
+
+  @Override
+  public String getName() {
+    return this.name;
+  }
+
+  final String format(String from, Object... arguments) {
+    if (from != null) {
+      String computed = from;
+      for (Object argument : arguments) {
+        computed =
+            computed.replaceFirst("\\{\\}", Matcher.quoteReplacement(String.valueOf(argument)));
+      }
+      return computed;
+    }
+    return null;
+  }
+
+  @Override
+  public boolean isTraceEnabled() {
+    return logDebugLvl;
+  }
+
+  @Override
+  public synchronized void trace(String msg) {
+    if (!logDebugLvl) {
+      return;
+    }
+    this.log.format("[TRACE] (%s) %s\n", Thread.currentThread().getName(), msg);
+  }
+
+  @Override
+  public synchronized void trace(String format, Object... arguments) {
+    if (!logDebugLvl) {
+      return;
+    }
+    this.log.format(
+        "[TRACE] (%s) %s\n", Thread.currentThread().getName(), format(format, arguments));
+  }
+
+  @Override
+  public synchronized void trace(String msg, Throwable t) {
+    if (!logDebugLvl) {
+      return;
+    }
+    this.log.format("[TRACE] (%s) %s - %s\n", Thread.currentThread().getName(), msg, t);
+    t.printStackTrace(this.log);
+  }
+
+  @Override
+  public boolean isDebugEnabled() {
+    return logDebugLvl;
+  }
+
+  @Override
+  public synchronized void debug(String msg) {
+    if (!logDebugLvl) {
+      return;
+    }
+    this.log.format("[DEBUG] (%s) %s\n", Thread.currentThread().getName(), msg);
+  }
+
+  @Override
+  public synchronized void debug(String format, Object... arguments) {
+    if (!logDebugLvl) {
+      return;
+    }
+    this.log.format(
+        "[DEBUG] (%s) %s\n", Thread.currentThread().getName(), format(format, arguments));
+  }
+
+  @Override
+  public synchronized void debug(String msg, Throwable t) {
+    if (!logDebugLvl) {
+      return;
+    }
+    this.log.format("[DEBUG] (%s) %s - %s\n", Thread.currentThread().getName(), msg, t);
+    t.printStackTrace(this.log);
+  }
+
+  @Override
+  public boolean isInfoEnabled() {
+    return true;
+  }
+
+  @Override
+  public synchronized void info(String msg) {
+    this.log.format("[ INFO] (%s) %s\n", Thread.currentThread().getName(), msg);
+  }
+
+  @Override
+  public synchronized void info(String format, Object... arguments) {
+    this.log.format(
+        "[ INFO] (%s) %s\n", Thread.currentThread().getName(), format(format, arguments));
+  }
+
+  @Override
+  public synchronized void info(String msg, Throwable t) {
+    this.log.format("[ INFO] (%s) %s - %s\n", Thread.currentThread().getName(), msg, t);
+    t.printStackTrace(this.log);
+  }
+
+  @Override
+  public boolean isWarnEnabled() {
+    return true;
+  }
+
+  @Override
+  public synchronized void warn(String msg) {
+    this.err.format("[ WARN] (%s) %s\n", Thread.currentThread().getName(), msg);
+  }
+
+  @Override
+  public synchronized void warn(String format, Object... arguments) {
+    this.err.format(
+        "[ WARN] (%s) %s\n", Thread.currentThread().getName(), format(format, arguments));
+  }
+
+  @Override
+  public synchronized void warn(String msg, Throwable t) {
+    this.err.format("[ WARN] (%s) %s - %s\n", Thread.currentThread().getName(), msg, t);
+    t.printStackTrace(this.err);
+  }
+
+  @Override
+  public boolean isErrorEnabled() {
+    return true;
+  }
+
+  @Override
+  public synchronized void error(String msg) {
+    this.err.format("[ERROR] (%s) %s\n", Thread.currentThread().getName(), msg);
+  }
+
+  @Override
+  public synchronized void error(String format, Object... arguments) {
+    this.err.format(
+        "[ERROR] (%s) %s\n", Thread.currentThread().getName(), format(format, arguments));
+  }
+
+  @Override
+  public synchronized void error(String msg, Throwable t) {
+    this.err.format("[ERROR] (%s) %s - %s\n", Thread.currentThread().getName(), msg, t);
+    t.printStackTrace(this.err);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/log/JdkLogger.java b/src/main/java/org/mariadb/jdbc/util/log/JdkLogger.java
new file mode 100644
index 0000000..d06e42e
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/log/JdkLogger.java
@@ -0,0 +1,141 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util.log;
+
+import java.util.logging.Level;
+import java.util.regex.Matcher;
+
+/** Logger using standard java logging */
+public class JdkLogger implements Logger {
+
+  /** logger implementation */
+  private final java.util.logging.Logger logger;
+
+  /**
+   * Constructor
+   *
+   * @param logger java logger
+   */
+  public JdkLogger(java.util.logging.Logger logger) {
+    this.logger = logger;
+  }
+
+  @Override
+  public String getName() {
+    return logger.getName();
+  }
+
+  @Override
+  public boolean isTraceEnabled() {
+    return logger.isLoggable(Level.FINEST);
+  }
+
+  @Override
+  public void trace(String msg) {
+    logger.log(Level.FINEST, msg);
+  }
+
+  @Override
+  public void trace(String format, Object... arguments) {
+    logger.log(Level.FINEST, format(format, arguments));
+  }
+
+  @Override
+  public void trace(String msg, Throwable t) {
+    logger.log(Level.FINEST, msg, t);
+  }
+
+  @Override
+  public boolean isDebugEnabled() {
+    return logger.isLoggable(Level.FINE);
+  }
+
+  @Override
+  public void debug(String msg) {
+    logger.log(Level.FINE, msg);
+  }
+
+  @Override
+  public void debug(String format, Object... arguments) {
+    logger.log(Level.FINE, format(format, arguments));
+  }
+
+  @Override
+  public void debug(String msg, Throwable t) {
+    logger.log(Level.FINE, msg, t);
+  }
+
+  @Override
+  public boolean isInfoEnabled() {
+    return logger.isLoggable(Level.INFO);
+  }
+
+  @Override
+  public void info(String msg) {
+    logger.log(Level.INFO, msg);
+  }
+
+  @Override
+  public void info(String format, Object... arguments) {
+    logger.log(Level.INFO, format(format, arguments));
+  }
+
+  @Override
+  public void info(String msg, Throwable t) {
+    logger.log(Level.INFO, msg, t);
+  }
+
+  @Override
+  public boolean isWarnEnabled() {
+    return logger.isLoggable(Level.WARNING);
+  }
+
+  @Override
+  public void warn(String msg) {
+    logger.log(Level.WARNING, msg);
+  }
+
+  @Override
+  public void warn(String format, Object... arguments) {
+    logger.log(Level.WARNING, format(format, arguments));
+  }
+
+  @Override
+  public void warn(String msg, Throwable t) {
+    logger.log(Level.WARNING, msg, t);
+  }
+
+  @Override
+  public boolean isErrorEnabled() {
+    return logger.isLoggable(Level.SEVERE);
+  }
+
+  @Override
+  public void error(String msg) {
+    logger.log(Level.SEVERE, msg);
+  }
+
+  @Override
+  public void error(String format, Object... arguments) {
+    logger.log(Level.SEVERE, format(format, arguments));
+  }
+
+  @Override
+  public void error(String msg, Throwable t) {
+    logger.log(Level.SEVERE, msg, t);
+  }
+
+  final String format(String from, Object... arguments) {
+    if (from != null) {
+      String computed = from;
+      for (Object argument : arguments) {
+        computed =
+            computed.replaceFirst("\\{}", Matcher.quoteReplacement(String.valueOf(argument)));
+      }
+      return computed;
+    }
+    return null;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/log/Logger.java b/src/main/java/org/mariadb/jdbc/util/log/Logger.java
new file mode 100644
index 0000000..baa47f1
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/log/Logger.java
@@ -0,0 +1,165 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util.log;
+
+/** Logger interface */
+public interface Logger {
+  /**
+   * Logger name
+   *
+   * @return logger name
+   */
+  String getName();
+
+  /**
+   * Must TRACE level be logged
+   *
+   * @return if TRACE level be logger
+   */
+  boolean isTraceEnabled();
+
+  /**
+   * Log a message at the TRACE level.
+   *
+   * @param msg the message string to be logged
+   */
+  void trace(String msg);
+
+  /**
+   * Log a message at the TRACE level.
+   *
+   * @param format the format string
+   * @param arguments arguments
+   */
+  void trace(String format, Object... arguments);
+
+  /**
+   * Log a message with an error at the TRACE level.
+   *
+   * @param msg message
+   * @param t error
+   */
+  void trace(String msg, Throwable t);
+
+  /**
+   * must DEBUG level message be logged
+   *
+   * @return are DEBUG message to be logged
+   */
+  boolean isDebugEnabled();
+
+  /**
+   * Log a message at the DEBUG level.
+   *
+   * @param msg the message string to be logged
+   */
+  void debug(String msg);
+
+  /**
+   * Log a message at the DEBUG level.
+   *
+   * @param format the format string
+   * @param arguments arguments
+   */
+  void debug(String format, Object... arguments);
+
+  /**
+   * Log a message with an error at the DEBUG level.
+   *
+   * @param msg message
+   * @param t error
+   */
+  void debug(String msg, Throwable t);
+
+  /**
+   * Must INFO level be logged
+   *
+   * @return if INFO level be logger
+   */
+  boolean isInfoEnabled();
+
+  /**
+   * Log a message at the INFO level.
+   *
+   * @param msg the message string to be logged
+   */
+  void info(String msg);
+
+  /**
+   * Log a message at the INFO level.
+   *
+   * @param format the format string
+   * @param arguments arguments
+   */
+  void info(String format, Object... arguments);
+
+  /**
+   * Log a message with an error at the INFO level.
+   *
+   * @param msg message
+   * @param t error
+   */
+  void info(String msg, Throwable t);
+
+  /**
+   * Must WARN level be logged
+   *
+   * @return if WARN level be logger
+   */
+  boolean isWarnEnabled();
+
+  /**
+   * Log a message at the WARN level.
+   *
+   * @param msg the message string to be logged
+   */
+  void warn(String msg);
+
+  /**
+   * Log a message at the WARNING level.
+   *
+   * @param format the format string
+   * @param arguments arguments
+   */
+  void warn(String format, Object... arguments);
+
+  /**
+   * Log a message with an error at the WARNING level.
+   *
+   * @param msg message
+   * @param t error
+   */
+  void warn(String msg, Throwable t);
+
+  /**
+   * Must ERROR level be logged
+   *
+   * @return if ERROR level be logger
+   */
+  boolean isErrorEnabled();
+
+  /**
+   * Log a message at the ERROR level.
+   *
+   * @param msg the message string to be logged
+   */
+  void error(String msg);
+
+  /**
+   * Log a message at the ERROR level.
+   *
+   * @param format the format string
+   * @param arguments arguments
+   */
+  void error(String format, Object... arguments);
+
+  /**
+   * Log a message with an error at the ERROR level.
+   *
+   * @param msg message
+   * @param t error
+   */
+  void error(String msg, Throwable t);
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/log/LoggerHelper.java b/src/main/java/org/mariadb/jdbc/util/log/LoggerHelper.java
new file mode 100644
index 0000000..9fce199
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/log/LoggerHelper.java
@@ -0,0 +1,137 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util.log;
+
+/** Logger helper to display network exchange */
+public final class LoggerHelper {
+
+  private static final char[] hexArray = "0123456789ABCDEF".toCharArray();
+
+  /**
+   * Write bytes/hexadecimal value of a byte array to a StringBuilder.
+   *
+   * <p>String output example :
+   *
+   * <pre>{@code
+   * +--------------------------------------------------+
+   * |  0  1  2  3  4  5  6  7   8  9  a  b  c  d  e  f |
+   * +--------------------------------------------------+------------------+
+   * | 5F 00 00 00 03 73 65 74  20 61 75 74 6F 63 6F 6D | _....set autocom |
+   * | 6D 69 74 3D 31 2C 20 73  65 73 73 69 6F 6E 5F 74 | mit=1, session_t |
+   * | 72 61 63 6B 5F 73 63 68  65 6D 61 3D 31 2C 20 73 | rack_schema=1, s |
+   * | 71 6C 5F 6D 6F 64 65 20  3D 20 63 6F 6E 63 61 74 | ql_mode = concat |
+   * | 28 40 40 73 71 6C 5F 6D  6F 64 65 2C 27 2C 53 54 | (@@sql_mode,',ST |
+   * | 52 49 43 54 5F 54 52 41  4E 53 5F 54 41 42 4C 45 | RICT_TRANS_TABLE |
+   * | 53 27 29                                         | S')              |
+   * +--------------------------------------------------+------------------+
+   * }</pre>
+   *
+   * @param bytes byte array
+   * @param offset offset
+   * @param dataLength byte length to write
+   * @return formated hexa
+   */
+  public static String hex(byte[] bytes, int offset, int dataLength) {
+    return hex(bytes, offset, dataLength, Integer.MAX_VALUE);
+  }
+
+  /**
+   * Transform bytes into readable string format
+   *
+   * @param bytes bytes
+   * @param offset offset
+   * @param dataLength data length
+   * @param trunkLength truncation limit
+   * @return readable string format
+   */
+  public static String hex(byte[] bytes, int offset, int dataLength, int trunkLength) {
+
+    if (bytes == null || bytes.length == 0) {
+      return "";
+    }
+
+    char[] hexaValue = new char[16];
+    hexaValue[8] = ' ';
+
+    int pos = offset;
+    int line = 1;
+    int posHexa = 0;
+    int logLength = Math.min(dataLength, trunkLength);
+    StringBuilder sb = new StringBuilder(logLength * 3);
+    sb.append(
+        "       +--------------------------------------------------+\n"
+            + "       |  0  1  2  3  4  5  6  7   8  9  a  b  c  d  e  f |\n"
+            + "+------+--------------------------------------------------+------------------+\n"
+            + "|000000| ");
+
+    while (pos < logLength + offset) {
+      int byteValue = bytes[pos] & 0xFF;
+      sb.append(hexArray[byteValue >>> 4]).append(hexArray[byteValue & 0x0F]).append(" ");
+
+      hexaValue[posHexa++] = (byteValue > 31 && byteValue < 127) ? (char) byteValue : '.';
+
+      if (posHexa == 8) {
+        sb.append(" ");
+      }
+      if (posHexa == 16) {
+        sb.append("| ").append(hexaValue).append(" |\n");
+        if (pos + 1 != logLength + offset)
+          sb.append("|").append(mediumIntTohexa(line++)).append("| ");
+        posHexa = 0;
+      }
+      pos++;
+    }
+
+    int remaining = posHexa;
+    if (remaining > 0) {
+      if (remaining < 8) {
+        for (; remaining < 8; remaining++) {
+          sb.append("   ");
+        }
+        sb.append(" ");
+      }
+
+      for (; remaining < 16; remaining++) {
+        sb.append("   ");
+      }
+
+      for (; posHexa < 16; posHexa++) {
+        hexaValue[posHexa] = ' ';
+      }
+
+      sb.append("| ").append(hexaValue).append(" |\n");
+    }
+    if (dataLength > trunkLength) {
+      sb.append("+------+-------------------truncated----------------------+------------------+\n");
+    } else {
+      sb.append("+------+--------------------------------------------------+------------------+\n");
+    }
+    return sb.toString();
+  }
+
+  private static String mediumIntTohexa(int value) {
+    String st = Integer.toHexString(value * 16);
+    while (st.length() < 6) st = "0" + st;
+    return st;
+  }
+
+  /**
+   * return a string containing hexa displayable value of arrays
+   *
+   * @param header header array
+   * @param bytes data content
+   * @param offset data offset
+   * @param dataLength data length
+   * @param trunkLength data limit
+   * @return displayable value of arrays
+   */
+  public static String hex(
+      byte[] header, byte[] bytes, int offset, int dataLength, int trunkLength) {
+    byte[] complete = new byte[dataLength + header.length];
+    System.arraycopy(header, 0, complete, 0, header.length);
+    System.arraycopy(bytes, offset, complete, header.length, dataLength);
+    return hex(complete, 0, dataLength + header.length, trunkLength);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/log/Loggers.java b/src/main/java/org/mariadb/jdbc/util/log/Loggers.java
new file mode 100644
index 0000000..c1ad699
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/log/Loggers.java
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util.log;
+
+import java.util.HashMap;
+
+/** Loggers factory */
+public final class Loggers {
+
+  /** defined if using JDK if sfl4j is not present. possible values: JDK/CONSOLE */
+  public static final String FALLBACK_PROPERTY = "mariadb.logging.fallback";
+
+  /** set if console must log debug level */
+  public static final String CONSOLE_DEBUG_PROPERTY = "mariadb.logging.fallback.console.debug";
+
+  /** !! testing only !! permit to disable SLF4J implementation even if SLF4j is present */
+  public static final String TEST_ENABLE_SLF4J = "mariadb.logging.slf4j.enable";
+
+  /** disable logging */
+  public static final String NO_LOGGER_PROPERTY = "mariadb.logging.disable";
+
+  /** factory */
+  private static LoggerFactory LOGGER_FACTORY;
+
+  static {
+    init();
+  }
+
+  /**
+   * Return default logger implementation
+   *
+   * @param name logger name
+   * @return logger implementation
+   */
+  public static Logger getLogger(String name) {
+    return LOGGER_FACTORY.getLogger(name);
+  }
+
+  /**
+   * Return default logger implementation
+   *
+   * @param cls class
+   * @return logger implementation
+   */
+  public static Logger getLogger(Class<?> cls) {
+    return LOGGER_FACTORY.getLogger(cls.getName());
+  }
+
+  private interface LoggerFactory {
+    Logger getLogger(String name);
+  }
+
+  private static class NoLoggerFactory implements LoggerFactory {
+    @Override
+    public Logger getLogger(String name) {
+      return new NoLogger();
+    }
+  }
+
+  private static class Slf4JLoggerFactory implements LoggerFactory {
+    @Override
+    public Logger getLogger(String name) {
+      return new Slf4JLogger(org.slf4j.LoggerFactory.getLogger(name));
+    }
+  }
+
+  /** JDK wrapper */
+  private static class JdkLoggerFactory implements LoggerFactory {
+    @Override
+    public Logger getLogger(String name) {
+      return new JdkLogger(java.util.logging.Logger.getLogger(name));
+    }
+  }
+
+  private static final class ConsoleLoggerFactory implements LoggerFactory {
+
+    private static final HashMap<String, Logger> consoleLoggers = new HashMap<>();
+
+    @Override
+    public Logger getLogger(String name) {
+      return consoleLoggers.computeIfAbsent(
+          name, n -> new ConsoleLogger(n, System.getProperty(CONSOLE_DEBUG_PROPERTY) != null));
+    }
+  }
+
+  /** Initialize factory */
+  public static void init() {
+    String name = LoggerFactory.class.getName();
+    LoggerFactory loggerFactory = null;
+    if (Boolean.parseBoolean(System.getProperty(NO_LOGGER_PROPERTY, "false"))) {
+      loggerFactory = new NoLoggerFactory();
+    } else {
+
+      try {
+        if (Boolean.parseBoolean(System.getProperty(TEST_ENABLE_SLF4J, "true"))) {
+          Class.forName("org.slf4j.LoggerFactory");
+          loggerFactory = new Slf4JLoggerFactory();
+        }
+      } catch (ClassNotFoundException cle) {
+        // slf4j not in the classpath
+      }
+      if (loggerFactory == null) {
+        // default to console or use JDK logger if explicitly set by System property
+        if ("JDK".equalsIgnoreCase(System.getProperty(FALLBACK_PROPERTY))) {
+          loggerFactory = new JdkLoggerFactory();
+        } else {
+          loggerFactory = new ConsoleLoggerFactory();
+        }
+      }
+      try {
+        loggerFactory.getLogger(name);
+      } catch (Throwable e) {
+        // eat
+      }
+    }
+    LOGGER_FACTORY = loggerFactory;
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/log/NoLogger.java b/src/main/java/org/mariadb/jdbc/util/log/NoLogger.java
new file mode 100644
index 0000000..dc80ebc
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/log/NoLogger.java
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util.log;
+
+/** No logger implementation */
+public class NoLogger implements org.mariadb.jdbc.util.log.Logger {
+
+  @Override
+  public String getName() {
+    return "";
+  }
+
+  @Override
+  public boolean isTraceEnabled() {
+    return false;
+  }
+
+  @Override
+  public void trace(String msg) {}
+
+  @Override
+  public void trace(String format, Object... arguments) {}
+
+  @Override
+  public void trace(String msg, Throwable t) {}
+
+  @Override
+  public boolean isDebugEnabled() {
+    return false;
+  }
+
+  @Override
+  public void debug(String msg) {}
+
+  @Override
+  public void debug(String format, Object... arguments) {}
+
+  @Override
+  public void debug(String msg, Throwable t) {}
+
+  @Override
+  public boolean isInfoEnabled() {
+    return false;
+  }
+
+  @Override
+  public void info(String msg) {}
+
+  @Override
+  public void info(String format, Object... arguments) {}
+
+  @Override
+  public void info(String msg, Throwable t) {}
+
+  @Override
+  public boolean isWarnEnabled() {
+    return false;
+  }
+
+  @Override
+  public void warn(String msg) {}
+
+  @Override
+  public void warn(String format, Object... arguments) {}
+
+  @Override
+  public void warn(String msg, Throwable t) {}
+
+  @Override
+  public boolean isErrorEnabled() {
+    return false;
+  }
+
+  @Override
+  public void error(String msg) {}
+
+  @Override
+  public void error(String format, Object... arguments) {}
+
+  @Override
+  public void error(String msg, Throwable t) {}
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/log/Slf4JLogger.java b/src/main/java/org/mariadb/jdbc/util/log/Slf4JLogger.java
new file mode 100644
index 0000000..40fa949
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/log/Slf4JLogger.java
@@ -0,0 +1,127 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util.log;
+
+import org.slf4j.Logger;
+
+/** SLF4J logger wrapper */
+public class Slf4JLogger implements org.mariadb.jdbc.util.log.Logger {
+
+  private final Logger logger;
+
+  /**
+   * Constructor
+   *
+   * @param logger SLF4J logger
+   */
+  public Slf4JLogger(Logger logger) {
+    this.logger = logger;
+  }
+
+  @Override
+  public String getName() {
+    return logger.getName();
+  }
+
+  @Override
+  public boolean isTraceEnabled() {
+    return logger.isTraceEnabled();
+  }
+
+  @Override
+  public void trace(String msg) {
+    logger.trace(msg);
+  }
+
+  @Override
+  public void trace(String format, Object... arguments) {
+    logger.trace(format, arguments);
+  }
+
+  @Override
+  public void trace(String msg, Throwable t) {
+    logger.trace(msg, t);
+  }
+
+  @Override
+  public boolean isDebugEnabled() {
+    return logger.isDebugEnabled();
+  }
+
+  @Override
+  public void debug(String msg) {
+    logger.debug(msg);
+  }
+
+  @Override
+  public void debug(String format, Object... arguments) {
+    logger.debug(format, arguments);
+  }
+
+  @Override
+  public void debug(String msg, Throwable t) {
+    logger.debug(msg, t);
+  }
+
+  @Override
+  public boolean isInfoEnabled() {
+    return logger.isInfoEnabled();
+  }
+
+  @Override
+  public void info(String msg) {
+    logger.info(msg);
+  }
+
+  @Override
+  public void info(String format, Object... arguments) {
+    logger.info(format, arguments);
+  }
+
+  @Override
+  public void info(String msg, Throwable t) {
+    logger.info(msg, t);
+  }
+
+  @Override
+  public boolean isWarnEnabled() {
+    return logger.isWarnEnabled();
+  }
+
+  @Override
+  public void warn(String msg) {
+    logger.warn(msg);
+  }
+
+  @Override
+  public void warn(String format, Object... arguments) {
+    logger.warn(format, arguments);
+  }
+
+  @Override
+  public void warn(String msg, Throwable t) {
+    logger.warn(msg, t);
+  }
+
+  @Override
+  public boolean isErrorEnabled() {
+    return logger.isErrorEnabled();
+  }
+
+  @Override
+  public void error(String msg) {
+    logger.error(msg);
+  }
+
+  @Override
+  public void error(String format, Object... arguments) {
+    logger.error(format, arguments);
+  }
+
+  @Override
+  public void error(String msg, Throwable t) {
+    logger.error(msg, t);
+  }
+}
diff --git a/src/main/java/org/mariadb/jdbc/util/options/OptionAliases.java b/src/main/java/org/mariadb/jdbc/util/options/OptionAliases.java
new file mode 100644
index 0000000..78cc9b1
--- /dev/null
+++ b/src/main/java/org/mariadb/jdbc/util/options/OptionAliases.java
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.util.options;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Option alias name */
+public final class OptionAliases {
+
+  /** list of aliases */
+  public static final Map<String, String> OPTIONS_ALIASES;
+
+  static {
+    OPTIONS_ALIASES = new HashMap<>();
+    OPTIONS_ALIASES.put("clientcertificatekeystoreurl", "keyStore");
+    OPTIONS_ALIASES.put("clientcertificatekeystorepassword", "keyStorePassword");
+    OPTIONS_ALIASES.put("clientcertificatekeystoretype", "keyStoreType");
+  }
+}
diff --git a/src/main/java11/org/mariadb/jdbc/client/SocketHelper.java b/src/main/java11/org/mariadb/jdbc/client/SocketHelper.java
new file mode 100644
index 0000000..a26f708
--- /dev/null
+++ b/src/main/java11/org/mariadb/jdbc/client/SocketHelper.java
@@ -0,0 +1,39 @@
+package org.mariadb.jdbc.client;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import jdk.net.ExtendedSocketOptions;
+import org.mariadb.jdbc.Configuration;
+
+public class SocketHelper {
+  public static void setSocketOption(final Configuration conf, final Socket socket)
+      throws IOException {
+    socket.setTcpNoDelay(true);
+    socket.setSoTimeout(conf.socketTimeout());
+    if (conf.tcpKeepAlive()) {
+      socket.setKeepAlive(true);
+    }
+    if (conf.tcpAbortiveClose()) {
+      socket.setSoLinger(true, 0);
+    }
+
+    // java 11 only
+    if (conf.tcpKeepIdle() > 0) {
+      socket.setOption(ExtendedSocketOptions.TCP_KEEPIDLE, conf.tcpKeepIdle());
+    }
+    if (conf.tcpKeepCount() > 0) {
+      socket.setOption(ExtendedSocketOptions.TCP_KEEPCOUNT, conf.tcpKeepCount());
+    }
+    if (conf.tcpKeepInterval() > 0) {
+      socket.setOption(ExtendedSocketOptions.TCP_KEEPINTERVAL, conf.tcpKeepInterval());
+    }
+
+    // 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);
+    }
+  }
+}
diff --git a/src/main/java9/module-info.java b/src/main/java9/module-info.java
new file mode 100644
index 0000000..5896c28
--- /dev/null
+++ b/src/main/java9/module-info.java
@@ -0,0 +1,86 @@
+import org.mariadb.jdbc.plugin.codec.*;
+
+module org.mariadb.jdbc {
+  requires transitive java.sql;
+  requires transitive java.management;
+  requires transitive java.naming;
+  requires transitive java.security.jgss;
+  requires transitive jdk.net;
+  requires waffle.jna;
+  requires static software.amazon.awssdk.services.rds;
+  requires static software.amazon.awssdk.regions;
+  requires static software.amazon.awssdk.auth;
+  requires static com.sun.jna;
+  requires static com.sun.jna.platform;
+  requires static org.slf4j;
+
+  exports org.mariadb.jdbc;
+  exports org.mariadb.jdbc.client;
+  exports org.mariadb.jdbc.client.util;
+  exports org.mariadb.jdbc.client.socket;
+  exports org.mariadb.jdbc.message;
+  exports org.mariadb.jdbc.type;
+  exports org.mariadb.jdbc.export;
+  exports org.mariadb.jdbc.plugin;
+  exports org.mariadb.jdbc.plugin.codec;
+  exports org.mariadb.jdbc.plugin.authentication.standard;
+  exports org.mariadb.jdbc.plugin.authentication.addon;
+  exports org.mariadb.jdbc.plugin.credential.aws;
+  exports org.mariadb.jdbc.plugin.credential.env;
+  exports org.mariadb.jdbc.plugin.credential.system;
+  exports org.mariadb.jdbc.plugin.tls.main;
+
+  uses java.sql.Driver;
+  uses org.mariadb.jdbc.plugin.CredentialPlugin;
+  uses org.mariadb.jdbc.plugin.Codec;
+  uses org.mariadb.jdbc.plugin.AuthenticationPlugin;
+  uses org.mariadb.jdbc.plugin.TlsSocketPlugin;
+
+  provides java.sql.Driver with
+      org.mariadb.jdbc.Driver;
+  provides org.mariadb.jdbc.plugin.AuthenticationPlugin with
+      org.mariadb.jdbc.plugin.authentication.addon.ClearPasswordPlugin,
+      org.mariadb.jdbc.plugin.authentication.addon.SendGssApiAuthPacket,
+      org.mariadb.jdbc.plugin.authentication.standard.Ed25519PasswordPlugin,
+      org.mariadb.jdbc.plugin.authentication.standard.NativePasswordPlugin,
+      org.mariadb.jdbc.plugin.authentication.standard.SendPamAuthPacket,
+      org.mariadb.jdbc.plugin.authentication.standard.CachingSha2PasswordPlugin;
+  provides org.mariadb.jdbc.plugin.Codec with
+      BigDecimalCodec,
+      BigIntegerCodec,
+      BitSetCodec,
+      BlobCodec,
+      BooleanCodec,
+      ByteArrayCodec,
+      ByteCodec,
+      ClobCodec,
+      DateCodec,
+      DoubleCodec,
+      DurationCodec,
+      FloatCodec,
+      GeometryCollectionCodec,
+      IntCodec,
+      LineStringCodec,
+      LocalDateCodec,
+      LocalDateTimeCodec,
+      LocalTimeCodec,
+      LongCodec,
+      MultiLinestringCodec,
+      MultiPointCodec,
+      MultiPolygonCodec,
+      PointCodec,
+      PolygonCodec,
+      ReaderCodec,
+      ShortCodec,
+      StreamCodec,
+      StringCodec,
+      TimeCodec,
+      TimestampCodec,
+      ZonedDateTimeCodec;
+  provides org.mariadb.jdbc.plugin.CredentialPlugin with
+      org.mariadb.jdbc.plugin.credential.aws.AwsIamCredentialPlugin,
+      org.mariadb.jdbc.plugin.credential.env.EnvCredentialPlugin,
+      org.mariadb.jdbc.plugin.credential.system.PropertiesCredentialPlugin;
+  provides org.mariadb.jdbc.plugin.TlsSocketPlugin with
+      org.mariadb.jdbc.plugin.tls.main.DefaultTlsSocketPlugin;
+}
diff --git a/src/main/resources/META-INF/MANIFEST.MF b/src/main/resources/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..07332bc
--- /dev/null
+++ b/src/main/resources/META-INF/MANIFEST.MF
@@ -0,0 +1,2 @@
+Manifest-Version: 1.0
+Multi-Release: true
diff --git a/src/main/resources/META-INF/services/java.sql.Driver b/src/main/resources/META-INF/services/java.sql.Driver
new file mode 100644
index 0000000..ad8b171
--- /dev/null
+++ b/src/main/resources/META-INF/services/java.sql.Driver
@@ -0,0 +1 @@
+org.mariadb.jdbc.Driver
\ No newline at end of file
diff --git a/src/main/resources/META-INF/services/org.mariadb.jdbc.plugin.AuthenticationPlugin b/src/main/resources/META-INF/services/org.mariadb.jdbc.plugin.AuthenticationPlugin
new file mode 100644
index 0000000..63d4f49
--- /dev/null
+++ b/src/main/resources/META-INF/services/org.mariadb.jdbc.plugin.AuthenticationPlugin
@@ -0,0 +1,6 @@
+org.mariadb.jdbc.plugin.authentication.addon.ClearPasswordPlugin
+org.mariadb.jdbc.plugin.authentication.addon.SendGssApiAuthPacket
+org.mariadb.jdbc.plugin.authentication.standard.Ed25519PasswordPlugin
+org.mariadb.jdbc.plugin.authentication.standard.NativePasswordPlugin
+org.mariadb.jdbc.plugin.authentication.standard.SendPamAuthPacket
+org.mariadb.jdbc.plugin.authentication.standard.CachingSha2PasswordPlugin
\ No newline at end of file
diff --git a/src/main/resources/META-INF/services/org.mariadb.jdbc.plugin.Codec b/src/main/resources/META-INF/services/org.mariadb.jdbc.plugin.Codec
new file mode 100644
index 0000000..3335aa2
--- /dev/null
+++ b/src/main/resources/META-INF/services/org.mariadb.jdbc.plugin.Codec
@@ -0,0 +1,34 @@
+org.mariadb.jdbc.plugin.codec.BigDecimalCodec
+org.mariadb.jdbc.plugin.codec.BigIntegerCodec
+org.mariadb.jdbc.plugin.codec.BitSetCodec
+org.mariadb.jdbc.plugin.codec.BlobCodec
+org.mariadb.jdbc.plugin.codec.BooleanCodec
+org.mariadb.jdbc.plugin.codec.ByteArrayCodec
+org.mariadb.jdbc.plugin.codec.ByteCodec
+org.mariadb.jdbc.plugin.codec.ClobCodec
+org.mariadb.jdbc.plugin.codec.DateCodec
+org.mariadb.jdbc.plugin.codec.DoubleCodec
+org.mariadb.jdbc.plugin.codec.DurationCodec
+org.mariadb.jdbc.plugin.codec.FloatCodec
+org.mariadb.jdbc.plugin.codec.GeometryCollectionCodec
+org.mariadb.jdbc.plugin.codec.IntCodec
+org.mariadb.jdbc.plugin.codec.InstantCodec
+org.mariadb.jdbc.plugin.codec.OffsetDateTimeCodec
+org.mariadb.jdbc.plugin.codec.LineStringCodec
+org.mariadb.jdbc.plugin.codec.LocalDateCodec
+org.mariadb.jdbc.plugin.codec.LocalDateTimeCodec
+org.mariadb.jdbc.plugin.codec.LocalTimeCodec
+org.mariadb.jdbc.plugin.codec.LongCodec
+org.mariadb.jdbc.plugin.codec.MultiLinestringCodec
+org.mariadb.jdbc.plugin.codec.MultiPointCodec
+org.mariadb.jdbc.plugin.codec.MultiPolygonCodec
+org.mariadb.jdbc.plugin.codec.PointCodec
+org.mariadb.jdbc.plugin.codec.PolygonCodec
+org.mariadb.jdbc.plugin.codec.ReaderCodec
+org.mariadb.jdbc.plugin.codec.ShortCodec
+org.mariadb.jdbc.plugin.codec.StreamCodec
+org.mariadb.jdbc.plugin.codec.StringCodec
+org.mariadb.jdbc.plugin.codec.TimeCodec
+org.mariadb.jdbc.plugin.codec.TimestampCodec
+org.mariadb.jdbc.plugin.codec.UuidCodec
+org.mariadb.jdbc.plugin.codec.ZonedDateTimeCodec
diff --git a/src/main/resources/META-INF/services/org.mariadb.jdbc.plugin.CredentialPlugin b/src/main/resources/META-INF/services/org.mariadb.jdbc.plugin.CredentialPlugin
new file mode 100644
index 0000000..cc51428
--- /dev/null
+++ b/src/main/resources/META-INF/services/org.mariadb.jdbc.plugin.CredentialPlugin
@@ -0,0 +1,3 @@
+org.mariadb.jdbc.plugin.credential.aws.AwsIamCredentialPlugin
+org.mariadb.jdbc.plugin.credential.env.EnvCredentialPlugin
+org.mariadb.jdbc.plugin.credential.system.PropertiesCredentialPlugin
diff --git a/src/main/resources/META-INF/services/org.mariadb.jdbc.plugin.TlsSocketPlugin b/src/main/resources/META-INF/services/org.mariadb.jdbc.plugin.TlsSocketPlugin
new file mode 100644
index 0000000..471e4d2
--- /dev/null
+++ b/src/main/resources/META-INF/services/org.mariadb.jdbc.plugin.TlsSocketPlugin
@@ -0,0 +1 @@
+org.mariadb.jdbc.plugin.tls.main.DefaultTlsSocketPlugin
\ No newline at end of file
diff --git a/src/main/resources/deprecated.properties b/src/main/resources/deprecated.properties
new file mode 100644
index 0000000..396dc2c
--- /dev/null
+++ b/src/main/resources/deprecated.properties
@@ -0,0 +1,3 @@
+useSsl=`useSsl` option is deprecated, replaced by option `sslMode`
+trustServerCertificate=`trustServerCertificate` option is deprecated, replaced by setting option `sslMode=trust`
+disableSslHostnameVerification=`disableSslHostnameVerification` option is deprecated, replaced by setting option `sslMode=verify-ca`
diff --git a/src/main/resources/driver.properties b/src/main/resources/driver.properties
new file mode 100644
index 0000000..db5ff93
--- /dev/null
+++ b/src/main/resources/driver.properties
@@ -0,0 +1,69 @@
+user=Database user name
+password=Password of database user
+database=Database
+socketFactory=to use a custom socket factory, set it to the full name of the class that implements javax.net.SocketFactory.
+connectTimeout=The connect timeout value, in milliseconds, or zero for no timeout.
+pipe=On Windows, specify named pipe name to connect.
+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)
+tcpKeepAlive=Sets corresponding option on the connection socket. Default: True
+tcpAbortiveClose=This option can be used in environments where connections are created and closed in rapid succession. Often, it is not possible to create a socket in such an environment after a while, since all local �ephemeral� ports are used up by TCP connections in TCP_WAIT state. Using tcpAbortiveClose works around this problem by resetting TCP connections (abortive or hard close) rather than doing an orderly close. It is accomplished by using socket.setSoLinger(true,0) for abortive close
+localSocketAddress=Hostname or IP address to bind the connection socket to a local (UNIX domain) socket.
+socketTimeout=Defined 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, since MariaDB 10.1.1, the server has permitted a solution to limit the query time by setting a system variable, max_statement_time. The advantage is that the connection then is still usable. Default: 0ms.
+allowMultiQueries=permit multi-queries like insert into ab (i) values (1); insert into ab (i) values (2). Default: false.
+useCompression=Compresses the exchange with the database through gzip. This permits better performance when the database is not in the same location.
+blankTableNameMeta=Resultset metadata getTableName always return blank. This option is mainly for ORACLE db compatibility.
+credentialType=Indicate the credential plugin type to use. Plugin must be present in classpath
+sslMode=Indicate SSL demand. Possible value disable, trust (no certificate and hostname validation), verify-ca (Encryption, certificates validation, BUT no hostname verification), verify-full (Standard SSL use: Encryption, certificate validation and hostname)
+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"
+sessionVariables=<var>=<value> pairs separated by comma, mysql session variables, set upon establishing successful connection.
+tinyInt1isBit=Datatype mapping flag, handle MySQL Tiny as BIT(boolean).
+yearIsDateType=Year is date type, rather than numerical.
+dumpQueriesOnException=If set to 'true', an exception is thrown during query execution containing a query string.
+prepStmtCacheSize=if useServerPrepStmts = true, defines the prepared statement cache size that option `cachePrepStmts` use. Default: 250
+useAffectedRows=If false (default), use "found rows" for the row count of statements. This corresponds to the JDBC standard. If true, use "affected rows" for the row count. This changes the behavior of, for example, UPDATE... ON DUPLICATE KEY statements.
+useServerPrepStmts=PrepareStatement are prepared on the server side before executing. The applications that repeatedly use the same queries have value to activate this option, but the general case is to use the direct command (text protocol).
+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
+useBulkStmts=Use dedicated COM_STMT_BULK_EXECUTE protocol for batch insert when possible. (batch without Statement.RETURN_GENERATED_KEYS and streams) to have faster batch. (significant only on >= MariaDB 10.2.7). Default: false.
+autocommit=Set default autocommit value on connection initialization. Default: true.
+includeInnodbStatusInDeadlockExceptions=add "SHOW ENGINE INNODB STATUS" result to exception trace when having a deadlock exception.
+includeThreadDumpInDeadlockExceptions=add thread dump to exception trace when having a deadlock exception.
+servicePrincipalName=When using GSSAPI authentication, use this value as the Service Principal Name (SPN) instead of the one defined for the user account on the database server.
+defaultFetchSize=The driver will call setFetchSize(n) with this value on all newly-created Statements. Default: 0.
+tlsSocketType=Indicate the TLS org.mariadb.jdbc.tls.TlsSocketPlugin plugin type to use. Plugin must be present in classpath
+maxQuerySizeToLog=Only the first characters corresponding to this options size will be displayed in logs. Default: 1024
+retriesAllDown=When the connector is performing a failover and all hosts are down, this parameter defines the maximum number of connection attempts the connector will make before throwing an exception. Default: 120 seconds.
+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 to know more.
+enabledSslProtocolSuites=Force TLS/SSL protocol to a specific set of TLS versions (comma separated list). Example : "TLSv1, TLSv1.1, TLSv1.2"
+pool=Use pool. This option is useful only if not using a DataSource object, but only a connection object. Default: false.
+poolName=Pool name that permits identifying threads. default: auto-generated as MariaDb-pool-<pool-index>
+maxPoolSize=The maximum number of physical connections that the pool should contain. Default: 8.
+minPoolSize=When connections are removed due to not being used for longer than than "maxIdleTime", connections are closed and removed from the pool. "minPoolSize" indicates the number of physical connections the pool should keep available at all times. Should be less or equal to maxPoolSize.
+maxIdleTime=The maximum amount of time in seconds that a connection can stay in the 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.
+registerJmxPool=Register JMX monitoring pools. Default: true.
+poolValidMinDelay=When asking a connection to pool, the pool will validate the connection state. "poolValidMinDelay" permits disabling this validation if the connection has been borrowed recently avoiding useless verifications in case of frequent reuse of connections. 0 means validation is done each time the connection is asked. Default: 1000 (in milliseconds).
+useResetConnection=When a connection is closed() (given back to pool), the pool resets the connection state. Setting this option, the prepare command will be deleted, session variables changed will be reset, and user variables will be destroyed when the server permits it (>= MariaDB 10.2.4, >= MySQL 5.7.3), permitting saving memory on the server if the application make extensive use of variables. Must not be used with the useServerPrepStmts option. Default: false.
+serverSslCert=Permits providing server's certificate in DER form, or server's CA certificate. The server will be added to trustStor. This permits a self-signed certificate to be trusted. Can be used in one of 3 forms : * serverSslCert=/path/to/cert.pem (full path to certificate) * serverSslCert=classpath:relative/cert.pem (relative to current classpath) * or as verbatim DER-encoded certificate string "------BEGIN CERTIFICATE-----" .
+serverRsaPublicKeyFile=Indicate path to RSA server public key file for sha256_password and caching_sha2_password authentication password
+allowPublicKeyRetrieval=Authorize client to retrieve RSA server public key when serverRsaPublicKeyFile is not set (for sha256_password and caching_sha2_password authentication password). Default: false.
+useReadAheadInput=use a buffered inputSteam that read socket available data. This cost a bit more in CPU, but permit returning result-set faster. Default true
+cachePrepStmts=enable/disable prepare Statement cache. When enable, PreparedStatement.close won't close prepare immediately, keeping a pool of most used prepared results. Default true.
+timezone=permits to force session timezone in case of client having a different timezone compare to server. The option `timezone` can have 3 types of value: 'disabled' (default) : connector doesn't change time_zone. '<a timezone>': connector will set connection variable to value. see timezone consideration tp know more
+transactionReplay=When having a failover, can current transaction being re-executed, having a completely transparent failover. All commands must be idempotent. Default false.
+transactionReplaySize=replay cache buffer maximum size. If a transaction has more command that this size and a failover occurs, transaction will then not be replayed, just throwing an exception error. (Integer) default 64.
+allowLocalInfile=Indicate if LOAD DATA LOCAL INFILE commands are permitted. This will disable all pipelining implementation. Default false.
+geometryDefaultType=Indicate what default Object type Geometry a resultset.getObject must return. null or empty is WKB byte array. 'default' will return org.mariadb.mariadb.jdbc.type Object. Default null
+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)
+keyStorePassword="Password for the client certificate keyStore (similar to java System property "javax.net.ssl.keyStorePassword")
+keyStoreType=indicate key store type (JKS/PKCS12). default is null, then using java default type
+transactionIsolation=Default transaction isolation level. Possible values READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE. default REPEATABLE-READ
+restrictedAuth=if set, restrict authentication plugin to secure list. Default provided plugins are mysql_native_password,client_ed25519,auth_gssapi_client,caching_sha2_password,dialog and mysql_clear_password. (String) Default null
+tcpKeepIdle=permit setting socket TCP_KEEPIDLE value. Only valid for java 11+(for previous version, this option will have no use). (Integer) Default null
+tcpKeepCount=permit setting socket TCP_KEEPCOUNT value. Only valid for java 11+(for previous version, this option will have no use). (Integer) Default null
+tcpKeepInterval=permit setting socket TCP_KEEPINTERVAL value. Only valid for java 11+(for previous version, this option will have no use). (Integer) Default null
+useMysqlMetadata=force DatabaseMetadata.getDatabaseProductName() to return "MySQL" (for compatibility reason)
+maxAllowedPacket=permit to driver to avoid sending command with size > to server max_allowed_packet, throwing an error in place of server dropping the connection.
+createDatabaseIfNotExist=the specified database in the url will be created if nonexistent. Default: false
+initSql=Initial command to execute on new connection creation
+transformedBitIsBoolean=Indicate for TINYINT(1) in column metadata that type is BOOLEAN in place of BIT 
+disablePipeline=option permitting disabling pipelining
+uuidAsString=must uuid fields return as String and not java.util.UUID when using getObject()
\ No newline at end of file
diff --git a/src/main/resources/mariadb.properties b/src/main/resources/mariadb.properties
new file mode 100644
index 0000000..e5683df
--- /dev/null
+++ b/src/main/resources/mariadb.properties
@@ -0,0 +1 @@
+version=${project.version}
\ No newline at end of file
diff --git a/src/test/java/org/mariadb/jdbc/integration/BatchTest.java b/src/test/java/org/mariadb/jdbc/integration/BatchTest.java
new file mode 100644
index 0000000..814c750
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/BatchTest.java
@@ -0,0 +1,493 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.*;
+import java.util.Calendar;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.*;
+import org.mariadb.jdbc.Connection;
+import org.mariadb.jdbc.Statement;
+
+public class BatchTest extends Common {
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    after2();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE BatchTest (t1 int not null primary key auto_increment, t2 LONGTEXT)");
+    createSequenceTables();
+    stmt.execute("CREATE TABLE timestampCal(id int, val TIMESTAMP)");
+  }
+
+  @AfterAll
+  public static void after2() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS timestampCal");
+    stmt.execute("DROP TABLE IF EXISTS BatchTest");
+  }
+
+  @Test
+  public void wrongParameter() throws SQLException {
+    try (Connection con = createCon("&useServerPrepStmts=false")) {
+      wrongParameter(con);
+    }
+    try (Connection con = createCon("&useServerPrepStmts=true")) {
+      wrongParameter(con);
+    }
+  }
+
+  public void wrongParameter(Connection con) throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE BatchTest");
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO BatchTest(t1, t2) VALUES (?,?)")) {
+      prep.setInt(1, 5);
+      try {
+        prep.addBatch();
+      } catch (SQLTransientConnectionException e) {
+        assertTrue(e.getMessage().contains("Parameter at position 2 is not set"));
+      }
+      try {
+        prep.addBatch();
+      } catch (SQLTransientConnectionException e) {
+        assertTrue(
+            e.getMessage().contains("Parameter at position 2 is not set")
+                || e.getMessage()
+                    .contains(
+                        "batch set of parameters differ from previous set. All parameters must be"
+                            + " set"));
+      }
+
+      prep.setInt(1, 5);
+      prep.setString(3, "wrong position");
+      Common.assertThrowsContains(
+          SQLTransientConnectionException.class,
+          prep::addBatch,
+          "Parameter at position 2 is not set");
+
+      prep.setInt(1, 5);
+      prep.setString(2, "ok");
+      prep.addBatch();
+      prep.setString(2, "without position 1");
+      prep.addBatch();
+    }
+  }
+
+  @Test
+  public void differentParameterType() throws SQLException {
+    try (Connection con = createCon("&useServerPrepStmts=false&useBulkStmts=false")) {
+      differentParameterType(con, false);
+    }
+    try (Connection con = createCon("&useServerPrepStmts=false&useBulkStmts=true")) {
+      differentParameterType(con, isMariaDBServer() && !isXpand());
+    }
+    try (Connection con =
+        createCon("&useServerPrepStmts=false&useBulkStmts=true&disablePipeline")) {
+      differentParameterType(con, isMariaDBServer() && !isXpand());
+    }
+    try (Connection con = createCon("&useServerPrepStmts&useBulkStmts=false")) {
+      differentParameterType(con, false);
+    }
+    try (Connection con = createCon("&useServerPrepStmts&useBulkStmts&allowLocalInfile=false")) {
+      differentParameterType(con, isMariaDBServer() && !isXpand());
+    }
+    try (Connection con = createCon("&useServerPrepStmts=false&allowLocalInfile")) {
+      differentParameterType(con, isMariaDBServer() && !isXpand());
+    }
+    try (Connection con = createCon("&useServerPrepStmts&useBulkStmts=false")) {
+      differentParameterType(con, false);
+    }
+    try (Connection con = createCon("&useServerPrepStmts&useBulkStmts")) {
+      differentParameterType(con, isMariaDBServer() && !isXpand());
+    }
+    try (Connection con = createCon("&useServerPrepStmts&useBulkStmts&allowLocalInfile=false")) {
+      differentParameterType(con, isMariaDBServer() && !isXpand());
+    }
+    try (Connection con =
+        createCon("&useServerPrepStmts&useBulkStmts=false&disablePipeline=true")) {
+      differentParameterType(con, false);
+    }
+  }
+
+  public void differentParameterType(Connection con, boolean expectSuccessUnknown)
+      throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE BatchTest");
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO BatchTest(t1, t2) VALUES (?,?)")) {
+      prep.setInt(1, 1);
+      prep.setString(2, "1");
+      prep.addBatch();
+
+      prep.setInt(1, 2);
+      prep.setInt(2, 2);
+      prep.addBatch();
+      prep.setInt(1, 3);
+      prep.setNull(2, Types.INTEGER);
+      prep.addBatch();
+      int[] res = prep.executeBatch();
+      assertEquals(3, res.length);
+      if (expectSuccessUnknown) {
+        assertEquals(Statement.SUCCESS_NO_INFO, res[0]);
+        assertEquals(Statement.SUCCESS_NO_INFO, res[1]);
+        assertEquals(Statement.SUCCESS_NO_INFO, res[2]);
+      } else {
+        assertEquals(1, res[0]);
+        assertEquals(1, res[1]);
+        assertEquals(1, res[2]);
+      }
+    }
+    ResultSet rs = stmt.executeQuery("SELECT * FROM BatchTest");
+    assertTrue(rs.next());
+    assertEquals(1, rs.getInt(1));
+    assertEquals("1", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals(2, rs.getInt(1));
+    assertEquals("2", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals(3, rs.getInt(1));
+    assertNull(rs.getString(2));
+    assertFalse(rs.next());
+    stmt.execute("TRUNCATE BatchTest");
+
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO BatchTest(t1, t2) VALUES (?,?)")) {
+      prep.setInt(1, 1);
+      prep.setInt(2, 1);
+      prep.addBatch();
+
+      prep.setInt(1, 2);
+      prep.setInt(2, 2);
+      prep.addBatch();
+      int[] res = prep.executeBatch();
+      assertEquals(2, res.length);
+      if (expectSuccessUnknown) {
+        assertEquals(Statement.SUCCESS_NO_INFO, res[0]);
+        assertEquals(Statement.SUCCESS_NO_INFO, res[1]);
+      } else {
+        assertEquals(1, res[0]);
+        assertEquals(1, res[1]);
+      }
+    }
+    rs = stmt.executeQuery("SELECT * FROM BatchTest");
+    assertTrue(rs.next());
+    assertEquals(1, rs.getInt(1));
+    assertEquals("1", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals(2, rs.getInt(1));
+    assertEquals("2", rs.getString(2));
+    assertFalse(rs.next());
+
+    stmt.execute("TRUNCATE BatchTest");
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO BatchTest(t1, t2) VALUES (?,?)")) {
+      prep.setInt(1, 1);
+      prep.setInt(2, 1);
+      prep.addBatch();
+
+      int[] res = prep.executeBatch();
+      assertEquals(1, res.length);
+      assertEquals(1, res[0]);
+    }
+    rs = stmt.executeQuery("SELECT * FROM BatchTest");
+    assertTrue(rs.next());
+    assertEquals(1, rs.getInt(1));
+    assertEquals("1", rs.getString(2));
+    assertFalse(rs.next());
+
+    stmt.execute("TRUNCATE BatchTest");
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO BatchTest(t1, t2) VALUES (?,?)")) {
+      prep.setInt(1, 1);
+      prep.setString(2, "1");
+      prep.addBatch();
+
+      prep.setInt(1, 2);
+      prep.setInt(2, 2);
+      prep.addBatch();
+      int[] res = prep.executeBatch();
+      assertEquals(2, res.length);
+      assertEquals(1, res[0]);
+      assertEquals(1, res[1]);
+
+      stmt.execute("TRUNCATE BatchTest");
+
+      stmt.setFetchSize(1);
+      rs = stmt.executeQuery("SELECT * FROM sequence_1_to_10");
+      rs.next();
+
+      prep.setInt(1, 1);
+      prep.setString(2, "1");
+      prep.addBatch();
+
+      prep.setInt(1, 2);
+      prep.setInt(2, 2);
+      prep.addBatch();
+      res = prep.executeBatch();
+      assertEquals(2, res.length);
+      assertEquals(1, res[0]);
+      assertEquals(1, res[1]);
+    }
+    rs = stmt.executeQuery("SELECT * FROM BatchTest");
+    assertTrue(rs.next());
+    assertEquals(1, rs.getInt(1));
+    assertEquals("1", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals(2, rs.getInt(1));
+    assertEquals("2", rs.getString(2));
+    assertFalse(rs.next());
+  }
+
+  @Test
+  public void largeBatch() throws SQLException {
+    for (int i = 0; i < 32; i++) {
+      boolean useServerPrepStmts = (i & 2) > 0;
+      boolean useBulkStmts = (i & 4) > 0;
+      boolean allowLocalInfile = (i & 8) > 0;
+      boolean useCompression = (i & 16) > 0;
+
+      try (Connection con =
+          createCon(
+              String.format(
+                  "&useServerPrepStmts=%s&useBulkStmts=%s&allowLocalInfile=%s&useCompression=%s",
+                  useServerPrepStmts, useBulkStmts, allowLocalInfile, useCompression))) {
+        largeBatch(con);
+      }
+    }
+  }
+
+  public void largeBatch(Connection con) throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE BatchTest");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO BatchTest(t1, t2) VALUES (?,?)")) {
+      prep.setInt(1, 1);
+      prep.setString(2, "1");
+      prep.addBatch();
+
+      prep.setInt(1, 2);
+      prep.setInt(2, 2);
+      prep.addBatch();
+      long[] res = prep.executeLargeBatch();
+      assertEquals(2, res.length);
+      assertEquals(1, res[0]);
+      assertEquals(1, res[1]);
+    }
+    ResultSet rs = stmt.executeQuery("SELECT * FROM BatchTest");
+    assertTrue(rs.next());
+    assertEquals(1, rs.getInt(1));
+    assertEquals("1", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals(2, rs.getInt(1));
+    assertEquals("2", rs.getString(2));
+    assertFalse(rs.next());
+    con.commit();
+  }
+
+  @Test
+  public void bulkPacketSplitMaxAllowedPacket() throws SQLException {
+    Assumptions.assumeTrue(runLongTest());
+    int maxAllowedPacket = getMaxAllowedPacket();
+    bulkPacketSplit(2, maxAllowedPacket - 40, maxAllowedPacket);
+    if (maxAllowedPacket >= 16 * 1024 * 1024) bulkPacketSplit(2, maxAllowedPacket - 40, null);
+  }
+
+  @Test
+  public void bulkPacketSplitMultiplePacket() throws SQLException {
+    Assumptions.assumeTrue(runLongTest());
+    int maxAllowedPacket = getMaxAllowedPacket();
+    bulkPacketSplit(4, getMaxAllowedPacket() / 3, maxAllowedPacket);
+    if (maxAllowedPacket >= 16 * 1024 * 1024) bulkPacketSplit(4, getMaxAllowedPacket() / 3, null);
+  }
+
+  @Test
+  public void bulkPacketSplitHugeNbPacket() throws SQLException {
+    Assumptions.assumeTrue(runLongTest());
+    int maxAllowedPacket = getMaxAllowedPacket();
+    bulkPacketSplit(getMaxAllowedPacket() / 8000, 20, maxAllowedPacket);
+    if (maxAllowedPacket >= 16 * 1024 * 1024)
+      bulkPacketSplit(getMaxAllowedPacket() / 8000, 20, null);
+  }
+
+  public void bulkPacketSplit(int nb, int len, Integer maxAllowedPacket) throws SQLException {
+    byte[] arr = new byte[Math.min(16 * 1024 * 1024, len)];
+    for (int pos = 0; pos < arr.length; pos++) {
+      arr[pos] = (byte) ((pos % 60) + 65);
+    }
+
+    try (Connection con =
+        createCon(
+            "&useServerPrepStmts&useBulkStmts"
+                + (maxAllowedPacket != null ? "&maxAllowedPacket=" + maxAllowedPacket : ""))) {
+      Statement stmt = con.createStatement();
+      stmt.execute("TRUNCATE BatchTest");
+      stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+      try (PreparedStatement prep =
+          con.prepareStatement("INSERT INTO BatchTest(t1, t2) VALUES (?,?)")) {
+        for (int i = 1; i <= nb; i++) {
+          prep.setInt(1, i);
+          prep.setBytes(2, arr);
+          prep.addBatch();
+        }
+
+        int[] res = prep.executeBatch();
+        assertEquals(nb, res.length);
+        for (int i = 0; i < nb; i++) {
+          assertTrue(res[i] == 1 || res[i] == Statement.SUCCESS_NO_INFO);
+        }
+      }
+      ResultSet rs = stmt.executeQuery("SELECT * FROM BatchTest");
+      for (int i = 1; i <= nb; i++) {
+        assertTrue(rs.next());
+        assertEquals(i, rs.getInt(1));
+        assertArrayEquals(arr, rs.getBytes(2));
+      }
+      assertFalse(rs.next());
+
+      // check same ending with error
+      stmt.execute("TRUNCATE BatchTest");
+      try (PreparedStatement prep =
+          con.prepareStatement("INSERT INTO BatchTest(t1, t2) VALUES (?,?)")) {
+        for (int i = 1; i <= nb; i++) {
+          prep.setInt(1, i);
+          prep.setBytes(2, arr);
+          prep.addBatch();
+        }
+        prep.setInt(1, nb);
+        prep.setBytes(2, arr);
+        prep.addBatch();
+
+        BatchUpdateException e =
+            Assertions.assertThrows(BatchUpdateException.class, prep::executeBatch);
+        int[] updateCounts = e.getUpdateCounts();
+        assertEquals(nb + 1, updateCounts.length);
+      }
+      con.rollback();
+      con.rollback();
+    }
+  }
+
+  @Test
+  public void batchWithError() throws SQLException {
+    try (Connection con = createCon("&useServerPrepStmts=false&useBulkStmts=false")) {
+      batchWithError(con);
+    }
+    try (Connection con = createCon("&useServerPrepStmts=false&useBulkStmts=true")) {
+      batchWithError(con);
+    }
+    try (Connection con = createCon("&useServerPrepStmts&useBulkStmts=false")) {
+      batchWithError(con);
+    }
+    try (Connection con = createCon("&useServerPrepStmts&useBulkStmts=true")) {
+      batchWithError(con);
+    }
+    try (Connection con =
+        createCon("&useServerPrepStmts=false&useBulkStmts=false&allowLocalInfile")) {
+      batchWithError(con);
+    }
+    try (Connection con =
+        createCon("&useServerPrepStmts=false&useBulkStmts=true&allowLocalInfile")) {
+      batchWithError(con);
+    }
+    try (Connection con = createCon("&useServerPrepStmts&useBulkStmts=false&allowLocalInfile")) {
+      batchWithError(con);
+    }
+    try (Connection con = createCon("&useServerPrepStmts&useBulkStmts=true&allowLocalInfile")) {
+      batchWithError(con);
+    }
+  }
+
+  private void batchWithError(Connection con) throws SQLException {
+    Assumptions.assumeTrue(isMariaDBServer());
+    Statement stmt = con.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS prepareError");
+    stmt.setFetchSize(3);
+    stmt.execute("CREATE TABLE prepareError(id int primary key, val varchar(10))");
+    stmt.execute("INSERT INTO prepareError(id, val) values (1, 'val1')");
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO prepareError(id, val) VALUES (?,?)")) {
+      prep.setInt(1, 1);
+      prep.setString(2, "val3");
+      prep.addBatch();
+      // Duplicate entry '1' for key 'PRIMARY'
+      assertThrows(BatchUpdateException.class, prep::executeBatch);
+    }
+  }
+
+  private class TimestampCal {
+    private Timestamp val;
+    private int id;
+
+    public TimestampCal(Timestamp val, int id) {
+      this.val = val;
+      this.id = id;
+    }
+
+    public Timestamp getVal() {
+      return val;
+    }
+
+    public int getId() {
+      return id;
+    }
+
+    @Override
+    public String toString() {
+      return "TimestampCal{" + "val=" + val + ", id=" + id + '}';
+    }
+  }
+
+  @Test
+  public void ensureCalendarSync() throws SQLException {
+    Assumptions.assumeTrue(isMariaDBServer() && !isXpand());
+    // to ensure that calendar is use at the same time, using BULK command
+    TimestampCal[] t1 = new TimestampCal[50];
+    for (int i = 0; i < 50; i++) {
+      t1[i] = new TimestampCal(Timestamp.valueOf((1970 + i) + "-01-31 12:00:00.0"), i);
+    }
+    TimestampCal[] t2 = new TimestampCal[50];
+    for (int i = 0; i < 50; i++) {
+      t2[i] = new TimestampCal(Timestamp.valueOf((1970 + i) + "-12-01 01:12:15.0"), i + 50);
+    }
+
+    Calendar cal = Calendar.getInstance();
+    sharedConn.createStatement().execute("START TRANSACTION");
+    int inserts = Stream.of(t1, t2).parallel().mapToInt(l -> insertTimestamp(l, cal)).sum();
+    assertEquals(100, inserts);
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM timestampCal order by ID");
+    for (int i = 0; i < 50; i++) {
+      rs.next();
+      assertEquals(t1[i].getVal().toString(), rs.getTimestamp(2, cal).toString());
+    }
+    for (int i = 0; i < 50; i++) {
+      rs.next();
+      assertEquals(t2[i].getVal().toString(), rs.getTimestamp(2, cal).toString());
+    }
+    sharedConn.commit();
+  }
+
+  private int insertTimestamp(TimestampCal[] vals, Calendar cal) {
+    try (Connection con = createCon()) {
+      try (PreparedStatement prep =
+          con.prepareStatement("INSERT INTO timestampCal(val, id) VALUES (?,?)")) {
+        for (int i = 0; i < vals.length; i++) {
+          prep.setTimestamp(1, vals[i].getVal(), cal);
+          prep.setInt(2, vals[i].getId());
+          prep.addBatch();
+        }
+        return prep.executeBatch().length;
+      }
+    } catch (SQLException e) {
+      e.printStackTrace();
+      return -1;
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/BlobTest.java b/src/test/java/org/mariadb/jdbc/integration/BlobTest.java
new file mode 100644
index 0000000..d78dfa8
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/BlobTest.java
@@ -0,0 +1,319 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.MariaDbBlob;
+
+public class BlobTest extends Common {
+
+  private final byte[] bytes = new byte[] {0, 1, 2, 3, 4, 5};
+
+  @Test
+  public void length() {
+    MariaDbBlob blob = new MariaDbBlob(bytes);
+    assertEquals(6, blob.length());
+
+    MariaDbBlob blob2 = new MariaDbBlob(bytes, 2, 3);
+    assertEquals(3, blob2.length());
+  }
+
+  @Test
+  public void getBytes() throws SQLException {
+    MariaDbBlob blob = new MariaDbBlob(bytes);
+    assertArrayEquals(bytes, blob.getBytes(1, 6));
+    assertArrayEquals(new byte[] {0, 1, 2, 3, 4, 5, 0}, blob.getBytes(1, 7));
+    assertArrayEquals(new byte[] {0, 1}, blob.getBytes(1, 2));
+
+    MariaDbBlob blob2 = new MariaDbBlob(bytes, 2, 3);
+    assertArrayEquals(new byte[] {2, 3, 4}, blob2.getBytes(1, 3));
+    assertArrayEquals(new byte[] {2, 3, 4, 0, 0, 0}, blob2.getBytes(1, 6));
+    assertArrayEquals(new byte[] {2, 3}, blob2.getBytes(1, 2));
+    assertArrayEquals(new byte[] {3, 4, 0}, blob2.getBytes(2, 3));
+    assertArrayEquals(new byte[] {3, 4, 0, 0, 0, 0}, blob2.getBytes(2, 6));
+    assertArrayEquals(new byte[] {3, 4}, blob2.getBytes(2, 2));
+
+    try {
+      blob2.getBytes(0, 3);
+      fail("must have thrown exception, min pos is 1");
+    } catch (SQLException sqle) {
+      // normal exception
+    }
+  }
+
+  @Test
+  public void getBinaryStream() throws SQLException {
+    MariaDbBlob blob = new MariaDbBlob(bytes);
+    assureInputStreamEqual(bytes, blob.getBinaryStream(1, 6));
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> assureInputStreamEqual(new byte[] {0, 1, 2, 3, 4, 5, 0}, blob.getBinaryStream(1, 7)),
+        "Out of range (position + length - 1 > streamSize)");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> assureInputStreamEqual(new byte[] {0, 1, 2, 3, 4, 5, 0}, blob.getBinaryStream(-2, 7)),
+        "Out of range (position should be > 0)");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> assureInputStreamEqual(new byte[] {0, 1, 2, 3, 4, 5, 0}, blob.getBinaryStream(20, 7)),
+        "Out of range (position > stream size)");
+
+    assureInputStreamEqual(new byte[] {0, 1}, blob.getBinaryStream(1, 2));
+
+    MariaDbBlob blob2 = new MariaDbBlob(bytes, 2, 3);
+    assureInputStreamEqual(new byte[] {2, 3, 4}, blob2.getBinaryStream(1, 3));
+    try {
+      assureInputStreamEqual(new byte[] {2, 3, 4, 0, 0, 0}, blob2.getBinaryStream(1, 6));
+      fail("must have thrown exception, max length is 3");
+    } catch (SQLException sqle) {
+      // normal exception
+    }
+    assureInputStreamEqual(new byte[] {2, 3}, blob2.getBinaryStream(1, 2));
+    try {
+      assureInputStreamEqual(new byte[] {3, 4, 0}, blob2.getBinaryStream(2, 3));
+    } catch (SQLException sqle) {
+      // normal exception
+    }
+    assureInputStreamEqual(new byte[] {3, 4}, blob2.getBinaryStream(2, 2));
+
+    try {
+      blob2.getBytes(0, 3);
+      fail("must have thrown exception, min pos is 1");
+    } catch (SQLException sqle) {
+      // normal exception
+    }
+  }
+
+  private void assureInputStreamEqual(byte[] expected, InputStream stream) {
+    try {
+      for (byte expectedVal : expected) {
+        int val = stream.read();
+        assertEquals(expectedVal, val);
+      }
+      assertEquals(-1, stream.read());
+    } catch (IOException ioe) {
+      ioe.printStackTrace();
+      fail();
+    }
+  }
+
+  @Test
+  public void position() throws SQLException {
+    MariaDbBlob blob = new MariaDbBlob(bytes);
+    assertEquals(5, blob.position(new byte[] {4, 5}, 2));
+
+    MariaDbBlob blob2 = new MariaDbBlob(bytes, 2, 4);
+    assertEquals(3, blob2.position(new byte[] {4, 5}, 2));
+    assertEquals(0, blob2.position(new byte[0], 2));
+    assertEquals(-1, blob2.position(new byte[] {4, 9}, 2));
+
+    assertEquals(3, blob2.position(new MariaDbBlob(new byte[] {4, 5}), 2));
+
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> blob2.position(new byte[] {4, 5}, -2),
+        "Out of range (position should be > 0, " + "but is -2)");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> blob2.position(new byte[] {4, 5}, 20),
+        "Out of range (start > stream size)");
+  }
+
+  @Test
+  public void setBytes() throws SQLException {
+    final byte[] otherBytes = new byte[] {10, 11, 12, 13};
+
+    MariaDbBlob blob = new MariaDbBlob(new byte[0]);
+    blob.setBytes(1, new byte[0]);
+    assertEquals(0, blob.length());
+    blob.setBytes(1, new byte[0], 0, 0);
+    assertEquals(0, blob.length());
+
+    blob = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5});
+    blob.setBytes(2, otherBytes);
+    assertArrayEquals(new byte[] {0, 10, 11, 12, 13, 5}, blob.getBytes(1, 6));
+
+    MariaDbBlob blob2 = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5});
+    blob2.setBytes(4, otherBytes);
+    assertArrayEquals(new byte[] {0, 1, 2, 10, 11, 12, 13}, blob2.getBytes(1, 7));
+
+    MariaDbBlob blob3 = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
+    blob3.setBytes(2, otherBytes);
+    assertArrayEquals(new byte[] {2, 10, 11, 12, 13, 0, 0}, blob3.getBytes(1, 7));
+
+    MariaDbBlob blob4 = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
+    blob4.setBytes(4, otherBytes);
+    assertArrayEquals(new byte[] {2, 3, 4, 10, 11, 12}, blob4.getBytes(1, 6));
+
+    try {
+      MariaDbBlob blob5 = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
+      blob5.setBytes(0, otherBytes);
+    } catch (SQLException sqle) {
+      // normal exception
+    }
+  }
+
+  @Test
+  public void setBytesOffset() throws SQLException {
+    final byte[] otherBytes = new byte[] {10, 11, 12, 13};
+
+    MariaDbBlob blob = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5});
+    blob.setBytes(2, otherBytes, 2, 3);
+    assertArrayEquals(new byte[] {0, 12, 13, 3, 4, 5}, blob.getBytes(1, 6));
+
+    MariaDbBlob blob2 = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5});
+    blob2.setBytes(4, otherBytes, 3, 2);
+    assertArrayEquals(new byte[] {0, 1, 2, 13, 4, 5, 0}, blob2.getBytes(1, 7));
+
+    MariaDbBlob blob3 = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 4);
+    blob3.setBytes(2, otherBytes, 2, 3);
+    assertArrayEquals(new byte[] {2, 12, 13, 5, 0, 0, 0}, blob3.getBytes(1, 7));
+
+    MariaDbBlob blob4 = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
+    blob4.setBytes(4, otherBytes, 2, 2);
+    assertArrayEquals(new byte[] {2, 3, 4, 12, 13, 0}, blob4.getBytes(1, 6));
+
+    MariaDbBlob blob5 = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
+    blob5.setBytes(4, otherBytes, 2, 20);
+    assertArrayEquals(new byte[] {2, 3, 4, 12, 13, 0}, blob5.getBytes(1, 6));
+
+    try {
+      MariaDbBlob blob6 = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
+      blob6.setBytes(0, otherBytes, 2, 3);
+    } catch (SQLException sqle) {
+      // normal exception
+    }
+  }
+
+  @Test
+  public void setBinaryStream() throws SQLException, IOException {
+    final byte[] otherBytes = new byte[] {10, 11, 12, 13};
+
+    MariaDbBlob blob = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5});
+    OutputStream out = blob.setBinaryStream(2);
+    out.write(otherBytes);
+    out.write(0x09);
+    out.write(0x08);
+    assertArrayEquals(new byte[] {0, 10, 11, 12, 13, 9, 8}, blob.getBytes(1, 7));
+
+    MariaDbBlob blob2 = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5});
+    OutputStream out2 = blob2.setBinaryStream(4);
+    out2.write(otherBytes);
+    assertArrayEquals(new byte[] {0, 1, 2, 10, 11, 12, 13}, blob2.getBytes(1, 7));
+
+    MariaDbBlob blob3 = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
+    OutputStream out3 = blob3.setBinaryStream(2);
+    out3.write(otherBytes);
+    assertArrayEquals(new byte[] {2, 10, 11, 12, 13, 0, 0}, blob3.getBytes(1, 7));
+
+    MariaDbBlob blob4 = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
+    OutputStream out4 = blob4.setBinaryStream(4);
+    out4.write(otherBytes);
+    assertArrayEquals(new byte[] {2, 3, 4, 10, 11, 12}, blob4.getBytes(1, 6));
+
+    MariaDbBlob blob5 = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
+    Common.assertThrowsContains(
+        SQLException.class, () -> blob5.setBinaryStream(0), "Invalid position in blob");
+    Common.assertThrowsContains(
+        IOException.class,
+        () -> blob5.setBinaryStream(2).write(new byte[] {1}, 0, -5),
+        "Invalid len -5");
+    Common.assertThrowsContains(
+        IOException.class,
+        () -> blob5.setBinaryStream(2).write(new byte[] {1}, -2, 1),
+        "Invalid offset -2");
+  }
+
+  @Test
+  public void setBinaryStreamOffset() throws SQLException, IOException {
+    final byte[] otherBytes = new byte[] {10, 11, 12, 13};
+
+    MariaDbBlob blob = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5});
+    OutputStream out = blob.setBinaryStream(2);
+    out.write(otherBytes, 2, 3);
+    assertArrayEquals(new byte[] {0, 12, 13, 3, 4, 5}, blob.getBytes(1, 6));
+
+    MariaDbBlob blob2 = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5});
+    OutputStream out2 = blob2.setBinaryStream(4);
+    out2.write(otherBytes, 3, 2);
+    assertArrayEquals(new byte[] {0, 1, 2, 13, 4, 5, 0}, blob2.getBytes(1, 7));
+
+    MariaDbBlob blob3 = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 4);
+    OutputStream out3 = blob3.setBinaryStream(2);
+    out3.write(otherBytes, 2, 3);
+    assertArrayEquals(new byte[] {2, 12, 13, 5, 0, 0, 0}, blob3.getBytes(1, 7));
+
+    MariaDbBlob blob4 = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
+    OutputStream out4 = blob4.setBinaryStream(4);
+    out4.write(otherBytes, 2, 2);
+    assertArrayEquals(new byte[] {2, 3, 4, 12, 13, 0}, blob4.getBytes(1, 6));
+
+    MariaDbBlob blob5 = new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
+    OutputStream out5 = blob5.setBinaryStream(4);
+    out5.write(otherBytes, 2, 20);
+    assertArrayEquals(new byte[] {2, 3, 4, 12, 13, 0}, blob5.getBytes(1, 6));
+  }
+
+  @Test
+  public void truncate() throws SQLException {
+    MariaDbBlob blob = new MariaDbBlob(bytes);
+    blob.truncate(20);
+    assertArrayEquals(bytes, blob.getBytes(1, 6));
+    blob.truncate(-5);
+    assertArrayEquals(bytes, blob.getBytes(1, 6));
+    blob.truncate(5);
+    assertArrayEquals(new byte[] {0, 1, 2, 3, 4, 0, 0}, blob.getBytes(1, 7));
+    blob.truncate(0);
+    assertArrayEquals(new byte[] {0, 0}, blob.getBytes(1, 2));
+
+    MariaDbBlob blob2 = new MariaDbBlob(bytes, 2, 3);
+    blob2.truncate(20);
+    assertArrayEquals(new byte[] {2, 3, 4}, blob2.getBytes(1, 3));
+    blob2.truncate(2);
+    assertArrayEquals(new byte[] {2, 3, 0, 0, 0, 0}, blob2.getBytes(1, 6));
+
+    blob2.truncate(1);
+    assertArrayEquals(new byte[] {2, 0}, blob2.getBytes(1, 2));
+  }
+
+  @Test
+  public void free() {
+    MariaDbBlob blob = new MariaDbBlob(bytes);
+    blob.free();
+    assertEquals(0, blob.length());
+  }
+
+  @Test
+  public void expectedErrors() {
+    Common.assertThrowsContains(
+        IllegalArgumentException.class, () -> new MariaDbBlob(null), "byte array is null");
+    Common.assertThrowsContains(
+        IllegalArgumentException.class, () -> new MariaDbBlob(null, 0, 2), "byte array is null");
+  }
+
+  @Test
+  public void equal() {
+    MariaDbBlob blob = new MariaDbBlob(bytes);
+    assertEquals(blob, blob);
+    assertEquals(new MariaDbBlob(bytes), blob);
+    assertFalse(blob.equals(null));
+    assertFalse(blob.equals(""));
+    byte[] bytes = new byte[] {5, 1, 2, 3, 4, 5};
+    assertNotEquals(new MariaDbBlob(bytes), blob);
+    assertNotEquals(new MariaDbBlob(new byte[] {5, 1}), blob);
+  }
+
+  @Test
+  public void hashCodeTest() {
+    MariaDbBlob blob = new MariaDbBlob(bytes);
+    assertEquals(-859797942, blob.hashCode());
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/ClientPreparedStatementParsingTest.java b/src/test/java/org/mariadb/jdbc/integration/ClientPreparedStatementParsingTest.java
new file mode 100644
index 0000000..e3b04b4
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/ClientPreparedStatementParsingTest.java
@@ -0,0 +1,287 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.*;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.util.ClientParser;
+import org.mariadb.jdbc.util.constants.ServerStatus;
+
+public class ClientPreparedStatementParsingTest extends Common {
+
+  private void checkParsing(String sql, int paramNumber, String[] partsMulti) throws Exception {
+    boolean noBackslashEscapes =
+        (sharedConn.getContext().getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES) > 0;
+    ClientParser parser = ClientParser.parameterParts(sql, noBackslashEscapes);
+    assertEquals(paramNumber, parser.getParamCount());
+    int pos = 0;
+    int paramPos = parser.getQuery().length;
+    for (int i = 0; i < parser.getParamPositions().size(); i++) {
+      paramPos = parser.getParamPositions().get(i);
+      assertEquals(partsMulti[i], new String(parser.getQuery(), pos, paramPos - pos));
+      pos = paramPos + 1;
+    }
+    assertEquals(
+        partsMulti[partsMulti.length - 1],
+        new String(parser.getQuery(), pos, parser.getQuery().length - pos));
+  }
+
+  @Test
+  public void stringEscapeParsing() throws Exception {
+    checkParsing(
+        "select '\\'' as a, ? as b, \"\\\"\" as c, ? as d",
+        2,
+        new String[] {"select '\\'' as a, ", " as b, \"\\\"\" as c, ", " as d"});
+  }
+
+  @Test
+  public void testRewritableWithConstantParameter() throws Exception {
+    checkParsing(
+        "INSERT INTO TABLE(col1,col2,col3,col4, col5) VALUES (9, ?, 5, ?, 8) ON DUPLICATE KEY"
+            + " UPDATE col2=col2+10",
+        2,
+        new String[] {
+          "INSERT INTO TABLE(col1,col2,col3,col4, col5) VALUES (9, ",
+          ", 5, ",
+          ", 8) ON DUPLICATE KEY UPDATE col2=col2+10"
+        });
+  }
+
+  @Test
+  public void testComment() throws Exception {
+    checkParsing(
+        "/* insert Select INSERT INTO tt VALUES (?,?,?,?)  */"
+            + " INSERT into "
+            + "/* insert Select INSERT INTO tt VALUES (?,?,?,?)  */"
+            + " tt VALUES "
+            + "/* insert Select INSERT INTO tt VALUES (?,?,?,?)  */"
+            + " (?) "
+            + "/* insert Select INSERT INTO tt VALUES (?,?,?,?)  */",
+        1,
+        new String[] {
+          "/* insert Select INSERT INTO tt VALUES (?,?,?,?)  */"
+              + " INSERT into "
+              + "/* insert Select INSERT INTO tt VALUES (?,?,?,?)  */"
+              + " tt VALUES "
+              + "/* insert Select INSERT INTO tt VALUES (?,?,?,?)  */"
+              + " (",
+          ") /* insert Select INSERT INTO tt VALUES (?,?,?,?)  */"
+        });
+  }
+
+  @Test
+  public void testRewritableWithConstantParameterAndParamAfterValue() throws Exception {
+    checkParsing(
+        "INSERT INTO TABLE(col1,col2,col3,col4, col5) VALUES (9, ?, 5, ?, 8) ON DUPLICATE KEY"
+            + " UPDATE col2=?",
+        3,
+        new String[] {
+          "INSERT INTO TABLE(col1,col2,col3,col4, col5) VALUES (9, ",
+          ", 5, ",
+          ", 8) ON DUPLICATE KEY UPDATE col2=",
+          ""
+        });
+  }
+
+  @Test
+  public void testRewritableMultipleInserts() throws Exception {
+    checkParsing(
+        "INSERT INTO TABLE(col1,col2) VALUES (?, ?), (?, ?)",
+        4,
+        new String[] {"INSERT INTO TABLE(col1,col2) VALUES (", ", ", "), (", ", ", ")"});
+  }
+
+  @Test
+  public void testCall() throws Exception {
+    checkParsing("CALL dsdssd(?,?)", 2, new String[] {"CALL dsdssd(", ",", ")"});
+  }
+
+  @Test
+  public void testUpdate() throws Exception {
+    checkParsing(
+        "UPDATE MultiTestt4 SET test = ? WHERE test = ?",
+        2,
+        new String[] {"UPDATE MultiTestt4 SET test = ", " WHERE test = ", ""});
+  }
+
+  @Test
+  public void testInsertSelect() throws Exception {
+    checkParsing(
+        "insert into test_insert_select ( field1) (select  TMP.field1 from "
+            + "(select CAST(? as binary) `field1` from dual) TMP)",
+        1,
+        new String[] {
+          "insert into test_insert_select ( field1) (select  TMP.field1 from (select CAST(",
+          " as binary) `field1` from dual) TMP)"
+        });
+  }
+
+  @Test
+  public void testWithoutParameter() throws Exception {
+    checkParsing("SELECT testFunction()", 0, new String[] {"SELECT testFunction()"});
+  }
+
+  @Test
+  public void testWithoutParameterAndParenthesis() throws Exception {
+    checkParsing("SELECT 1", 0, new String[] {"SELECT 1"});
+  }
+
+  @Test
+  public void testWithoutParameterAndValues() throws Exception {
+    checkParsing("INSERT INTO tt VALUES (1)", 0, new String[] {"INSERT INTO tt VALUES (1)"});
+  }
+
+  @Test
+  public void testSemiColon() throws Exception {
+    checkParsing(
+        "INSERT INTO tt (tt) VALUES (?); INSERT INTO tt (tt) VALUES ('multiple')",
+        1,
+        new String[] {
+          "INSERT INTO tt (tt) VALUES (", "); INSERT INTO tt (tt) VALUES ('multiple')"
+        });
+  }
+
+  @Test
+  public void testSemicolonRewritableIfAtEnd() throws Exception {
+    checkParsing(
+        "INSERT INTO table (column1) VALUES (?); ",
+        1,
+        new String[] {"INSERT INTO table (column1) VALUES (", "); "});
+  }
+
+  @Test
+  public void testSemicolonNotRewritableIfNotAtEnd() throws Exception {
+    checkParsing(
+        "INSERT INTO table (column1) VALUES (?); SELECT 1",
+        1,
+        new String[] {"INSERT INTO table (column1) VALUES (", "); SELECT 1"});
+  }
+
+  @Test
+  public void testError() throws Exception {
+    checkParsing(
+        "INSERT INTO tt (tt) VALUES (?); INSERT INTO tt (tt) VALUES ('multiple')",
+        1,
+        new String[] {
+          "INSERT INTO tt (tt) VALUES (", "); INSERT INTO tt (tt) VALUES ('multiple')"
+        });
+  }
+
+  @Test
+  public void testLineComment() throws Exception {
+    checkParsing(
+        "INSERT INTO tt (tt) VALUES (?) --fin",
+        1,
+        new String[] {"INSERT INTO tt (tt) VALUES (", ") --fin"});
+  }
+
+  @Test
+  public void testLineCommentFinished() throws Exception {
+    checkParsing(
+        "INSERT INTO tt (tt) VALUES --fin\n (?)",
+        1,
+        new String[] {"INSERT INTO tt (tt) VALUES --fin\n (", ")"});
+  }
+
+  @Test
+  public void testSelect1() throws Exception {
+    checkParsing("SELECT 1", 0, new String[] {"SELECT 1"});
+  }
+
+  @Test
+  public void rewriteBatchedError() throws Exception {
+    try (Connection connection =
+        createCon("&rewriteBatchedStatements=true&dumpQueriesOnException")) {
+      PreparedStatement preparedStatement =
+          connection.prepareStatement("INSERT INTO errorTable (a, b) VALUES (?, ?, ?)");
+
+      preparedStatement.setString(1, "1");
+      preparedStatement.setString(2, "2");
+      preparedStatement.setString(3, "3");
+      preparedStatement.addBatch();
+      try {
+        preparedStatement.executeBatch();
+        fail("must have thrown error since parameters are not good");
+      } catch (SQLException e) {
+        assertTrue(
+            e.getCause()
+                .getMessage()
+                .contains("Query is: INSERT INTO errorTable (a, b) VALUES (?, ?, ?)"));
+      }
+    }
+  }
+
+  @Test
+  public void errorException() throws Exception {
+    try (Connection connection = createCon("dumpQueriesOnException")) {
+      ensureErrorException(connection);
+    }
+  }
+
+  private void ensureErrorException(Connection connection) throws SQLException {
+    connection.createStatement().execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement pstmt =
+        connection.prepareStatement("UPDATE unknownTable SET col1 = ?, col2 = 0 WHERE col3 = ?");
+    pstmt.setInt(1, 10);
+    pstmt.setInt(2, 20);
+    pstmt.addBatch();
+    pstmt.setInt(1, 100);
+    pstmt.setInt(2, 200);
+    try {
+      pstmt.executeBatch();
+      fail("Must have thrown error");
+    } catch (SQLException sqle) {
+      assertTrue(
+          sqle.getMessage().contains("doesn't exist")
+              || sqle.getMessage().contains("Relation not found: \"testj\".\"unknownTable\"")
+              || sqle.getMessage().contains("Unknown prepared statement handler"),
+          sqle.getMessage());
+      assertTrue(
+          sqle.getMessage()
+              .contains("Query is: UPDATE unknownTable SET col1 = ?, col2 = 0 WHERE col3 = ?"),
+          sqle.getMessage());
+    }
+  }
+
+  @Test
+  public void testLastInsertId() throws Exception {
+    checkParsing(
+        "INSERT INTO tt (tt, tt2) VALUES (LAST_INSERT_ID(), ?)",
+        1,
+        new String[] {"INSERT INTO tt (tt, tt2) VALUES (LAST_INSERT_ID(), ", ")"});
+  }
+
+  @Test
+  public void testValuesForPartition() throws Exception {
+    checkParsing(
+        "ALTER table test_partitioning PARTITION BY RANGE COLUMNS( created_at ) "
+            + "(PARTITION test_p201605 VALUES LESS THAN ('2016-06-01'))",
+        0,
+        new String[] {
+          "ALTER table test_partitioning PARTITION BY RANGE COLUMNS( created_at ) "
+              + "(PARTITION test_p201605 VALUES LESS THAN ('2016-06-01'))"
+        });
+  }
+
+  @Test
+  public void testParse() throws Exception {
+    checkParsing(
+        "INSERT INTO `myTable` VALUES ('\\n"
+            + "\"\\'', \"'\\\n"
+            + "\\\"\")  \n"
+            + " // comment\n"
+            + " , ('a', 'b') # EOL comment",
+        0,
+        new String[] {
+          "INSERT INTO `myTable` VALUES ('\\n"
+              + "\"\\'', \"'\\\n"
+              + "\\\"\")  \n"
+              + " // comment\n"
+              + " , ('a', 'b') # EOL comment"
+        });
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/ClobTest.java b/src/test/java/org/mariadb/jdbc/integration/ClobTest.java
new file mode 100644
index 0000000..193beeb
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/ClobTest.java
@@ -0,0 +1,405 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.sql.Clob;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.MariaDbClob;
+
+public class ClobTest extends Common {
+
+  private final byte[] bytes = "abc£de🙏fgh".getBytes(StandardCharsets.UTF_8);
+
+  @Test
+  public void length() {
+    MariaDbClob clob = new MariaDbClob(bytes);
+    assertEquals(11, clob.length());
+
+    MariaDbClob clob2 = new MariaDbClob(bytes, 2, 3);
+    assertEquals(2, clob2.length());
+  }
+
+  @Test
+  public void getSubString() throws SQLException {
+    MariaDbClob clob = new MariaDbClob(bytes);
+    assertEquals("abc£de🙏", clob.getSubString(1, 8));
+    assertEquals("abc£de🙏fgh", clob.getSubString(1, 21));
+    assertEquals("abc£de🙏fgh", clob.getSubString(1, (int) clob.length()));
+    assertEquals("ab", clob.getSubString(1, 2));
+    assertEquals("🙏", clob.getSubString(7, 2));
+
+    MariaDbClob clob2 = new MariaDbClob(bytes, 6, 6);
+
+    assertEquals("e🙏f", clob2.getSubString(1, 20));
+    assertEquals("🙏f", clob2.getSubString(2, 3));
+
+    try {
+      clob2.getSubString(0, 3);
+      fail("must have thrown exception, min pos is 1");
+    } catch (SQLException sqle) {
+      // normal exception
+    }
+  }
+
+  @Test
+  public void getCharacterStream() throws SQLException {
+    MariaDbClob clob = new MariaDbClob(bytes);
+    assureReaderEqual("abc£de🙏", clob.getCharacterStream(1, 8));
+    assureReaderEqual("abc£de🙏fgh", clob.getCharacterStream(1, 11));
+    try {
+      assureReaderEqual("abc£de🙏fgh", clob.getCharacterStream(1, 20));
+      fail("must have throw exception, length > to number of characters");
+    } catch (SQLException sqle) {
+      // normal error
+    }
+    assureReaderEqual("bc£de🙏", clob.getCharacterStream(2, 7));
+
+    MariaDbClob clob2 = new MariaDbClob(bytes, 2, 9);
+    assureReaderEqual("c£de🙏", clob2.getCharacterStream(1, 6));
+    try {
+      assureReaderEqual("c£de🙏fg", clob2.getCharacterStream(1, 20));
+      fail("must have throw exception, length > to number of characters");
+    } catch (SQLException sqle) {
+      // normal error
+    }
+
+    assureReaderEqual("de🙏", clob2.getCharacterStream(3, 4));
+  }
+
+  private void assureReaderEqual(String expectedStr, Reader reader) {
+    try {
+      char[] expected = expectedStr.toCharArray();
+      char[] readArr = new char[expected.length];
+      assertEquals(expected.length, reader.read(readArr));
+      assertArrayEquals(expected, readArr);
+    } catch (IOException ioe) {
+      ioe.printStackTrace();
+      fail();
+    }
+  }
+
+  @Test
+  public void setCharacterStream() throws SQLException, IOException {
+    final byte[] bytes = "abcde🙏fgh".getBytes(StandardCharsets.UTF_8);
+    MariaDbClob clob = new MariaDbClob(bytes);
+    assureReaderEqual("abcde🙏", clob.getCharacterStream(1, 7));
+
+    Writer writer = clob.setCharacterStream(2);
+    writer.write("tuvxyz", 2, 3);
+    writer.flush();
+    assertEquals("avxye🙏", clob.getSubString(1, 7));
+
+    clob = new MariaDbClob(bytes);
+
+    writer = clob.setCharacterStream(2);
+    writer.write("1234567890lmnopqrstu", 1, 19);
+    writer.flush();
+    assertEquals("a234567890lmnopqrstu", clob.getSubString(1, 100));
+  }
+
+  @Test
+  public void position() {
+    MariaDbClob clob = new MariaDbClob(bytes);
+    assertEquals(5, clob.position("de", 2));
+    assertEquals(5, clob.position((Clob) new MariaDbClob("de".getBytes()), 2));
+
+    clob = new MariaDbClob(bytes, 2, 10);
+    assertEquals(5, clob.position("🙏", 2));
+  }
+
+  @Test
+  public void setString() throws SQLException {
+    final byte[] bytes = "abcd£e🙏fgh".getBytes(StandardCharsets.UTF_8);
+    final MariaDbClob clob = new MariaDbClob(bytes);
+    assureReaderEqual("abcd£e🙏", clob.getCharacterStream(1, 8));
+    clob.setString(2, "zuv");
+    assertEquals("azuv£e🙏", clob.getSubString(1, 8));
+    clob.setString(9, "zzz");
+    assertEquals("azuv£e🙏fzzz", clob.getSubString(1, 12));
+
+    MariaDbClob clob2 = new MariaDbClob("abcde🙏fgh".getBytes(StandardCharsets.UTF_8), 2, 9);
+    assureReaderEqual("cde🙏fg", clob2.getCharacterStream(1, 7));
+    assertEquals("cde🙏fg", clob2.getSubString(1, 7));
+
+    clob2.setString(2, "zg");
+    assertEquals("czg🙏f", clob2.getSubString(1, 6));
+    clob2.setString(7, "zzz");
+    assertEquals("czg🙏fgzzz", clob2.getSubString(1, 12));
+
+    Common.assertThrowsContains(
+        SQLException.class, () -> clob2.setString(2, "abcd", 2, -2), "len must be > 0");
+    clob2.setString(2, "abcd", 2, 2);
+    assertEquals("ccd🙏f", clob2.getSubString(1, 6));
+    clob2.setString(2, "opml", 3, 200);
+    assertEquals("cld🙏f", clob2.getSubString(1, 6));
+
+    clob2.setString(5, "අข\uD800\uDFA2");
+    assertEquals("cld🙏අข\uD800\uDFA2", clob2.getSubString(1, 20));
+    assertEquals(9, clob2.length());
+    clob2.setString(6, "ข\uD800\uDFA2");
+    assertEquals("cld🙏අข\uD800\uDFA2", clob2.getSubString(1, 20));
+    clob2.setString(7, "\uD800\uDFA2");
+    assertEquals("cld🙏අข\uD800\uDFA2", clob2.getSubString(1, 20));
+    clob2.truncate(9);
+    assertEquals("cld🙏අข\uD800\uDFA2", clob2.getSubString(1, 20));
+    clob2.truncate(7);
+    assertEquals("cld🙏අข", clob2.getSubString(1, 20));
+    clob2.truncate(6);
+    assertEquals("cld🙏අ", clob2.getSubString(1, 20));
+    clob2.truncate(5);
+    assertEquals("cld🙏", clob2.getSubString(1, 20));
+    clob2.truncate(3);
+    assertEquals("cld", clob2.getSubString(1, 20));
+
+    Common.assertThrowsContains(
+        SQLException.class, () -> clob.setString(-1, "7"), "position must be >= 0");
+    Common.assertThrowsContains(
+        SQLException.class, () -> clob.setString(1, null), "cannot add null string");
+    Common.assertThrowsContains(
+        SQLException.class, () -> clob.setString(-1, null, 1, 2), "cannot add null string");
+    Common.assertThrowsContains(
+        SQLException.class, () -> clob.setString(0, "dd", -1, 2), "offset must be >= 0");
+    Common.assertThrowsContains(
+        SQLException.class, () -> clob.getSubString(-1, 7), "position must be >= 1");
+    Common.assertThrowsContains(
+        SQLException.class, () -> clob.getSubString(1, -7), "length must be > 0");
+    Common.assertThrowsContains(
+        SQLException.class, () -> clob.setString(-2, "rrr"), "position must be >= 0");
+  }
+
+  @Test
+  public void setAsciiStream() throws SQLException, IOException {
+    final byte[] bytes = "abcde🙏fgh".getBytes(StandardCharsets.UTF_8);
+    MariaDbClob clob = new MariaDbClob(bytes);
+    assureReaderEqual("abcde🙏", clob.getCharacterStream(1, 7));
+
+    OutputStream stream = clob.setAsciiStream(2);
+    stream.write("tuvxyz".getBytes(), 2, 3);
+    stream.flush();
+
+    assertEquals("avxye🙏", clob.getSubString(1, 7));
+
+    clob = new MariaDbClob(bytes);
+
+    stream = clob.setAsciiStream(2);
+    stream.write("1234567890lmnopqrstu".getBytes(), 1, 19);
+    stream.flush();
+    assertEquals("a234567890lmnopqrstu", clob.getSubString(1, 100));
+
+    MariaDbClob clob2 = new MariaDbClob(bytes);
+    InputStream stream2 = clob2.getAsciiStream();
+    byte[] b = new byte[12];
+    stream2.read(b);
+    assertArrayEquals(bytes, b);
+  }
+
+  @Test
+  public void wrongUtf8() {
+    final byte[] utf8Wrong2bytes = new byte[] {0x08, (byte) 0xFF, (byte) 0x6F, (byte) 0x6F};
+    final byte[] utf8Wrong3bytes =
+        new byte[] {0x07, (byte) 0x0a, (byte) 0xff, (byte) 0x6F, (byte) 0x6F};
+    final byte[] utf8Wrong4bytes =
+        new byte[] {0x10, (byte) 0x20, (byte) 0x0a, (byte) 0xff, (byte) 0x6F, (byte) 0x6F};
+    final byte[] utf8Wrong4bytes2 = new byte[] {-16, (byte) -97, (byte) -103};
+
+    Common.assertThrowsContains(
+        UncheckedIOException.class,
+        () -> new MariaDbClob(utf8Wrong2bytes).length(),
+        "invalid UTF8");
+    Common.assertThrowsContains(
+        UncheckedIOException.class,
+        () -> new MariaDbClob(new byte[] {(byte) 225}).length(),
+        "invalid UTF8");
+
+    Common.assertThrowsContains(
+        UncheckedIOException.class,
+        () -> new MariaDbClob(utf8Wrong3bytes).length(),
+        "invalid UTF8");
+    Common.assertThrowsContains(
+        UncheckedIOException.class,
+        () -> new MariaDbClob(utf8Wrong4bytes).length(),
+        "invalid UTF8");
+    Common.assertThrowsContains(
+        UncheckedIOException.class,
+        () -> new MariaDbClob(new byte[] {(byte) 225}).truncate(2),
+        "invalid UTF8");
+    Common.assertThrowsContains(
+        UncheckedIOException.class,
+        () -> new MariaDbClob(utf8Wrong2bytes).truncate(2),
+        "invalid UTF8");
+    Common.assertThrowsContains(
+        UncheckedIOException.class,
+        () -> new MariaDbClob(utf8Wrong3bytes).truncate(3),
+        "invalid UTF8");
+    Common.assertThrowsContains(
+        UncheckedIOException.class,
+        () -> new MariaDbClob(utf8Wrong4bytes2).truncate(4),
+        "invalid UTF8");
+  }
+
+  @Test
+  public void setBinaryStream() throws SQLException, IOException {
+    final byte[] otherBytes = new byte[] {10, 11, 12, 13};
+
+    MariaDbClob blob = new MariaDbClob(new byte[] {0, 1, 2, 3, 4, 5});
+    OutputStream out = blob.setBinaryStream(2);
+    out.write(otherBytes);
+    assertArrayEquals(new byte[] {0, 10, 11, 12, 13, 5}, blob.getBytes(1, 6));
+
+    MariaDbClob blob2 = new MariaDbClob(new byte[] {0, 1, 2, 3, 4, 5});
+    OutputStream out2 = blob2.setBinaryStream(4);
+    out2.write(otherBytes);
+    assertArrayEquals(new byte[] {0, 1, 2, 10, 11, 12, 13}, blob2.getBytes(1, 7));
+
+    MariaDbClob blob3 = new MariaDbClob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
+    OutputStream out3 = blob3.setBinaryStream(2);
+    out3.write(otherBytes);
+    assertArrayEquals(new byte[] {2, 10, 11, 12, 13, 0, 0}, blob3.getBytes(1, 7));
+
+    MariaDbClob blob4 = new MariaDbClob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
+    OutputStream out4 = blob4.setBinaryStream(4);
+    out4.write(otherBytes);
+    assertArrayEquals(new byte[] {2, 3, 4, 10, 11, 12}, blob4.getBytes(1, 6));
+
+    try {
+      MariaDbClob blob5 = new MariaDbClob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
+      blob5.setBinaryStream(0);
+    } catch (SQLException sqle) {
+      // normal exception
+    }
+  }
+
+  @Test
+  public void setBinaryStreamOffset() throws SQLException, IOException {
+    final byte[] otherBytes = new byte[] {10, 11, 12, 13};
+
+    MariaDbClob blob = new MariaDbClob(new byte[] {0, 1, 2, 3, 4, 5});
+    OutputStream out = blob.setBinaryStream(2);
+    out.write(otherBytes, 2, 3);
+    assertArrayEquals(new byte[] {0, 12, 13, 3, 4, 5}, blob.getBytes(1, 6));
+
+    MariaDbClob blob2 = new MariaDbClob(new byte[] {0, 1, 2, 3, 4, 5});
+    OutputStream out2 = blob2.setBinaryStream(4);
+    out2.write(otherBytes, 3, 2);
+    assertArrayEquals(new byte[] {0, 1, 2, 13, 4, 5, 0}, blob2.getBytes(1, 7));
+
+    MariaDbClob blob3 = new MariaDbClob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 4);
+    OutputStream out3 = blob3.setBinaryStream(2);
+    out3.write(otherBytes, 2, 3);
+    assertArrayEquals(new byte[] {2, 12, 13, 5, 0, 0, 0}, blob3.getBytes(1, 7));
+
+    MariaDbClob blob4 = new MariaDbClob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
+    OutputStream out4 = blob4.setBinaryStream(4);
+    out4.write(otherBytes, 2, 2);
+    assertArrayEquals(new byte[] {2, 3, 4, 12, 13, 0}, blob4.getBytes(1, 6));
+
+    MariaDbClob blob5 = new MariaDbClob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
+    OutputStream out5 = blob5.setBinaryStream(4);
+    out5.write(otherBytes, 2, 20);
+    assertArrayEquals(new byte[] {2, 3, 4, 12, 13, 0}, blob5.getBytes(1, 6));
+  }
+
+  @Test
+  public void truncate() throws SQLException {
+    MariaDbClob clob = new MariaDbClob(bytes);
+    clob.truncate(20);
+    assertEquals("abc£de🙏f", clob.getSubString(1, 9));
+    clob.truncate(8);
+    assertEquals("abc£de🙏", clob.getSubString(1, 9));
+    assertEquals("abc£de🙏", clob.getSubString(1, 8));
+    clob.truncate(7);
+    assertEquals("abc£de�", clob.getSubString(1, 9));
+    clob.truncate(6);
+    assertEquals("abc£de", clob.getSubString(1, 9));
+    clob.truncate(4);
+    assertEquals("abc£", clob.getSubString(1, 8));
+    clob.truncate(3);
+    assertEquals("abc", clob.getSubString(1, 8));
+    clob.truncate(0);
+    assertEquals("", clob.getSubString(1, 8));
+
+    MariaDbClob clob2 = new MariaDbClob("abc£de🙏fgh".getBytes(StandardCharsets.UTF_8), 2, 10);
+    clob2.truncate(20);
+    assertEquals("c£de🙏f", clob2.getSubString(1, 9));
+    clob2.truncate(6);
+    assertEquals("c£de🙏", clob2.getSubString(1, 9));
+    clob2.truncate(5);
+    assertEquals("c£de�", clob2.getSubString(1, 9));
+    clob2.truncate(4);
+    assertEquals("c£de", clob2.getSubString(1, 9));
+    clob2.truncate(0);
+    assertEquals("", clob2.getSubString(1, 7));
+  }
+
+  @Test
+  public void free() {
+    MariaDbClob blob = new MariaDbClob(bytes);
+    blob.free();
+    assertEquals(0, blob.length());
+  }
+
+  @Test
+  @SuppressWarnings("checkstyle:AvoidEscapedUnicodeCharacters")
+  public void clobLength() throws Exception {
+    Statement stmt = sharedConn.createStatement();
+    try (ResultSet rs =
+        stmt.executeQuery("SELECT 'ab$c', 'ab¢c', 'abहc', 'ab\uD801\uDC37c', 'ab𐍈c' from dual")) {
+      while (rs.next()) {
+
+        Clob clob1 = rs.getClob(1);
+        assertEquals(4, clob1.length());
+
+        Clob clob2 = rs.getClob(2);
+        assertEquals(4, clob2.length());
+
+        Clob clob3 = rs.getClob(3);
+        assertEquals(4, clob3.length());
+
+        Clob clob4 = rs.getClob(4);
+        assertEquals(5, clob4.length());
+
+        Clob clob5 = rs.getClob(5);
+        assertEquals(5, clob5.length());
+
+        clob1.truncate(3);
+        clob2.truncate(3);
+        clob3.truncate(3);
+        clob4.truncate(3);
+        clob5.truncate(3);
+
+        assertEquals(3, clob1.length());
+        assertEquals(3, clob2.length());
+        assertEquals(3, clob3.length());
+        assertEquals(3, clob4.length());
+        assertEquals(3, clob5.length());
+
+        assertEquals("ab$", clob1.getSubString(1, 3));
+        assertEquals("ab¢", clob2.getSubString(1, 3));
+        assertEquals("abह", clob3.getSubString(1, 3));
+        assertEquals("ab�", clob4.getSubString(1, 3));
+        assertEquals("ab�", clob5.getSubString(1, 3));
+      }
+    }
+  }
+
+  @Test
+  public void equal() {
+    MariaDbClob clob = new MariaDbClob(bytes);
+    assertEquals(clob, clob);
+    assertEquals(new MariaDbClob(bytes), clob);
+    assertFalse(clob.equals(null));
+    assertFalse(clob.equals(""));
+    byte[] bytes = "Abc£de🙏fgh".getBytes(StandardCharsets.UTF_8);
+    assertNotEquals(new MariaDbClob(bytes), clob);
+    assertNotEquals(new MariaDbClob("Abc".getBytes(StandardCharsets.UTF_8)), clob);
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/Common.java b/src/test/java/org/mariadb/jdbc/integration/Common.java
new file mode 100644
index 0000000..b0003e8
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/Common.java
@@ -0,0 +1,287 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Properties;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.extension.*;
+import org.junit.jupiter.api.function.Executable;
+import org.mariadb.jdbc.*;
+import org.mariadb.jdbc.export.HaMode;
+import org.mariadb.jdbc.export.SslMode;
+import org.mariadb.jdbc.integration.tools.TcpProxy;
+
+public class Common {
+
+  public static Connection sharedConn;
+  public static Connection sharedConnBinary;
+  public static String hostname;
+  public static int port;
+  public static String user;
+  public static String password;
+  public static String database;
+  public static String defaultOther;
+  public static TcpProxy proxy;
+  public static String mDefUrl;
+  private static Instant initialTest;
+
+  static {
+    try (InputStream inputStream =
+        Common.class.getClassLoader().getResourceAsStream("conf.properties")) {
+      Properties prop = new Properties();
+      prop.load(inputStream);
+      String val = System.getenv("TEST_REQUIRE_TLS");
+      if ("1".equals(val)) {
+        String cert = System.getenv("TEST_DB_SERVER_CERT");
+        defaultOther = "&sslMode=verify-full&serverSslCert=" + cert;
+      } else {
+        defaultOther = get("DB_OTHER", prop);
+      }
+      hostname = get("DB_HOST", prop);
+      user = get("DB_USER", prop);
+      port = Integer.parseInt(get("DB_PORT", prop));
+      password = get("DB_PASSWORD", prop);
+      database = get("DB_DATABASE", prop);
+      mDefUrl =
+          password == null || password.isEmpty()
+              ? String.format(
+                  "jdbc:mariadb://%s:%s/%s?user=%s%s", hostname, port, database, user, defaultOther)
+              : String.format(
+                  "jdbc:mariadb://%s:%s/%s?user=%s&password=%s%s",
+                  hostname, port, database, user, password, defaultOther);
+
+    } catch (IOException io) {
+      io.printStackTrace();
+    }
+  }
+
+  private static String get(String name, Properties prop) {
+    String val = System.getenv("TEST_" + name);
+    if (val == null) val = System.getProperty("TEST_" + name);
+    if (val == null) val = prop.getProperty(name);
+    return val;
+  }
+
+  @BeforeAll
+  public static void beforeAll() throws Exception {
+    sharedConn = (Connection) DriverManager.getConnection(mDefUrl);
+    String binUrl = mDefUrl + (mDefUrl.indexOf("?") > 0 ? "&" : "?") + "useServerPrepStmts=true";
+    sharedConnBinary = (Connection) DriverManager.getConnection(binUrl);
+  }
+
+  @AfterAll
+  public static void afterEAll() throws SQLException {
+    if (sharedConn != null) {
+      sharedConn.close();
+      sharedConnBinary.close();
+    }
+    if (proxy != null) {
+      proxy.forceClose();
+    }
+  }
+
+  public static boolean isMariaDBServer() {
+    return sharedConn.getContext().getVersion().isMariaDBServer();
+  }
+
+  public static boolean hasCapability(long capability) {
+    return sharedConn.getContext().hasClientCapability(capability);
+  }
+
+  public static boolean runLongTest() {
+    String runLongTest = System.getenv("RUN_LONG_TEST");
+    if (runLongTest != null) {
+      return Boolean.parseBoolean(runLongTest);
+    }
+    return false;
+  }
+
+  public static boolean isXpand() {
+    String srv = System.getenv("srv");
+    if (srv != null) {
+      return "xpand".equals(srv);
+    }
+    return sharedConn.getContext().getVersion().isMariaDBServer()
+        && sharedConn.getContext().getVersion().getQualifier().toLowerCase().contains("xpand");
+  }
+
+  public static boolean minVersion(int major, int minor, int patch) {
+    return sharedConn.getContext().getVersion().versionGreaterOrEqual(major, minor, patch);
+  }
+
+  public static boolean exactVersion(int major, int minor, int patch) {
+    return sharedConn.getContext().getVersion().getMajorVersion() == major
+        && sharedConn.getContext().getVersion().getMinorVersion() == minor
+        && sharedConn.getContext().getVersion().getPatchVersion() == patch;
+  }
+
+  public static Connection createCon() throws SQLException {
+    return (Connection) DriverManager.getConnection(mDefUrl);
+  }
+
+  public static void createSequenceTables() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    boolean seq10_ok = false;
+    boolean seq10_000_ok = false;
+    try {
+      ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM sequence_1_to_10");
+      if (rs.next() && rs.getInt(1) == 10) {
+        seq10_ok = true;
+      }
+    } catch (SQLException e) {
+      // eat
+    }
+    if (!seq10_ok) {
+      stmt.execute("DROP TABLE IF EXISTS sequence_1_to_10");
+      stmt.execute("CREATE TABLE sequence_1_to_10 (t1 int)");
+      stmt.execute("insert into sequence_1_to_10 VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(10)");
+    }
+
+    try {
+      ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM sequence_1_to_10000");
+      if (rs.next() && rs.getInt(1) == 10_000) {
+        seq10_000_ok = true;
+      }
+    } catch (SQLException e) {
+      // eat
+    }
+    if (!seq10_000_ok) {
+      stmt.execute("DROP TABLE IF EXISTS sequence_1_to_10000");
+      stmt.execute("CREATE TABLE sequence_1_to_10000 (t1 int)");
+      try (PreparedStatement prep =
+          sharedConnBinary.prepareStatement("INSERT INTO sequence_1_to_10000 VALUES (?)")) {
+        for (int i = 1; i <= 10_000; i++) {
+          prep.setInt(1, i);
+          prep.addBatch();
+        }
+        prep.executeBatch();
+      }
+    }
+  }
+
+  public Connection createProxyCon(HaMode mode, String opts) throws SQLException {
+    Configuration conf = Configuration.parse(mDefUrl);
+    HostAddress hostAddress = conf.addresses().get(0);
+    try {
+      proxy = new TcpProxy(hostAddress.host, hostAddress.port);
+    } catch (IOException i) {
+      throw new SQLException("proxy error", i);
+    }
+
+    String url = mDefUrl.replaceAll("//([^/]*)/", "//localhost:" + proxy.getLocalPort() + "/");
+    if (mode != HaMode.NONE) {
+      url =
+          url.replaceAll(
+              "jdbc:mariadb:", "jdbc:mariadb:" + mode.name().toLowerCase(Locale.ROOT) + ":");
+    }
+    if (conf.sslMode() == SslMode.VERIFY_FULL) {
+      url = url.replaceAll("sslMode=verify-full", "sslMode=verify-ca");
+    }
+
+    return (Connection) DriverManager.getConnection(url + opts);
+  }
+
+  public static boolean haveSsl() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("show variables like '%ssl%'");
+    //    while (rs.next()) {
+    //      System.out.println(rs.getString(1) + ":" + rs.getString(2));
+    //    }
+    try {
+      rs = stmt.executeQuery("select @@have_ssl");
+      assertTrue(rs.next());
+      return "YES".equals(rs.getString(1));
+    } catch (SQLException e) {
+      return false;
+    }
+  }
+
+  public boolean isWindows() {
+    return System.getProperty("os.name").toLowerCase().contains("win");
+  }
+
+  public void cancelForVersion(int major, int minor) {
+    String dbVersion = sharedConn.getMetaData().getDatabaseProductVersion();
+    Assumptions.assumeFalse(dbVersion.startsWith(major + "." + minor));
+  }
+
+  public static Connection createCon(String option) throws SQLException {
+    return (Connection) DriverManager.getConnection(mDefUrl + "&" + option);
+  }
+
+  public static Connection createCon(String option, Integer sslPort) throws SQLException {
+    Configuration conf = Configuration.parse(mDefUrl + "&" + option);
+    if (sslPort != null) {
+      for (HostAddress hostAddress : conf.addresses()) {
+        hostAddress.port = sslPort;
+      }
+    }
+    return Driver.connect(conf);
+  }
+
+  @AfterEach
+  public void afterEach1() throws SQLException {
+    sharedConn.isValid(2000);
+    sharedConnBinary.isValid(2000);
+  }
+
+  public static int getMaxAllowedPacket() throws SQLException {
+    java.sql.Statement st = sharedConn.createStatement();
+    ResultSet rs = st.executeQuery("select @@max_allowed_packet");
+    assertTrue(rs.next());
+    return rs.getInt(1);
+  }
+
+  public static void assertThrowsContains(
+      Class<? extends Exception> expectedType, Executable executable, String expected) {
+    Exception e = Assertions.assertThrows(expectedType, executable);
+    Assertions.assertTrue(e.getMessage().contains(expected), "real message:" + e.getMessage());
+  }
+
+  @RegisterExtension public Extension watcher = new Follow();
+
+  private static class Follow implements TestWatcher, BeforeEachCallback {
+    @Override
+    public void testDisabled(ExtensionContext context, Optional<String> reason) {
+      System.out.printf(
+          "  - Disabled %s: with reason :- %s%n",
+          context.getDisplayName(), reason.orElse("No reason"));
+    }
+
+    @Override
+    public void testAborted(ExtensionContext context, Throwable cause) {
+      System.out.printf("  - Aborted %s: %n", context.getDisplayName());
+    }
+
+    @Override
+    public void testFailed(ExtensionContext context, Throwable cause) {
+      System.out.printf("  ✗ Failed %s: %n", context.getDisplayName());
+    }
+
+    @Override
+    public void testSuccessful(ExtensionContext context) {
+      System.out.printf(
+          "  ✓ %s: %sms%n",
+          context.getDisplayName(), Duration.between(initialTest, Instant.now()).toMillis());
+    }
+
+    @Override
+    public void beforeEach(ExtensionContext extensionContext) {
+      initialTest = Instant.now();
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/CompressTest.java b/src/test/java/org/mariadb/jdbc/integration/CompressTest.java
new file mode 100644
index 0000000..9fcbf5d
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/CompressTest.java
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.*;
+import org.junit.jupiter.api.*;
+import org.mariadb.jdbc.Connection;
+import org.mariadb.jdbc.Statement;
+
+public class CompressTest extends Common {
+  private static Connection shareCompressCon;
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS compressTest");
+    stmt.execute(
+        "CREATE TABLE compressTest (t1 int not null primary key auto_increment, "
+            + "t2 LONGTEXT, t3 LONGTEXT, t4 LONGTEXT, t5 LONGTEXT, t6 LONGTEXT)");
+    shareCompressCon = createCon("useCompression=true");
+  }
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    sharedConn.createStatement().execute("DROP TABLE IF EXISTS compressTest");
+    shareCompressCon.close();
+  }
+
+  @Test
+  public void bigSend() throws SQLException {
+    int[] maxSize = new int[] {8 * 1024, 128 * 1024, 1024 * 1024, 16 * 1024 * 1024};
+
+    for (int i = 0; i < maxSize.length; i++) {
+      bigSend(sharedConn, maxSize[i]);
+      bigSend(sharedConnBinary, maxSize[i]);
+      bigSend(shareCompressCon, maxSize[i]);
+    }
+  }
+
+  public void bigSend(Connection con, int maxLen) throws SQLException {
+    int maxAllowed = getMaxAllowedPacket();
+    Assumptions.assumeTrue(maxAllowed > 16 * 1024 * 1024);
+    char[] arr2 = new char[Math.min(maxLen, Math.min(16 * 1024 * 1024, (maxAllowed / 2) - 1000))];
+    for (int pos = 0; pos < arr2.length; pos++) {
+      arr2[pos] = (char) ('A' + (pos % 60));
+    }
+    Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE compressTest");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    String st2 = new String(arr2, 0, 5_000);
+    String st3 = new String(arr2, 0, Math.min(maxLen, 150_000));
+    String st4 = new String(arr2, 0, Math.min(maxLen, 1_100_000));
+    String st5 = new String(arr2, 0, Math.min(maxLen, 1_100_000));
+    String st6 = new String(arr2);
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO compressTest VALUES (?, ?, ?, ?, ?, ?)")) {
+      prep.setInt(1, 1);
+      prep.setString(2, st2);
+      prep.setString(3, st3);
+      prep.setString(4, st4);
+      prep.setString(5, st5);
+      prep.setString(6, st6);
+      prep.execute();
+
+      prep.setInt(1, 2);
+      prep.setString(2, st2);
+      prep.setString(3, st3);
+      prep.setString(4, st4);
+      prep.setString(5, st5);
+      prep.setString(6, st6);
+      prep.execute();
+    }
+    stmt.setFetchSize(1);
+    ResultSet rs = stmt.executeQuery("SELECT * from compressTest");
+    assertTrue(rs.next());
+
+    assertEquals(st2, rs.getString(2));
+    assertEquals(st3, rs.getString(3));
+    assertEquals(st4, rs.getString(4));
+    assertEquals(st5, rs.getString(5));
+    assertEquals(st6, rs.getString(6));
+    rs.close();
+    stmt.execute("COMMIT");
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/ConfigurationTest.java b/src/test/java/org/mariadb/jdbc/integration/ConfigurationTest.java
new file mode 100644
index 0000000..afbd5c5
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/ConfigurationTest.java
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class ConfigurationTest extends Common {
+
+  @BeforeAll
+  public static void begin() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE testSessionVariable(id int not null primary key auto_increment, test"
+            + " varchar(10))");
+    stmt.execute("FLUSH TABLES");
+  }
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS testSessionVariable");
+  }
+
+  @Test
+  public void testSessionVariable() throws SQLException {
+    try (Connection connection =
+        createCon("sessionVariables=auto_increment_increment=2&allowMultiQueries=true")) {
+      Statement stmt = connection.createStatement();
+      stmt.execute(
+          "INSERT INTO testSessionVariable (test) values ('bb'),('cc');"
+              + "INSERT INTO testSessionVariable (test) values ('dd'),('ee')",
+          Statement.RETURN_GENERATED_KEYS);
+
+      ResultSet rs = stmt.getGeneratedKeys();
+
+      assertTrue(rs.next());
+      assertEquals(1, rs.getInt(1));
+      assertTrue(rs.next());
+      if (!"galera".equals(System.getenv("srv"))) {
+        assertEquals(5, rs.getInt(1));
+      }
+      assertFalse(rs.next());
+      assertFalse(stmt.getMoreResults());
+      rs.clearWarnings();
+    }
+
+    // Xpand doesn't support session_track_system_variables
+    if (!isXpand()) {
+      try (Connection connection =
+          createCon("sessionVariables=session_track_system_variables='some\\';f,\"ff'")) {
+        Statement stmt = connection.createStatement();
+        ResultSet rs = stmt.executeQuery("SELECT @@session_track_system_variables");
+        assertTrue(rs.next());
+      }
+      try (Connection connection =
+          createCon("sessionVariables=session_track_system_variables=\"some\\\";f,'ff'\"")) {
+        Statement stmt = connection.createStatement();
+        ResultSet rs = stmt.executeQuery("SELECT @@session_track_system_variables");
+        assertTrue(rs.next());
+      }
+    }
+  }
+
+  @Test
+  public void connectionAttributes() throws SQLException {
+    // xpand doesn't support @@performance_schema variable
+    Assumptions.assumeTrue(!"maxscale".equals(System.getenv("srv")) && !isXpand());
+
+    try (org.mariadb.jdbc.Connection conn =
+        createCon("&connectionAttributes=test:test1,test2:test2Val,test3")) {
+      Statement stmt = conn.createStatement();
+      ResultSet rs1 = stmt.executeQuery("SELECT @@performance_schema");
+      rs1.next();
+      Assumptions.assumeTrue("1".equals(rs1.getString(1)));
+
+      ResultSet rs =
+          stmt.executeQuery(
+              "SELECT * from performance_schema.session_connect_attrs where processlist_id="
+                  + conn.getThreadId()
+                  + " AND ATTR_NAME like 'test%'");
+      assertTrue(rs.next());
+      assertEquals("test1", rs.getString("ATTR_VALUE"));
+      assertTrue(rs.next());
+      assertEquals("test2Val", rs.getString("ATTR_VALUE"));
+      assertTrue(rs.next());
+      assertNull(rs.getString("ATTR_VALUE"));
+    }
+  }
+
+  @Test
+  public void useMysqlMetadata() throws SQLException {
+    assertEquals(
+        isMariaDBServer() ? "MariaDB" : "MySQL", sharedConn.getMetaData().getDatabaseProductName());
+    try (org.mariadb.jdbc.Connection conn = createCon("&useMysqlMetadata=true")) {
+      assertEquals("MySQL", conn.getMetaData().getDatabaseProductName());
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/ConnectionTest.java b/src/test/java/org/mariadb/jdbc/integration/ConnectionTest.java
new file mode 100644
index 0000000..5541946
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/ConnectionTest.java
@@ -0,0 +1,1194 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.*;
+import java.sql.Connection;
+import java.sql.Statement;
+import java.util.Locale;
+import java.util.Properties;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.Executor;
+import org.junit.jupiter.api.*;
+import org.mariadb.jdbc.*;
+import org.mariadb.jdbc.integration.util.SocketFactoryBasicTest;
+import org.mariadb.jdbc.integration.util.SocketFactoryTest;
+
+@DisplayName("Connection Test")
+public class ConnectionTest extends Common {
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    createSequenceTables();
+  }
+
+  @Test
+  public void isValid() throws SQLException {
+    Connection sharedConn = DriverManager.getConnection(mDefUrl);
+    assertTrue(sharedConn.isValid(2000));
+    sharedConn.close();
+    assertFalse(sharedConn.isValid(2000));
+  }
+
+  @Test
+  public void tcpKeepAlive() throws SQLException {
+    try (Connection con = createCon("&tcpKeepAlive=false")) {
+      con.isValid(1);
+    }
+  }
+
+  @Test
+  void isValidWrongValue() {
+    try {
+      sharedConn.isValid(-2000);
+      fail("most have thrown an error");
+    } catch (SQLException e) {
+      assertTrue(e.getMessage().contains("the value supplied for timeout is negative"));
+    }
+  }
+
+  @Test
+  void missingHost() {
+    assertThrowsContains(
+        SQLException.class,
+        () -> DriverManager.getConnection("jdbc:mariadb:///db"),
+        "hostname must be set to connect socket if not using local socket or pipe");
+    assertThrowsContains(
+        SQLException.class,
+        () -> DriverManager.getConnection("jdbc:mariadb:///db?socketFactory=test"),
+        "hostname must be set to connect socket");
+  }
+
+  @Test
+  void socketTimeout() throws SQLException {
+    Assumptions.assumeTrue(
+        !"maxscale".equals(System.getenv("srv"))
+            && !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv"))
+            && !isXpand());
+
+    try (Connection con = createCon("&socketTimeout=50")) {
+      assertEquals(50, con.getNetworkTimeout());
+      Statement stmt = con.createStatement();
+      stmt.execute("SELECT 1");
+      Common.assertThrowsContains(SQLException.class, () -> stmt.execute("SELECT SLEEP(0.1)"), "");
+    }
+
+    try (Connection con = createCon("&socketTimeout=500")) {
+      assertEquals(500, con.getNetworkTimeout());
+      Statement stmt = con.createStatement();
+      stmt.execute("SELECT SLEEP(0.1)");
+      Common.assertThrowsContains(SQLException.class, () -> stmt.execute("SELECT SLEEP(1)"), "");
+    }
+
+    try (Connection con = createCon("&socketTimeout=0")) {
+      assertEquals(0, con.getNetworkTimeout());
+      Statement stmt = con.createStatement();
+      stmt.execute("SELECT SLEEP(0.5)");
+    }
+  }
+
+  @Test
+  public void autoCommit() throws SQLException {
+    Connection con = DriverManager.getConnection(mDefUrl);
+    assertTrue(con.getAutoCommit());
+    con.setAutoCommit(false);
+    assertFalse(con.getAutoCommit());
+    con.setAutoCommit(false);
+    assertFalse(con.getAutoCommit());
+    con.setAutoCommit(true);
+    assertTrue(con.getAutoCommit());
+    con.setAutoCommit(true);
+    assertTrue(con.getAutoCommit());
+    Statement stmt = con.createStatement();
+    stmt.execute("SET autocommit=false");
+    assertFalse(con.getAutoCommit());
+    con.close();
+  }
+
+  @Test
+  public void initSQL() throws SQLException {
+    try (Connection con = createCon("&initSql=SET @myVar='YourVar'")) {
+      Statement stmt = con.createStatement();
+      ResultSet rs = stmt.executeQuery("SELECT @myVar");
+      assertTrue(rs.next());
+      assertEquals("YourVar", rs.getString(1));
+    }
+  }
+
+  @Test
+  public void nativeSQL() throws SQLException {
+    String[] inputs =
+        new String[] {
+          "select {fn TIMESTAMPDIFF ( SQL_TSI_HOUR, {fn convert('SQL_', SQL_INTEGER)})}, {fn"
+              + " TIMESTAMPDIFF (HOUR, {fn convert  ('sQL_'   , SQL_INTEGER)})}",
+          "{call foo({fn now()})} //end",
+          "{call foo({fn '{' now()} /* {test}# \"'\" */) \"\\\"'#\" '\"\\''} #{test2}",
+          "{  call foo({fn now()})}",
+          "{\r\ncall foo({fn now()})}",
+          "{\r\n  call foo({fn now()})}",
+          "{call foo(/*{fn now()}*/)}",
+          "{CALL foo({fn now() /* -- * */ -- test \n })}",
+          "{?=call foo({fn now()})}",
+          "SELECT 'David_' LIKE 'David|_' {escape '|'}",
+          "select {fn dayname ({fn abs({fn now()})})}",
+          "{d '1997-05-24'}",
+          "{d'1997-05-24'}",
+          "{\nt'10:30:29'}",
+          "{t '10:30:29'}",
+          "{t'10:30:29'}",
+          "{ts '1997-05-24 10:30:29.123'}",
+          "{ts'1997-05-24 10:30:29.123'}",
+          "'{string data with { or } will not be altered'",
+          "`{string data with { or } will not be altered`",
+          "--  Also note that you can safely include { and } in comments",
+          "SELECT * FROM {oj TABLE1 LEFT OUTER JOIN TABLE2 ON DEPT_NO = 003420930}"
+        };
+    String[] outputs =
+        new String[] {
+          "select TIMESTAMPDIFF ( HOUR, convert('SQL_', INTEGER)), TIMESTAMPDIFF (HOUR, convert "
+              + " ('sQL_'   , INTEGER))",
+          "call foo(now()) //end",
+          "call foo('{' now() /* {test}# \"'\" */) \"\\\"'#\" '\"\\'' #{test2}",
+          "call foo(now())",
+          "call foo(now())",
+          "call foo(now())",
+          "call foo(/*{fn now()}*/)",
+          "CALL foo(now() /* -- * */ -- test \n )",
+          "?=call foo(now())",
+          "SELECT 'David_' LIKE 'David|_' escape '|'",
+          "select dayname (abs(now()))",
+          "'1997-05-24'",
+          "'1997-05-24'",
+          "'10:30:29'",
+          "'10:30:29'",
+          "'10:30:29'",
+          "'1997-05-24 10:30:29.123'",
+          "'1997-05-24 10:30:29.123'",
+          "'{string data with { or } will not be altered'",
+          "`{string data with { or } will not be altered`",
+          "--  Also note that you can safely include { and } in comments",
+          "SELECT * FROM TABLE1 LEFT OUTER JOIN TABLE2 ON DEPT_NO = 003420930"
+        };
+    for (int i = 0; i < inputs.length; i++) {
+      assertEquals(outputs[i], sharedConn.nativeSQL(inputs[i]));
+    }
+    assertEquals(
+        "INSERT INTO TEST_SYNTAX_ERROR(str_value, json_value) VALUES ('abc\\\\', '{\"data\":"
+            + " \"test\"}')",
+        sharedConn.nativeSQL(
+            "INSERT INTO TEST_SYNTAX_ERROR(str_value, json_value) VALUES ('abc\\\\', '{\"data\":"
+                + " \"test\"}')"));
+
+    try {
+      sharedConn.nativeSQL("{call foo({fn now())}");
+      fail("most have thrown an error");
+    } catch (SQLException e) {
+      assertTrue(
+          e.getMessage().contains("Invalid escape sequence , missing closing '}' character in '"));
+    }
+
+    try {
+      sharedConn.nativeSQL("{call foo({unknown} )}");
+      fail("most have thrown an error");
+    } catch (SQLException e) {
+      assertTrue(e.getMessage().contains("unknown escape sequence {unknown}"));
+    }
+  }
+
+  @Test
+  public void nativeSQLNoBackSlash() throws SQLException {
+    try (Connection con = createCon()) {
+      java.sql.Statement stmt = con.createStatement();
+      stmt.execute("SET sql_mode = concat(@@sql_mode,',NO_BACKSLASH_ESCAPES')");
+      assertEquals("call foo('{' now())", con.nativeSQL("{call foo({fn '{' now()})}"));
+    }
+  }
+
+  @Test
+  public void nativeSqlTest() throws SQLException {
+    String exp;
+    if (isMariaDBServer() || minVersion(8, 0, 17)) {
+      exp =
+          "SELECT convert(foo(a,b,c), SIGNED INTEGER)"
+              + ", convert(convert(?, CHAR), SIGNED INTEGER)"
+              + ", 1=?"
+              + ", 1=?"
+              + ", convert(?,   SIGNED INTEGER   )"
+              + ",  convert (?,   SIGNED INTEGER   )"
+              + ", convert(?, UNSIGNED INTEGER)"
+              + ", convert(?, BINARY)"
+              + ", convert(?, BINARY)"
+              + ", convert(?, BINARY)"
+              + ", convert(?, BINARY)"
+              + ", convert(?, BINARY)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, DOUBLE)"
+              + ", convert(?, DOUBLE)"
+              + ", convert(?, DECIMAL)"
+              + ", convert(?, DECIMAL)"
+              + ", convert(?, DECIMAL)"
+              + ", convert(?, DATETIME)"
+              + ", convert(?, DATETIME)";
+    } else {
+      exp =
+          "SELECT convert(foo(a,b,c), SIGNED INTEGER)"
+              + ", convert(convert(?, CHAR), SIGNED INTEGER)"
+              + ", 1=?"
+              + ", 1=?"
+              + ", convert(?,   SIGNED INTEGER   )"
+              + ",  convert (?,   SIGNED INTEGER   )"
+              + ", convert(?, UNSIGNED INTEGER)"
+              + ", convert(?, BINARY)"
+              + ", convert(?, BINARY)"
+              + ", convert(?, BINARY)"
+              + ", convert(?, BINARY)"
+              + ", convert(?, BINARY)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, CHAR)"
+              + ", convert(?, CHAR)"
+              + ", 0.0+?"
+              + ", 0.0+?"
+              + ", convert(?, DECIMAL)"
+              + ", convert(?, DECIMAL)"
+              + ", convert(?, DECIMAL)"
+              + ", convert(?, DATETIME)"
+              + ", convert(?, DATETIME)";
+    }
+
+    assertEquals(
+        exp,
+        sharedConn.nativeSQL(
+            "SELECT {fn convert(foo(a,b,c), SQL_BIGINT)}"
+                + ", {fn convert({fn convert(?, SQL_VARCHAR)}, SQL_BIGINT)}"
+                + ", {fn convert(?, SQL_BOOLEAN )}"
+                + ", {fn convert(?, BOOLEAN)}"
+                + ", {fn convert(?,   SMALLINT   )}"
+                + ", {fn  convert (?,   TINYINT   )}"
+                + ", {fn convert(?, SQL_BIT)}"
+                + ", {fn convert(?, SQL_BLOB)}"
+                + ", {fn convert(?, SQL_VARBINARY)}"
+                + ", {fn convert(?, SQL_LONGVARBINARY)}"
+                + ", {fn convert(?, SQL_ROWID)}"
+                + ", {fn convert(?, SQL_BINARY)}"
+                + ", {fn convert(?, SQL_NCHAR)}"
+                + ", {fn convert(?, SQL_CLOB)}"
+                + ", {fn convert(?, SQL_NCLOB)}"
+                + ", {fn convert(?, SQL_DATALINK)}"
+                + ", {fn convert(?, SQL_VARCHAR)}"
+                + ", {fn convert(?, SQL_NVARCHAR)}"
+                + ", {fn convert(?, SQL_LONGVARCHAR)}"
+                + ", {fn convert(?, SQL_LONGNVARCHAR)}"
+                + ", {fn convert(?, SQL_SQLXML)}"
+                + ", {fn convert(?, SQL_LONGNCHAR)}"
+                + ", {fn convert(?, SQL_CHAR)}"
+                + ", {fn convert(?, SQL_FLOAT)}"
+                + ", {fn convert(?, SQL_DOUBLE)}"
+                + ", {fn convert(?, SQL_DECIMAL)}"
+                + ", {fn convert(?, SQL_REAL)}"
+                + ", {fn convert(?, SQL_NUMERIC)}"
+                + ", {fn convert(?, SQL_TIMESTAMP)}"
+                + ", {fn convert(?, SQL_DATETIME)}"));
+  }
+
+  @Test
+  public void doubleBackslash() throws SQLException {
+    java.sql.Statement stmt = sharedConn.createStatement();
+    stmt.setEscapeProcessing(true);
+    stmt.execute("DROP TABLE IF EXISTS TEST_SYNTAX_ERROR");
+    stmt.execute(
+        "CREATE TABLE TEST_SYNTAX_ERROR("
+            + "     id INTEGER unsigned NOT NULL AUTO_INCREMENT, "
+            + "     str_value MEDIUMTEXT CHARACTER SET utf8mb4 NOT NULL,"
+            + "     json_value  MEDIUMTEXT CHARACTER SET utf8mb4 NOT NULL, "
+            + "    PRIMARY KEY ( id ))");
+    stmt.execute(
+        "INSERT INTO TEST_SYNTAX_ERROR(str_value, json_value) VALUES ('abc\\\\', '{\"data\":"
+            + " \"test\"}')");
+  }
+
+  @Test
+  public void databaseStateChange() throws SQLException {
+    Assumptions.assumeTrue(
+        (isMariaDBServer() && minVersion(10, 2, 0)) || (!isMariaDBServer() && minVersion(5, 7, 0)));
+    try (Connection connection = createCon()) {
+      try (Statement stmt = connection.createStatement()) {
+        stmt.execute("drop database if exists _test_db");
+        stmt.execute("create database _test_db");
+        ResultSet rs = stmt.executeQuery("SELECT DATABASE()");
+        rs.next();
+        assertEquals(rs.getString(1), connection.getCatalog());
+        stmt.execute("USE _test_db");
+        assertEquals("_test_db", connection.getCatalog());
+        stmt.execute("drop database _test_db");
+      }
+    }
+  }
+
+  @Test
+  public void catalog() throws SQLException {
+    Assumptions.assumeTrue(
+        (isMariaDBServer() && minVersion(10, 2, 0)) || (!isMariaDBServer() && minVersion(5, 7, 0)));
+    try (Connection connection = createCon()) {
+      try (Statement stmt = connection.createStatement()) {
+        stmt.execute("drop database if exists _test_db");
+        stmt.execute("create database _test_db");
+        ResultSet rs = stmt.executeQuery("SELECT DATABASE()");
+        rs.next();
+        String initialCatalog = connection.getCatalog();
+        assertEquals(rs.getString(1), initialCatalog);
+        connection.setCatalog(initialCatalog);
+        assertEquals(initialCatalog, connection.getCatalog());
+        connection.setCatalog("_test_db");
+        assertEquals("_test_db", connection.getCatalog());
+        stmt.execute("USE _test_db");
+        assertEquals("_test_db", connection.getCatalog());
+        connection.setCatalog(null);
+        assertEquals("_test_db", connection.getCatalog());
+        connection.setCatalog("_test_db");
+        assertEquals("_test_db", connection.getCatalog());
+        stmt.execute("drop database _test_db");
+        assertTrue(connection.getCatalog() == null || "_test_db".equals(connection.getCatalog()));
+      }
+    }
+  }
+
+  @Test
+  public void checkFixedData() throws SQLException {
+    sharedConn.unwrap(java.sql.Connection.class);
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> sharedConn.unwrap(String.class),
+        "The receiver is not a wrapper for java.lang.String");
+    assertTrue(sharedConn.createBlob() instanceof Blob);
+    assertTrue(sharedConn.createClob() instanceof Clob);
+    assertTrue(sharedConn.createNClob() instanceof NClob);
+    assertThrows(SQLException.class, () -> sharedConn.createSQLXML());
+    assertThrows(SQLException.class, () -> sharedConn.createArrayOf("", null));
+    assertThrows(SQLException.class, () -> sharedConn.createStruct("", null));
+    assertNull(sharedConn.getSchema());
+    sharedConn.setSchema("fff");
+    assertNull(sharedConn.getSchema());
+  }
+
+  @Test
+  public void clientInfo() throws SQLException {
+    assertTrue(sharedConn.getClientInfo().isEmpty());
+    sharedConn.setClientInfo("some", "value");
+    Properties props = new Properties();
+    props.put("another", "one");
+    props.put("and another", "two");
+    sharedConn.setClientInfo(props);
+    assertEquals(3, sharedConn.getClientInfo().size());
+    assertEquals("value", sharedConn.getClientInfo("some"));
+    assertNull(sharedConn.getClientInfo("some33"));
+  }
+
+  @Test
+  public void abortTestAlreadyClosed() throws SQLException {
+    Connection connection = createCon();
+    connection.close();
+    Executor executor = Runnable::run;
+    connection.abort(executor);
+  }
+
+  @Test
+  public void abortTestNoExecutor() {
+    try {
+      sharedConn.abort(null);
+    } catch (SQLException e) {
+      assertTrue(e.getMessage().contains("Cannot abort the connection: null executor passed"));
+    }
+  }
+
+  @Test
+  public void abortClose() throws Throwable {
+    Connection connection = createCon();
+    Statement stmt = connection.createStatement();
+    stmt.setFetchSize(1);
+    ResultSet rs =
+        stmt.executeQuery(
+            "select * from information_schema.columns as c1, "
+                + "information_schema.tables, information_schema.tables as t2");
+    assertTrue(rs.next());
+    connection.abort(Runnable::run);
+    // must still work
+
+    Thread.sleep(20);
+    try {
+      assertTrue(rs.next());
+      fail();
+    } catch (SQLException sqle) {
+      assertTrue(sqle.getMessage().contains("Operation not permit on a closed resultSet"));
+    }
+  }
+
+  @Test
+  public void verificationAbort() throws Throwable {
+    Timer timer = new Timer();
+    try (Connection connection = createCon()) {
+      timer.schedule(
+          new TimerTask() {
+            @Override
+            public void run() {
+              try {
+                connection.abort(Runnable::run);
+              } catch (SQLException sqle) {
+                fail(sqle.getMessage());
+              }
+            }
+          },
+          10);
+
+      Statement stmt = connection.createStatement();
+      assertThrows(
+          SQLException.class,
+          () ->
+              stmt.executeQuery(
+                  "select * from information_schema.columns as c1,  information_schema.tables,"
+                      + " information_schema.tables as t2"));
+    }
+  }
+
+  @Test
+  public void networkTimeoutTest() throws SQLException {
+    try (Connection connection = createCon()) {
+      assertEquals(0, connection.getNetworkTimeout());
+      int timeout = 1000;
+
+      Executor executor = Runnable::run;
+      connection.setNetworkTimeout(executor, timeout);
+      connection.isValid(2);
+      assertEquals(timeout, connection.getNetworkTimeout());
+
+      try {
+        Statement stmt = connection.createStatement();
+        stmt.execute("select sleep(2)");
+        fail("Network timeout is " + timeout / 1000 + "sec, but slept for 2 sec");
+      } catch (SQLException sqlex) {
+        assertTrue(connection.isClosed());
+      }
+    }
+  }
+
+  @Test
+  public void testConnectionAttributes() throws Exception {
+    // check if performance_schema is ON
+    Statement stmt = sharedConn.createStatement();
+    ResultSet res = stmt.executeQuery("show variables like 'performance_schema'");
+    if (res.next()) {
+      Assumptions.assumeFalse(res.getString("Value").equals("OFF"));
+
+      try (Connection connection = createCon()) {
+        Statement attributeStatement = connection.createStatement();
+        ResultSet result =
+            attributeStatement.executeQuery(
+                "select * from performance_schema.session_connect_attrs where"
+                    + " ATTR_NAME='_server_host' and processlist_id = connection_id()");
+        while (result.next()) {
+          String strVal = result.getString("ATTR_VALUE");
+          assertEquals(Configuration.parse(mDefUrl).addresses().get(0).host, strVal);
+        }
+      }
+    }
+  }
+
+  @Test
+  public void isolationLevel() throws SQLException {
+    java.sql.Connection connection = createCon();
+    int[] levels =
+        new int[] {
+          java.sql.Connection.TRANSACTION_READ_UNCOMMITTED,
+          java.sql.Connection.TRANSACTION_READ_COMMITTED,
+          java.sql.Connection.TRANSACTION_SERIALIZABLE,
+          java.sql.Connection.TRANSACTION_REPEATABLE_READ
+        };
+    for (int level : levels) {
+      connection.setTransactionIsolation(level);
+      assertEquals(level, connection.getTransactionIsolation());
+    }
+    connection.close();
+    assertThrows(
+        SQLException.class,
+        () -> connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED));
+
+    try (java.sql.Connection con2 = createCon()) {
+      try {
+        con2.setTransactionIsolation(10_000);
+        fail();
+      } catch (SQLException e) {
+        assertTrue(e.getMessage().contains("Unsupported transaction isolation level"));
+      }
+    }
+  }
+
+  @Test
+  public void savepointTest() throws SQLException {
+    try (Connection con = createCon()) {
+      Statement stmt = con.createStatement();
+      stmt.execute("CREATE TEMPORARY TABLE spt(test varchar(10))");
+      con.setAutoCommit(false);
+      stmt.executeUpdate("INSERT INTO spt values('hej1')");
+      stmt.executeUpdate("INSERT INTO spt values('hej2')");
+      Savepoint savepoint = con.setSavepoint("ye`\\\\`p");
+      stmt.executeUpdate("INSERT INTO spt  values('hej3')");
+      stmt.executeUpdate("INSERT INTO spt values('hej4')");
+      assertEquals("ye``\\\\``p", savepoint.getSavepointName());
+      Common.assertThrowsContains(
+          SQLException.class,
+          savepoint::getSavepointId,
+          "Cannot retrieve savepoint id of a named savepoint");
+      con.rollback(savepoint);
+      stmt.executeUpdate("INSERT INTO spt values('hej5')");
+      stmt.executeUpdate("INSERT INTO spt values('hej6')");
+      con.commit();
+      ResultSet rs = stmt.executeQuery("SELECT * FROM spt");
+      assertTrue(rs.next());
+      assertEquals("hej1", rs.getString(1));
+      assertTrue(rs.next());
+      assertEquals("hej2", rs.getString(1));
+      assertTrue(rs.next());
+      assertEquals("hej5", rs.getString(1));
+      assertTrue(rs.next());
+      assertEquals("hej6", rs.getString(1));
+      assertFalse(rs.next());
+    }
+  }
+
+  @Test
+  public void netWorkTimeout() throws SQLException {
+    Connection con = createCon();
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> con.setNetworkTimeout(Runnable::run, -200),
+        "Connection.setNetworkTimeout cannot be called with a negative timeout");
+    con.close();
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> con.setNetworkTimeout(Runnable::run, 200),
+        "Connection.setNetworkTimeout cannot be called on a closed connection");
+  }
+
+  @Test
+  public void savepointUnname() throws SQLException {
+    try (Connection con = createCon()) {
+      Statement stmt = con.createStatement();
+      stmt.execute("CREATE TEMPORARY TABLE spt(test varchar(10))");
+      con.setAutoCommit(false);
+      stmt.executeUpdate("INSERT INTO spt values('hej1')");
+      stmt.executeUpdate("INSERT INTO spt values('hej2')");
+      Savepoint savepoint = con.setSavepoint();
+      stmt.executeUpdate("INSERT INTO spt  values('hej3')");
+      stmt.executeUpdate("INSERT INTO spt values('hej4')");
+      assertTrue(savepoint.getSavepointId() > 0);
+      Common.assertThrowsContains(
+          SQLException.class,
+          savepoint::getSavepointName,
+          "Cannot retrieve savepoint name of an unnamed savepoint");
+      con.rollback(savepoint);
+      Common.assertThrowsContains(
+          SQLException.class, () -> con.rollback(new MySavepoint()), "Unknown savepoint type");
+      stmt.executeUpdate("INSERT INTO spt values('hej5')");
+      stmt.executeUpdate("INSERT INTO spt values('hej6')");
+      con.commit();
+      con.commit();
+      ResultSet rs = stmt.executeQuery("SELECT * FROM spt");
+      assertTrue(rs.next());
+      assertEquals("hej1", rs.getString(1));
+      assertTrue(rs.next());
+      assertEquals("hej2", rs.getString(1));
+      assertTrue(rs.next());
+      assertEquals("hej5", rs.getString(1));
+      assertTrue(rs.next());
+      assertEquals("hej6", rs.getString(1));
+      assertFalse(rs.next());
+    }
+  }
+
+  @Test
+  public void releaseSavepoint() throws SQLException {
+    try (Connection con = createCon()) {
+      Statement stmt = con.createStatement();
+      stmt.execute("CREATE TEMPORARY TABLE spt(test varchar(10)) ");
+      con.setAutoCommit(false);
+      stmt.executeUpdate("INSERT INTO spt values('hej1')");
+      stmt.executeUpdate("INSERT INTO spt values('hej2')");
+      Savepoint savepoint = con.setSavepoint();
+      stmt.executeUpdate("INSERT INTO spt  values('hej3')");
+      stmt.executeUpdate("INSERT INTO spt values('hej4')");
+      con.releaseSavepoint(savepoint);
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> con.releaseSavepoint(new MySavepoint()),
+          "Unknown savepoint type");
+      stmt.executeUpdate("INSERT INTO spt values('hej5')");
+      stmt.executeUpdate("INSERT INTO spt values('hej6')");
+      con.commit();
+      ResultSet rs = stmt.executeQuery("SELECT * FROM spt");
+      for (int i = 1; i < 7; i++) {
+        assertTrue(rs.next());
+        assertEquals("hej" + i, rs.getString(1));
+      }
+      assertFalse(rs.next());
+    }
+  }
+
+  static class MySavepoint implements Savepoint {
+    @Override
+    public int getSavepointId() throws SQLException {
+      return 0;
+    }
+
+    @Override
+    public String getSavepointName() throws SQLException {
+      return null;
+    }
+  }
+
+  @Nested
+  @DisplayName("Transaction Test")
+  class Transaction {
+
+    @Test
+    public void testProperRollback() throws Exception {
+      java.sql.Statement stmt = sharedConn.createStatement();
+      try {
+        stmt.execute("CREATE TABLE tx_prim_key(id int not null primary key) engine=innodb");
+        stmt.execute(
+            "CREATE TABLE tx_fore_key (id int not null primary key, id_ref int not null, foreign"
+                + " key (id_ref) references tx_prim_key(id) on delete restrict on update restrict) "
+                + "engine=innodb");
+        stmt.executeUpdate("insert into tx_prim_key(id) values(32)");
+        stmt.executeUpdate("insert into tx_fore_key(id, id_ref) values(42, 32)");
+
+        // 2. try to delete entry in Primary table in a transaction - which will fail due
+        // foreign key.
+        sharedConn.setAutoCommit(false);
+        try (java.sql.Statement st = sharedConn.createStatement()) {
+          st.executeUpdate("delete from tx_prim_key where id = 32");
+          sharedConn.commit();
+          fail("Expected SQLException");
+        } catch (SQLException e) {
+          // This exception is expected
+          assertTrue(
+              e.getMessage().contains("a foreign key constraint fails")
+                  || e.getMessage().contains("Foreign key constraint violation"));
+          sharedConn.rollback();
+        }
+
+        try (java.sql.Connection conn2 = createCon();
+            java.sql.Statement st = conn2.createStatement()) {
+          st.setQueryTimeout(30000);
+          st.executeUpdate("delete from tx_fore_key where id = 42");
+          st.executeUpdate("delete from tx_prim_key where id = 32");
+        }
+
+      } finally {
+        stmt.execute("drop table if exists tx_fore_key");
+        stmt.execute("drop table if exists tx_prim_key");
+      }
+    }
+
+    @Test
+    public void transactionTest() throws SQLException {
+      Statement stmt = sharedConn.createStatement();
+      try {
+        stmt.execute(
+            "CREATE TABLE transaction_test "
+                + "(id int not null primary key auto_increment, test varchar(20)) "
+                + "engine=innodb");
+        sharedConn.setAutoCommit(false);
+        stmt.executeUpdate("INSERT INTO transaction_test (test) VALUES ('heja')");
+        stmt.executeUpdate("INSERT INTO transaction_test (test) VALUES ('japp')");
+        sharedConn.commit();
+        ResultSet rs = stmt.executeQuery("SELECT * FROM transaction_test");
+        assertEquals(true, rs.next());
+        assertEquals("heja", rs.getString("test"));
+        assertEquals(true, rs.next());
+        assertEquals("japp", rs.getString("test"));
+        assertEquals(false, rs.next());
+        stmt.executeUpdate(
+            "INSERT INTO transaction_test (test) VALUES ('rollmeback')",
+            java.sql.Statement.RETURN_GENERATED_KEYS);
+        ResultSet rsGen = stmt.getGeneratedKeys();
+        rsGen.next();
+        int[] autoInc = setAutoInc();
+        assertEquals(autoInc[1] + autoInc[0] * 3, rsGen.getInt(1));
+        sharedConn.rollback();
+        rs = stmt.executeQuery("SELECT * FROM transaction_test WHERE id=3");
+        assertEquals(false, rs.next());
+        sharedConn.setAutoCommit(true);
+      } finally {
+        stmt.execute("DROP TABLE IF EXISTS transaction_test");
+      }
+    }
+
+    /**
+     * Get current autoincrement value, since Galera values are automatically set.
+     *
+     * @throws SQLException if any error occur.
+     */
+    public int[] setAutoInc() throws SQLException {
+      return setAutoInc(1, 0);
+    }
+
+    /**
+     * Get current autoincrement value, since Galera values are automatically set.
+     *
+     * @param autoIncInit default increment
+     * @param autoIncOffsetInit default increment offset
+     * @throws SQLException if any error occur
+     * @see <a
+     *     href="https://mariadb.org/auto-increments-in-galera/">https://mariadb.org/auto-increments-in-galera/</a>
+     */
+    public int[] setAutoInc(int autoIncInit, int autoIncOffsetInit) throws SQLException {
+
+      // in case of galera
+      //      if (isGalera()) {
+      //        ResultSet rs =
+      //            sharedConn.createStatement().executeQuery("show variables like
+      // '%auto_increment%'");
+      //        while (rs.next()) {
+      //          if ("auto_increment_increment".equals(rs.getString(1))) {
+      //            autoInc = rs.getInt(2);
+      //          }
+      //          if ("auto_increment_offset".equals(rs.getString(1))) {
+      //            autoIncOffset = rs.getInt(2);
+      //          }
+      //        }
+      //        if (autoInc == 1) {
+      //          // galera with one node only, then offset is not used
+      //          autoIncOffset = 0;
+      //        }
+      //      }
+      return new int[] {autoIncInit, autoIncOffsetInit};
+    }
+  }
+
+  @Test
+  public void various() throws SQLException {
+    assertThrows(SQLException.class, () -> sharedConn.setTypeMap(null));
+    assertTrue(sharedConn.getTypeMap().isEmpty());
+    assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, sharedConn.getHoldability());
+    sharedConn.setHoldability(ResultSet.CLOSE_CURSORS_AT_COMMIT);
+    assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, sharedConn.getHoldability());
+  }
+
+  @Test
+  public void verificationEd25519AuthPlugin() throws Throwable {
+    Assumptions.assumeTrue(
+        !"maxscale".equals(System.getenv("srv"))
+            && !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv"))
+            && isMariaDBServer()
+            && minVersion(10, 2, 0));
+    Statement stmt = sharedConn.createStatement();
+
+    try {
+      stmt.execute("INSTALL SONAME 'auth_ed25519'");
+    } catch (SQLException sqle) {
+      Assumptions.assumeTrue(false, "server doesn't have ed25519 plugin, cancelling test");
+    }
+    try {
+      stmt.execute("drop user verificationEd25519AuthPlugin@'%'");
+    } catch (SQLException e) {
+      // eat
+    }
+    try {
+      if (minVersion(10, 4, 0)) {
+        stmt.execute(
+            "CREATE USER IF NOT EXISTS verificationEd25519AuthPlugin@'%' IDENTIFIED "
+                + "VIA ed25519 USING PASSWORD('MySup8%rPassw@ord')");
+      } else {
+        stmt.execute(
+            "CREATE USER IF NOT EXISTS verificationEd25519AuthPlugin@'%' IDENTIFIED "
+                + "VIA ed25519 USING '6aW9C7ENlasUfymtfMvMZZtnkCVlcb1ssxOLJ0kj/AA'");
+      }
+    } catch (SQLException sqle) {
+      // already existing
+    }
+    stmt.execute(
+        "GRANT SELECT on " + sharedConn.getCatalog() + ".* to verificationEd25519AuthPlugin");
+
+    try (Connection connection =
+        createCon("user=verificationEd25519AuthPlugin&password=MySup8%rPassw@ord")) {
+      // must have succeeded
+      connection.getCatalog();
+    }
+    stmt.execute("drop user verificationEd25519AuthPlugin@'%'");
+  }
+
+  @Test
+  public void pamAuthPlugin() throws Throwable {
+    // https://mariadb.com/kb/en/authentication-plugin-pam/
+    // only test on travis, because only work on Unix-like operating systems.
+    // /etc/pam.d/mariadb pam configuration is created beforehand
+
+    Assumptions.assumeTrue(
+        isMariaDBServer()
+            && System.getenv("TEST_PAM_USER") != null
+            && !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv")));
+
+    Statement stmt = sharedConn.createStatement();
+    try {
+      stmt.execute("INSTALL PLUGIN pam SONAME 'auth_pam'");
+    } catch (SQLException sqle) {
+      // might be already set
+    }
+
+    String pamUser = System.getenv("TEST_PAM_USER");
+    String pamPwd = System.getenv("TEST_PAM_PWD");
+    try {
+      stmt.execute("DROP USER '" + pamUser + "'@'%'");
+    } catch (SQLException e) {
+      // eat
+    }
+    stmt.execute("CREATE USER '" + pamUser + "'@'%' IDENTIFIED VIA pam USING 'mariadb'");
+    stmt.execute("GRANT SELECT ON *.* TO '" + pamUser + "'@'%' IDENTIFIED VIA pam");
+
+    // mysql 8.0.31 broken public key retrieval, so avoid FLUSHING for now
+    Assumptions.assumeTrue(!isMariaDBServer() && !exactVersion(8, 0, 31));
+    stmt.execute("FLUSH PRIVILEGES");
+
+    int testPort = port;
+    if (System.getenv("TEST_PAM_PORT") != null) {
+      testPort = Integer.parseInt(System.getenv("TEST_PAM_PORT"));
+    }
+    String connStr =
+        String.format(
+            "jdbc:mariadb://%s:%s/%s?user=%s&password=%s&%s",
+            hostname, testPort, database, pamUser, pamPwd, defaultOther);
+    try {
+      try (Connection connection =
+          DriverManager.getConnection(connStr + "&restrictedAuth=dialog")) {
+        // must have succeeded
+        connection.getCatalog();
+      }
+    } catch (SQLException e) {
+      System.err.println("fail with connectionString : " + connStr + "&restrictedAuth=dialog");
+      throw e;
+    }
+
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> DriverManager.getConnection(connStr + "&restrictedAuth=other"),
+        "Client restrict authentication plugin to a limited set of authentication");
+
+    stmt.execute("drop user " + pamUser + "@'%'");
+  }
+
+  @Nested
+  @DisplayName("Compression Test")
+  class Compression {
+
+    @Test
+    public void testConnection() throws Exception {
+      try (Connection connection = createCon("useCompression")) {
+        // must have succeeded
+        connection.getCatalog();
+      }
+    }
+  }
+
+  @Test
+  public void testNoUseReadAheadInputConnection() throws Exception {
+    try (Connection connection = createCon("useReadAheadInput=false")) {
+      // must have succeeded
+      Statement stmt = connection.createStatement();
+      ResultSet rs = stmt.executeQuery("SELECT * FROM sequence_1_to_10");
+      int i = 0;
+      while (rs.next()) i++;
+      assertTrue(i > 0);
+    }
+  }
+
+  @Test
+  public void useNoDatabase() throws SQLException {
+    try (Connection con = createCon()) {
+      con.getCatalog();
+      Statement stmt = con.createStatement();
+      stmt.execute("CREATE DATABASE IF NOT EXISTS someDb");
+      con.setCatalog("someDb");
+      stmt.execute("DROP DATABASE someDb");
+      if (minVersion(10, 4, 0)
+          && !"maxscale".equals(System.getenv("srv"))
+          && !"skysql-ha".equals(System.getenv("srv"))) {
+        assertNull(con.getCatalog());
+      }
+    }
+  }
+
+  @Test
+  public void windowsNamedPipe() throws SQLException {
+    Assumptions.assumeTrue(isMariaDBServer() || !minVersion(8, 0, 14));
+    ResultSet rs = null;
+    try {
+      rs = sharedConn.createStatement().executeQuery("select @@named_pipe,@@socket");
+    } catch (SQLException sqle) {
+      // on non Windows system, named_pipe doesn't exist.
+    }
+    if (rs != null) {
+      assertTrue(rs.next());
+      System.out.println("named_pipe:" + rs.getString(1));
+      Assumptions.assumeTrue(rs.getBoolean(1));
+      String namedPipeName = rs.getString(2);
+      System.out.println("namedPipeName:" + namedPipeName);
+
+      // skip test if no namedPipeName was obtained because then we do not use a socket connection
+      Assumptions.assumeTrue(namedPipeName != null);
+      try (Connection connection = createCon("pipe=" + namedPipeName)) {
+        java.sql.Statement stmt = connection.createStatement();
+        try (ResultSet rs2 = stmt.executeQuery("SELECT 1")) {
+          assertTrue(rs2.next());
+        }
+      }
+      // connection without host name
+      try (java.sql.Connection connection =
+          DriverManager.getConnection(
+              String.format(
+                  "jdbc:mariadb:///%s?%s&pipe=%s&tcpAbortiveClose&tcpKeepAlive",
+                  sharedConn.getCatalog(),
+                  mDefUrl.substring(mDefUrl.indexOf("?user=") + 1),
+                  namedPipeName))) {
+        connection.setNetworkTimeout(null, 300);
+        java.sql.Statement stmt = connection.createStatement();
+        try (ResultSet rs2 = stmt.executeQuery("SELECT 1")) {
+          assertTrue(rs2.next());
+        }
+      }
+    }
+  }
+
+  @Test
+  public void localSocket() throws Exception {
+    Assumptions.assumeTrue(
+        System.getenv("local") != null
+            && "1".equals(System.getenv("local"))
+            && !System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win"));
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("select @@version_compile_os,@@socket");
+    if (!rs.next() || rs.getString(2) == null) {
+      return;
+    }
+    String path = rs.getString(2);
+    try {
+      stmt.execute("DROP USER testSocket");
+    } catch (SQLException e) {
+      // eat
+    }
+    boolean useOldNotation =
+        (!isMariaDBServer() || !minVersion(10, 2, 0))
+            && (isMariaDBServer() || !minVersion(8, 0, 0));
+    if (useOldNotation) {
+      stmt.execute("CREATE USER testSocket IDENTIFIED BY 'heyPassw!µ20§rd'");
+      stmt.execute("GRANT SELECT on *.* to testSocket IDENTIFIED BY 'heyPassw!µ20§rd'");
+    } else {
+      stmt.execute("CREATE USER testSocket IDENTIFIED BY 'heyPassw!µ20§rd'");
+      stmt.execute("GRANT SELECT on *.* to testSocket");
+    }
+    // mysql 8.0.31 broken public key retrieval, so avoid FLUSHING for now
+    Assumptions.assumeTrue(!isMariaDBServer() && !exactVersion(8, 0, 31));
+    stmt.execute("FLUSH PRIVILEGES");
+
+    String url =
+        String.format(
+            "jdbc:mariadb:///%s?user=testSocket&password=heyPassw!µ20§rd&localSocket=%s&tcpAbortiveClose&tcpKeepAlive",
+            sharedConn.getCatalog(), path);
+
+    try (java.sql.Connection connection = DriverManager.getConnection(url)) {
+      connection.setNetworkTimeout(null, 300);
+      rs = connection.createStatement().executeQuery("select 1");
+      assertTrue(rs.next());
+    }
+
+    Common.assertThrowsContains(
+        SQLException.class,
+        () ->
+            DriverManager.getConnection(
+                "jdbc:mariadb:///"
+                    + sharedConn.getCatalog()
+                    + "?user=testSocket&password=heyPassw!µ20§rd&localSocket=/wrongPath"),
+        "Socket fail to connect to host");
+
+    if (haveSsl()) {
+      String serverCertPath = SslTest.retrieveCertificatePath();
+      if (serverCertPath != null) {
+        try (Connection con =
+            DriverManager.getConnection(
+                "jdbc:mariadb:///"
+                    + sharedConn.getCatalog()
+                    + "?sslMode=verify-full&user=testSocket&password=heyPassw!µ20§rd"
+                    + "&serverSslCert="
+                    + serverCertPath
+                    + "&localSocket="
+                    + path)) {
+          rs = con.createStatement().executeQuery("select 1");
+          assertTrue(rs.next());
+        }
+      }
+    }
+    stmt.execute("DROP USER testSocket");
+  }
+
+  @Test
+  public void socketFactoryTest() throws SQLException {
+    try (Connection conn = createCon("socketFactory=" + SocketFactoryBasicTest.class.getName())) {
+      conn.isValid(1);
+    }
+
+    try (Connection conn = createCon("socketFactory=" + SocketFactoryTest.class.getName())) {
+      conn.isValid(1);
+    }
+
+    Common.assertThrowsContains(
+        SQLNonTransientConnectionException.class,
+        () -> createCon("socketFactory=wrongClass"),
+        "Socket factory failed to initialized with option \"socketFactory\" set to \"wrongClass\"");
+  }
+
+  @Test
+  public void socketOption() throws SQLException {
+    try (Connection con = createCon("tcpKeepAlive=true&tcpAbortiveClose=true")) {
+      con.isValid(1);
+    }
+  }
+
+  @Test
+  public void sslNotSet() throws SQLException {
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+    Assumptions.assumeFalse(haveSsl());
+    Common.assertThrowsContains(
+        SQLException.class, () -> createCon("sslMode=trust"), "ssl not enabled in the server");
+  }
+
+  @Test
+  public void localSocketAddress() throws SQLException {
+    Assumptions.assumeTrue(
+        System.getenv("local") != null
+            && "1".equals(System.getenv("local"))
+            && !System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win"));
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+    Configuration conf = Configuration.parse(mDefUrl);
+    HostAddress hostAddress = conf.addresses().get(0);
+    try (Connection con = createCon("localSocketAddress=" + hostAddress.host)) {
+      con.isValid(1);
+    }
+  }
+
+  @Test
+  public void setReadOnly() throws SQLException {
+    Connection con = createCon();
+    con.setReadOnly(true);
+    con.close();
+    Common.assertThrowsContains(
+        SQLNonTransientConnectionException.class,
+        () -> con.setReadOnly(false),
+        "Connection is closed");
+  }
+
+  @Test
+  public void timezone() throws SQLException {
+    try (Connection con = createCon("timezone=GMT-8")) {
+      Statement statement = con.createStatement();
+      ResultSet rs = statement.executeQuery("SELECT @@time_zone");
+      rs.next();
+      assertEquals("-08:00", rs.getString(1));
+    }
+
+    try (Connection con = createCon("timezone=UTC")) {
+      Statement statement = con.createStatement();
+      ResultSet rs = statement.executeQuery("SELECT @@time_zone, @@system_time_zone");
+      rs.next();
+      String srvTz = rs.getString(1);
+      if ("SYSTEM".equals(rs.getString(1))) {
+        srvTz = rs.getString(2);
+      }
+      assertTrue("+00:00".equals(srvTz) || "UTC".equals(srvTz));
+    }
+
+    try (Connection con = createCon("timezone=disable")) {
+      con.isValid(1);
+    }
+  }
+
+  @Test
+  public void createDatabaseIfNotExist() throws SQLException {
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+
+    // ensure connecting without DB
+    String connStr =
+        String.format(
+            "jdbc:mariadb://%s:%s/?user=%s&password=%s&%s&createDatabaseIfNotExist",
+            hostname, port, user, password, defaultOther);
+    try (Connection con = DriverManager.getConnection(connStr)) {
+      con.createStatement().executeQuery("SELECT 1");
+    }
+    sharedConn.createStatement().execute("DROP DATABASE IF EXISTS `bla``f``l`");
+    String nonExistentDatabase = "bla`f`l";
+    connStr =
+        String.format(
+            "jdbc:mariadb://%s:%s/%s?user=%s&password=%s&%s&createDatabaseIfNotExist",
+            hostname, port, nonExistentDatabase, user, password, defaultOther);
+    try (Connection con = DriverManager.getConnection(connStr)) {
+      ResultSet rs = con.createStatement().executeQuery("select DATABASE()");
+      assertTrue(rs.next());
+      assertEquals(nonExistentDatabase, rs.getString(1));
+    }
+
+    nonExistentDatabase = "bla`f`l0";
+    connStr =
+        String.format(
+            "jdbc:mariadb:replication://%s:%s,%s:%s/%s?user=%s&password=%s&%s&createDatabaseIfNotExist",
+            hostname, port, hostname, port, nonExistentDatabase, user, password, defaultOther);
+    try (Connection con = DriverManager.getConnection(connStr)) {
+      ResultSet rs = con.createStatement().executeQuery("select DATABASE()");
+      assertTrue(rs.next());
+      assertEquals(nonExistentDatabase, rs.getString(1));
+    }
+
+    sharedConn.createStatement().execute("DROP DATABASE IF EXISTS `bla``f``l`");
+    sharedConn.createStatement().execute("DROP DATABASE IF EXISTS `bla``f``l0`");
+  }
+
+  @Test
+  public void loopHost() throws SQLException {
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+
+    // ensure connecting without DB
+    String connStr =
+        String.format(
+            "jdbc:mariadb://wronghost,%s:%s/%s?user=%s&password=%s&%s",
+            hostname, port, database, user, password, defaultOther);
+    try (Connection con = DriverManager.getConnection(connStr)) {
+      con.createStatement().executeQuery("SELECT 1");
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/CredentialPluginTest.java b/src/test/java/org/mariadb/jdbc/integration/CredentialPluginTest.java
new file mode 100644
index 0000000..fbf197c
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/CredentialPluginTest.java
@@ -0,0 +1,221 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junitpioneer.jupiter.SetEnvironmentVariable;
+
+public class CredentialPluginTest extends Common {
+
+  /**
+   * Create temporary test User.
+   *
+   * @throws SQLException if any
+   */
+  @BeforeAll
+  public static void beforeTest() throws SQLException {
+    String version = System.getProperty("java.version");
+    int majorVersion = Integer.parseInt(version.substring(0, version.indexOf(".")));
+    Assumptions.assumeTrue(majorVersion < 17);
+
+    Assumptions.assumeTrue(isMariaDBServer());
+    drop();
+    boolean useOldNotation =
+        (!isMariaDBServer() || !minVersion(10, 2, 0))
+            && (isMariaDBServer() || !minVersion(8, 0, 0));
+    Statement stmt = sharedConn.createStatement();
+    if (useOldNotation) {
+      stmt.execute("CREATE USER 'identityUser'@'localhost'");
+      stmt.execute(
+          "GRANT SELECT ON "
+              + sharedConn.getCatalog()
+              + ".* TO 'identityUser'@'localhost' IDENTIFIED BY '!Passw0rd3Works'");
+      stmt.execute("CREATE USER 'identityUser'@'%'");
+      stmt.execute(
+          "GRANT SELECT ON "
+              + sharedConn.getCatalog()
+              + ".* TO 'identityUser'@'%' IDENTIFIED BY '!Passw0rd3Works'");
+    } else {
+      stmt.execute("CREATE USER 'identityUser'@'localhost' IDENTIFIED BY '!Passw0rd3Works'");
+      stmt.execute(
+          "GRANT SELECT ON " + sharedConn.getCatalog() + ".* TO 'identityUser'@'localhost'");
+      stmt.execute("CREATE USER 'identityUser'@'%' IDENTIFIED BY '!Passw0rd3Works'");
+      stmt.execute("GRANT SELECT ON " + sharedConn.getCatalog() + ".* TO 'identityUser'@'%'");
+    }
+    // mysql 8.0.31 broken public key retrieval, so avoid FLUSHING for now
+    Assumptions.assumeTrue(!isMariaDBServer() && !exactVersion(8, 0, 31));
+    stmt.execute("FLUSH PRIVILEGES");
+  }
+
+  /**
+   * remove temporary test User.
+   *
+   * @throws SQLException if any
+   */
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    try {
+      stmt.execute("DROP USER 'identityUser'@'%'");
+    } catch (SQLException e) {
+      // eat
+    }
+    try {
+      stmt.execute("DROP USER 'identityUser'@'localhost'");
+    } catch (SQLException e) {
+      // eat
+    }
+  }
+
+  @Test
+  public void propertiesIdentityTest() throws SQLException {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> createCon("credentialType=PROPERTY&user=identityUser"),
+        "Access denied");
+
+    System.setProperty("mariadb.user", "identityUser");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> createCon("credentialType=PROPERTY&pwdKey=myPwdKey"),
+        "Access denied");
+
+    System.setProperty("myPwdKey", "!Passw0rd3Works");
+    try (Connection conn = createCon("credentialType=PROPERTY&pwdKey=myPwdKey")) {
+      Statement stmt = conn.createStatement();
+      ResultSet rs = stmt.executeQuery("SELECT '5'");
+      assertTrue(rs.next());
+      assertEquals("5", rs.getString(1));
+    }
+
+    System.setProperty("mariadb.pwd", "!Passw0rd3Works");
+
+    try (Connection conn = createCon("credentialType=PROPERTY")) {
+      Statement stmt = conn.createStatement();
+      ResultSet rs = stmt.executeQuery("SELECT '5'");
+      assertTrue(rs.next());
+      assertEquals("5", rs.getString(1));
+    }
+  }
+
+  @Test
+  public void specificPropertiesIdentityTest() throws SQLException {
+
+    System.setProperty("myUserKey", "identityUser");
+    System.setProperty("myPwdKey", "!Passw0rd3Works");
+
+    try (Connection conn = createCon("credentialType=PROPERTY&userKey=myUserKey&pwdKey=myPwdKey")) {
+      Statement stmt = conn.createStatement();
+      ResultSet rs = stmt.executeQuery("SELECT '5'");
+      assertTrue(rs.next());
+      assertEquals("5", rs.getString(1));
+    }
+  }
+
+  @Test
+  public void unknownCredentialTest() {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> createCon("credentialType=UNKNOWN"),
+        "No identity plugin registered with the type \"UNKNOWN\"");
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void noEnvsIdentityTest() throws Exception {
+    Assumptions.assumeTrue(
+        !"maxscale".equals(System.getenv("srv"))
+            && !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv")));
+
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> createCon("&user=toti&credentialType=ENV&pwdKey=myPwdKey"),
+        "Access denied");
+  }
+
+  @Test
+  @SetEnvironmentVariable(key = "myPwdKey", value = "!Passw0rd3Works")
+  public void envsPwdTest() throws Exception {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> createCon("&user=toto&credentialType=ENV&pwdKey=myPwdKey"),
+        "Access denied");
+
+    try (Connection conn = createCon("user=identityUser&credentialType=ENV&pwdKey=myPwdKey")) {
+      Statement stmt = conn.createStatement();
+      ResultSet rs = stmt.executeQuery("SELECT '5'");
+      assertTrue(rs.next());
+      assertEquals("5", rs.getString(1));
+    }
+  }
+
+  @Test
+  @SetEnvironmentVariable(key = "myPwdKey", value = "!Passw0rd3Works")
+  @SetEnvironmentVariable(key = "MARIADB_USER", value = "identityUser")
+  public void envsDefaultIdentityAndPwdTest() throws Exception {
+    try (Connection conn = createCon("credentialType=ENV&pwdKey=myPwdKey")) {
+      Statement stmt = conn.createStatement();
+      ResultSet rs = stmt.executeQuery("SELECT '5'");
+      assertTrue(rs.next());
+      assertEquals("5", rs.getString(1));
+    }
+  }
+
+  @Test
+  @SetEnvironmentVariable(key = "MARIADB_PWD", value = "!Passw0rd3Works")
+  @SetEnvironmentVariable(key = "MARIADB_USER", value = "identityUser")
+  public void envsIdentityDefaultPwdTest() throws Exception {
+
+    try (Connection conn = createCon("credentialType=ENV")) {
+      Statement stmt = conn.createStatement();
+      ResultSet rs = stmt.executeQuery("SELECT '5'");
+      assertTrue(rs.next());
+      assertEquals("5", rs.getString(1));
+    }
+  }
+
+  @Test
+  @SetEnvironmentVariable(key = "myPwdKey", value = "!Passw0rd3Works")
+  @SetEnvironmentVariable(key = "myUserKey", value = "identityUser")
+  public void envsIdentityAndPwdTest() throws Exception {
+
+    try (Connection conn = createCon("credentialType=ENV&userKey=myUserKey&pwdKey=myPwdKey")) {
+      Statement stmt = conn.createStatement();
+      ResultSet rs = stmt.executeQuery("SELECT '5'");
+      assertTrue(rs.next());
+      assertEquals("5", rs.getString(1));
+    }
+  }
+
+  @Test
+  // @ClearSystemProperty(key = "some key")
+  @SetEnvironmentVariable(key = "MARIADB2_USER", value = "identityUser")
+  @SetEnvironmentVariable(key = "MARIADB2_PWD", value = "!Passw0rd3Works")
+  @SuppressWarnings("unchecked")
+  public void envTestsIdentityTest() throws Exception {
+    Assumptions.assumeTrue(
+        !"maxscale".equals(System.getenv("srv"))
+            && !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv")));
+    Assumptions.assumeTrue(isMariaDBServer() && haveSsl());
+
+    assertThrows(SQLException.class, () -> createCon("credentialType=ENVTEST&sslMode=DISABLE"));
+    assertThrows(SQLException.class, () -> createCon("credentialType=ENVTEST"));
+
+    try (Connection conn = createCon("credentialType=ENVTEST&sslMode=TRUST")) {
+      Statement stmt = conn.createStatement();
+      ResultSet rs = stmt.executeQuery("SELECT '5'");
+      assertTrue(rs.next());
+      assertEquals("5", rs.getString(1));
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/DataSourceTest.java b/src/test/java/org/mariadb/jdbc/integration/DataSourceTest.java
new file mode 100644
index 0000000..b591d86
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/DataSourceTest.java
@@ -0,0 +1,211 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.*;
+import javax.sql.DataSource;
+import javax.sql.PooledConnection;
+import javax.sql.XAConnection;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.MariaDbDataSource;
+
+public class DataSourceTest extends Common {
+
+  @Test
+  public void basic() throws SQLException {
+    MariaDbDataSource ds = new MariaDbDataSource(mDefUrl);
+    testDs(ds);
+
+    ds = new MariaDbDataSource();
+    ds.setUrl(mDefUrl);
+    testDs(ds);
+
+    ds = new MariaDbDataSource();
+    ds.setPassword("ttt");
+    ds.setUrl(mDefUrl);
+    assertThrows(SQLException.class, ds::getConnection);
+  }
+
+  private void testDs(MariaDbDataSource ds) throws SQLException {
+    try (Connection con1 = ds.getConnection()) {
+      try (Connection con2 = ds.getConnection()) {
+
+        ResultSet rs1 = con1.createStatement().executeQuery("SELECT 1");
+        ResultSet rs2 = con2.createStatement().executeQuery("SELECT 2");
+        while (rs1.next()) {
+          assertEquals(1, rs1.getInt(1));
+        }
+        while (rs2.next()) {
+          assertEquals(2, rs2.getInt(1));
+        }
+      }
+    }
+
+    PooledConnection con1 = null;
+    PooledConnection con2 = null;
+    try {
+      con1 = ds.getPooledConnection();
+      con2 = ds.getPooledConnection();
+
+      ResultSet rs1 = con1.getConnection().createStatement().executeQuery("SELECT 1");
+      ResultSet rs2 = con2.getConnection().createStatement().executeQuery("SELECT 2");
+      while (rs1.next()) {
+        assertEquals(1, rs1.getInt(1));
+      }
+      while (rs2.next()) {
+        assertEquals(2, rs2.getInt(1));
+      }
+
+    } finally {
+      if (con1 != null) con1.close();
+      if (con2 != null) con2.close();
+    }
+
+    XAConnection conx1 = null;
+    XAConnection conx2 = null;
+    try {
+      conx1 = ds.getXAConnection();
+      conx2 = ds.getXAConnection();
+
+      ResultSet rs1 = conx1.getConnection().createStatement().executeQuery("SELECT 1");
+      ResultSet rs2 = conx2.getConnection().createStatement().executeQuery("SELECT 2");
+      while (rs1.next()) {
+        assertEquals(1, rs1.getInt(1));
+      }
+      while (rs2.next()) {
+        assertEquals(2, rs2.getInt(1));
+      }
+
+    } finally {
+      if (conx1 != null) con1.close();
+      if (conx2 != null) con2.close();
+    }
+  }
+
+  @Test
+  public void basic2() throws SQLException {
+    MariaDbDataSource ds = new MariaDbDataSource();
+    assertNull(ds.getUrl());
+    assertNull(ds.getUser());
+    assertEquals(30, ds.getLoginTimeout());
+    DriverManager.setLoginTimeout(40);
+    assertEquals(40, ds.getLoginTimeout());
+    DriverManager.setLoginTimeout(0);
+    ds.setLoginTimeout(50);
+    assertEquals(50, ds.getLoginTimeout());
+
+    assertThrows(SQLException.class, () -> ds.getConnection());
+    assertThrows(SQLException.class, () -> ds.getConnection("user", "password"));
+    assertThrows(SQLException.class, () -> ds.getPooledConnection());
+    assertThrows(SQLException.class, () -> ds.getPooledConnection("user", "password"));
+    assertThrows(SQLException.class, () -> ds.getXAConnection());
+    assertThrows(SQLException.class, () -> ds.getXAConnection("user", "password"));
+
+    ds.setUser("dd");
+    assertEquals("dd", ds.getUser());
+
+    ds.setPassword("pwd");
+    assertThrows(SQLException.class, () -> ds.getConnection());
+    assertThrows(SQLException.class, () -> ds.getPooledConnection());
+
+    assertThrows(SQLException.class, () -> ds.setUrl("jdbc:wrong://d"));
+
+    ds.setUrl("jdbc:mariadb://myhost:5500/db?someOption=val");
+    assertEquals(
+        "jdbc:mariadb://myhost:5500/db?user=dd&password=***&someOption=val&connectTimeout=50000",
+        ds.getUrl());
+  }
+
+  @Test
+  public void switchUser() throws SQLException {
+    Assumptions.assumeTrue(
+        !"maxscale".equals(System.getenv("srv"))
+            && !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv")));
+    Statement stmt = sharedConn.createStatement();
+    try {
+      stmt.execute("DROP USER 'dsUser'");
+    } catch (SQLException e) {
+      // eat
+    }
+
+    if (minVersion(8, 0, 0)) {
+      if (isMariaDBServer()) {
+        stmt.execute("CREATE USER 'dsUser'@'%' IDENTIFIED BY 'MySup8%rPassw@ord'");
+        stmt.execute("GRANT SELECT ON " + sharedConn.getCatalog() + ".* TO 'dsUser'@'%'");
+      } else {
+        stmt.execute(
+            "CREATE USER 'dsUser'@'%' IDENTIFIED WITH mysql_native_password BY"
+                + " 'MySup8%rPassw@ord'");
+        stmt.execute("GRANT SELECT ON " + sharedConn.getCatalog() + ".* TO 'dsUser'@'%'");
+      }
+    } else {
+      stmt.execute("CREATE USER 'dsUser'@'%'");
+      stmt.execute(
+          "GRANT SELECT ON "
+              + sharedConn.getCatalog()
+              + ".* TO 'dsUser'@'%' IDENTIFIED BY 'MySup8%rPassw@ord'");
+    }
+    // mysql 8.0.31 broken public key retrieval, so avoid FLUSHING for now
+    Assumptions.assumeTrue(!isMariaDBServer() && !exactVersion(8, 0, 31));
+    stmt.execute("FLUSH PRIVILEGES");
+
+    DataSource ds = new MariaDbDataSource(mDefUrl + "&allowPublicKeyRetrieval");
+    try (Connection con1 = ds.getConnection()) {
+      try (Connection con2 = ds.getConnection("dsUser", "MySup8%rPassw@ord")) {
+        ResultSet rs1 = con1.createStatement().executeQuery("SELECT 1");
+        ResultSet rs2 = con2.createStatement().executeQuery("SELECT 2");
+        while (rs1.next()) {
+          assertEquals(1, rs1.getInt(1));
+        }
+        while (rs2.next()) {
+          assertEquals(2, rs2.getInt(1));
+        }
+      }
+    } finally {
+      try {
+        stmt.execute("DROP USER 'dsUser'");
+      } catch (SQLException e) {
+        // eat
+      }
+    }
+
+    // mysql has issue when creating new user with native password
+    if (haveSsl() && !isMariaDBServer() && minVersion(8, 0, 0)) {
+      try (Connection con = createCon("sslMode=trust")) {
+        con.createStatement().execute("DO 1");
+      }
+    }
+  }
+
+  @Test
+  public void exceptions() throws SQLException {
+    DataSource ds = new MariaDbDataSource(mDefUrl);
+    ds.unwrap(javax.sql.DataSource.class);
+    ds.unwrap(MariaDbDataSource.class);
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> ds.unwrap(String.class),
+        "Datasource is not a wrapper for java.lang.String");
+
+    assertTrue(ds.isWrapperFor(javax.sql.DataSource.class));
+    assertTrue(ds.isWrapperFor(MariaDbDataSource.class));
+    assertFalse(ds.isWrapperFor(String.class));
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> new MariaDbDataSource("jdbc:wrongUrl"),
+        "Wrong mariaDB url: jdbc:wrongUrl");
+    assertNull(ds.getLogWriter());
+    assertNull(ds.getParentLogger());
+    ds.setLogWriter(null);
+
+    assertEquals(30, ds.getLoginTimeout());
+    ds.setLoginTimeout(60);
+    assertEquals(60, ds.getLoginTimeout());
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/DatabaseMetadataTest.java b/src/test/java/org/mariadb/jdbc/integration/DatabaseMetadataTest.java
new file mode 100644
index 0000000..6ca03cb
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/DatabaseMetadataTest.java
@@ -0,0 +1,2176 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.*;
+import org.junit.jupiter.api.*;
+import org.mariadb.jdbc.Statement;
+
+public class DatabaseMetadataTest extends Common {
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP PROCEDURE IF EXISTS getProcTimePrecision");
+    stmt.execute("DROP PROCEDURE IF EXISTS getProcTimePrecision2");
+    stmt.execute("DROP PROCEDURE IF EXISTS testMetaCatalog");
+    stmt.execute("DROP TABLE IF EXISTS json_test");
+    stmt.execute("DROP TABLE IF EXISTS dbpk_test");
+    stmt.execute("DROP TABLE IF EXISTS datetime_test");
+    stmt.execute("DROP TABLE IF EXISTS manycols");
+    stmt.execute("DROP TABLE IF EXISTS ytab");
+    stmt.execute("DROP TABLE IF EXISTS maxcharlength");
+    stmt.execute("DROP TABLE IF EXISTS conj72");
+    stmt.execute("DROP TABLE IF EXISTS getTimePrecision");
+    stmt.execute("DROP TABLE IF EXISTS getPrecision");
+    stmt.execute("DROP TABLE IF EXISTS versionTable");
+    stmt.execute("DROP TABLE IF EXISTS tablegetcolumns");
+    stmt.execute("drop table if exists getBestRowIdentifier1");
+    stmt.execute("drop table if exists getBestRowIdentifier2");
+    stmt.execute("drop table if exists cross3");
+    stmt.execute("drop table if exists cross2");
+    stmt.execute("drop table if exists cross1");
+    stmt.execute("drop table if exists get_index_info");
+    stmt.execute("drop table if exists text_types_text");
+  }
+
+  @BeforeAll
+  public static void initClass() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE IF NOT EXISTS getTimePrecision("
+            + "d date, "
+            + "t1 datetime(0),"
+            + "t2 datetime(6),"
+            + "t3 timestamp(0) DEFAULT '2000-01-01 00:00:00',"
+            + "t4 timestamp(6) DEFAULT '2000-01-01 00:00:00',"
+            + "t5 time(0),"
+            + "t6 time(6))");
+    stmt.execute("CREATE TABLE json_test(t1 JSON)");
+    stmt.execute(
+        "CREATE PROCEDURE testMetaCatalog(x int, out y int) COMMENT 'comments' \n"
+            + "BEGIN\n"
+            + "SELECT 1;end\n");
+    stmt.execute(
+        "CREATE TABLE IF NOT EXISTS dbpk_test(val varchar(20), id1 int not null, id2 int not"
+            + " null,primary key(id1, id2)) engine=innodb");
+    stmt.execute("CREATE TABLE IF NOT EXISTS datetime_test(dt datetime)");
+    stmt.execute(
+        "CREATE TABLE IF NOT EXISTS `manycols`(  `tiny` tinyint(4) DEFAULT NULL,  `tiny_uns`"
+            + " tinyint(3) unsigned DEFAULT NULL,  `small` smallint(6) DEFAULT NULL,  `small_uns`"
+            + " smallint(5) unsigned DEFAULT NULL,  `medium` mediumint(9) DEFAULT NULL, "
+            + " `medium_uns` mediumint(8) unsigned DEFAULT NULL,  `int_col` int(11) DEFAULT NULL, "
+            + " `int_col_uns` int(10) unsigned DEFAULT NULL,  `big` bigint(20) DEFAULT NULL, "
+            + " `big_uns` bigint(20) unsigned DEFAULT NULL,  `decimal_col` decimal(10,5) DEFAULT"
+            + " NULL,  `fcol` float DEFAULT NULL,  `fcol_uns` float unsigned DEFAULT NULL,  `dcol`"
+            + " double DEFAULT NULL,  `dcol_uns` double unsigned DEFAULT NULL,  `date_col` date"
+            + " DEFAULT NULL,  `time_col` time DEFAULT NULL,  `timestamp_col` timestamp NOT NULL"
+            + " DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,  `year_col` year(4) DEFAULT"
+            + " NULL,  `bit_col` bit(5) DEFAULT NULL,  `char_col` char(5) DEFAULT NULL, "
+            + " `varchar_col` varchar(10) DEFAULT NULL,  `binary_col` binary(10) DEFAULT NULL, "
+            + " `varbinary_col` varbinary(10) DEFAULT NULL,  `tinyblob_col` tinyblob,  `blob_col`"
+            + " blob,  `mediumblob_col` mediumblob,  `longblob_col` longblob,  `tinytext_col`"
+            + " tinytext,  `text_col` text,  `mediumtext_col` mediumtext,  `longtext_col`"
+            + " longtext)");
+    stmt.execute("CREATE TABLE IF NOT EXISTS ytab(y year)");
+    stmt.execute(
+        "CREATE TABLE IF NOT EXISTS maxcharlength(maxcharlength char(1)) character set utf8");
+    stmt.execute("CREATE TABLE IF NOT EXISTS conj72(t tinyint(1))");
+    if (isMariaDBServer() && minVersion(10, 3, 4)) {
+      stmt.execute("CREATE TABLE IF NOT EXISTS versionTable(x INT) WITH SYSTEM VERSIONING");
+    }
+    stmt.execute("drop table if exists cross3");
+    stmt.execute("drop table if exists cross2");
+    stmt.execute("drop table if exists cross1");
+    stmt.execute("create table cross1 (id int not null primary key, val varchar(20))");
+    stmt.execute(
+        "create table cross2 (id int not null, id2 int not null,  id_ref0 int, foreign key"
+            + " (id_ref0) references cross1(id), UNIQUE unik_name (id, id2))");
+    stmt.execute(
+        "create table cross3 (id int not null primary key, id_ref1 int, id_ref2 int, foreign key"
+            + " fk_my_name (id_ref1, id_ref2) references cross2(id, id2) on update cascade)");
+    stmt.execute(
+        "create table getBestRowIdentifier1(i int not null primary key auto_increment, id int,"
+            + " id_ref1 int, id_ref2 int, foreign key fk_my_name_1 (id_ref1, id_ref2) references"
+            + " cross2(id, id2) on update cascade, UNIQUE getBestRowIdentifier_unik (id))");
+    stmt.execute(
+        "create table getBestRowIdentifier2(id_ref0 int not null, id_ref1 int, id_ref2 int not"
+            + " null, UNIQUE (id_ref1, id_ref2) , UNIQUE (id_ref0, id_ref2))");
+    stmt.execute(
+        "CREATE TABLE IF NOT EXISTS get_index_info(\n"
+            + "    no INT NOT NULL AUTO_INCREMENT,\n"
+            + "    product_category INT NOT NULL,\n"
+            + "    product_id INT NOT NULL,\n"
+            + "    customer_id INT NOT NULL,\n"
+            + "    PRIMARY KEY(no),\n"
+            + "    INDEX ind_prod (product_category, product_id),\n"
+            + "    INDEX ind_cust (customer_id))");
+    stmt.execute(
+        "create table text_types_text (varchar100           varchar(100),\n"
+            + "  varchar255           varchar(255),\n"
+            + "  text                 text,\n"
+            + "  `tinytext`           tinytext,\n"
+            + "  `mediumtext`         mediumtext,\n"
+            + "  `longtext`           longtext)");
+  }
+
+  private static void checkType(String name, int actualType, String colName, int expectedType) {
+    if (name.equals(colName)) {
+      assertEquals(actualType, expectedType);
+    }
+  }
+
+  @Test
+  public void primaryKeysTest() throws SQLException {
+    DatabaseMetaData meta = sharedConn.getMetaData();
+    ResultSet rs = meta.getPrimaryKeys(sharedConn.getCatalog(), null, "dbpk_test");
+    int counter = 0;
+    while (rs.next()) {
+      counter++;
+      assertEquals(sharedConn.getCatalog(), rs.getString("table_cat"));
+      assertEquals(null, rs.getString("table_schem"));
+      assertEquals("dbpk_test", rs.getString("table_name"));
+      assertEquals("id" + counter, rs.getString("column_name"));
+      assertEquals("id" + counter, rs.getString("column_name"));
+      assertEquals("PRIMARY", rs.getString("PK_NAME"));
+    }
+    assertEquals(2, counter);
+  }
+
+  @Test
+  public void primaryKeyTest2() throws SQLException {
+    java.sql.Statement stmt = sharedConn.createStatement();
+    stmt.execute("drop table if exists primarykeytest2");
+    stmt.execute("drop table if exists primarykeytest1");
+    stmt.execute("CREATE TABLE primarykeytest1 ( id1 integer, constraint pk primary key(id1))");
+    stmt.execute(
+        "CREATE TABLE primarykeytest2 (id2a integer, id2b integer, constraint pk primary key(id2a,"
+            + " id2b), constraint fk1 foreign key(id2a) references primarykeytest1(id1), "
+            + " constraint fk2 foreign key(id2b) references primarykeytest1(id1))");
+
+    DatabaseMetaData dbmd = sharedConn.getMetaData();
+    ResultSet rs = dbmd.getPrimaryKeys(sharedConn.getCatalog(), null, "primarykeytest2");
+    int counter = 0;
+    while (rs.next()) {
+      counter++;
+      assertEquals(sharedConn.getCatalog(), rs.getString("table_cat"));
+      assertEquals(null, rs.getString("table_schem"));
+      assertEquals("primarykeytest2", rs.getString("table_name"));
+      assertEquals(counter, rs.getShort("key_seq"));
+      assertEquals("PRIMARY", rs.getString("pk_name"));
+    }
+    assertEquals(2, counter);
+    stmt.execute("drop table if exists primarykeytest2");
+    stmt.execute("drop table if exists primarykeytest1");
+  }
+
+  @Test
+  public void datetimeTest() throws SQLException {
+    java.sql.Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("select * from datetime_test");
+    assertEquals(93, rs.getMetaData().getColumnType(1));
+  }
+
+  @Test
+  public void functionColumns() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    DatabaseMetaData meta = sharedConn.getMetaData();
+
+    if (meta.getDatabaseMajorVersion() < 5) {
+      return;
+    } else if (meta.getDatabaseMajorVersion() == 5 && meta.getDatabaseMinorVersion() < 5) {
+      return;
+    }
+
+    stmt.execute("DROP FUNCTION IF EXISTS hello");
+    stmt.execute(
+        "CREATE FUNCTION hello (s CHAR(20), i int) RETURNS CHAR(50) DETERMINISTIC  "
+            + "RETURN CONCAT('Hello, ',s,'!')");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs = meta.getFunctionColumns(null, null, "hello", null);
+
+    assertTrue(rs.next());
+    /* First row is for return value */
+    assertEquals(rs.getString("FUNCTION_CAT"), sharedConn.getCatalog());
+    assertEquals(rs.getString("FUNCTION_SCHEM"), null);
+    assertEquals(rs.getString("COLUMN_NAME"), null); /* No name, since it is return value */
+    assertEquals(rs.getInt("COLUMN_TYPE"), DatabaseMetaData.functionReturn);
+    assertEquals(rs.getInt("DATA_TYPE"), Types.CHAR);
+    assertEquals(rs.getString("TYPE_NAME"), "char");
+
+    assertTrue(rs.next());
+    assertEquals(rs.getString("COLUMN_NAME"), "s"); /* input parameter 's' (CHAR) */
+    assertEquals(rs.getInt("COLUMN_TYPE"), DatabaseMetaData.functionColumnIn);
+    assertEquals(rs.getInt("DATA_TYPE"), Types.CHAR);
+    assertEquals(rs.getString("TYPE_NAME"), "char");
+
+    assertTrue(rs.next());
+    assertEquals(rs.getString("COLUMN_NAME"), "i"); /* input parameter 'i' (INT) */
+    assertEquals(rs.getInt("COLUMN_TYPE"), DatabaseMetaData.functionColumnIn);
+    assertEquals(rs.getInt("DATA_TYPE"), Types.INTEGER);
+    assertEquals(rs.getString("TYPE_NAME"), "int");
+    stmt.execute("DROP FUNCTION IF EXISTS hello");
+  }
+
+  /** Same as getImportedKeys, with one foreign key in a table in another catalog. */
+  @Test
+  public void getImportedKeys() throws Exception {
+    getImportedKeys(sharedConn, true);
+    try (org.mariadb.jdbc.Connection con = createCon()) {
+      java.sql.Statement stmt = con.createStatement();
+      stmt.execute("SET sql_mode = concat(@@sql_mode,',NO_BACKSLASH_ESCAPES')");
+      getImportedKeys(con, true);
+    }
+    try (org.mariadb.jdbc.Connection con = createCon("importedKeysWithConstraintNames=false")) {
+      java.sql.Statement stmt = con.createStatement();
+      stmt.execute("SET sql_mode = concat(@@sql_mode,',NO_BACKSLASH_ESCAPES')");
+      getImportedKeys(con, false);
+    }
+  }
+
+  private void getImportedKeys(
+      org.mariadb.jdbc.Connection con, boolean importedKeysWithConstraintNames) throws Exception {
+    // cancel for MySQL 8.0, since CASCADE with I_S give importedKeySetDefault, not
+    // importedKeyCascade
+    //    Assumptions.assumeFalse(!isMariaDBServer() && minVersion(8, 0, 0));
+    Assumptions.assumeFalse(isXpand());
+    java.sql.Statement st = con.createStatement();
+    st.execute("DROP TABLE IF EXISTS `product order 1`");
+    st.execute("DROP TABLE IF EXISTS `other sch'ema`.`product order.2`");
+    st.execute("DROP DATABASE IF EXISTS `other sch'ema`");
+    st.execute("DROP TABLE IF EXISTS `product_order.3`");
+    st.execute("DROP TABLE IF EXISTS product_order4");
+    st.execute("DROP TABLE IF EXISTS t1.product ");
+    st.execute("DROP TABLE IF EXISTS `cus``tomer`");
+    st.execute("DROP DATABASE IF EXISTS test1");
+
+    st.execute("CREATE DATABASE IF NOT EXISTS t1");
+    st.execute("CREATE DATABASE IF NOT EXISTS `other sch'ema`");
+
+    st.execute(
+        "CREATE TABLE t1.product ( category INT NOT NULL, id INT NOT NULL, price DECIMAL,"
+            + " UNIQUE unik_name (category, id) )");
+
+    st.execute(
+        "CREATE TABLE `cus``tomer` (id INT NOT NULL, id2 INT NOT NULL, PRIMARY KEY (id), UNIQUE"
+            + " unikConst (id2))");
+    String constraint = "ON UPDATE SET DEFAULT ON DELETE SET DEFAULT";
+    if (!isMariaDBServer() || !minVersion(10, 5, 0))
+      constraint = "ON UPDATE CASCADE ON DELETE CASCADE";
+    st.execute(
+        "CREATE TABLE `product order 1` (\n"
+            + "    no INT NOT NULL AUTO_INCREMENT,\n"
+            + "    product_category INT NOT NULL,\n"
+            + "    product_id INT NOT NULL,\n"
+            + "    customer_id INT DEFAULT NULL,\n"
+            + "    PRIMARY KEY(no),\n"
+            + "    INDEX (product_category, product_id),\n"
+            + "    INDEX (customer_id),\n"
+            + "    FOREIGN KEY (product_category, product_id)\n"
+            + "      REFERENCES t1.product(category, id)\n"
+            + "      ON UPDATE CASCADE ON DELETE CASCADE,\n"
+            + "    FOREIGN KEY (customer_id)\n"
+            + "      REFERENCES `cus``tomer`(id)\n"
+            + constraint
+            + ")");
+
+    st.execute(
+        "CREATE TABLE `other sch'ema`.`product order.2` (\n"
+            + "    no INT NOT NULL,\n"
+            + "    customer_id INT,\n"
+            + "    FOREIGN KEY (customer_id)\n"
+            + "      REFERENCES "
+            + sharedConn.getCatalog()
+            + ".`cus``tomer`(id)\n"
+            + "      ON UPDATE RESTRICT ON DELETE RESTRICT)");
+
+    st.execute(
+        "CREATE TABLE `product_order.3` (\n"
+            + "    no INT NOT NULL,\n"
+            + "    customer_id INT,\n"
+            + "    FOREIGN KEY (customer_id)\n"
+            + "      REFERENCES `cus``tomer`(id)\n"
+            + "      ON UPDATE SET NULL ON DELETE SET NULL)");
+
+    st.execute(
+        "CREATE TABLE product_order4 (\n"
+            + "    no INT NOT NULL,\n"
+            + "    customer_id INT,\n"
+            + "    customer_id2 INT,\n"
+            + "    FOREIGN KEY fk1 (customer_id)\n"
+            + "      REFERENCES `cus``tomer`(id)\n"
+            + "      ON UPDATE NO ACTION ON DELETE NO ACTION,"
+            + "    FOREIGN KEY fk2 (customer_id2)\n"
+            + "      REFERENCES `cus``tomer`(id2)\n"
+            + "      ON UPDATE SET NULL ON DELETE SET NULL)");
+
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> con.getMetaData().getImportedKeys(con.getCatalog(), null, null),
+        "'table' parameter in getImportedKeys cannot be null");
+    /*
+    Test that I_S implementation is equivalent to parsing "show create table" .
+     Get result sets using either method and compare (ignore minor differences INT vs SMALLINT
+    */
+    ResultSet rs1 =
+        con.getMetaData().getImportedKeysUsingShowCreateTable(con.getCatalog(), "product order 1");
+    ResultSet rs2 =
+        con.getMetaData()
+            .getImportedKeysUsingInformationSchema(con.getCatalog(), "product order 1");
+    assertEquals(rs1.getMetaData().getColumnCount(), rs2.getMetaData().getColumnCount());
+    for (int i = 0; i < 2; i++) {
+      ResultSet rs = i == 0 ? rs1 : rs2;
+      assertTrue(rs.next());
+
+      assertEquals("t1", rs.getString("PKTABLE_CAT"));
+      assertEquals(null, rs.getString("PKTABLE_SCHEM"));
+      assertEquals("product", rs.getString("PKTABLE_NAME"));
+      assertEquals("category", rs.getString("PKCOLUMN_NAME"));
+      assertEquals(sharedConn.getCatalog(), rs.getString("FKTABLE_CAT"));
+      assertEquals(null, rs.getString("FKTABLE_SCHEM"));
+      assertEquals("product order 1", rs.getString("FKTABLE_NAME"));
+      assertEquals("product_category", rs.getString("FKCOLUMN_NAME"));
+      assertEquals(1, rs.getInt("KEY_SEQ"));
+      assertEquals(DatabaseMetaData.importedKeyCascade, rs.getInt("UPDATE_RULE"));
+      assertEquals(DatabaseMetaData.importedKeyCascade, rs.getInt("DELETE_RULE"));
+      assertEquals("product order 1_ibfk_1", rs.getString("FK_NAME"));
+      // with show, meta don't know contraint name
+      assertEquals(
+          (i == 0 && !importedKeysWithConstraintNames) ? null : "unik_name",
+          rs.getString("PK_NAME"));
+      assertEquals(DatabaseMetaData.importedKeyNotDeferrable, rs.getInt("DEFERRABILITY"));
+
+      assertTrue(rs.next());
+      assertEquals("t1", rs.getString("PKTABLE_CAT"));
+      assertEquals(null, rs.getString("PKTABLE_SCHEM"));
+      assertEquals("product", rs.getString("PKTABLE_NAME"));
+      assertEquals("id", rs.getString("PKCOLUMN_NAME"));
+      assertEquals(sharedConn.getCatalog(), rs.getString("FKTABLE_CAT"));
+      assertEquals(null, rs.getString("FKTABLE_SCHEM"));
+      assertEquals("product order 1", rs.getString("FKTABLE_NAME"));
+      assertEquals("product_id", rs.getString("FKCOLUMN_NAME"));
+      assertEquals(2, rs.getInt("KEY_SEQ"));
+      assertEquals(DatabaseMetaData.importedKeyCascade, rs.getInt("UPDATE_RULE"));
+      assertEquals(DatabaseMetaData.importedKeyCascade, rs.getInt("DELETE_RULE"));
+      assertEquals("product order 1_ibfk_1", rs.getString("FK_NAME"));
+      // with show, meta don't know contraint name
+      assertEquals(
+          (i == 0 && !importedKeysWithConstraintNames) ? null : "unik_name",
+          rs.getString("PK_NAME"));
+      assertEquals(DatabaseMetaData.importedKeyNotDeferrable, rs.getInt("DEFERRABILITY"));
+
+      assertTrue(rs.next());
+      assertEquals(sharedConn.getCatalog(), rs.getString("PKTABLE_CAT"));
+      assertEquals(null, rs.getString("PKTABLE_SCHEM"));
+      assertEquals("cus`tomer", rs.getString("PKTABLE_NAME"));
+      assertEquals("id", rs.getString("PKCOLUMN_NAME"));
+      assertEquals(sharedConn.getCatalog(), rs.getString("FKTABLE_CAT"));
+      assertEquals(null, rs.getString("FKTABLE_SCHEM"));
+      assertEquals("product order 1", rs.getString("FKTABLE_NAME"));
+      assertEquals("customer_id", rs.getString("FKCOLUMN_NAME"));
+      assertEquals(1, rs.getInt("KEY_SEQ"));
+      if (isMariaDBServer() && minVersion(10, 5, 0)) {
+        assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("UPDATE_RULE"));
+        assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("DELETE_RULE"));
+      } else {
+        assertEquals(DatabaseMetaData.importedKeyCascade, rs.getInt("UPDATE_RULE"));
+        assertEquals(DatabaseMetaData.importedKeyCascade, rs.getInt("DELETE_RULE"));
+      }
+      assertEquals("product order 1_ibfk_2", rs.getString("FK_NAME"));
+      // with show, meta don't know contraint name
+      assertEquals(
+          (i == 0 && !importedKeysWithConstraintNames) ? null : "PRIMARY", rs.getString("PK_NAME"));
+      assertEquals(DatabaseMetaData.importedKeyNotDeferrable, rs.getInt("DEFERRABILITY"));
+    }
+
+    /* Also compare metadata */
+    ResultSetMetaData md1 = rs1.getMetaData();
+    ResultSetMetaData md2 = rs2.getMetaData();
+    for (int i = 1; i <= md1.getColumnCount(); i++) {
+      assertEquals(md1.getColumnLabel(i), md2.getColumnLabel(i));
+    }
+
+    rs1 = con.getMetaData().getImportedKeysUsingShowCreateTable("other sch'ema", "product order.2");
+    rs2 =
+        con.getMetaData().getImportedKeysUsingInformationSchema("other sch'ema", "product order.2");
+    assertEquals(rs1.getMetaData().getColumnCount(), rs2.getMetaData().getColumnCount());
+    for (int i = 0; i < 2; i++) {
+      ResultSet rs = i == 0 ? rs1 : rs2;
+      assertTrue(rs.next());
+      assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("UPDATE_RULE"));
+      assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("DELETE_RULE"));
+    }
+
+    rs1 =
+        con.getMetaData()
+            .getImportedKeysUsingShowCreateTable(sharedConn.getCatalog(), "product_order.3");
+    rs2 =
+        con.getMetaData()
+            .getImportedKeysUsingInformationSchema(sharedConn.getCatalog(), "product_order.3");
+    assertEquals(rs1.getMetaData().getColumnCount(), rs2.getMetaData().getColumnCount());
+    for (int i = 0; i < 2; i++) {
+      ResultSet rs = i == 0 ? rs1 : rs2;
+      assertTrue(rs.next());
+      assertEquals(DatabaseMetaData.importedKeySetNull, rs.getInt("UPDATE_RULE"));
+      assertEquals(DatabaseMetaData.importedKeySetNull, rs.getInt("DELETE_RULE"));
+    }
+
+    rs1 =
+        con.getMetaData()
+            .getImportedKeysUsingShowCreateTable(sharedConn.getCatalog(), "product_order4");
+    rs2 =
+        con.getMetaData()
+            .getImportedKeysUsingInformationSchema(sharedConn.getCatalog(), "product_order4");
+    assertEquals(rs1.getMetaData().getColumnCount(), rs2.getMetaData().getColumnCount());
+    for (int i = 0; i < 2; i++) {
+      ResultSet rs = i == 0 ? rs1 : rs2;
+      if (isMariaDBServer()) {
+        for (int j = 0; j < 2; j++) {
+          assertTrue(rs.next());
+          if ("fk1".equals(rs.getString("FK_NAME"))) {
+            assertEquals(DatabaseMetaData.importedKeyNoAction, rs.getInt("UPDATE_RULE"));
+            assertEquals(DatabaseMetaData.importedKeyNoAction, rs.getInt("DELETE_RULE"));
+          } else {
+            assertEquals(DatabaseMetaData.importedKeySetNull, rs.getInt("UPDATE_RULE"));
+            assertEquals(DatabaseMetaData.importedKeySetNull, rs.getInt("DELETE_RULE"));
+          }
+        }
+      }
+    }
+
+    Common.assertThrowsContains(
+        SQLException.class,
+        () ->
+            con.getMetaData()
+                .getImportedKeysUsingShowCreateTable(sharedConn.getCatalog(), "UNKNO>NTABLE"),
+        " doesn't exist");
+
+    st.execute("DROP TABLE IF EXISTS `product order 1`");
+    st.execute("DROP TABLE IF EXISTS `other sch'ema`.`product order.2`");
+    st.execute("DROP TABLE IF EXISTS `product_order.3`");
+    st.execute("DROP TABLE IF EXISTS product_order4");
+    st.execute("DROP TABLE IF EXISTS t1.product ");
+    st.execute("DROP TABLE IF EXISTS `cus``tomer`");
+    st.execute("DROP DATABASE IF EXISTS test1");
+    st.execute("DROP DATABASE IF EXISTS `other sch'ema`");
+  }
+
+  @Test
+  public void exportedKeysTest() throws SQLException {
+
+    DatabaseMetaData dbmd = sharedConn.getMetaData();
+    ResultSet rs = dbmd.getExportedKeys(sharedConn.getCatalog(), null, "cross%");
+    assertTrue(rs.next());
+    assertEquals(sharedConn.getCatalog(), rs.getString("PKTABLE_CAT"));
+    assertEquals(null, rs.getString("PKTABLE_SCHEM"));
+    assertEquals("cross1", rs.getString("PKTABLE_NAME"));
+    assertEquals("id", rs.getString("PKCOLUMN_NAME"));
+    assertEquals(sharedConn.getCatalog(), rs.getString("FKTABLE_CAT"));
+    assertEquals(null, rs.getString("FKTABLE_SCHEM"));
+    assertEquals("cross2", rs.getString("FKTABLE_NAME"));
+    assertEquals("id_ref0", rs.getString("FKCOLUMN_NAME"));
+    assertTrue(rs.getInt("KEY_SEQ") == 1 || rs.wasNull());
+    if (!isMariaDBServer() && minVersion(8, 0, 0)) {
+      assertEquals(DatabaseMetaData.importedKeyNoAction, rs.getInt("UPDATE_RULE"));
+      assertEquals(DatabaseMetaData.importedKeyNoAction, rs.getInt("DELETE_RULE"));
+    } else {
+      assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("UPDATE_RULE"));
+      assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("DELETE_RULE"));
+    }
+    assertEquals("cross2_ibfk_1", rs.getString("FK_NAME"));
+    assertTrue(
+        "PRIMARY".equals(rs.getString("PK_NAME"))
+            || "__idx_cross1__PRIMARY".equals(rs.getString("PK_NAME")));
+
+    assertTrue(rs.next());
+
+    assertEquals(sharedConn.getCatalog(), rs.getString("PKTABLE_CAT"));
+    assertEquals(null, rs.getString("PKTABLE_SCHEM"));
+    assertEquals("cross2", rs.getString("PKTABLE_NAME"));
+    assertEquals("id", rs.getString("PKCOLUMN_NAME"));
+    assertEquals(sharedConn.getCatalog(), rs.getString("FKTABLE_CAT"));
+    assertEquals(null, rs.getString("FKTABLE_SCHEM"));
+    assertEquals("cross3", rs.getString("FKTABLE_NAME"));
+    assertEquals("id_ref1", rs.getString("FKCOLUMN_NAME"));
+    assertTrue(rs.getInt("KEY_SEQ") == 1 || rs.wasNull());
+    assertEquals(DatabaseMetaData.importedKeyCascade, rs.getInt("UPDATE_RULE"));
+    if (!isMariaDBServer() && minVersion(8, 0, 0)) {
+      assertEquals(DatabaseMetaData.importedKeyNoAction, rs.getInt("DELETE_RULE"));
+    } else {
+      assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("DELETE_RULE"));
+    }
+    assertTrue(
+        "cross3_ibfk_1".equals(rs.getString("FK_NAME"))
+            || "fk_my_name".equals(rs.getString("FK_NAME")));
+    assertEquals("unik_name", rs.getString("PK_NAME"));
+
+    assertTrue(rs.next());
+    assertEquals(sharedConn.getCatalog(), rs.getString("PKTABLE_CAT"));
+    assertEquals(null, rs.getString("PKTABLE_SCHEM"));
+    assertEquals("cross2", rs.getString("PKTABLE_NAME"));
+    assertEquals("id2", rs.getString("PKCOLUMN_NAME"));
+    assertEquals(sharedConn.getCatalog(), rs.getString("FKTABLE_CAT"));
+    assertEquals(null, rs.getString("FKTABLE_SCHEM"));
+    assertEquals("cross3", rs.getString("FKTABLE_NAME"));
+    assertEquals("id_ref2", rs.getString("FKCOLUMN_NAME"));
+    assertTrue(rs.getInt("KEY_SEQ") == 2 || rs.wasNull());
+    assertEquals(DatabaseMetaData.importedKeyCascade, rs.getInt("UPDATE_RULE"));
+    if (!isMariaDBServer() && minVersion(8, 0, 0)) {
+      assertEquals(DatabaseMetaData.importedKeyNoAction, rs.getInt("DELETE_RULE"));
+    } else {
+      assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("DELETE_RULE"));
+    }
+    assertTrue(
+        "fk_my_name".equals(rs.getString("FK_NAME"))
+            || "cross3_ibfk_1".equals(rs.getString("FK_NAME")));
+    assertEquals("unik_name", rs.getString("PK_NAME"));
+  }
+
+  @Test
+  public void importedKeysTest() throws SQLException {
+    java.sql.Statement stmt = sharedConn.createStatement();
+    stmt.execute("drop table if exists fore_key0");
+    stmt.execute("drop table if exists fore_key1");
+    stmt.execute("drop table if exists prim_key");
+
+    stmt.execute(
+        "create table prim_key (id int not null primary key, " + "val varchar(20)) engine=innodb");
+    stmt.execute(
+        "create table fore_key0 (id int not null primary key, "
+            + "id_ref0 int, foreign key (id_ref0) references prim_key(id)) engine=innodb");
+    stmt.execute(
+        "create table fore_key1 (id int not null primary key, id_ref1 int, foreign key (id_ref1)"
+            + " references prim_key(id) on update cascade) engine=innodb");
+
+    DatabaseMetaData dbmd = sharedConn.getMetaData();
+    ResultSet rs = dbmd.getImportedKeys(sharedConn.getCatalog(), null, "fore_key0");
+    int counter = 0;
+    while (rs.next()) {
+      assertEquals("id", rs.getString("pkcolumn_name"));
+      assertEquals("prim_key", rs.getString("pktable_name"));
+      counter++;
+    }
+    assertEquals(1, counter);
+    stmt.execute("drop table if exists fore_key0");
+    stmt.execute("drop table if exists fore_key1");
+    stmt.execute("drop table if exists prim_key");
+  }
+
+  @Test
+  public void testGetCatalogs() throws SQLException {
+    DatabaseMetaData dbmd = sharedConn.getMetaData();
+
+    ResultSet rs = dbmd.getCatalogs();
+    boolean haveMysql = false;
+    boolean haveInformationSchema = false;
+    while (rs.next()) {
+      String cat = rs.getString(1);
+
+      if (cat.equalsIgnoreCase("mysql")) {
+        haveMysql = true;
+      } else if (cat.equalsIgnoreCase("information_schema")) {
+        haveInformationSchema = true;
+      }
+    }
+    if (!isXpand()) assertTrue(haveMysql);
+    assertTrue(haveInformationSchema);
+  }
+
+  @Test
+  public void testGetTables() throws SQLException {
+    java.sql.Statement stmt = sharedConn.createStatement();
+    stmt.execute("drop table if exists fore_key0");
+    stmt.execute("drop table if exists fore_key1");
+    stmt.execute("drop table if exists prim_key");
+
+    stmt.execute(
+        "create table prim_key (id int not null primary key, " + "val varchar(20)) engine=innodb");
+    stmt.execute(
+        "create table fore_key0 (id int not null primary key, "
+            + "id_ref0 int, foreign key (id_ref0) references prim_key(id)) engine=innodb");
+    stmt.execute(
+        "create table fore_key1 (id int not null primary key, id_ref1 int, foreign key (id_ref1)"
+            + " references prim_key(id) on update cascade) engine=innodb");
+
+    DatabaseMetaData dbmd = sharedConn.getMetaData();
+    ResultSet rs = dbmd.getTables(null, null, "prim_key", null);
+
+    assertEquals(true, rs.next());
+    rs = dbmd.getTables("", null, "prim_key", null);
+    assertEquals(true, rs.next());
+
+    rs = dbmd.getTables("", null, "prim_key", new String[] {"BASE TABLE", "OTHER"});
+    assertEquals(true, rs.next());
+
+    rs = dbmd.getTables("", null, "prim_key", new String[] {"TABLE", null});
+    assertEquals(true, rs.next());
+
+    // ensure no regression CONJ-921
+    rs = dbmd.getTables(null, null, null, new String[] {"TABLE"});
+    assertEquals(true, rs.next());
+  }
+
+  @Test
+  public void testGetTables2() throws SQLException {
+    DatabaseMetaData dbmd = sharedConn.getMetaData();
+    Assumptions.assumeTrue(!isXpand());
+    ResultSet rs =
+        dbmd.getTables(
+            "information_schema", null, "TABLE_PRIVILEGES", new String[] {"SYSTEM VIEW"});
+    assertEquals(true, rs.next());
+    assertEquals(false, rs.next());
+    rs = dbmd.getTables(null, null, "TABLE_PRIVILEGES", new String[] {"TABLE"});
+    assertEquals(false, rs.next());
+  }
+
+  @Test
+  public void testGetTablesSystemVersionTables() throws SQLException {
+    Assumptions.assumeTrue(isMariaDBServer() && minVersion(10, 3, 4));
+    DatabaseMetaData dbmd = sharedConn.getMetaData();
+    ResultSet rs = dbmd.getTables(null, null, "versionTable", null);
+    assertEquals(true, rs.next());
+    assertEquals(false, rs.next());
+    rs = dbmd.getTables(null, null, "versionTable", new String[] {"TABLE"});
+    assertEquals(true, rs.next());
+    assertEquals(false, rs.next());
+    rs = dbmd.getTables(null, null, "versionTable", new String[] {"SYSTEM VIEW"});
+    assertEquals(false, rs.next());
+  }
+
+  @Test
+  public void testGetTables3() throws SQLException {
+    java.sql.Statement stmt = sharedConn.createStatement();
+    stmt.execute("drop table if exists table_type_test");
+
+    stmt.execute(
+        "create table table_type_test (id int not null primary key, "
+            + "val varchar(20)) engine=innodb");
+
+    DatabaseMetaData dbmd = sharedConn.getMetaData();
+    ResultSet tableSet = dbmd.getTables(null, null, "table_type_test", null);
+
+    assertEquals(true, tableSet.next());
+
+    String tableName = tableSet.getString("TABLE_NAME");
+    assertEquals("table_type_test", tableName);
+
+    String tableType = tableSet.getString("TABLE_TYPE");
+    assertEquals("TABLE", tableType);
+    // see for possible values
+    // https://docs.oracle.com/javase/7/docs/api/java/sql/DatabaseMetaData.html#getTableTypes%28%29
+  }
+
+  @Test
+  public void testGetColumns() throws SQLException {
+    // mysql 5.6 doesn't permit VIRTUAL keyword
+    Assumptions.assumeTrue(isMariaDBServer() || !isMariaDBServer() && minVersion(5, 7, 0));
+    // Xpand doesn't support PERSISTENT keyword
+    Assumptions.assumeFalse(isXpand());
+
+    Statement stmt = sharedConn.createStatement();
+    if (minVersion(10, 2, 0) || !isMariaDBServer()) {
+      stmt.execute(
+          "CREATE TABLE IF NOT EXISTS `ta\n"
+              + "ble'getcolumns`(a INT NOT NULL primary key auto_increment, b VARCHAR(32), c INT AS"
+              + " (CHAR_LENGTH(b)) VIRTUAL, d VARCHAR(5) AS (left(b,5)) STORED) CHARACTER SET"
+              + " 'utf8mb4'");
+    } else {
+      stmt.execute(
+          "CREATE TABLE IF NOT EXISTS `ta\n"
+              + "ble'getcolumns`(a INT NOT NULL primary key auto_increment, b VARCHAR(32), c INT AS"
+              + " (CHAR_LENGTH(b)) VIRTUAL, d VARCHAR(5) AS (left(b,5)) PERSISTENT) CHARACTER SET"
+              + " 'utf8mb4'");
+    }
+
+    DatabaseMetaData dbmd = sharedConn.getMetaData();
+    ResultSet rs = dbmd.getColumns(null, null, "ta\nble'getcolumns", null);
+
+    assertTrue(rs.next());
+    assertEquals(sharedConn.getCatalog(), rs.getString(1)); // TABLE_CAT
+    assertEquals(null, rs.getString(2)); // TABLE_SCHEM
+    assertEquals("ta\nble'getcolumns", rs.getString(3)); // TABLE_NAME
+    assertEquals("a", rs.getString(4)); // COLUMN_NAME
+    assertEquals(Types.INTEGER, rs.getInt(5)); // DATA_TYPE
+    assertEquals("INT", rs.getString(6)); // "TYPE_NAME
+    assertEquals(10, rs.getInt(7)); // "COLUMN_SIZE
+    assertEquals(0, rs.getInt(9)); // DECIMAL_DIGITS
+    assertEquals(10, rs.getInt(10)); // NUM_PREC_RADIX
+    assertEquals(0, rs.getInt(11)); // NULLABLE
+    assertEquals("", rs.getString(12)); // REMARKS
+    assertEquals(null, rs.getString(13)); // COLUMN_DEF
+    assertEquals(0, rs.getInt(16)); // CHAR_OCTET_LENGTH
+    assertEquals(1, rs.getInt(17)); // ORDINAL_POSITION
+    assertEquals("NO", rs.getString(18)); // IS_NULLABLE
+    assertEquals(null, rs.getString(19)); // SCOPE_CATALOG
+    assertEquals(null, rs.getString(20)); // SCOPE_SCHEMA
+    assertEquals(null, rs.getString(21)); // SCOPE_TABLE
+    assertEquals(0, rs.getShort(22)); // SOURCE_DATA_TYPE
+    assertEquals("YES", rs.getString(23)); // IS_AUTOINCREMENT
+    assertEquals("NO", rs.getString(24)); // IS_GENERATEDCOLUMN
+
+    assertTrue(rs.next());
+    assertEquals(sharedConn.getCatalog(), rs.getString(1)); // TABLE_CAT
+    assertEquals(null, rs.getString(2)); // TABLE_SCHEM
+    assertEquals("ta\nble'getcolumns", rs.getString(3)); // TABLE_NAME
+    assertEquals("b", rs.getString(4)); // COLUMN_NAME
+    assertEquals(Types.VARCHAR, rs.getInt(5)); // DATA_TYPE
+    assertEquals("VARCHAR", rs.getString(6)); // "TYPE_NAME
+    assertEquals(32, rs.getInt(7)); // "COLUMN_SIZE
+    assertEquals(0, rs.getInt(9)); // DECIMAL_DIGITS
+    assertEquals(10, rs.getInt(10)); // NUM_PREC_RADIX
+    assertEquals(1, rs.getInt(11)); // NULLABLE
+    assertEquals("", rs.getString(12)); // REMARKS
+
+    // since 10.2.7, value that are expected as String are enclosed with single quotes as javadoc
+    // require
+    assertTrue("null".equalsIgnoreCase(rs.getString(13)) || rs.getString(13) == null); // COLUMN_DEF
+    assertEquals(32 * 4, rs.getInt(16)); // CHAR_OCTET_LENGTH
+    assertEquals(2, rs.getInt(17)); // ORDINAL_POSITION
+    assertEquals("YES", rs.getString(18)); // IS_NULLABLE
+    assertEquals(null, rs.getString(19)); // SCOPE_CATALOG
+    assertEquals(null, rs.getString(20)); // SCOPE_SCHEMA
+    assertEquals(null, rs.getString(21)); // SCOPE_TABLE
+    assertEquals(0, rs.getShort(22)); // SOURCE_DATA_TYPE
+    assertEquals("NO", rs.getString(23)); // IS_AUTOINCREMENT
+    assertEquals("NO", rs.getString(24)); // IS_GENERATEDCOLUMN
+
+    assertTrue(rs.next());
+    assertEquals(sharedConn.getCatalog(), rs.getString(1)); // TABLE_CAT
+    assertEquals(null, rs.getString(2)); // TABLE_SCHEM
+    assertEquals("ta\nble'getcolumns", rs.getString(3)); // TABLE_NAME
+    assertEquals("c", rs.getString(4)); // COLUMN_NAME
+    assertEquals(Types.INTEGER, rs.getInt(5)); // DATA_TYPE
+    assertEquals("INT", rs.getString(6)); // "TYPE_NAME
+    assertEquals(10, rs.getInt(7)); // "COLUMN_SIZE
+    assertEquals(0, rs.getInt(9)); // DECIMAL_DIGITS
+    assertEquals(10, rs.getInt(10)); // NUM_PREC_RADIX
+    assertEquals(1, rs.getInt(11)); // NULLABLE
+    assertEquals("", rs.getString(12)); // REMARKS
+
+    // since 10.2.7, value that are expected as String are enclosed with single quotes as javadoc
+    // require
+    assertTrue("null".equalsIgnoreCase(rs.getString(13)) || rs.getString(13) == null); // COLUMN_DEF
+
+    assertEquals(0, rs.getInt(16)); // CHAR_OCTET_LENGTH
+    assertEquals(3, rs.getInt(17)); // ORDINAL_POSITION
+    assertEquals("YES", rs.getString(18)); // IS_NULLABLE
+    assertEquals(null, rs.getString(19)); // SCOPE_CATALOG
+    assertEquals(null, rs.getString(20)); // SCOPE_SCHEMA
+    assertEquals(null, rs.getString(21)); // SCOPE_TABLE
+    assertEquals(0, rs.getShort(22)); // SOURCE_DATA_TYPE
+    assertEquals("NO", rs.getString(23)); // IS_AUTOINCREMENT
+    assertEquals("YES", rs.getString(24)); // IS_GENERATEDCOLUMN
+
+    assertTrue(rs.next());
+    assertEquals(sharedConn.getCatalog(), rs.getString(1)); // TABLE_CAT
+    assertEquals(null, rs.getString(2)); // TABLE_SCHEM
+    assertEquals("ta\nble'getcolumns", rs.getString(3)); // TABLE_NAME
+    assertEquals("d", rs.getString(4)); // COLUMN_NAME
+    assertEquals(Types.VARCHAR, rs.getInt(5)); // DATA_TYPE
+    assertEquals("VARCHAR", rs.getString(6)); // "TYPE_NAME
+    assertEquals(5, rs.getInt(7)); // "COLUMN_SIZE
+    assertEquals(0, rs.getInt(9)); // DECIMAL_DIGITS
+    assertEquals(10, rs.getInt(10)); // NUM_PREC_RADIX
+    assertEquals(1, rs.getInt(11)); // NULLABLE
+    assertEquals("", rs.getString(12)); // REMARKS
+    // since 10.2.7, value that are expected as String are enclosed with single quotes as javadoc
+    // require
+    assertTrue("null".equalsIgnoreCase(rs.getString(13)) || rs.getString(13) == null); // COLUMN_DEF
+    assertEquals(5 * 4, rs.getInt(16)); // CHAR_OCTET_LENGTH
+    assertEquals(4, rs.getInt(17)); // ORDINAL_POSITION
+    assertEquals("YES", rs.getString(18)); // IS_NULLABLE
+    assertEquals(null, rs.getString(19)); // SCOPE_CATALOG
+    assertEquals(null, rs.getString(20)); // SCOPE_SCHEMA
+    assertEquals(null, rs.getString(21)); // SCOPE_TABLE
+    assertEquals(0, rs.getShort(22)); // SOURCE_DATA_TYPE
+    assertEquals("NO", rs.getString(23)); // IS_AUTOINCREMENT
+    assertEquals("YES", rs.getString(24)); // IS_GENERATEDCOLUMN
+    assertFalse(rs.next());
+  }
+
+  @Test
+  public void testGetColumnstinyInt1isBit() throws SQLException {
+    try (Connection con = createCon("tinyInt1isBit=false")) {
+      testGetColumnstinyInt1isBit(con);
+    }
+    try (Connection con = createCon("tinyInt1isBit=false")) {
+      java.sql.Statement stmt = con.createStatement();
+      stmt.execute("SET sql_mode = concat(@@sql_mode,',NO_BACKSLASH_ESCAPES')");
+      testGetColumnstinyInt1isBit(con);
+    }
+  }
+
+  private void testGetColumnstinyInt1isBit(Connection con) throws SQLException {
+    try {
+      java.sql.Statement stmt = con.createStatement();
+      stmt.execute(
+          "CREATE TABLE IF NOT EXISTS `tinyInt1\nisBitCols`(id1 tinyint(1), id2 tinyint(2))");
+      stmt.execute("INSERT INTO `tinyInt1\nisBitCols` VALUES (1,2)");
+
+      ResultSet rs1 =
+          sharedConn.createStatement().executeQuery("SELECT * FROM `tinyInt1\nisBitCols`");
+      assertTrue(rs1.next());
+      assertEquals(Boolean.TRUE, rs1.getObject(1));
+      assertEquals(2, rs1.getObject(2));
+
+      rs1 = stmt.executeQuery("SELECT * FROM `tinyInt1\nisBitCols`");
+      assertTrue(rs1.next());
+      assertEquals(1, rs1.getObject(1));
+      assertEquals(2, rs1.getObject(2));
+
+      DatabaseMetaData dbmd = sharedConn.getMetaData();
+      ResultSet rs = dbmd.getColumns(null, null, "tinyInt1\nisBitCols", null);
+
+      assertTrue(rs.next());
+      assertEquals(Types.BOOLEAN, rs.getInt(5));
+      assertTrue(rs.next());
+      assertEquals(Types.TINYINT, rs.getInt(5));
+
+      dbmd = con.getMetaData();
+      rs = dbmd.getColumns(null, null, "tinyInt1\nisBitCols", null);
+
+      assertTrue(rs.next());
+      assertEquals(Types.TINYINT, rs.getInt(5));
+      assertTrue(rs.next());
+      assertEquals(Types.TINYINT, rs.getInt(5));
+
+    } finally {
+      con.createStatement().execute("DROP TABLE IF EXISTS `tinyInt1\nisBitCols`");
+    }
+  }
+
+  @Test
+  public void testTransformedBitIsBoolean() throws SQLException {
+    try (Connection con = createCon("tinyInt1isBit=true&transformedBitIsBoolean=true")) {
+      testTransformedBitIsBoolean(con, true, true);
+    }
+    try (Connection con = createCon("tinyInt1isBit=true&transformedBitIsBoolean=false")) {
+      testTransformedBitIsBoolean(con, true, false);
+    }
+    try (Connection con = createCon("tinyInt1isBit=false&transformedBitIsBoolean=true")) {
+      testTransformedBitIsBoolean(con, false, true);
+    }
+    try (Connection con = createCon("transformedBitIsBoolean=true")) {
+      java.sql.Statement stmt = con.createStatement();
+      stmt.execute("SET sql_mode = concat(@@sql_mode,',NO_BACKSLASH_ESCAPES')");
+      testTransformedBitIsBoolean(con, true, true);
+    }
+  }
+
+  private void testTransformedBitIsBoolean(
+      Connection con, boolean tinyInt1isBit, boolean transformedBitIsBoolean) throws SQLException {
+    try {
+      java.sql.Statement stmt = con.createStatement();
+      stmt.execute(
+          "CREATE TABLE IF NOT EXISTS `tinyInt1\nisBitCols`(id1 tinyint(1), id2 tinyint(2))");
+      stmt.execute("INSERT INTO `tinyInt1\nisBitCols` VALUES (1,2)");
+
+      ResultSet rs1 = con.createStatement().executeQuery("SELECT * FROM `tinyInt1\nisBitCols`");
+      assertTrue(rs1.next());
+      if (tinyInt1isBit) {
+        assertEquals(Boolean.TRUE, rs1.getObject(1));
+        assertEquals(2, rs1.getObject(2));
+      } else {
+        assertEquals(1, rs1.getObject(1));
+        assertEquals(2, rs1.getObject(2));
+      }
+
+      ResultSetMetaData rsm = rs1.getMetaData();
+      if (tinyInt1isBit) {
+        if (transformedBitIsBoolean) {
+          assertEquals(Types.BOOLEAN, rsm.getColumnType(1));
+          assertEquals("BOOLEAN", rsm.getColumnTypeName(1));
+        } else {
+          assertEquals(Types.BIT, rsm.getColumnType(1));
+          assertEquals("BIT", rsm.getColumnTypeName(1));
+        }
+      } else {
+        assertEquals(Types.TINYINT, rsm.getColumnType(1));
+        assertEquals("TINYINT", rsm.getColumnTypeName(1));
+      }
+
+      DatabaseMetaData dbmd = con.getMetaData();
+      ResultSet rs = dbmd.getColumns(null, null, "tinyInt1\nisBitCols", null);
+
+      assertTrue(rs.next());
+      if (tinyInt1isBit) {
+        if (transformedBitIsBoolean) {
+          assertEquals(Types.BOOLEAN, rs.getInt(5));
+          assertEquals("BOOLEAN", rs.getString(6));
+        } else {
+          assertEquals(Types.BIT, rs.getInt(5));
+          assertEquals("BIT", rs.getString(6));
+        }
+      } else {
+        assertEquals(Types.TINYINT, rs.getInt(5));
+        assertEquals("TINYINT", rs.getString(6));
+      }
+      assertTrue(rs.next());
+      assertEquals(Types.TINYINT, rs.getInt(5));
+
+    } finally {
+      con.createStatement().execute("DROP TABLE IF EXISTS `tinyInt1\nisBitCols`");
+    }
+  }
+
+  private void testResultSetColumns(ResultSet rs, String spec) throws SQLException {
+    ResultSetMetaData rsmd = rs.getMetaData();
+    String[] tokens = spec.split(",");
+
+    for (int i = 0; i < tokens.length; i++) {
+      String[] splitTokens = tokens[i].trim().split(" ");
+      String label = splitTokens[0];
+      String type = splitTokens[1];
+
+      int col = i + 1;
+      assertEquals(label, rsmd.getColumnLabel(col));
+      int columnType = rsmd.getColumnType(col);
+      switch (type) {
+        case "String":
+          assertTrue(
+              columnType == Types.VARCHAR
+                  || columnType == Types.CHAR
+                  || columnType == Types.NULL
+                  || columnType == Types.LONGVARCHAR,
+              "invalid type "
+                  + columnType
+                  + " for "
+                  + rsmd.getColumnLabel(col)
+                  + ",expected String");
+          break;
+        case "decimal":
+          assertTrue(
+              columnType == Types.DECIMAL,
+              "invalid type  "
+                  + columnType
+                  + "( "
+                  + rsmd.getColumnTypeName(col)
+                  + " ) for "
+                  + rsmd.getColumnLabel(col)
+                  + ",expected decimal");
+          break;
+        case "int":
+        case "short":
+          assertTrue(
+              columnType == Types.BIGINT
+                  || columnType == Types.INTEGER
+                  || columnType == Types.SMALLINT
+                  || columnType == Types.TINYINT,
+              "invalid type  "
+                  + columnType
+                  + "( "
+                  + rsmd.getColumnTypeName(col)
+                  + " ) for "
+                  + rsmd.getColumnLabel(col)
+                  + ",expected numeric");
+
+          break;
+        case "boolean":
+          assertTrue(
+              columnType == Types.BOOLEAN || columnType == Types.BIT,
+              "invalid type  "
+                  + columnType
+                  + "( "
+                  + rsmd.getColumnTypeName(col)
+                  + " ) for "
+                  + rsmd.getColumnLabel(col)
+                  + ",expected boolean");
+
+          break;
+        case "null":
+          assertTrue(
+              columnType == Types.NULL,
+              "invalid type  "
+                  + columnType
+                  + " for "
+                  + rsmd.getColumnLabel(col)
+                  + ",expected null");
+          break;
+        default:
+          fail("invalid type '" + type + "'");
+          break;
+      }
+    }
+  }
+
+  @Test
+  public void getSchemas() throws SQLException {
+    DatabaseMetaData dbmd = sharedConn.getMetaData();
+    ResultSet rs = dbmd.getSchemas();
+    assertFalse(rs.next());
+
+    rs = dbmd.getSchemas("*", "*");
+    assertFalse(rs.next());
+  }
+
+  @Test
+  public void getAttributesBasic() throws Exception {
+    testResultSetColumns(
+        sharedConn.getMetaData().getAttributes(null, null, null, null),
+        "TYPE_CAT String,TYPE_SCHEM String,TYPE_NAME String,ATTR_NAME String,DATA_TYPE"
+            + " int,ATTR_TYPE_NAME String,ATTR_SIZE int,DECIMAL_DIGITS int,NUM_PREC_RADIX"
+            + " int,NULLABLE int,REMARKS String,ATTR_DEF String,SQL_DATA_TYPE int,SQL_DATETIME_SUB"
+            + " int, CHAR_OCTET_LENGTH int,ORDINAL_POSITION int,IS_NULLABLE String,SCOPE_CATALOG"
+            + " String,SCOPE_SCHEMA String,SCOPE_TABLE String,SOURCE_DATA_TYPE short");
+  }
+
+  @Test
+  public void identifierCaseSensitivity() throws Exception {
+    // https://jira.mariadb.org/browse/XPT-281
+    Assumptions.assumeFalse(isXpand());
+    java.sql.Statement stmt = sharedConn.createStatement();
+    try {
+      if (sharedConn.getMetaData().supportsMixedCaseIdentifiers()) {
+        /* Case-sensitive identifier handling, we can create both t1 and T1 */
+        stmt.execute("create table aB (i int)");
+        stmt.execute("create table AB (i int)");
+        /* Check there is an entry for both T1 and t1 in getTables */
+        ResultSet rs = sharedConn.getMetaData().getTables(null, null, "aB", null);
+        assertTrue(rs.next());
+        assertFalse(rs.next());
+        rs = sharedConn.getMetaData().getTables(null, null, "AB", null);
+        assertTrue(rs.next());
+        assertFalse(rs.next());
+      }
+
+      if (sharedConn.getMetaData().storesMixedCaseIdentifiers()) {
+        /* Case-insensitive, case-preserving */
+        stmt.execute("create table aB (i int)");
+        try {
+          stmt.execute("create table AB (i int)");
+          fail("should not get there, since names are case-insensitive");
+        } catch (SQLException e) {
+          // normal error
+        }
+
+        /* Check that table is stored case-preserving */
+        ResultSet rs = sharedConn.getMetaData().getTables(null, null, "aB%", null);
+        while (rs.next()) {
+          String tableName = rs.getString("TABLE_NAME");
+          if (tableName.length() == 2) {
+            assertEquals("aB", tableName);
+          }
+        }
+
+        rs = sharedConn.getMetaData().getTables(null, null, "AB", null);
+        assertTrue(rs.next());
+        assertFalse(rs.next());
+      }
+
+      if (sharedConn.getMetaData().storesLowerCaseIdentifiers()) {
+        /* case-insensitive, identifiers converted to lowercase */
+        /* Case-insensitive, case-preserving */
+        stmt.execute("create table aB (i int)");
+        try {
+          stmt.execute("create table AB (i int)");
+          fail("should not get there, since names are case-insensitive");
+        } catch (SQLException e) {
+          // normal error
+        }
+
+        /* Check that table is stored lowercase */
+        ResultSet rs = sharedConn.getMetaData().getTables(null, null, "aB%", null);
+        while (rs.next()) {
+          String tableName = rs.getString("TABLE_NAME");
+          if (tableName.length() == 2) {
+            assertEquals("ab", tableName);
+          }
+        }
+
+        rs = sharedConn.getMetaData().getTables(null, null, "AB", null);
+        assertTrue(rs.next());
+        assertFalse(rs.next());
+      }
+      assertFalse(sharedConn.getMetaData().storesUpperCaseIdentifiers());
+    } finally {
+      try {
+        stmt.execute("DROP TABLE aB");
+      } catch (SQLException sqle) {
+        // ignore
+      }
+      try {
+        stmt.execute("DROP TABLE AB");
+      } catch (SQLException sqle) {
+        // ignore
+      }
+    }
+  }
+
+  @Test
+  public void getBestRowIdentifier() throws SQLException {
+    Assumptions.assumeFalse(isXpand());
+    DatabaseMetaData meta = sharedConn.getMetaData();
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> meta.getBestRowIdentifier(null, null, null, 0, true),
+        "'table' parameter cannot be null in getBestRowIdentifier()");
+    testResultSetColumns(
+        meta.getBestRowIdentifier(null, null, "", 0, true),
+        "SCOPE short,COLUMN_NAME String,DATA_TYPE int, TYPE_NAME String,"
+            + "COLUMN_SIZE int,BUFFER_LENGTH int,"
+            + "DECIMAL_DIGITS short,PSEUDO_COLUMN short");
+
+    ResultSet rs = meta.getBestRowIdentifier(null, null, "cross1", 0, true);
+    assertTrue(rs.next());
+
+    assertEquals(org.mariadb.jdbc.DatabaseMetaData.bestRowSession, rs.getInt(1));
+    assertEquals("id", rs.getString(2));
+    assertEquals(Types.INTEGER, rs.getInt(3));
+    assertEquals("int", rs.getString(4));
+    assertEquals(10, rs.getInt(5));
+    assertEquals(0, rs.getInt(6));
+    assertEquals(0, rs.getInt(7));
+    assertEquals(org.mariadb.jdbc.DatabaseMetaData.bestRowNotPseudo, rs.getInt(8));
+    assertFalse(rs.next());
+
+    rs = meta.getBestRowIdentifier(null, null, "cross1", 0, false);
+    assertTrue(rs.next());
+    assertFalse(rs.next());
+
+    rs = meta.getBestRowIdentifier(null, null, "cross2", 0, true);
+    assertTrue(rs.next());
+    assertEquals(org.mariadb.jdbc.DatabaseMetaData.bestRowSession, rs.getInt(1));
+    assertEquals("id", rs.getString(2));
+    assertEquals(Types.INTEGER, rs.getInt(3));
+    assertEquals("int", rs.getString(4));
+    assertEquals(10, rs.getInt(5));
+    assertEquals(0, rs.getInt(6));
+    assertEquals(0, rs.getInt(7));
+    assertEquals(org.mariadb.jdbc.DatabaseMetaData.bestRowNotPseudo, rs.getInt(8));
+    assertTrue(rs.next());
+    assertEquals(org.mariadb.jdbc.DatabaseMetaData.bestRowSession, rs.getInt(1));
+    assertEquals("id2", rs.getString(2));
+    assertEquals(Types.INTEGER, rs.getInt(3));
+    assertEquals("int", rs.getString(4));
+    assertEquals(10, rs.getInt(5));
+    assertEquals(0, rs.getInt(6));
+    assertEquals(0, rs.getInt(7));
+    assertEquals(org.mariadb.jdbc.DatabaseMetaData.bestRowNotPseudo, rs.getInt(8));
+    assertFalse(rs.next());
+
+    rs = meta.getBestRowIdentifier(null, null, "cross3", 0, true);
+    assertTrue(rs.next());
+
+    assertEquals(org.mariadb.jdbc.DatabaseMetaData.bestRowSession, rs.getInt(1));
+    assertEquals("id", rs.getString(2));
+    assertEquals(Types.INTEGER, rs.getInt(3));
+    assertEquals("int", rs.getString(4));
+    assertEquals(10, rs.getInt(5));
+    assertEquals(0, rs.getInt(6));
+    assertEquals(0, rs.getInt(7));
+    assertEquals(org.mariadb.jdbc.DatabaseMetaData.bestRowNotPseudo, rs.getInt(8));
+    assertFalse(rs.next());
+
+    // CHECK using PRI even if exist UNI
+
+    rs = meta.getBestRowIdentifier(null, null, "getBestRowIdentifier1", 0, true);
+    assertTrue(rs.next());
+
+    assertEquals(org.mariadb.jdbc.DatabaseMetaData.bestRowSession, rs.getInt(1));
+    assertEquals("i", rs.getString(2));
+    assertEquals(Types.INTEGER, rs.getInt(3));
+    assertEquals("int", rs.getString(4));
+    assertEquals(10, rs.getInt(5));
+    assertEquals(0, rs.getInt(6));
+    assertEquals(0, rs.getInt(7));
+    assertEquals(org.mariadb.jdbc.DatabaseMetaData.bestRowNotPseudo, rs.getInt(8));
+    assertFalse(rs.next());
+
+    rs = meta.getBestRowIdentifier(null, null, "getBestRowIdentifier2", 0, true);
+    assertTrue(rs.next());
+    assertEquals(org.mariadb.jdbc.DatabaseMetaData.bestRowSession, rs.getInt(1));
+    assertEquals("id_ref0", rs.getString(2));
+    assertEquals(Types.INTEGER, rs.getInt(3));
+    assertEquals("int", rs.getString(4));
+    assertEquals(10, rs.getInt(5));
+    assertEquals(0, rs.getInt(6));
+    assertEquals(0, rs.getInt(7));
+    assertEquals(org.mariadb.jdbc.DatabaseMetaData.bestRowNotPseudo, rs.getInt(8));
+    assertTrue(rs.next());
+
+    assertEquals(org.mariadb.jdbc.DatabaseMetaData.bestRowSession, rs.getInt(1));
+    assertEquals("id_ref2", rs.getString(2));
+    assertEquals(Types.INTEGER, rs.getInt(3));
+    assertEquals("int", rs.getString(4));
+    assertEquals(10, rs.getInt(5));
+    assertEquals(0, rs.getInt(6));
+    assertEquals(0, rs.getInt(7));
+    assertEquals(org.mariadb.jdbc.DatabaseMetaData.bestRowNotPseudo, rs.getInt(8));
+    assertFalse(rs.next());
+  }
+
+  @Test
+  public void getClientInfoPropertiesBasic() throws Exception {
+    testResultSetColumns(
+        sharedConn.getMetaData().getClientInfoProperties(),
+        "NAME String, MAX_LEN int, DEFAULT_VALUE String, DESCRIPTION String");
+    ResultSet rs = sharedConn.getMetaData().getClientInfoProperties();
+    assertTrue(rs.next());
+    assertEquals("ApplicationName", rs.getString(1));
+    assertEquals(0x00ffffff, rs.getInt(2));
+    assertEquals("", rs.getString(3));
+    assertEquals("The name of the application currently utilizing the connection", rs.getString(4));
+
+    assertTrue(rs.next());
+    assertEquals("ClientUser", rs.getString(1));
+    assertEquals(0x00ffffff, rs.getInt(2));
+    assertEquals("", rs.getString(3));
+    assertEquals(
+        "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.",
+        rs.getString(4));
+
+    assertTrue(rs.next());
+    assertEquals("ClientHostname", rs.getString(1));
+    assertEquals(0x00ffffff, rs.getInt(2));
+    assertEquals("", rs.getString(3));
+    assertEquals(
+        "The hostname of the computer the application using the connection is running on",
+        rs.getString(4));
+
+    assertFalse(rs.next());
+  }
+
+  @Test
+  public void getCatalogsBasic() throws SQLException {
+    testResultSetColumns(sharedConn.getMetaData().getCatalogs(), "TABLE_CAT String");
+  }
+
+  @Test
+  public void getColumnsBasic() throws SQLException {
+    Assumptions.assumeFalse(isXpand());
+    cancelForVersion(10, 1); // due to server error MDEV-8984
+    if (minVersion(10, 2, 0)) {
+      testResultSetColumns(
+          sharedConn.getMetaData().getColumns(null, null, null, null),
+          "TABLE_CAT String,TABLE_SCHEM String,TABLE_NAME String,COLUMN_NAME String,"
+              + "DATA_TYPE int,TYPE_NAME String,COLUMN_SIZE decimal,BUFFER_LENGTH int,"
+              + "DECIMAL_DIGITS int,NUM_PREC_RADIX int,NULLABLE int,"
+              + "REMARKS String,COLUMN_DEF String,SQL_DATA_TYPE int,"
+              + "SQL_DATETIME_SUB int, CHAR_OCTET_LENGTH decimal,"
+              + "ORDINAL_POSITION int,IS_NULLABLE String,"
+              + "SCOPE_CATALOG String,SCOPE_SCHEMA String,"
+              + "SCOPE_TABLE String,SOURCE_DATA_TYPE null");
+    } else {
+      testResultSetColumns(
+          sharedConn.getMetaData().getColumns(null, null, null, null),
+          "TABLE_CAT String,TABLE_SCHEM String,TABLE_NAME String,COLUMN_NAME String,"
+              + "DATA_TYPE int,TYPE_NAME String,COLUMN_SIZE int,BUFFER_LENGTH int,"
+              + "DECIMAL_DIGITS int,NUM_PREC_RADIX int,NULLABLE int,"
+              + "REMARKS String,COLUMN_DEF String,SQL_DATA_TYPE int,"
+              + "SQL_DATETIME_SUB int, CHAR_OCTET_LENGTH int,"
+              + "ORDINAL_POSITION int,IS_NULLABLE String,"
+              + "SCOPE_CATALOG String,SCOPE_SCHEMA String,"
+              + "SCOPE_TABLE String,SOURCE_DATA_TYPE null");
+    }
+  }
+
+  @Test
+  public void getProcedureColumnsBasic() throws SQLException {
+    Assumptions.assumeTrue(!isXpand());
+    testResultSetColumns(
+        sharedConn.getMetaData().getProcedureColumns(null, null, null, null),
+        "PROCEDURE_CAT String,PROCEDURE_SCHEM String,PROCEDURE_NAME String,COLUMN_NAME String"
+            + " ,COLUMN_TYPE short,DATA_TYPE int,TYPE_NAME String,PRECISION int,LENGTH int,SCALE"
+            + " short,RADIX short,NULLABLE short,REMARKS String,COLUMN_DEF String,SQL_DATA_TYPE"
+            + " int,SQL_DATETIME_SUB int ,CHAR_OCTET_LENGTH int,ORDINAL_POSITION int,IS_NULLABLE"
+            + " String,SPECIFIC_NAME String");
+  }
+
+  @Test
+  public void getFunctionColumnsBasic() throws SQLException {
+    // https://jira.mariadb.org/browse/XPT-267
+    Assumptions.assumeFalse(isXpand());
+    testResultSetColumns(
+        sharedConn.getMetaData().getFunctionColumns(null, null, null, null),
+        "FUNCTION_CAT String,FUNCTION_SCHEM String,FUNCTION_NAME String,COLUMN_NAME"
+            + " String,COLUMN_TYPE short,DATA_TYPE int,TYPE_NAME String,PRECISION int,LENGTH"
+            + " int,SCALE short,RADIX short,NULLABLE short,REMARKS String,CHAR_OCTET_LENGTH"
+            + " int,ORDINAL_POSITION int,IS_NULLABLE String,SPECIFIC_NAME String");
+  }
+
+  @Test
+  public void getColumnPrivilegesBasic() throws SQLException {
+    Assumptions.assumeFalse(isXpand());
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> sharedConn.getMetaData().getColumnPrivileges(null, null, null, null),
+        "'table' parameter must not be null");
+    testResultSetColumns(
+        sharedConn.getMetaData().getColumnPrivileges(null, null, "", null),
+        "TABLE_CAT String,TABLE_SCHEM String,TABLE_NAME String,COLUMN_NAME String,"
+            + "GRANTOR String,GRANTEE String,PRIVILEGE String,IS_GRANTABLE String");
+  }
+
+  @Test
+  public void getTablePrivilegesBasic() throws SQLException {
+    testResultSetColumns(
+        sharedConn.getMetaData().getTablePrivileges(null, null, null),
+        "TABLE_CAT String,TABLE_SCHEM String,TABLE_NAME String,GRANTOR String,"
+            + "GRANTEE String,PRIVILEGE String,IS_GRANTABLE String");
+  }
+
+  @Test
+  public void getVersionColumnsBasic() throws SQLException {
+    testResultSetColumns(
+        sharedConn.getMetaData().getVersionColumns(null, null, null),
+        "SCOPE short, COLUMN_NAME String,DATA_TYPE int,TYPE_NAME String,"
+            + "COLUMN_SIZE int,BUFFER_LENGTH int,DECIMAL_DIGITS short,"
+            + "PSEUDO_COLUMN short");
+  }
+
+  @Test
+  public void getPrimaryKeysBasic() throws SQLException {
+    testResultSetColumns(
+        sharedConn.getMetaData().getPrimaryKeys(null, null, null),
+        "TABLE_CAT String,TABLE_SCHEM String,TABLE_NAME String,COLUMN_NAME String,KEY_SEQ"
+            + " short,PK_NAME String");
+  }
+
+  @Test
+  public void getImportedKeysBasic() throws SQLException {
+    Assumptions.assumeFalse(isXpand());
+    testResultSetColumns(
+        sharedConn.getMetaData().getImportedKeys(null, null, ""),
+        "PKTABLE_CAT String,PKTABLE_SCHEM String,PKTABLE_NAME String, PKCOLUMN_NAME"
+            + " String,FKTABLE_CAT String,FKTABLE_SCHEM String,FKTABLE_NAME String,FKCOLUMN_NAME"
+            + " String,KEY_SEQ short,UPDATE_RULE short,DELETE_RULE short,FK_NAME String,PK_NAME"
+            + " String,DEFERRABILITY short");
+  }
+
+  @Test
+  public void getExportedKeysBasic() throws SQLException {
+    Assumptions.assumeFalse(isXpand());
+    testResultSetColumns(
+        sharedConn.getMetaData().getExportedKeys(null, null, ""),
+        "PKTABLE_CAT String,PKTABLE_SCHEM String,PKTABLE_NAME String, PKCOLUMN_NAME"
+            + " String,FKTABLE_CAT String,FKTABLE_SCHEM String,FKTABLE_NAME String,FKCOLUMN_NAME"
+            + " String,KEY_SEQ short,UPDATE_RULE short, DELETE_RULE short,FK_NAME String,PK_NAME"
+            + " String,DEFERRABILITY short");
+  }
+
+  @Test
+  public void getCrossReferenceBasic() throws SQLException {
+    Assumptions.assumeFalse(isXpand());
+    testResultSetColumns(
+        sharedConn.getMetaData().getCrossReference(null, null, "", null, null, ""),
+        "PKTABLE_CAT String,PKTABLE_SCHEM String,PKTABLE_NAME String, PKCOLUMN_NAME"
+            + " String,FKTABLE_CAT String,FKTABLE_SCHEM String,FKTABLE_NAME String,FKCOLUMN_NAME"
+            + " String,KEY_SEQ short,UPDATE_RULE short,DELETE_RULE short,FK_NAME String,PK_NAME"
+            + " String,DEFERRABILITY short");
+  }
+
+  @Test
+  public void getCrossReferenceResults() throws SQLException {
+    DatabaseMetaData dbmd = sharedConn.getMetaData();
+    ResultSet rs = dbmd.getCrossReference(null, null, "cross%", null, null, "cross%");
+
+    assertTrue(rs.next());
+    assertEquals(sharedConn.getCatalog(), rs.getString(1));
+    assertEquals(null, rs.getString(2));
+    assertEquals("cross1", rs.getString(3));
+    assertEquals("id", rs.getString(4));
+    assertEquals(sharedConn.getCatalog(), rs.getString(5));
+    assertEquals(null, rs.getString(6));
+    assertEquals("cross2", rs.getString(7));
+    assertEquals("id_ref0", rs.getString(8));
+    assertTrue(rs.getInt(9) == 1 || rs.wasNull());
+    if (!isMariaDBServer() && minVersion(8, 0, 0)) {
+      assertEquals(DatabaseMetaData.importedKeyNoAction, rs.getInt("UPDATE_RULE"));
+      assertEquals(DatabaseMetaData.importedKeyNoAction, rs.getInt("DELETE_RULE"));
+    } else {
+      assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("UPDATE_RULE"));
+      assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("DELETE_RULE"));
+    }
+    assertEquals("cross2_ibfk_1", rs.getString(12));
+
+    assertTrue(rs.next());
+    assertEquals(sharedConn.getCatalog(), rs.getString(1));
+    assertEquals(null, rs.getString(2));
+    assertEquals("cross2", rs.getString(3));
+    assertEquals("id", rs.getString(4));
+    assertEquals(sharedConn.getCatalog(), rs.getString(5));
+    assertEquals(null, rs.getString(6));
+    assertEquals("cross3", rs.getString(7));
+    assertEquals("id_ref1", rs.getString(8));
+    assertTrue(rs.getInt(9) == 1 || rs.wasNull());
+    assertEquals(DatabaseMetaData.importedKeyCascade, rs.getInt(10));
+    if (!isMariaDBServer() && minVersion(8, 0, 0)) {
+      assertEquals(DatabaseMetaData.importedKeyNoAction, rs.getInt("DELETE_RULE"));
+    } else {
+      assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("DELETE_RULE"));
+    }
+    assertTrue(
+        "fk_my_name".equals(rs.getString("FK_NAME"))
+            || "cross3_ibfk_1".equals(rs.getString("FK_NAME")));
+
+    assertTrue(rs.next());
+    assertEquals(sharedConn.getCatalog(), rs.getString(1));
+    assertEquals(null, rs.getString(2));
+    assertEquals("cross2", rs.getString(3));
+    assertEquals("id2", rs.getString(4));
+    assertEquals(sharedConn.getCatalog(), rs.getString(5));
+    assertEquals(null, rs.getString(6));
+    assertEquals("cross3", rs.getString(7));
+    assertEquals("id_ref2", rs.getString(8));
+    assertTrue(rs.getInt(9) == 2 || rs.wasNull());
+    assertEquals(DatabaseMetaData.importedKeyCascade, rs.getInt(10));
+    if (!isMariaDBServer() && minVersion(8, 0, 0)) {
+      assertEquals(DatabaseMetaData.importedKeyNoAction, rs.getInt("DELETE_RULE"));
+    } else {
+      assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("DELETE_RULE"));
+    }
+    assertTrue("fk_my_name".equals(rs.getString(12)) || "cross3_ibfk_1".equals(rs.getString(12)));
+
+    assertFalse(rs.next());
+  }
+
+  @Test
+  public void getUdtsBasic() throws SQLException {
+    testResultSetColumns(
+        sharedConn.getMetaData().getUDTs(null, null, null, null),
+        "TYPE_CAT String,TYPE_SCHEM String,TYPE_NAME String,CLASS_NAME String,DATA_TYPE int,"
+            + "REMARKS String,BASE_TYPE short");
+  }
+
+  @Test
+  public void getSuperTypesBasic() throws SQLException {
+    testResultSetColumns(
+        sharedConn.getMetaData().getSuperTypes(null, null, null),
+        "TYPE_CAT String,TYPE_SCHEM String,TYPE_NAME String,SUPERTYPE_CAT String,"
+            + "SUPERTYPE_SCHEM String,SUPERTYPE_NAME String");
+  }
+
+  @Test
+  public void getFunctionsBasic() throws SQLException {
+    testResultSetColumns(
+        sharedConn.getMetaData().getFunctions(null, null, null),
+        "FUNCTION_CAT String, FUNCTION_SCHEM String,FUNCTION_NAME String,REMARKS"
+            + " String,FUNCTION_TYPE short, SPECIFIC_NAME String");
+  }
+
+  @Test
+  public void getSuperTablesBasic() throws SQLException {
+    testResultSetColumns(
+        sharedConn.getMetaData().getSuperTables(null, null, null),
+        "TABLE_CAT String,TABLE_SCHEM String,TABLE_NAME String, SUPERTABLE_NAME String");
+  }
+
+  @Test
+  public void testGetSchemas2() throws SQLException {
+    DatabaseMetaData dbmd = sharedConn.getMetaData();
+    ResultSet rs = dbmd.getCatalogs();
+    boolean foundTestUnitsJdbc = false;
+    while (rs.next()) {
+      if (rs.getString(1).equals(sharedConn.getCatalog())) {
+        foundTestUnitsJdbc = true;
+      }
+    }
+    assertEquals(true, foundTestUnitsJdbc);
+  }
+
+  @Test
+  public void testGetTypeInfoBasic() throws SQLException {
+    ResultSet rs = sharedConn.getMetaData().getTypeInfo();
+    while (rs.next()) {
+      rs.getInt(2);
+    }
+    ResultSetMetaData rmeta = rs.getMetaData();
+    assertTrue(rmeta.isSigned(1));
+
+    testResultSetColumns(
+        rs,
+        "TYPE_NAME String,DATA_TYPE int,PRECISION int,LITERAL_PREFIX String,LITERAL_SUFFIX"
+            + " String,CREATE_PARAMS String, NULLABLE short,CASE_SENSITIVE boolean,SEARCHABLE"
+            + " short,UNSIGNED_ATTRIBUTE boolean,FIXED_PREC_SCALE boolean, AUTO_INCREMENT boolean,"
+            + " LOCAL_TYPE_NAME String,MINIMUM_SCALE short,MAXIMUM_SCALE short,SQL_DATA_TYPE"
+            + " int,SQL_DATETIME_SUB int, NUM_PREC_RADIX int");
+  }
+
+  @Test
+  public void getColumnsTest() throws SQLException {
+    // https://jira.mariadb.org/browse/XPT-280
+    Assumptions.assumeTrue(!isXpand());
+
+    DatabaseMetaData dmd = sharedConn.getMetaData();
+    ResultSet rs = dmd.getColumns(sharedConn.getCatalog(), null, "manycols", null);
+    while (rs.next()) {
+      String columnName = rs.getString("column_name");
+      int type = rs.getInt("data_type");
+      String typeName = rs.getString("type_name");
+      assertFalse(typeName.contains("("));
+      for (char c : typeName.toCharArray()) {
+        assertTrue(c == ' ' || Character.isUpperCase(c), "bad typename " + typeName);
+      }
+      checkType(columnName, type, "tiny", Types.TINYINT);
+      checkType(columnName, type, "tiny_uns", Types.TINYINT);
+      checkType(columnName, type, "small", Types.SMALLINT);
+      checkType(columnName, type, "small_uns", Types.SMALLINT);
+      checkType(columnName, type, "medium", Types.INTEGER);
+      checkType(columnName, type, "medium_uns", Types.INTEGER);
+      checkType(columnName, type, "int_col", Types.INTEGER);
+      checkType(columnName, type, "int_col_uns", Types.INTEGER);
+      checkType(columnName, type, "big", Types.BIGINT);
+      checkType(columnName, type, "big_uns", Types.BIGINT);
+      checkType(columnName, type, "decimal_col", Types.DECIMAL);
+      checkType(columnName, type, "fcol", Types.REAL);
+      checkType(columnName, type, "fcol_uns", Types.REAL);
+      checkType(columnName, type, "dcol", Types.DOUBLE);
+      checkType(columnName, type, "dcol_uns", Types.DOUBLE);
+      checkType(columnName, type, "date_col", Types.DATE);
+      checkType(columnName, type, "time_col", Types.TIME);
+      checkType(columnName, type, "timestamp_col", Types.TIMESTAMP);
+      checkType(columnName, type, "year_col", Types.DATE);
+      checkType(columnName, type, "bit_col", Types.BIT);
+      checkType(columnName, type, "char_col", Types.CHAR);
+      checkType(columnName, type, "varchar_col", Types.VARCHAR);
+      checkType(columnName, type, "binary_col", Types.BINARY);
+      checkType(columnName, type, "tinyblob_col", Types.VARBINARY);
+      checkType(columnName, type, "blob_col", Types.LONGVARBINARY);
+      checkType(columnName, type, "longblob_col", Types.LONGVARBINARY);
+      checkType(columnName, type, "mediumblob_col", Types.LONGVARBINARY);
+      checkType(columnName, type, "tinytext_col", Types.VARCHAR);
+      checkType(columnName, type, "text_col", Types.LONGVARCHAR);
+      checkType(columnName, type, "mediumtext_col", Types.LONGVARCHAR);
+      checkType(columnName, type, "longtext_col", Types.LONGVARCHAR);
+    }
+  }
+
+  @Test
+  public void yearIsShortType() throws Exception {
+    try (java.sql.Connection connection = createCon("&yearIsDateType=false")) {
+      connection.createStatement().execute("insert into ytab values(72)");
+
+      ResultSet rs2 =
+          connection.getMetaData().getColumns(connection.getCatalog(), null, "ytab", null);
+      assertTrue(rs2.next());
+      assertEquals(Types.SMALLINT, rs2.getInt("DATA_TYPE"));
+
+      try (ResultSet rs =
+          connection.getMetaData().getColumns(connection.getCatalog(), null, "ytab", null)) {
+        assertTrue(rs.next());
+        assertEquals(Types.SMALLINT, rs.getInt("DATA_TYPE"));
+      }
+
+      try (ResultSet rs1 = connection.createStatement().executeQuery("select * from ytab")) {
+        assertEquals(rs1.getMetaData().getColumnType(1), Types.SMALLINT);
+        assertTrue(rs1.next());
+        assertTrue(rs1.getObject(1) instanceof Short);
+        assertEquals(rs1.getShort(1), 1972);
+      }
+    }
+  }
+
+  @Test
+  public void yearIsDateType() throws Exception {
+    try (Connection connection = createCon("&yearIsDateType=true")) {
+      connection.createStatement().execute("insert into ytab values(72)");
+
+      ResultSet rs2 =
+          connection.getMetaData().getColumns(connection.getCatalog(), null, "ytab", null);
+      assertTrue(rs2.next());
+      assertEquals(Types.DATE, rs2.getInt("DATA_TYPE"));
+
+      try (ResultSet rs =
+          connection.getMetaData().getColumns(connection.getCatalog(), null, "ytab", null)) {
+        assertTrue(rs.next());
+        assertEquals(Types.DATE, rs.getInt("DATA_TYPE"));
+      }
+
+      try (ResultSet rs1 = connection.createStatement().executeQuery("select * from ytab")) {
+        assertEquals(Types.DATE, rs1.getMetaData().getColumnType(1));
+        assertTrue(rs1.next());
+        assertTrue(rs1.getObject(1) instanceof Date);
+        assertEquals("1972-01-01", rs1.getDate(1).toString());
+      }
+    }
+  }
+
+  /* CONJ-15 */
+  @Test
+  public void maxCharLengthUtf8() throws Exception {
+    DatabaseMetaData dmd = sharedConn.getMetaData();
+    ResultSet rs = dmd.getColumns(sharedConn.getCatalog(), null, "maxcharlength", null);
+    assertTrue(rs.next());
+    assertEquals(rs.getInt("COLUMN_SIZE"), 1);
+  }
+
+  @Test
+  public void getPrecision() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE IF NOT EXISTS getPrecision("
+            + "num1 NUMERIC(9,4), "
+            + "num2 NUMERIC (9,0),"
+            + "num3 NUMERIC (9,4) UNSIGNED,"
+            + "num4 NUMERIC (9,0) UNSIGNED,"
+            + "num5 FLOAT(9,4),"
+            + "num6 FLOAT(9,4) UNSIGNED,"
+            + "num7 DOUBLE(9,4),"
+            + "num8 DOUBLE(9,4) UNSIGNED)");
+    ResultSet rs = stmt.executeQuery("SELECT * FROM getPrecision");
+    ResultSetMetaData rsmd = rs.getMetaData();
+    assertEquals(9, rsmd.getPrecision(1));
+    assertEquals(4, rsmd.getScale(1));
+    assertEquals(9, rsmd.getPrecision(2));
+    assertEquals(0, rsmd.getScale(2));
+    assertEquals(9, rsmd.getPrecision(3));
+    assertEquals(4, rsmd.getScale(3));
+    assertEquals(9, rsmd.getPrecision(4));
+    assertEquals(0, rsmd.getScale(4));
+    assertEquals(9, rsmd.getPrecision(5));
+    assertEquals(4, rsmd.getScale(5));
+    assertEquals(9, rsmd.getPrecision(6));
+    assertEquals(4, rsmd.getScale(6));
+    assertEquals(9, rsmd.getPrecision(7));
+    assertEquals(4, rsmd.getScale(7));
+    assertEquals(9, rsmd.getPrecision(8));
+    assertEquals(4, rsmd.getScale(8));
+  }
+
+  @Test
+  public void getTimePrecision() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM getTimePrecision");
+    ResultSetMetaData rsmd = rs.getMetaData();
+    // date
+    assertEquals(10, rsmd.getPrecision(1));
+    assertEquals(0, rsmd.getScale(1));
+    // datetime(0)
+    assertEquals(19, rsmd.getPrecision(2));
+    assertEquals(0, rsmd.getScale(2));
+    // datetime(6)
+    assertEquals(26, rsmd.getPrecision(3));
+    if (!isXpand()) assertEquals(6, rsmd.getScale(3));
+    // timestamp(0)
+    assertEquals(19, rsmd.getPrecision(4));
+    assertEquals(0, rsmd.getScale(4));
+    // timestamp(6)
+    assertEquals(26, rsmd.getPrecision(5));
+    if (!isXpand()) assertEquals(6, rsmd.getScale(5));
+    // time(0)
+    assertEquals(10, rsmd.getPrecision(6));
+    assertEquals(0, rsmd.getScale(6));
+    // time(6)
+    assertEquals(17, rsmd.getPrecision(7));
+    if (!isXpand()) assertEquals(6, rsmd.getScale(7));
+  }
+
+  @Test
+  public void metaTimeResultSet() throws SQLException {
+    final int columnSizeField = 7;
+
+    DatabaseMetaData dmd = sharedConn.getMetaData();
+    ResultSet rs = dmd.getColumns(null, null, "getTimePrecision", null);
+    // date
+    assertTrue(rs.next());
+    assertEquals(10, rs.getInt(columnSizeField));
+    // datetime(0)
+    assertTrue(rs.next());
+    assertEquals(19, rs.getInt(columnSizeField));
+    // datetime(6)
+    assertTrue(rs.next());
+    if (!isXpand()) assertEquals(26, rs.getInt(columnSizeField));
+    // timestamp(0)
+    assertTrue(rs.next());
+    assertEquals(19, rs.getInt(columnSizeField));
+    // timestamp(6)
+    assertTrue(rs.next());
+    if (!isXpand()) assertEquals(26, rs.getInt(columnSizeField));
+    // time(0)
+    assertTrue(rs.next());
+    assertEquals(10, rs.getInt(columnSizeField));
+    // time(6)
+    assertTrue(rs.next());
+    if (!isXpand()) assertEquals(17, rs.getInt(columnSizeField));
+
+    assertFalse(rs.next());
+  }
+
+  /**
+   * CONJ-401 - getProcedureColumns precision when server doesn't support precision.
+   *
+   * @throws SQLException if connection error occur
+   */
+  @Test
+  public void metaTimeNoPrecisionProcedureResultSet() throws SQLException {
+    Assumptions.assumeFalse(isXpand());
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE PROCEDURE getProcTimePrecision2(IN  I date, "
+            + "IN t1 DATETIME,"
+            + "IN t3 timestamp,"
+            + "IN t5 time) BEGIN SELECT I; END");
+
+    final int precisionField = 8;
+    final int lengthField = 9;
+    final int scaleField = 10;
+
+    DatabaseMetaData dmd = sharedConn.getMetaData();
+    ResultSet rs = dmd.getProcedureColumns(null, null, "getProcTimePrecision2", null);
+    // date
+    assertTrue(rs.next());
+    assertEquals(10, rs.getInt(precisionField));
+    assertEquals(10, rs.getInt(lengthField));
+    assertEquals(0, rs.getInt(scaleField));
+    assertTrue(rs.wasNull());
+    // datetime(0)
+    assertTrue(rs.next());
+    assertEquals(19, rs.getInt(precisionField));
+    assertEquals(19, rs.getInt(lengthField));
+    assertEquals(0, rs.getInt(scaleField));
+    // timestamp(0)
+    assertTrue(rs.next());
+    assertEquals(19, rs.getInt(precisionField));
+    assertEquals(19, rs.getInt(lengthField));
+    assertEquals(0, rs.getInt(scaleField));
+    // time(0)
+    assertTrue(rs.next());
+    assertEquals(10, rs.getInt(precisionField));
+    assertEquals(10, rs.getInt(lengthField));
+    assertEquals(0, rs.getInt(scaleField));
+
+    assertFalse(rs.next());
+  }
+
+  /**
+   * CONJ-381 - getProcedureColumns returns NULL as TIMESTAMP/DATETIME precision instead of 19.
+   *
+   * @throws SQLException if connection error occur
+   */
+  @Test
+  public void metaTimeProcedureResultSet() throws SQLException {
+    Assumptions.assumeTrue(!isXpand());
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE PROCEDURE getProcTimePrecision"
+            + "(IN  I date, "
+            + "IN t1 DATETIME(0),"
+            + "IN t2 DATETIME(6),"
+            + "IN t3 timestamp(0),"
+            + "IN t4 timestamp(6),"
+            + "IN t5 time ,"
+            + "IN t6 time(6)) BEGIN SELECT I; END");
+
+    final int precisionField = 8;
+    final int lengthField = 9;
+    final int scaleField = 10;
+
+    DatabaseMetaData dmd = sharedConn.getMetaData();
+    ResultSet rs = dmd.getProcedureColumns(null, null, "getProcTimePrecision", null);
+    // date
+    assertTrue(rs.next());
+    assertEquals(10, rs.getInt(precisionField));
+    assertEquals(10, rs.getInt(lengthField));
+    assertEquals(0, rs.getInt(scaleField));
+    assertTrue(rs.wasNull());
+    // datetime(0)
+    assertTrue(rs.next());
+    assertEquals(19, rs.getInt(precisionField));
+    assertEquals(19, rs.getInt(lengthField));
+    assertEquals(0, rs.getInt(scaleField));
+    // datetime(6)
+    assertTrue(rs.next());
+    assertEquals(26, rs.getInt(precisionField));
+    assertEquals(26, rs.getInt(lengthField));
+    assertEquals(6, rs.getInt(scaleField));
+    // timestamp(0)
+    assertTrue(rs.next());
+    assertEquals(19, rs.getInt(precisionField));
+    assertEquals(19, rs.getInt(lengthField));
+    assertEquals(0, rs.getInt(scaleField));
+    // timestamp(6)
+    assertTrue(rs.next());
+    assertEquals(26, rs.getInt(precisionField));
+    assertEquals(26, rs.getInt(lengthField));
+    assertEquals(6, rs.getInt(scaleField));
+    // time(0)
+    assertTrue(rs.next());
+    assertEquals(10, rs.getInt(precisionField));
+    assertEquals(10, rs.getInt(lengthField));
+    assertEquals(0, rs.getInt(scaleField));
+    // time(6)
+    assertTrue(rs.next());
+    assertEquals(17, rs.getInt(precisionField));
+    assertEquals(17, rs.getInt(lengthField));
+    assertEquals(6, rs.getInt(scaleField));
+
+    assertFalse(rs.next());
+  }
+
+  @Test
+  public void various() {
+    org.mariadb.jdbc.DatabaseMetaData meta = sharedConn.getMetaData();
+    assertEquals(64, meta.getMaxProcedureNameLength());
+  }
+
+  @Test
+  public void getIndexInfo() throws SQLException {
+    DatabaseMetaData meta = sharedConn.getMetaData();
+
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> meta.getIndexInfo(null, null, null, true, true),
+        "'table' parameter must not be null");
+
+    ResultSet rs = meta.getIndexInfo(null, null, "get_index_info", false, true);
+    rs.next();
+    assertEquals(sharedConn.getCatalog(), rs.getString(1));
+    assertNull(rs.getString(2));
+    assertEquals("get_index_info", rs.getString(3));
+    assertFalse(rs.getBoolean(4));
+    assertEquals(sharedConn.getCatalog(), rs.getString(5));
+    assertEquals("PRIMARY", rs.getString(6));
+    assertEquals(DatabaseMetaData.tableIndexOther, rs.getShort(7));
+    assertEquals(1, rs.getShort(8));
+    assertEquals("no", rs.getString(9));
+    assertEquals("A", rs.getString(10));
+    assertEquals(0L, rs.getLong(11));
+    assertNull(rs.getString(12));
+    assertNull(rs.getString(13));
+
+    assertTrue(rs.next());
+    assertEquals(sharedConn.getCatalog(), rs.getString(1));
+    assertNull(rs.getString(2));
+    assertEquals("get_index_info", rs.getString(3));
+    assertTrue(rs.getBoolean(4));
+    assertEquals(sharedConn.getCatalog(), rs.getString(5));
+    assertEquals("ind_cust", rs.getString(6));
+    assertEquals(DatabaseMetaData.tableIndexOther, rs.getShort(7));
+    assertEquals(1, rs.getShort(8));
+    assertEquals("customer_id", rs.getString(9));
+    assertEquals("A", rs.getString(10));
+    assertEquals(0L, rs.getLong(11));
+    assertNull(rs.getString(12));
+    assertNull(rs.getString(13));
+
+    assertTrue(rs.next());
+    assertEquals(sharedConn.getCatalog(), rs.getString(1));
+    assertNull(rs.getString(2));
+    assertEquals("get_index_info", rs.getString(3));
+    assertTrue(rs.getBoolean(4));
+    assertEquals(sharedConn.getCatalog(), rs.getString(5));
+    assertEquals("ind_prod", rs.getString(6));
+    assertEquals(DatabaseMetaData.tableIndexOther, rs.getShort(7));
+    assertEquals(1, rs.getShort(8));
+    assertEquals("product_category", rs.getString(9));
+    assertEquals("A", rs.getString(10));
+    assertEquals(0L, rs.getLong(11));
+    assertNull(rs.getString(12));
+    assertNull(rs.getString(13));
+
+    assertTrue(rs.next());
+    assertEquals(sharedConn.getCatalog(), rs.getString(1));
+    assertNull(rs.getString(2));
+    assertEquals("get_index_info", rs.getString(3));
+    assertTrue(rs.getBoolean(4));
+    assertEquals(sharedConn.getCatalog(), rs.getString(5));
+    assertEquals("ind_prod", rs.getString(6));
+    assertEquals(DatabaseMetaData.tableIndexOther, rs.getShort(7));
+    assertEquals(2, rs.getShort(8));
+    assertEquals("product_id", rs.getString(9));
+    assertEquals("A", rs.getString(10));
+    assertEquals(0L, rs.getLong(11));
+    assertNull(rs.getString(12));
+    assertNull(rs.getString(13));
+  }
+
+  @Test
+  public void getPseudoColumns() throws SQLException {
+    DatabaseMetaData meta = sharedConn.getMetaData();
+    ResultSet rs = meta.getPseudoColumns(null, null, null, null);
+    assertFalse(rs.next());
+  }
+
+  @Test
+  public void constantTest() throws SQLException {
+    DatabaseMetaData meta = sharedConn.getMetaData();
+    assertFalse(meta.othersUpdatesAreVisible(ResultSet.TYPE_FORWARD_ONLY));
+    assertFalse(meta.othersDeletesAreVisible(ResultSet.TYPE_FORWARD_ONLY));
+    assertFalse(meta.othersInsertsAreVisible(ResultSet.TYPE_FORWARD_ONLY));
+    assertFalse(meta.updatesAreDetected(ResultSet.TYPE_FORWARD_ONLY));
+    assertFalse(meta.deletesAreDetected(ResultSet.TYPE_FORWARD_ONLY));
+    assertFalse(meta.insertsAreDetected(ResultSet.TYPE_FORWARD_ONLY));
+    assertTrue(meta.supportsBatchUpdates());
+    assertEquals(sharedConn, sharedConn.getMetaData().getConnection());
+    assertTrue(meta.supportsSavepoints());
+    assertFalse(meta.supportsNamedParameters());
+    assertFalse(meta.supportsMultipleOpenResults());
+    assertTrue(meta.supportsGetGeneratedKeys());
+    assertTrue(meta.supportsResultSetHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT));
+    assertFalse(meta.supportsResultSetHoldability(ResultSet.CLOSE_CURSORS_AT_COMMIT));
+    assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, meta.getResultSetHoldability());
+    assertTrue(meta.getDatabaseMajorVersion() >= 5);
+    assertTrue(meta.getDatabaseMinorVersion() >= 0);
+    assertEquals(4, meta.getJDBCMajorVersion());
+    assertEquals(2, meta.getJDBCMinorVersion());
+    assertEquals(DatabaseMetaData.sqlStateSQL99, meta.getSQLStateType());
+    assertFalse(meta.locatorsUpdateCopy());
+    assertFalse(meta.supportsStatementPooling());
+    assertEquals(RowIdLifetime.ROWID_UNSUPPORTED, meta.getRowIdLifetime());
+    assertTrue(meta.supportsStoredFunctionsUsingCallSyntax());
+    assertFalse(meta.autoCommitFailureClosesAllResultSets());
+
+    meta.unwrap(java.sql.DatabaseMetaData.class);
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> meta.unwrap(String.class),
+        "The receiver is not a wrapper for java.lang.String");
+
+    assertEquals(4294967295L, meta.getMaxLogicalLobSize());
+    assertFalse(meta.supportsRefCursors());
+    assertTrue(meta.supportsGetGeneratedKeys());
+    assertTrue(meta.generatedKeyAlwaysReturned());
+    assertTrue(meta.allProceduresAreCallable());
+    assertTrue(meta.allTablesAreSelectable());
+    assertNotNull(meta.getURL());
+    assertNotNull(meta.getUserName());
+    assertFalse(meta.isReadOnly());
+    assertFalse(meta.nullsAreSortedHigh());
+    assertTrue(meta.nullsAreSortedLow());
+    assertFalse(meta.nullsAreSortedAtStart());
+    assertTrue(meta.nullsAreSortedAtEnd());
+    assertEquals(isMariaDBServer() ? "MariaDB" : "MySQL", meta.getDatabaseProductName());
+    assertEquals("MariaDB Connector/J", meta.getDriverName());
+    assertTrue(meta.getDriverVersion().startsWith("3."));
+    assertTrue(meta.getDriverMajorVersion() >= 0);
+    assertTrue(meta.getDriverMinorVersion() >= 0);
+    assertFalse(meta.usesLocalFiles());
+    assertFalse(meta.usesLocalFilePerTable());
+    assertEquals(meta.supportsMixedCaseIdentifiers(), meta.supportsMixedCaseQuotedIdentifiers());
+    assertEquals(meta.storesUpperCaseIdentifiers(), meta.storesUpperCaseQuotedIdentifiers());
+    assertEquals(meta.storesLowerCaseIdentifiers(), meta.storesLowerCaseQuotedIdentifiers());
+    assertEquals(meta.storesMixedCaseIdentifiers(), meta.storesMixedCaseQuotedIdentifiers());
+    assertEquals("`", meta.getIdentifierQuoteString());
+    assertNotNull(meta.getSQLKeywords());
+    assertNotNull(meta.getNumericFunctions());
+    assertNotNull(meta.getStringFunctions());
+    assertNotNull(meta.getSystemFunctions());
+    assertNotNull(meta.getTimeDateFunctions());
+    assertEquals("\\", meta.getSearchStringEscape());
+    assertEquals("#@", meta.getExtraNameCharacters());
+    assertTrue(meta.supportsAlterTableWithAddColumn());
+    assertTrue(meta.supportsAlterTableWithDropColumn());
+    assertTrue(meta.supportsColumnAliasing());
+    assertTrue(meta.nullPlusNonNullIsNull());
+    assertTrue(meta.supportsConvert());
+    assertTrue(meta.supportsAlterTableWithAddColumn());
+    assertTrue(meta.supportsConvert(Types.INTEGER, Types.REAL));
+    assertFalse(meta.supportsConvert(Types.INTEGER, Types.BLOB));
+    assertTrue(meta.supportsConvert(Types.BLOB, Types.TINYINT));
+    assertFalse(meta.supportsConvert(Types.BLOB, Types.ARRAY));
+    assertTrue(meta.supportsConvert(Types.CLOB, Types.NUMERIC));
+    assertFalse(meta.supportsConvert(Types.CLOB, Types.ARRAY));
+    assertTrue(meta.supportsConvert(Types.DATE, Types.VARCHAR));
+    assertFalse(meta.supportsConvert(Types.DATE, Types.ARRAY));
+    assertTrue(meta.supportsConvert(Types.TIME, Types.VARCHAR));
+    assertFalse(meta.supportsConvert(Types.TIME, Types.ARRAY));
+    assertTrue(meta.supportsConvert(Types.TIMESTAMP, Types.VARCHAR));
+    assertFalse(meta.supportsConvert(Types.TIMESTAMP, Types.ARRAY));
+    assertFalse(meta.supportsConvert(Types.ARRAY, Types.TIMESTAMP));
+    assertTrue(meta.supportsTableCorrelationNames());
+    assertTrue(meta.supportsDifferentTableCorrelationNames());
+    assertTrue(meta.supportsExpressionsInOrderBy());
+    assertTrue(meta.supportsOrderByUnrelated());
+    assertTrue(meta.supportsGroupBy());
+    assertTrue(meta.supportsGroupByUnrelated());
+    assertTrue(meta.supportsGroupByBeyondSelect());
+    assertTrue(meta.supportsLikeEscapeClause());
+    assertTrue(meta.supportsMultipleResultSets());
+    assertTrue(meta.supportsMultipleTransactions());
+    assertTrue(meta.supportsNonNullableColumns());
+    assertTrue(meta.supportsMinimumSQLGrammar());
+    assertTrue(meta.supportsCoreSQLGrammar());
+    assertTrue(meta.supportsExtendedSQLGrammar());
+    assertTrue(meta.supportsANSI92EntryLevelSQL());
+    assertTrue(meta.supportsANSI92IntermediateSQL());
+    assertTrue(meta.supportsANSI92FullSQL());
+    assertTrue(meta.supportsIntegrityEnhancementFacility());
+    assertTrue(meta.supportsOuterJoins());
+    assertTrue(meta.supportsFullOuterJoins());
+    assertTrue(meta.supportsLimitedOuterJoins());
+    assertEquals("schema", meta.getSchemaTerm());
+    assertEquals("procedure", meta.getProcedureTerm());
+    assertEquals("database", meta.getCatalogTerm());
+    assertTrue(meta.isCatalogAtStart());
+    assertEquals(".", meta.getCatalogSeparator());
+    assertFalse(meta.supportsSchemasInDataManipulation());
+    assertFalse(meta.supportsSchemasInProcedureCalls());
+    assertFalse(meta.supportsSchemasInTableDefinitions());
+    assertFalse(meta.supportsSchemasInIndexDefinitions());
+    assertFalse(meta.supportsSchemasInPrivilegeDefinitions());
+    assertTrue(meta.supportsCatalogsInDataManipulation());
+    assertTrue(meta.supportsCatalogsInProcedureCalls());
+    assertTrue(meta.supportsCatalogsInTableDefinitions());
+    assertTrue(meta.supportsCatalogsInIndexDefinitions());
+    assertTrue(meta.supportsCatalogsInPrivilegeDefinitions());
+    assertFalse(meta.supportsPositionedDelete());
+    assertFalse(meta.supportsPositionedUpdate());
+    assertTrue(meta.supportsSelectForUpdate());
+    assertTrue(meta.supportsStoredProcedures());
+    assertTrue(meta.supportsSubqueriesInComparisons());
+    assertTrue(meta.supportsSubqueriesInExists());
+    assertTrue(meta.supportsSubqueriesInIns());
+    assertTrue(meta.supportsSubqueriesInComparisons());
+    assertTrue(meta.supportsSubqueriesInQuantifieds());
+    assertTrue(meta.supportsCorrelatedSubqueries());
+    assertTrue(meta.supportsUnion());
+    assertTrue(meta.supportsUnionAll());
+    assertTrue(meta.supportsOpenCursorsAcrossCommit());
+    assertTrue(meta.supportsOpenCursorsAcrossRollback());
+    assertTrue(meta.supportsOpenStatementsAcrossCommit());
+    assertTrue(meta.supportsOpenStatementsAcrossRollback());
+    assertEquals(Integer.MAX_VALUE, meta.getMaxBinaryLiteralLength());
+    assertEquals(Integer.MAX_VALUE, meta.getMaxCharLiteralLength());
+    assertEquals(64, meta.getMaxColumnNameLength());
+    assertEquals(64, meta.getMaxColumnsInGroupBy());
+    assertEquals(16, meta.getMaxColumnsInIndex());
+    assertEquals(64, meta.getMaxColumnsInOrderBy());
+    assertEquals(Short.MAX_VALUE, meta.getMaxColumnsInSelect());
+    assertEquals(0, meta.getMaxColumnsInTable());
+    assertEquals(0, meta.getMaxConnections());
+    assertEquals(0, meta.getMaxCursorNameLength());
+    assertEquals(256, meta.getMaxIndexLength());
+    assertEquals(0, meta.getMaxSchemaNameLength());
+    assertEquals(64, meta.getMaxProcedureNameLength());
+    assertEquals(0, meta.getMaxCatalogNameLength());
+    assertEquals(0, meta.getMaxRowSize());
+    assertFalse(meta.doesMaxRowSizeIncludeBlobs());
+    assertEquals(0, meta.getMaxStatementLength());
+    assertEquals(0, meta.getMaxStatements());
+    assertEquals(64, meta.getMaxTableNameLength());
+    assertEquals(256, meta.getMaxTablesInSelect());
+    assertEquals(0, meta.getMaxUserNameLength());
+    assertEquals(Connection.TRANSACTION_REPEATABLE_READ, meta.getDefaultTransactionIsolation());
+    assertTrue(meta.supportsTransactions());
+    assertTrue(meta.supportsTransactionIsolationLevel(Connection.TRANSACTION_READ_UNCOMMITTED));
+    assertTrue(meta.supportsTransactionIsolationLevel(Connection.TRANSACTION_READ_COMMITTED));
+    assertTrue(meta.supportsTransactionIsolationLevel(Connection.TRANSACTION_REPEATABLE_READ));
+    assertTrue(meta.supportsTransactionIsolationLevel(Connection.TRANSACTION_SERIALIZABLE));
+    assertFalse(meta.supportsTransactionIsolationLevel(Connection.TRANSACTION_NONE));
+    assertTrue(meta.supportsDataDefinitionAndDataManipulationTransactions());
+    assertFalse(meta.supportsDataManipulationTransactionsOnly());
+    assertTrue(meta.dataDefinitionCausesTransactionCommit());
+    assertFalse(meta.dataDefinitionIgnoredInTransactions());
+  }
+
+  @Test
+  public void testMetaCatalog() throws Exception {
+    DatabaseMetaData meta = sharedConn.getMetaData();
+    ResultSet rs = meta.getProcedures(sharedConn.getCatalog(), null, "testMetaCatalog");
+    assertTrue(rs.next());
+    assertEquals(sharedConn.getCatalog(), rs.getString(1));
+    assertNull(rs.getString(2));
+    assertEquals("testMetaCatalog", rs.getString(3));
+    assertNull(rs.getString(4));
+    assertNull(rs.getString(5));
+    assertNull(rs.getString(6));
+    assertEquals("comments", rs.getString(7));
+    assertEquals(DatabaseMetaData.procedureNoResult, rs.getInt(8));
+    assertEquals("testMetaCatalog", rs.getString(9));
+    assertFalse(rs.next());
+
+    // test with bad catalog
+    rs = meta.getProcedures("yahoooo", null, "testMetaCatalog");
+    assertFalse(rs.next());
+
+    // test without catalog
+    rs = meta.getProcedures(null, null, "testMetaCatalog");
+    assertTrue(rs.next());
+    assertTrue("testMetaCatalog".equals(rs.getString(3)));
+    assertFalse(rs.next());
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = sharedConn.createStatement().executeQuery("SELECT * FROM json_test");
+    ResultSetMetaData meta = rs.getMetaData();
+    assertTrue(
+        "LONGTEXT".equals(meta.getColumnTypeName(1)) || "JSON".equals(meta.getColumnTypeName(1)));
+    assertEquals(Types.LONGVARCHAR, meta.getColumnType(1));
+    assertEquals("java.lang.String", meta.getColumnClassName(1));
+  }
+
+  @Test
+  public void getTypeMetaData() throws SQLException {
+    //            "create table text_types_text (varchar100           varchar(100),\n" +
+    //                    "  varchar255           varchar(255),\n" +
+    //                    "  text                 text,\n" +
+    //                    "  `tinytext`           tinytext,\n" +
+    //                    "  `mediumtext`         mediumtext,\n" +
+    //                    "  `longtext`           longtext)"
+    try (ResultSet resultSet =
+        sharedConn.createStatement().executeQuery("select * from text_types_text")) {
+      ResultSetMetaData metaData = resultSet.getMetaData();
+
+      String[] expected =
+          new String[] {
+            "varchar100 12 VARCHAR 100",
+            "varchar255 12 VARCHAR 255",
+            "text 12 TEXT 65535",
+            "tinytext 12 VARCHAR 255",
+            "mediumtext 12 MEDIUMTEXT 16777215",
+            "longtext -1 LONGTEXT 0"
+          };
+      for (int i = 0; i < expected.length; i++) {
+        assertEquals(
+            expected[i],
+            metaData.getColumnName(i + 1)
+                + " "
+                + metaData.getColumnType(i + 1)
+                + " "
+                + metaData.getColumnTypeName(i + 1)
+                + " "
+                + metaData.getPrecision(i + 1));
+      }
+    }
+  }
+
+  @Test
+  public void foreignKeyTest() throws SQLException {
+
+    try (Connection con = createCon()) {
+      String db = con.getCatalog();
+      java.sql.Statement stmt = con.createStatement();
+      stmt.execute("DROP DATABASE IF EXISTS dbTmp");
+      stmt.execute("DROP TABLE IF EXISTS tableWithForeignKey");
+      stmt.execute("DROP TABLE IF EXISTS tableWithPk");
+      stmt.execute("CREATE TABLE tableWithPk(id BIGINT PRIMARY KEY) ENGINE=InnoDB");
+
+      stmt.execute(
+          "CREATE TABLE IF NOT EXISTS tableWithForeignKey\n"
+              + "          (\n"
+              + "                  id BIGINT NOT NULL AUTO_INCREMENT,\n"
+              + "                  fk_id BIGINT,\n"
+              + "                  PRIMARY KEY (id),\n"
+              + "  CONSTRAINT fk_key FOREIGN KEY (fk_id) REFERENCES tableWithPk (id)\n"
+              + "          ) ENGINE=InnoDB");
+      DatabaseMetaData dbmd = con.getMetaData();
+      ResultSet rs = dbmd.getImportedKeys(null, null, "tableWithForeignKey");
+      assertTrue(rs.next());
+      rs.close();
+
+      rs = dbmd.getImportedKeys("", null, "tableWithForeignKey");
+      assertTrue(rs.next());
+
+      stmt.execute("CREATE DATABASE dbTmp");
+      con.setCatalog("dbTmp");
+      try {
+        rs = dbmd.getImportedKeys("", null, "tableWithForeignKey");
+        assertFalse(rs.next());
+
+        rs = dbmd.getImportedKeys(null, null, "tableWithForeignKey");
+        assertTrue(rs.next());
+      } finally {
+        con.setCatalog(db);
+        stmt.execute("DROP DATABASE dbTmp");
+      }
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/DriverTest.java b/src/test/java/org/mariadb/jdbc/integration/DriverTest.java
new file mode 100644
index 0000000..03d8712
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/DriverTest.java
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.sql.*;
+import java.util.Map;
+import java.util.Properties;
+import org.junit.jupiter.api.*;
+import org.mariadb.jdbc.Configuration;
+
+public class DriverTest extends Common {
+
+  @Test
+  public void ensureDescriptionFilled() throws Exception {
+    Properties descr = new Properties();
+    try (InputStream inputStream =
+        Common.class.getClassLoader().getResourceAsStream("driver.properties")) {
+      descr.load(inputStream);
+    }
+
+    // check that description is present
+    for (Field field : Configuration.Builder.class.getDeclaredFields()) {
+      if (!field.getName().startsWith("_")) {
+        if (descr.get(field.getName()) == null && !"$jacocoData".equals(field.getName()))
+          throw new Exception(String.format("Missing %s description", field.getName()));
+      }
+    }
+
+    // check that no description without option
+    for (Map.Entry<Object, Object> entry : descr.entrySet()) {
+      // NoSuchFieldException will be thrown if not present
+      Configuration.Builder.class.getDeclaredField(entry.getKey().toString());
+    }
+  }
+
+  @Test
+  public void getPropertyInfo() throws SQLException {
+    Driver driver = new org.mariadb.jdbc.Driver();
+    assertEquals(0, driver.getPropertyInfo(null, null).length);
+    assertEquals(0, driver.getPropertyInfo("jdbc:bla//", null).length);
+
+    Properties properties = new Properties();
+    properties.put("password", "myPwd");
+    DriverPropertyInfo[] driverPropertyInfos =
+        driver.getPropertyInfo("jdbc:mariadb://localhost/db?user=root", properties);
+    for (DriverPropertyInfo driverPropertyInfo : driverPropertyInfos) {
+      if (!"$jacocoData".equals(driverPropertyInfo.name)) {
+        assertNotNull(
+            driverPropertyInfo.description, "no description for " + driverPropertyInfo.name);
+      }
+    }
+  }
+
+  @Test
+  public void basicInfo() {
+    Driver driver = new org.mariadb.jdbc.Driver();
+    assertEquals(3, driver.getMajorVersion());
+    assertTrue(driver.getMinorVersion() > -1);
+    assertTrue(driver.jdbcCompliant());
+    assertThrows(SQLFeatureNotSupportedException.class, driver::getParentLogger);
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/EofTest.java b/src/test/java/org/mariadb/jdbc/integration/EofTest.java
new file mode 100644
index 0000000..5f1069e
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/EofTest.java
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.*;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Connection;
+
+public class EofTest extends Common {
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    createSequenceTables();
+  }
+
+  @Test
+  public void basicResultset() throws Exception {
+    basicResultset(sharedConn);
+    basicResultset(sharedConnBinary);
+    try (Connection con = createCon("deprecateEof=false")) {
+      basicResultset(con);
+    }
+    try (Connection con =
+        createCon("deprecateEof=false&useServerPrepStmts=true&enableSkipMeta=false")) {
+      basicResultset(con);
+    }
+    try (Connection con =
+        createCon("deprecateEof=false&useServerPrepStmts=true&enableSkipMeta=true")) {
+      basicResultset(con);
+    }
+  }
+
+  public void basicResultset(Connection con) throws Exception {
+    try (PreparedStatement prep =
+        con.prepareStatement("SELECT * FROM sequence_1_to_10 where 1 = ?")) {
+      prep.setFetchSize(2);
+      prep.setMaxRows(4);
+      prep.setInt(1, 1);
+      ResultSet rs = prep.executeQuery();
+      rs.next();
+      assertEquals(1, rs.getInt(1));
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/ErrorTest.java b/src/test/java/org/mariadb/jdbc/integration/ErrorTest.java
new file mode 100644
index 0000000..8fb519f
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/ErrorTest.java
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class ErrorTest extends Common {
+
+  @BeforeAll
+  public static void begin() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE deadlock(a int primary key)");
+    stmt.execute("CREATE TABLE deadlock2(a int primary key) ENGINE=InnoDB");
+
+    stmt.execute("FLUSH TABLES");
+  }
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS deadlock");
+    stmt.execute("DROP TABLE IF EXISTS deadlock2");
+  }
+
+  @Test
+  public void dumpQueryOnException() throws Exception {
+    try (Connection con = createCon("dumpQueriesOnException")) {
+      Statement stmt = con.createStatement();
+      try {
+        stmt.execute("SELECT 'long value' FROM wrongTable");
+        fail();
+      } catch (SQLException e) {
+        assertTrue(e.getMessage().contains("Query is: SELECT 'long value' FROM wrongTable"));
+      }
+    }
+
+    try (Connection con = createCon("maxQuerySizeToLog=100&dumpQueriesOnException")) {
+      Statement stmt = con.createStatement();
+      try {
+        stmt.execute("SELECT 'long value' FROM wrongTable");
+        fail();
+      } catch (SQLException e) {
+        assertTrue(e.getMessage().contains("Query is: SELECT 'long value' FROM wrongTable"));
+      }
+    }
+
+    try (Connection con = createCon("maxQuerySizeToLog=13&dumpQueriesOnException")) {
+      Statement stmt = con.createStatement();
+      try {
+        stmt.execute("SELECT 'long value' FROM wrongTable");
+        fail();
+      } catch (SQLException e) {
+        assertTrue(e.getMessage().contains("Query is: SELECT 'lo..."));
+      }
+    }
+  }
+
+  @Test
+  public void testPre41ErrorFormat() throws Exception {
+    testPre41ErrorFormat(sharedConn);
+    try (Connection con =
+        createCon("dumpQueriesOnException&includeInnodbStatusInDeadlockExceptions")) {
+      testPre41ErrorFormat(con);
+    }
+  }
+
+  private void testPre41ErrorFormat(Connection con) throws Exception {
+    Assumptions.assumeTrue(
+        !"maxscale".equals(System.getenv("srv"))
+            && !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv"))
+            && !isXpand());
+    SQLException exception = null;
+    int max_connections;
+    Statement stmt = con.createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT @@max_connections");
+    rs.next();
+    max_connections = rs.getInt(1);
+    Assumptions.assumeTrue(max_connections < 1000);
+    Connection[] cons = new Connection[max_connections];
+    for (int i = 0; i < max_connections; i++) {
+      try {
+        cons[i] = createCon();
+      } catch (SQLException sqle) {
+        exception = sqle;
+      }
+    }
+
+    for (int i = 0; i < max_connections; i++) {
+      try {
+        if (cons[i] != null) cons[i].close();
+      } catch (SQLException sqle) {
+        // eat
+      }
+    }
+    assertNotNull(exception);
+    assertTrue(exception.getMessage().contains("Too many"));
+  }
+
+  @Test
+  public void deadLockInformation() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("insert into deadlock(a) values(0), (1)");
+
+    try (Connection conn1 =
+        createCon(
+            "includeInnodbStatusInDeadlockExceptions&includeThreadDumpInDeadlockExceptions")) {
+
+      conn1.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
+      Statement stmt1 = conn1.createStatement();
+      try {
+        stmt1.execute("SET SESSION idle_transaction_timeout=2");
+      } catch (SQLException e) {
+        // eat ( for mariadb >= 10.3)
+      }
+      stmt.execute("start transaction");
+      stmt.execute("update deadlock set a = 2 where a <> 0");
+      Assumptions.assumeFalse(isXpand());
+      try (Connection conn2 =
+          createCon(
+              "&includeInnodbStatusInDeadlockExceptions&includeThreadDumpInDeadlockExceptions")) {
+
+        Statement stmt2 = conn2.createStatement();
+        conn2.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
+        try {
+          stmt2.execute("SET SESSION idle_transaction_timeout=2, innodb_lock_wait_timeout=2");
+        } catch (SQLException e) {
+          // eat ( for mariadb >= 10.3)
+        }
+        stmt2.execute("start transaction");
+        try {
+          stmt2.execute("update deadlock set a = 3 where a <> 1");
+          fail("Must have thrown deadlock exception");
+        } catch (SQLException sqle) {
+          assertTrue(sqle.getMessage().contains("current threads:"));
+          assertTrue(sqle.getMessage().contains("deadlock information"));
+        }
+      }
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/FailoverTest.java b/src/test/java/org/mariadb/jdbc/integration/FailoverTest.java
new file mode 100644
index 0000000..5925bfb
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/FailoverTest.java
@@ -0,0 +1,343 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.ByteArrayInputStream;
+import java.sql.*;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Connection;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.export.HaMode;
+
+public class FailoverTest extends Common {
+
+  @Test
+  public void simpleFailoverTransactionReplay() throws SQLException {
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+    try (Connection con = createProxyCon(HaMode.SEQUENTIAL, "")) {
+      con.setNetworkTimeout(Runnable::run, 200);
+      long threadId = con.getContext().getThreadId();
+      Statement stmt = con.createStatement();
+      proxy.restart(200);
+      assertThrowsContains(
+          SQLTransientConnectionException.class,
+          () -> stmt.execute("SELECT 1"),
+          "Driver has reconnect connection after a communications link failure");
+      ;
+      Assertions.assertTrue(con.getContext().getThreadId() != threadId);
+    }
+  }
+
+  @Test
+  public void simpleFailoverTransactionReplayNoQuery() throws SQLException {
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+    try (Connection con = createProxyCon(HaMode.SEQUENTIAL, "")) {
+      con.setNetworkTimeout(Runnable::run, 200);
+      long threadId = con.getContext().getThreadId();
+      Statement stmt = con.createStatement();
+      proxy.restart(200);
+
+      con.isValid(1000);
+      Assertions.assertTrue(con.getContext().getThreadId() != threadId);
+    }
+  }
+
+  @Test
+  public void transactionReplay() throws SQLException {
+    transactionReplay(true);
+    transactionReplay(false);
+  }
+
+  private void transactionReplay(boolean transactionReplay) throws SQLException {
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+    Statement st = sharedConn.createStatement();
+    st.execute("DROP TABLE IF EXISTS transaction_failover");
+    st.execute(
+        "CREATE TABLE transaction_failover "
+            + "(id int not null primary key auto_increment, test varchar(20)) "
+            + "engine=innodb");
+
+    try (Connection con =
+        createProxyCon(HaMode.SEQUENTIAL, "&transactionReplay=" + transactionReplay)) {
+      assertEquals(Connection.TRANSACTION_REPEATABLE_READ, con.getTransactionIsolation());
+      con.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
+      final Statement stmt = con.createStatement();
+      con.setNetworkTimeout(Runnable::run, 200);
+      long threadId = con.getContext().getThreadId();
+
+      stmt.executeUpdate("INSERT INTO transaction_failover (test) VALUES ('test0')");
+      con.setAutoCommit(false);
+      stmt.executeUpdate("INSERT INTO transaction_failover (test) VALUES ('test1')");
+      stmt.executeUpdate("INSERT INTO transaction_failover (test) VALUES ('test2')");
+      proxy.restart(300);
+      if (transactionReplay) {
+        stmt.executeUpdate("INSERT INTO transaction_failover (test) VALUES ('test3')");
+        con.commit();
+
+        ResultSet rs = stmt.executeQuery("SELECT * FROM transaction_failover");
+        for (int i = 0; i < 4; i++) {
+          assertTrue(rs.next());
+          assertEquals("test" + i, rs.getString("test"));
+        }
+        con.commit();
+        Assertions.assertTrue(con.getContext().getThreadId() != threadId);
+        assertFalse(con.getAutoCommit());
+        assertEquals(Connection.TRANSACTION_READ_UNCOMMITTED, con.getTransactionIsolation());
+      } else {
+        Common.assertThrowsContains(
+            SQLTransientConnectionException.class,
+            () -> stmt.executeUpdate("INSERT INTO transaction_failover (test) VALUES ('test3')"),
+            "In progress transaction was lost");
+      }
+    }
+  }
+
+  @Test
+  public void transactionReplayDuringCommit() throws SQLException {
+    transactionReplayDuringCommit(true);
+    transactionReplayDuringCommit(false);
+  }
+
+  private void transactionReplayDuringCommit(boolean transactionReplay) throws SQLException {
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+    Statement st = sharedConn.createStatement();
+    st.execute("DROP TABLE IF EXISTS transaction_failover");
+    st.execute(
+        "CREATE TABLE transaction_failover "
+            + "(id int not null primary key auto_increment, test varchar(20)) "
+            + "engine=innodb");
+
+    try (Connection con =
+        createProxyCon(HaMode.SEQUENTIAL, "&transactionReplay=" + transactionReplay)) {
+      assertEquals(Connection.TRANSACTION_REPEATABLE_READ, con.getTransactionIsolation());
+      con.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
+      final Statement stmt = con.createStatement();
+      con.setNetworkTimeout(Runnable::run, 200);
+      long threadId = con.getContext().getThreadId();
+
+      stmt.executeUpdate("INSERT INTO transaction_failover (test) VALUES ('test0')");
+      con.setAutoCommit(false);
+      stmt.executeUpdate("INSERT INTO transaction_failover (test) VALUES ('test1')");
+      stmt.executeUpdate("INSERT INTO transaction_failover (test) VALUES ('test2')");
+      proxy.restart(300);
+      if (transactionReplay) {
+        Common.assertThrowsContains(
+            SQLTransientConnectionException.class,
+            con::commit,
+            "Driver has reconnect connection after a communications failure");
+
+        ResultSet rs = stmt.executeQuery("SELECT * FROM transaction_failover");
+        for (int i = 0; i < 1; i++) {
+          assertTrue(rs.next());
+          assertEquals("test" + i, rs.getString("test"));
+        }
+
+        Assertions.assertTrue(con.getContext().getThreadId() != threadId);
+        assertFalse(con.getAutoCommit());
+        assertEquals(Connection.TRANSACTION_READ_UNCOMMITTED, con.getTransactionIsolation());
+      } else {
+        Common.assertThrowsContains(
+            SQLTransientConnectionException.class, con::commit, "during a COMMIT statement");
+      }
+    }
+  }
+
+  @Test
+  public void transactionReplayPreparedStatement() throws Exception {
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+    transactionReplayPreparedStatement(true, true);
+    transactionReplayPreparedStatement(false, true);
+    transactionReplayPreparedStatement(true, false);
+    transactionReplayPreparedStatement(false, false);
+  }
+
+  private void transactionReplayPreparedStatement(boolean binary, boolean transactionReplay)
+      throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS transaction_failover_3");
+    stmt.execute(
+        "CREATE TABLE transaction_failover_3 "
+            + "(id int not null primary key auto_increment, test varchar(20)) "
+            + "engine=innodb");
+
+    try (Connection con =
+        createProxyCon(
+            HaMode.SEQUENTIAL,
+            "&useServerPrepStmts=" + binary + "&transactionReplay=" + transactionReplay)) {
+      stmt = con.createStatement();
+      con.setNetworkTimeout(Runnable::run, 200);
+      long threadId = con.getContext().getThreadId();
+
+      stmt.executeUpdate("INSERT INTO transaction_failover_3 (test) VALUES ('test0')");
+      con.setAutoCommit(false);
+      stmt.executeUpdate("INSERT INTO transaction_failover_3 (test) VALUES ('test1')");
+      try (PreparedStatement p =
+          con.prepareStatement("INSERT INTO transaction_failover_3 (test) VALUES (?)")) {
+        p.setString(1, "test2");
+        p.execute();
+        p.setAsciiStream(1, new ByteArrayInputStream("test3".getBytes()));
+        p.execute();
+
+        proxy.restart(300);
+        p.setString(1, "test4");
+        if (transactionReplay) {
+          p.execute();
+        } else {
+          Common.assertThrowsContains(
+              SQLTransientConnectionException.class,
+              p::execute,
+              "In progress transaction was lost");
+        }
+      }
+      if (transactionReplay) {
+        con.commit();
+        ResultSet rs = stmt.executeQuery("SELECT * FROM transaction_failover_3");
+
+        for (int i = 0; i < 5; i++) {
+          assertTrue(rs.next());
+          assertEquals("test" + i, rs.getString("test"));
+        }
+        con.commit();
+        Assertions.assertTrue(con.getContext().getThreadId() != threadId);
+        assertFalse(con.getAutoCommit());
+      }
+    }
+  }
+
+  @Test
+  public void transactionReplayPreparedStatementBatch() throws Exception {
+    Assumptions.assumeTrue(
+        !"maxscale".equals(System.getenv("srv"))
+            && !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv")));
+    for (int i = 0; i < 8; i++) {
+      transactionReplayPreparedStatementBatch((i & 1) > 0, (i & 2) > 0, (i & 4) > 0);
+    }
+  }
+
+  private void transactionReplayPreparedStatementBatch(
+      boolean text, boolean useBulk, boolean transactionReplay) throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS transaction_failover_2");
+    stmt.execute(
+        "CREATE TABLE transaction_failover_2 "
+            + "(id int not null primary key auto_increment, test varchar(20)) "
+            + "engine=innodb");
+
+    try (Connection con =
+        createProxyCon(
+            HaMode.SEQUENTIAL,
+            "&useServerPrepStmts="
+                + !text
+                + "&useBulkStmts="
+                + useBulk
+                + "&transactionReplay="
+                + transactionReplay)) {
+      con.setNetworkTimeout(Runnable::run, 500);
+      long threadId = con.getContext().getThreadId();
+      execute(con, transactionReplay, threadId);
+      threadId = con.getContext().getThreadId();
+      execute(con, transactionReplay, threadId);
+    }
+  }
+
+  private void execute(Connection con, boolean transactionReplay, long threadId)
+      throws SQLException {
+    Statement stmt = con.createStatement();
+
+    stmt.executeUpdate("INSERT INTO transaction_failover_2 (test) VALUES ('test0')");
+    con.setAutoCommit(false);
+    stmt.executeUpdate("INSERT INTO transaction_failover_2 (test) VALUES ('test1')");
+    try (PreparedStatement p =
+        con.prepareStatement("INSERT INTO transaction_failover_2 (test) VALUES (?)")) {
+      p.setString(1, "test2");
+      p.execute();
+      p.setString(1, "test3");
+      p.addBatch();
+      p.setString(1, "test4");
+      p.addBatch();
+      p.executeBatch();
+
+      proxy.restart(300);
+      p.setString(1, "test5");
+      p.addBatch();
+      p.setString(1, "test6");
+      p.addBatch();
+
+      if (transactionReplay) {
+        p.executeBatch();
+        con.commit();
+
+        ResultSet rs = stmt.executeQuery("SELECT * FROM transaction_failover_2");
+        for (int i = 0; i < 6; i++) {
+          assertTrue(rs.next());
+          assertEquals("test" + i, rs.getString("test"));
+        }
+        con.commit();
+        Assertions.assertTrue(con.getContext().getThreadId() != threadId);
+        assertFalse(con.getAutoCommit());
+      } else {
+        try {
+          p.executeBatch();
+          Assertions.fail();
+        } catch (SQLException e) {
+          SQLException ee = (SQLException) ((e instanceof BatchUpdateException) ? e.getCause() : e);
+          assertEquals(ee.getMessage(), e.getMessage());
+          assertEquals(ee.getSQLState(), e.getSQLState());
+          assertEquals(ee.getErrorCode(), e.getErrorCode());
+          assertTrue(ee.getMessage().contains("In progress transaction was lost"));
+        }
+      }
+    }
+    stmt.execute("TRUNCATE transaction_failover_2");
+    stmt.executeUpdate("INSERT INTO transaction_failover_2 (test) VALUES ('test0')");
+    con.setAutoCommit(false);
+    stmt.executeUpdate("INSERT INTO transaction_failover_2 (test) VALUES ('test1')");
+    try (PreparedStatement p =
+        con.prepareStatement("INSERT INTO transaction_failover_2 (test)  VALUES (?)")) {
+
+      proxy.restart(300);
+      p.setString(1, "test2");
+      p.addBatch();
+      p.setString(1, "test3");
+      p.addBatch();
+      p.setString(1, "test4");
+      p.addBatch();
+      p.setString(1, "test5");
+      p.addBatch();
+
+      if (transactionReplay) {
+        p.executeBatch();
+        con.commit();
+
+        ResultSet rs = stmt.executeQuery("SELECT * FROM transaction_failover_2");
+        for (int i = 0; i < 5; i++) {
+          assertTrue(rs.next());
+          assertEquals("test" + i, rs.getString("test"));
+        }
+        con.commit();
+        Assertions.assertTrue(con.getContext().getThreadId() != threadId);
+        assertFalse(con.getAutoCommit());
+      } else {
+        try {
+          p.executeBatch();
+          Assertions.fail();
+        } catch (SQLException e) {
+          Throwable ee = (e instanceof BatchUpdateException) ? e.getCause() : e;
+          assertTrue(ee.getMessage().contains("In progress transaction was lost"));
+        }
+      }
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/FunctionTest.java b/src/test/java/org/mariadb/jdbc/integration/FunctionTest.java
new file mode 100644
index 0000000..b10b3f8
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/FunctionTest.java
@@ -0,0 +1,176 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.*;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.CallableParameterMetaData;
+import org.mariadb.jdbc.Connection;
+import org.mariadb.jdbc.Statement;
+
+public class FunctionTest extends Common {
+
+  @Test
+  public void basicFunction() throws SQLException {
+    // disabled for mysql : see https://bugs.mysql.com/bug.php?id=108545
+    Assumptions.assumeTrue(isMariaDBServer());
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP FUNCTION IF EXISTS basic_function");
+    stmt.execute(
+        "CREATE FUNCTION basic_function (t1 INT, t2 INT unsigned) RETURNS INT DETERMINISTIC RETURN"
+            + " t1 * t2;");
+    try (CallableStatement callableStatement =
+        sharedConn.prepareCall("{? = call basic_function(?,?)}")) {
+      callableStatement.registerOutParameter(1, JDBCType.INTEGER);
+      callableStatement.setInt(2, 2);
+      callableStatement.setInt(3, 3);
+      callableStatement.execute();
+
+      assertEquals(6, callableStatement.getInt(1));
+
+      callableStatement.clearParameters();
+      callableStatement.setInt(2, 3);
+      callableStatement.setInt(3, 3);
+      callableStatement.execute();
+      assertEquals(9, callableStatement.getInt(1));
+
+      callableStatement.clearParameters();
+      assertThrowsContains(
+          SQLTransientConnectionException.class,
+          () -> callableStatement.execute(),
+          "Parameter at position 1 is not set");
+    }
+
+    try (CallableStatement callableStatement =
+        sharedConn.prepareCall("{? = call basic_function(?,?)}")) {
+      callableStatement.setInt(2, 2);
+      callableStatement.setInt(3, 3);
+      callableStatement.execute();
+      assertEquals(6, callableStatement.getInt(1));
+    }
+  }
+
+  @Test
+  public void functionWithoutArg() throws SQLException {
+    functionWithoutArg(sharedConn);
+    functionWithoutArg(sharedConnBinary);
+  }
+
+  private void functionWithoutArg(Connection con) throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("DROP FUNCTION IF EXISTS no_arg_function");
+    stmt.execute("CREATE FUNCTION no_arg_function () RETURNS DOUBLE DETERMINISTIC RETURN RAND();");
+    try (CallableStatement callableStatement = con.prepareCall("{? = call no_arg_function()}")) {
+      callableStatement.registerOutParameter(1, JDBCType.DOUBLE);
+      callableStatement.execute();
+      callableStatement.getDouble(1);
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> callableStatement.registerOutParameter(2, JDBCType.DOUBLE),
+          " wrong parameter index 2");
+    }
+
+    try (CallableStatement callableStatement = con.prepareCall("{? = call no_arg_function()}")) {
+      callableStatement.execute();
+      callableStatement.getDouble(1);
+    }
+
+    try (CallableStatement callableStatement = con.prepareCall("{? = call no_arg_function}")) {
+      callableStatement.execute();
+      callableStatement.getDouble(1);
+    }
+  }
+
+  @Test
+  public void parameterMeta() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP FUNCTION IF EXISTS parameter_meta");
+    stmt.execute("CREATE FUNCTION parameter_meta () RETURNS DOUBLE DETERMINISTIC RETURN RAND();");
+    try (CallableStatement callableStatement =
+        sharedConn.prepareCall("{? = call parameter_meta()}")) {
+      Common.assertThrowsContains(
+          SQLSyntaxErrorException.class,
+          () -> callableStatement.registerOutParameter(-1, JDBCType.DOUBLE),
+          "wrong parameter index");
+      callableStatement.registerOutParameter(1, JDBCType.DOUBLE);
+
+      // XPAND doesn't support I_S.parameters: https://jira.mariadb.org/browse/XPT-267
+      if (!isXpand()) {
+        CallableParameterMetaData meta =
+            (CallableParameterMetaData) callableStatement.getParameterMetaData();
+        assertEquals(org.mariadb.jdbc.ParameterMetaData.parameterModeOut, meta.getParameterMode(1));
+        assertNull(meta.getParameterName(1));
+      }
+    } finally {
+      stmt.execute("DROP FUNCTION IF EXISTS parameter_meta");
+    }
+  }
+
+  @Test
+  public void functionError() throws SQLException {
+    functionError(sharedConn);
+    functionError(sharedConnBinary);
+  }
+
+  private void functionError(Connection con) throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("DROP FUNCTION IF EXISTS no_arg_function");
+    stmt.execute("CREATE FUNCTION no_arg_function () RETURNS DOUBLE DETERMINISTIC RETURN RAND();");
+    try (CallableStatement callableStatement = con.prepareCall("{? = call no_arg_function()}")) {
+      Common.assertThrowsContains(
+          SQLSyntaxErrorException.class,
+          () -> callableStatement.registerOutParameter(-1, JDBCType.DOUBLE),
+          "wrong parameter index");
+      callableStatement.registerOutParameter(1, JDBCType.DOUBLE);
+
+      Common.assertThrowsContains(
+          SQLSyntaxErrorException.class,
+          () -> callableStatement.registerOutParameter(-1, JDBCType.DOUBLE),
+          "wrong parameter index");
+      callableStatement.registerOutParameter(1, JDBCType.DOUBLE);
+      Common.assertThrowsContains(
+          SQLSyntaxErrorException.class,
+          () -> callableStatement.registerOutParameter(2, JDBCType.DOUBLE),
+          "wrong parameter index");
+
+      callableStatement.execute();
+      Common.assertThrowsContains(
+          SQLSyntaxErrorException.class,
+          () -> callableStatement.registerOutParameter(-1, JDBCType.DOUBLE),
+          "wrong parameter index");
+      callableStatement.registerOutParameter(1, JDBCType.DOUBLE);
+      if (!isXpand()) {
+        Common.assertThrowsContains(
+            SQLSyntaxErrorException.class,
+            () -> callableStatement.registerOutParameter("r", JDBCType.DOUBLE),
+            "parameter name r not found");
+      }
+      Common.assertThrowsContains(
+          SQLSyntaxErrorException.class,
+          () -> callableStatement.registerOutParameter(2, JDBCType.DOUBLE),
+          "wrong parameter index");
+    }
+  }
+
+  @Test
+  public void functionToString() throws SQLException {
+    try (CallableStatement callableStatement =
+        sharedConn.prepareCall("{? = call basic_function(?,?)}")) {
+
+      assertEquals(
+          "FunctionStatement{sql:'SELECT basic_function(?,?)', parameters:[<OUT>null]}",
+          callableStatement.toString());
+      callableStatement.setLong(2, 10L);
+      callableStatement.setBytes(3, new byte[] {(byte) 'a', (byte) 'b'});
+      assertEquals(
+          "FunctionStatement{sql:'SELECT basic_function(?,?)', parameters:[<OUT>null,10,_binary"
+              + " 'ab']}",
+          callableStatement.toString());
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/GaleraTest.java b/src/test/java/org/mariadb/jdbc/integration/GaleraTest.java
new file mode 100644
index 0000000..e55ae09
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/GaleraTest.java
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Connection;
+
+public class GaleraTest extends Common {
+
+  @Test
+  public void galeraAllowedStatesTest() throws SQLException {
+    if ("galera".equals(System.getenv("srv"))) {
+      try (Connection con = createCon("galeraAllowedState=4,5,6")) {
+        con.isValid(1);
+      }
+    } else {
+      try {
+        createCon("galeraAllowedState=4,5,6");
+        Assertions.fail();
+      } catch (SQLException e) {
+        Assertions.assertTrue(e.getMessage().contains("Initialization command fail"));
+        Assertions.assertTrue(
+            e.getCause()
+                .getMessage()
+                .contains("fail to validate Galera state (unknown 'wsrep_local_state' state)"));
+      }
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/GssapiAuthenticationTest.java b/src/test/java/org/mariadb/jdbc/integration/GssapiAuthenticationTest.java
new file mode 100644
index 0000000..d5df69d
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/GssapiAuthenticationTest.java
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import java.sql.*;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+
+public class GssapiAuthenticationTest extends Common {
+
+  @Test
+  public void nativePassword() throws Exception {
+    Assumptions.assumeTrue(isWindows());
+    Statement stmt = sharedConn.createStatement();
+    try {
+      stmt.execute("INSTALL SONAME 'auth_gssapi'");
+    } catch (SQLException e) {
+      // eat
+    }
+    System.out.println("user name:" + System.getProperty("user.name"));
+    stmt.execute("CREATE USER " + System.getProperty("user.name") + " IDENTIFIED VIA gssapi");
+    stmt.execute("GRANT ALL PRIVILEGES ON *.* TO " + System.getProperty("user.name"));
+
+    String gssapiUrl = String.format("jdbc:mariadb://%s:%s/%s", hostname, port, database);
+    try (Connection con = DriverManager.getConnection(gssapiUrl)) {
+      con.createStatement().execute("SELECT 1");
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/LocalInfileTest.java b/src/test/java/org/mariadb/jdbc/integration/LocalInfileTest.java
new file mode 100644
index 0000000..13fa4c6
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/LocalInfileTest.java
@@ -0,0 +1,463 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.sql.*;
+import java.util.Locale;
+import org.junit.jupiter.api.*;
+
+public class LocalInfileTest extends Common {
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE LocalInfileInputStreamTest(id int, test varchar(100))");
+    stmt.execute("CREATE TABLE LocalInfileInputStreamTest2(id int, test varchar(100))");
+    stmt.execute("CREATE TABLE ttlocal(id int, test varchar(100))");
+    stmt.execute("CREATE TABLE ldinfile(a varchar(10))");
+    stmt.execute(
+        "CREATE TABLE `infile`(`a` varchar(50) DEFAULT NULL, `b` varchar(50) DEFAULT NULL)"
+            + " ENGINE=InnoDB DEFAULT CHARSET=latin1");
+    stmt.execute(
+        "CREATE TABLE big_load_data_infile(id int not null primary key auto_increment, name"
+            + " char(20)) ENGINE=myisam");
+    stmt.execute("FLUSH TABLES");
+  }
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS AllowMultiQueriesTest");
+    stmt.execute("DROP TABLE IF EXISTS LocalInfileInputStreamTest");
+    stmt.execute("DROP TABLE IF EXISTS LocalInfileInputStreamTest2");
+    stmt.execute("DROP TABLE IF EXISTS ttlocal");
+    stmt.execute("DROP TABLE IF EXISTS ldinfile");
+    stmt.execute("DROP TABLE IF EXISTS `infile`");
+    stmt.execute("DROP TABLE IF EXISTS big_load_data_infile");
+  }
+
+  private static boolean checkLocal() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT @@local_infile");
+    if (rs.next()) {
+      return rs.getInt(1) == 1;
+    }
+    return false;
+  }
+
+  @Test
+  public void bigLoadDataInfileTest() throws SQLException, IOException {
+    Assumptions.assumeTrue(runLongTest());
+    try (VeryLongAutoGeneratedInputStream in = new VeryLongAutoGeneratedInputStream(5000000)) {
+      try (Connection connection = createCon("&allowLocalInfile=true")) {
+        Statement statement = connection.createStatement();
+        org.mariadb.jdbc.Statement mariaDbStatement =
+            statement.unwrap(org.mariadb.jdbc.Statement.class);
+        mariaDbStatement.setLocalInfileInputStream(in);
+
+        String sql =
+            "LOAD DATA LOCAL INFILE 'dummyFileName'"
+                + " INTO TABLE big_load_data_infile "
+                + " FIELDS TERMINATED BY '\\t' ENCLOSED BY ''"
+                + " ESCAPED BY '\\\\' LINES TERMINATED BY '\\n'";
+
+        assertFalse(statement.execute(sql));
+        ResultSet rs = statement.executeQuery("select count(*) from big_load_data_infile");
+        assertTrue(rs.next());
+        assertEquals(in.numberOfRows, rs.getInt(1));
+      }
+    }
+  }
+
+  /** Custom memory conserving generator of a LOAD DATA INFILE that generates a stream. */
+  private static class VeryLongAutoGeneratedInputStream extends InputStream {
+
+    private final int numberOfRows;
+    private int currentPosInBuffer;
+    private byte[] buffer;
+    private int currentRow;
+
+    private VeryLongAutoGeneratedInputStream(int numberOfRows) {
+      this.numberOfRows = numberOfRows;
+      currentRow = 0;
+    }
+
+    @Override
+    public int read() {
+      if (currentRow > numberOfRows) {
+        return -1;
+      }
+      if (buffer != null && currentPosInBuffer >= buffer.length) {
+        buffer = null;
+      }
+      if (buffer == null) {
+        currentRow++;
+        currentPosInBuffer = 0;
+        buffer = (currentRow + "\tname" + currentRow + "\n").getBytes();
+      }
+      return buffer[currentPosInBuffer++];
+    }
+  }
+
+  @Test
+  public void streamInBatch() throws SQLException, IOException {
+    Assumptions.assumeFalse((!isMariaDBServer() && minVersion(8, 0, 3)));
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+    String batch_update =
+        "LOAD DATA LOCAL INFILE 'dummy.tsv' INTO TABLE LocalInfileInputStreamTest2 (id, test)";
+    String builder = "1\thello\n2\tworld\n";
+
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("TRUNCATE LocalInfileInputStreamTest2");
+    InputStream inputStream = new ByteArrayInputStream(builder.getBytes());
+    ((org.mariadb.jdbc.Statement) stmt).setLocalInfileInputStream(inputStream);
+    stmt.addBatch(batch_update);
+    stmt.addBatch("SET UNIQUE_CHECKS=1");
+    stmt.executeBatch();
+    stmt.addBatch(batch_update);
+    try {
+      stmt.executeBatch();
+    } catch (SQLException e) {
+      assertTrue(e.getCause().getCause() instanceof FileNotFoundException);
+    }
+
+    try (PreparedStatement prep =
+        sharedConn.prepareStatement(
+            "LOAD DATA LOCAL INFILE 'dummy.tsv' INTO TABLE LocalInfileInputStreamTest2 (id,"
+                + " test)")) {
+      inputStream = new ByteArrayInputStream(builder.getBytes());
+      ((org.mariadb.jdbc.Statement) prep).setLocalInfileInputStream(inputStream);
+      prep.addBatch();
+      prep.executeBatch();
+      try {
+        prep.addBatch();
+        prep.executeBatch();
+      } catch (SQLException e) {
+        assertTrue(e.getCause().getCause() instanceof FileNotFoundException);
+      }
+    }
+    try (PreparedStatement prep =
+        sharedConnBinary.prepareStatement(
+            "LOAD DATA LOCAL INFILE 'dummy.tsv' INTO TABLE LocalInfileInputStreamTest2 (id,"
+                + " test)")) {
+      inputStream = new ByteArrayInputStream(builder.getBytes());
+      ((org.mariadb.jdbc.Statement) prep).setLocalInfileInputStream(inputStream);
+      prep.addBatch();
+      prep.executeBatch();
+      try {
+        prep.addBatch();
+        prep.executeBatch();
+      } catch (SQLException e) {
+        assertTrue(e.getCause().getCause() instanceof FileNotFoundException);
+      }
+    }
+  }
+
+  @Test
+  public void throwExceptions() throws Exception {
+    Assumptions.assumeTrue(
+        (isMariaDBServer() || !minVersion(8, 0, 3))
+            && !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv")));
+
+    // https://jira.mariadb.org/browse/XPT-270
+    Assumptions.assumeFalse(isXpand());
+
+    try (Connection con = createCon("&allowLocalInfile=false")) {
+      Statement stmt = con.createStatement();
+      stmt.execute("TRUNCATE LocalInfileInputStreamTest2");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () ->
+              stmt.execute(
+                  "LOAD DATA LOCAL INFILE 'someFile' INTO TABLE LocalInfileInputStreamTest2 (id,"
+                      + " test)"),
+          "Local infile is disabled by connector. Enable `allowLocalInfile` to allow local infile"
+              + " commands");
+      stmt.addBatch(
+          "LOAD DATA LOCAL INFILE 'someFile' INTO TABLE LocalInfileInputStreamTest2 (id, test)");
+      stmt.addBatch("SET UNIQUE_CHECKS=1");
+
+      try {
+        stmt.executeBatch();
+        fail();
+      } catch (SQLException e) {
+        assertEquals(e.getClass(), BatchUpdateException.class);
+        assertTrue(
+            e.getMessage()
+                .contains(
+                    "Local infile is disabled by connector. Enable `allowLocalInfile` to allow"
+                        + " local infile commands"));
+        assertNotNull(e.getCause());
+        assertEquals(e.getCause().getMessage(), e.getMessage());
+        assertEquals(((SQLException) e.getCause()).getSQLState(), e.getSQLState());
+        assertEquals(((SQLException) e.getCause()).getErrorCode(), e.getErrorCode());
+      }
+
+      try (PreparedStatement prep =
+          con.prepareStatement(
+              "LOAD DATA LOCAL INFILE ? INTO TABLE LocalInfileInputStreamTest2 (id, test)")) {
+        prep.setString(1, "someFile");
+        Common.assertThrowsContains(
+            SQLException.class,
+            prep::execute,
+            "Local infile is disabled by connector. Enable `allowLocalInfile` to allow local infile"
+                + " commands");
+      }
+    }
+  }
+
+  @Test
+  public void wrongFile() throws Exception {
+    Assumptions.assumeTrue(checkLocal());
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+
+    try (Connection con = createCon("allowLocalInfile")) {
+      Statement stmt = con.createStatement();
+      Common.assertThrowsContains(
+          SQLException.class,
+          () ->
+              stmt.execute(
+                  "LOAD DATA LOCAL INFILE 'someFile' INTO TABLE LocalInfileInputStreamTest2 (id,"
+                      + " test)"),
+          "Could not send file : someFile");
+      assertTrue(con.isValid(1));
+    }
+  }
+
+  @Test
+  public void unReadableFile() throws Exception {
+    Assumptions.assumeTrue(checkLocal());
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv"))
+            && !System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win"));
+
+    try (Connection con = createCon("allowLocalInfile")) {
+      File tempFile = File.createTempFile("hello", ".tmp");
+      tempFile.deleteOnExit();
+      tempFile.setReadable(false);
+      Statement stmt = con.createStatement();
+      Common.assertThrowsContains(
+          SQLException.class,
+          () ->
+              stmt.execute(
+                  "LOAD DATA LOCAL INFILE '"
+                      + tempFile.getCanonicalPath().replace("\\", "/")
+                      + "' INTO TABLE LocalInfileInputStreamTest2 (id, test)"),
+          "Could not send file");
+      assertTrue(con.isValid(1));
+    }
+  }
+
+  @Test
+  public void loadDataBasic() throws Exception {
+    Assumptions.assumeTrue(checkLocal());
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+    File temp = File.createTempFile("dummyloadDataBasic", ".txt");
+    try (BufferedWriter bw = new BufferedWriter(new FileWriter(temp))) {
+      bw.write("1\thello2\n2\tworld\n");
+    }
+
+    try (Connection con = createCon("allowLocalInfile")) {
+      Statement stmt = con.createStatement();
+      stmt.execute("TRUNCATE LocalInfileInputStreamTest2");
+      stmt.execute(
+          "LOAD DATA LOCAL INFILE '"
+              + temp.getCanonicalPath().replace("\\", "/")
+              + "' INTO TABLE LocalInfileInputStreamTest2 (id, test)");
+      ResultSet rs = stmt.executeQuery("SELECT * FROM LocalInfileInputStreamTest2");
+      assertTrue(rs.next());
+      assertEquals(1, rs.getInt(1));
+      assertEquals("hello2", rs.getString(2));
+      assertTrue(rs.next());
+      assertEquals(2, rs.getInt(1));
+      assertEquals("world", rs.getString(2));
+      while (rs.next()) {
+        System.out.println(rs.getString(2));
+      }
+      assertFalse(rs.next());
+
+      stmt.execute("TRUNCATE LocalInfileInputStreamTest2");
+      stmt.addBatch(
+          "LOAD DATA LOCAL INFILE '"
+              + temp.getCanonicalPath().replace("\\", "/")
+              + "' INTO TABLE LocalInfileInputStreamTest2 (id, test)");
+      stmt.addBatch("SET UNIQUE_CHECKS=1");
+      stmt.executeBatch();
+
+      rs = stmt.executeQuery("SELECT * FROM LocalInfileInputStreamTest2");
+      assertTrue(rs.next());
+      assertEquals(1, rs.getInt(1));
+      assertEquals("hello2", rs.getString(2));
+      assertTrue(rs.next());
+      assertEquals(2, rs.getInt(1));
+      assertEquals("world", rs.getString(2));
+      assertFalse(rs.next());
+    } finally {
+      temp.delete();
+    }
+  }
+
+  @Test
+  public void loadDataValidationFails() throws Exception {
+    Assumptions.assumeTrue(checkLocal());
+    loadDataValidationFails(false);
+    loadDataValidationFails(true);
+  }
+
+  public void loadDataValidationFails(boolean prepStmt) throws Exception {
+    File temp = File.createTempFile("dummy", ".txt");
+    File tempXml = File.createTempFile("xmldummy", ".txt");
+
+    try (Connection con = createCon("&allowLocalInfile&useServerPrepStmts=" + prepStmt)) {
+      try (BufferedWriter bw = new BufferedWriter(new FileWriter(temp))) {
+        bw.write("1\thello\n2\tworld\n");
+      }
+      try (BufferedWriter bw = new BufferedWriter(new FileWriter(tempXml))) {
+        bw.write("<row id=\"1\" test=\"hello\" />\n<row id=\"2\" test=\"world\" />\n");
+      }
+      try (PreparedStatement prep =
+          con.prepareStatement(
+              "LOAD DATA LOCAL INFILE ? INTO TABLE LocalInfileInputStreamTest2 (id, test)")) {
+        prep.setString(1, temp.getCanonicalPath().replace("\\", "/"));
+        prep.execute();
+      }
+      try (PreparedStatement prep =
+          con.prepareStatement(
+              "LOAD XML LOCAL INFILE ? INTO TABLE LocalInfileInputStreamTest2 (id, test)")) {
+        prep.setString(1, tempXml.getCanonicalPath().replace("\\", "/"));
+        prep.execute();
+      }
+      try (PreparedStatement prep =
+          con.prepareStatement(
+              "/* test */ LOAD  DATA LOCAL INFILE 'j' INTO TABLE LocalInfileInputStreamTest2 (id,"
+                  + " test)")) {
+        assertThrowsContains(SQLException.class, () -> prep.execute(), "Could not send file : j");
+      }
+      // special test comment inside LOAD DATA LOCAL are not checked, resulting in error
+      try (PreparedStatement prep =
+          con.prepareStatement(
+              "LOAD /**g*/ DATA LOCAL INFILE 'h' INTO TABLE LocalInfileInputStreamTest2 (id,"
+                  + " test)")) {
+        assertThrowsContains(
+            SQLException.class,
+            () -> prep.execute(),
+            "LOAD DATA LOCAL INFILE asked for file 'h' that doesn't correspond to initial query ");
+      }
+      // ensure connection state after errors
+      ResultSet rs = con.createStatement().executeQuery("SELECT 1");
+      rs.next();
+      assertEquals(1, rs.getInt(1));
+    } finally {
+      temp.delete();
+      tempXml.delete();
+    }
+  }
+
+  @Test
+  public void loadDataInfileEmpty() throws SQLException, IOException {
+    Assumptions.assumeTrue(
+        (isMariaDBServer() || !minVersion(8, 0, 3))
+            && !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv")));
+    // Create temp file.
+    File temp = File.createTempFile("validateInfile", ".tmp");
+    try (Connection connection = createCon("&allowLocalInfile=true")) {
+      Statement st = connection.createStatement();
+      st.execute(
+          "LOAD DATA LOCAL INFILE '"
+              + temp.getAbsolutePath().replace('\\', '/')
+              + "' INTO TABLE ldinfile");
+      try (ResultSet rs = st.executeQuery("SELECT * FROM ldinfile")) {
+        assertFalse(rs.next());
+      }
+    } finally {
+      temp.delete();
+    }
+  }
+
+  private File createTmpData(long recordNumber) throws Exception {
+    File file = File.createTempFile("infile" + recordNumber, ".tmp");
+
+    // write it
+    try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
+      // Every row is 8 bytes to make counting easier
+      for (long i = 0; i < recordNumber; i++) {
+        writer.write("\"a\",\"b\"");
+        writer.write("\n");
+      }
+    }
+
+    return file;
+  }
+
+  private void checkBigLocalInfile(long fileSize) throws Exception {
+    long recordNumber = fileSize / 8;
+    try (Connection connection = createCon("allowLocalInfile")) {
+      Statement stmt = connection.createStatement();
+      stmt.execute("truncate `infile`");
+      File file = createTmpData(recordNumber);
+      int insertNumber =
+          stmt.executeUpdate(
+              "LOAD DATA LOCAL INFILE '"
+                  + file.getCanonicalPath().replace("\\", "/")
+                  + "' "
+                  + "INTO TABLE `infile` "
+                  + "COLUMNS TERMINATED BY ',' ENCLOSED BY '\\\"' ESCAPED BY '\\\\' "
+                  + "LINES TERMINATED BY '\\n' (`a`, `b`)");
+      assertEquals(insertNumber, recordNumber);
+      file.delete();
+      stmt.setFetchSize(1000); // to avoid using too much memory for tests
+      try (ResultSet rs = stmt.executeQuery("SELECT * FROM `infile`")) {
+        for (int i = 0; i < recordNumber; i++) {
+          assertTrue(rs.next());
+          assertEquals("a", rs.getString(1));
+          assertEquals("b", rs.getString(2));
+        }
+        assertFalse(rs.next());
+      }
+    }
+  }
+
+  @Test
+  public void testSmallBigLocalInfileInputStream() throws Exception {
+    Assumptions.assumeTrue(
+        (isMariaDBServer() || !minVersion(8, 0, 3))
+            && !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv")));
+    checkBigLocalInfile(256);
+  }
+
+  @Test
+  public void test2xBigLocalInfileInputStream() throws Exception {
+    Assumptions.assumeTrue(
+        ((isMariaDBServer() || !minVersion(8, 0, 3)) && runLongTest())
+            && !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv")));
+    checkBigLocalInfile(16777216 * 2);
+  }
+
+  @Test
+  public void testMoreThanMaxAllowedPacketLocalInfileInputStream() throws Exception {
+    Assumptions.assumeTrue(
+        (isMariaDBServer() || !minVersion(8, 0, 3))
+            && !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv")));
+    Assumptions.assumeTrue(runLongTest());
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("select @@max_allowed_packet");
+    assertTrue(rs.next());
+    long maxAllowedPacket = rs.getLong(1);
+    Assumptions.assumeTrue(maxAllowedPacket < 100_000_000);
+    checkBigLocalInfile(maxAllowedPacket + 1024);
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/LoggingTest.java b/src/test/java/org/mariadb/jdbc/integration/LoggingTest.java
new file mode 100644
index 0000000..6e333cc
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/LoggingTest.java
@@ -0,0 +1,236 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.FileAppender;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.SSLException;
+import javax.security.auth.x500.X500Principal;
+import javax.sql.PooledConnection;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Connection;
+import org.mariadb.jdbc.MariaDbPoolDataSource;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.tls.HostnameVerifier;
+import org.slf4j.LoggerFactory;
+
+public class LoggingTest extends Common {
+
+  @Test
+  void basicLogging() throws Exception {
+    Assumptions.assumeTrue(isMariaDBServer());
+    File tempFile = File.createTempFile("log", ".tmp");
+
+    Logger logger = (Logger) LoggerFactory.getLogger("org.mariadb.jdbc");
+    Level initialLevel = logger.getLevel();
+    logger.setLevel(Level.TRACE);
+    logger.setAdditive(false);
+    logger.detachAndStopAllAppenders();
+
+    LoggerContext context = new LoggerContext();
+    FileAppender<ILoggingEvent> fa = new FileAppender<>();
+    fa.setName("FILE");
+    fa.setImmediateFlush(true);
+    PatternLayoutEncoder pa = new PatternLayoutEncoder();
+    pa.setPattern("%r %5p %c [%t] - %m%n");
+    pa.setContext(context);
+    pa.start();
+    fa.setEncoder(pa);
+
+    fa.setFile(tempFile.getPath());
+    fa.setAppend(true);
+    fa.setContext(context);
+    fa.start();
+
+    logger.addAppender(fa);
+
+    try (Connection conn = createCon()) {
+      Statement stmt = conn.createStatement();
+      stmt.execute("SELECT 1");
+    }
+    try (Connection conn = createCon("useCompression=true")) {
+      Statement stmt = conn.createStatement();
+      stmt.execute("SELECT 1");
+    }
+
+    MariaDbPoolDataSource ds =
+        new MariaDbPoolDataSource(
+            mDefUrl + "&sessionVariables=wait_timeout=1&maxIdleTime=2&testMinRemovalDelay=2");
+    Thread.sleep(4000);
+    PooledConnection pc = ds.getPooledConnection();
+    pc.getConnection().isValid(1);
+    pc.close();
+    ds.close();
+    try {
+      String contents = new String(Files.readAllBytes(Paths.get(tempFile.getPath())));
+      String selectOne =
+          "       +--------------------------------------------------+\n"
+              + "       |  0  1  2  3  4  5  6  7   8  9  a  b  c  d  e  f |\n"
+              + "+------+--------------------------------------------------+------------------+\n"
+              + "|000000| 09 00 00 00 03 53 45 4C  45 43 54 20 31          | .....SELECT 1    |\n"
+              + "+------+--------------------------------------------------+------------------+\n";
+      Assertions.assertTrue(
+          contents.contains(selectOne) || contents.contains(selectOne.replace("\r\n", "\n")),
+          contents);
+      String rowResult =
+          "       +--------------------------------------------------+\n"
+              + "       |  0  1  2  3  4  5  6  7   8  9  a  b  c  d  e  f |\n"
+              + "+------+--------------------------------------------------+------------------+\n"
+              + "|000000| 02 00 00 03 01 31                                | .....1           |\n"
+              + "+------+--------------------------------------------------+------------------+\n";
+      String rowResultWithEof =
+          "       +--------------------------------------------------+\n"
+              + "       |  0  1  2  3  4  5  6  7   8  9  a  b  c  d  e  f |\n"
+              + "+------+--------------------------------------------------+------------------+\n"
+              + "|000000| 02 00 00 04 01 31                                | .....1           |\n"
+              + "+------+--------------------------------------------------+------------------+\n";
+      Assertions.assertTrue(
+          contents.contains(rowResult)
+              || contents.contains(rowResult.replace("\r\n", "\n"))
+              || contents.contains(rowResultWithEof)
+              || contents.contains(rowResultWithEof.replace("\r\n", "\n")),
+          contents);
+
+      Assertions.assertTrue(
+          contents.contains("pool MariaDB-pool new physical connection ")
+              && contents.contains("created (total:1, active:0, pending:0)"),
+          contents);
+      Assertions.assertTrue(
+          contents.contains("pool MariaDB-pool connection ")
+              && contents.contains("removed due to inactivity"),
+          contents);
+    } catch (IOException e) {
+      e.printStackTrace();
+      Assertions.fail();
+    } finally {
+      logger.setLevel(initialLevel);
+      logger.detachAppender(fa);
+    }
+  }
+
+  @Test
+  void certLogging() throws Exception {
+    File tempFile = File.createTempFile("log", ".tmp");
+
+    Logger logger = (Logger) LoggerFactory.getLogger("org.mariadb.jdbc");
+    Level initialLevel = logger.getLevel();
+    logger.setLevel(Level.TRACE);
+    logger.setAdditive(false);
+    logger.detachAndStopAllAppenders();
+
+    LoggerContext context = new LoggerContext();
+    FileAppender<ILoggingEvent> fa = new FileAppender<>();
+    fa.setName("FILE");
+    fa.setImmediateFlush(true);
+    PatternLayoutEncoder pa = new PatternLayoutEncoder();
+    pa.setPattern("%r %5p %c [%t] - %m%n");
+    pa.setContext(context);
+    pa.start();
+    fa.setEncoder(pa);
+
+    fa.setFile(tempFile.getPath());
+
+    fa.setAppend(true);
+    fa.setContext(context);
+    fa.start();
+
+    logger.addAppender(fa);
+
+    String certString =
+        ""
+            + "-----BEGIN CERTIFICATE-----\n"
+            + "MIIDfDCCAmSgAwIBAgIURZJQVOWv+oaj+MLlHWc1B0TnOaowDQYJKoZIhvcNAQEL\n"
+            + "BQAwUjELMAkGA1UEBhMCQ04xCzAJBgNVBAgMAkdEMQswCQYDVQQHDAJTWjESMBAG\n"
+            + "A1UECgwJQWNtZSxJbmMuMRUwEwYDVQQDDAxBY21lIFJvb3QgQ0EwIBcNMjEwMzMw\n"
+            + "MDkwODAxWhgPMjEyMTAzMDYwOTA4MDFaMFMxCzAJBgNVBAYTAkNOMQswCQYDVQQI\n"
+            + "DAJHRDELMAkGA1UEBwwCU1oxEjAQBgNVBAoMCUFjbWUsSW5jLjEWMBQGA1UEAwwN\n"
+            + "Ki5tYXJpYWRiLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAJ\n"
+            + "xqbqTGmwO5n3kVd6QJPRSh+0M1HIQacyM/tkE7jLw3725/KtknuwuFbPpxKyTCLC\n"
+            + "IoNx4yaBbmx783OPP3pokXTWiMdrVZdLltBNamNzekNFN4YhR5oN479M5cKgrk94\n"
+            + "Ud+ql0NN5FscrSQ0fSdJf0idJMqThro1MJVp9rp5cdCba6/lKyDbdOybe5f7rmrg\n"
+            + "+37J+src67+rqwVT8ZwZgLTGDf4X9OSIzyw6+PCWYWr89aurrOuOyqA3QqXVRZa/\n"
+            + "IxOMHIdzXMgLN6+HduwdZ+DNv1NPT2MDlRQvOnDop3NoEVKWekOTv50LbKRgWTYO\n"
+            + "TK/dfcsDpZmdyHv7pb8CAwEAAaNHMEUwQwYDVR0RBDwwOoIVbG9jYWxob3N0Lmxv\n"
+            + "Y2FsZG9tYWlugglsb2NhbGhvc3SHBH8AAAGHECABDbg5AjRoAAAAAAAABEMwDQYJ\n"
+            + "KoZIhvcNAQELBQADggEBAHsiJz9cpmL8BTa/o10S+pmap3iOnYYuJT0llCRLJ+Ji\n"
+            + "msO2niyIwqCJHMLcEABCENJt0HDOEKlnunVgc+X/6K8DnPrYhfWQbYI/dwUBoSIQ\n"
+            + "siK/yKW0q+S+YjCVpNMA3iMfhJ9Qe9LDO+xdCBhzplgrV8YwG+J2FUNbZfvl5cML\n"
+            + "TjKLWrWo9dgZyH/7mjwryRzswfUfr/lRARCyrMotaXfYmjPjwTSRc0aPGrEjs3ns\n"
+            + "WMtimgh7Zw3Tbxc51miz9CRy767lq/9BGTdeBLmW0EXssIJb9uO0Ht3C/Pqy0ojk\n"
+            + "8e1eYtofjTsqWHZ1s2LhtT0HvXdL6BnWP9GWc/zxiKM=\n"
+            + "-----END CERTIFICATE-----\n";
+    CertificateFactory cf = CertificateFactory.getInstance("X.509");
+    X509Certificate cert =
+        (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certString.getBytes()));
+
+    assertEquals(
+        new X500Principal("CN=*.mariadb.org, O=\"Acme,Inc.\", L=SZ, ST=GD, C=CN"),
+        cert.getSubjectX500Principal());
+    HostnameVerifier.verify("localhost", cert, -1);
+    HostnameVerifier.verify("localhost.localdomain", cert, -1);
+    verifyExceptionEqual(
+        "local.host",
+        cert,
+        "DNS host \"local.host\" doesn't correspond to certificate CN \"*.mariadb.org\" and"
+            + " SAN[{DNS:\"localhost.localdomain\"},{DNS:\"localhost\"},{IP:\"127.0.0.1\"},{IP:\"2001:db8:3902:3468:0:0:0:443\"}]");
+
+    HostnameVerifier.verify("127.0.0.1", cert, -1);
+    verifyExceptionEqual(
+        "127.0.0.2",
+        cert,
+        "IPv4 host \"127.0.0.2\" doesn't correspond to certificate CN \"*.mariadb.org\" and"
+            + " SAN[{DNS:\"localhost.localdomain\"},{DNS:\"localhost\"},{IP:\"127.0.0.1\"},{IP:\"2001:db8:3902:3468:0:0:0:443\"}]");
+
+    HostnameVerifier.verify("2001:db8:3902:3468:0:0:0:443", cert, -1);
+    verifyExceptionEqual(
+        "2001:db8:1::",
+        cert,
+        "IPv6 host \"2001:db8:1::\" doesn't correspond to certificate CN \"*.mariadb.org\" and"
+            + " SAN[{DNS:\"localhost.localdomain\"},{DNS:\"localhost\"},{IP:\"127.0.0.1\"},{IP:\"2001:db8:3902:3468:0:0:0:443\"}]");
+    try {
+      String contents = new String(Files.readAllBytes(Paths.get(tempFile.getPath())));
+
+      assertTrue(
+          contents.contains(
+              "DNS verification of hostname : type=DNS value=localhost.localdomain to local.host"));
+      assertTrue(
+          contents.contains(
+              "IPv4 verification of hostname : type=IP value=127.0.0.1 to 127.0.0.2"));
+      assertTrue(
+          contents.contains(
+              "IPv6 verification of hostname : type=IP value=2001:db8:3902:3468:0:0:0:443 to"
+                  + " 2001:db8:1::"));
+
+      logger.setLevel(initialLevel);
+      logger.detachAppender(fa);
+    } catch (IOException e) {
+      e.printStackTrace();
+      Assertions.fail();
+    }
+  }
+
+  private void verifyExceptionEqual(String host, X509Certificate cert, String exceptionMessage) {
+    Exception e =
+        Assertions.assertThrows(SSLException.class, () -> HostnameVerifier.verify(host, cert, -1));
+    Assertions.assertTrue(
+        e.getMessage().contains(exceptionMessage), "real message:" + e.getMessage());
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/MultiHostTest.java b/src/test/java/org/mariadb/jdbc/integration/MultiHostTest.java
new file mode 100644
index 0000000..cda7d8e
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/MultiHostTest.java
@@ -0,0 +1,576 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.IOException;
+import java.sql.*;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.*;
+import org.mariadb.jdbc.Connection;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.export.SslMode;
+import org.mariadb.jdbc.integration.tools.TcpProxy;
+
+public class MultiHostTest extends Common {
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    createSequenceTables();
+  }
+
+  @Test
+  public void failoverReadonlyToMaster() throws Exception {
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+    try (Connection con = createProxyConKeep("&waitReconnectTimeout=300&deniedListTimeout=300")) {
+      long primaryThreadId = con.getThreadId();
+      con.setReadOnly(true);
+      long replicaThreadId = con.getThreadId();
+      assertTrue(primaryThreadId != replicaThreadId);
+
+      con.setReadOnly(false);
+      assertEquals(primaryThreadId, con.getThreadId());
+      con.setReadOnly(true);
+      assertEquals(replicaThreadId, con.getThreadId());
+      proxy.restart(250);
+
+      con.isValid(1);
+      assertEquals(primaryThreadId, con.getThreadId());
+    }
+  }
+
+  @Test
+  public void ensureReadOnlyOnReplica() throws Exception {
+    // mariadb1.example.com, mariadb2.example.com and mariadb3.example.com DNS alias must be defined
+    Assumptions.assumeTrue(
+        !isWindows()
+            && !"maxscale".equals(System.getenv("srv"))
+            && !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv"))
+            && !isXpand());
+
+    Configuration conf = Configuration.parse(mDefUrl);
+    HostAddress hostAddress = conf.addresses().get(0);
+    String url =
+        mDefUrl.replaceAll(
+            "//([^/]*)/",
+            String.format(
+                "//mariadb1.example.com:%s,mariadb2.example.com:%s,mariadb3.example.com:%s/",
+                hostAddress.port, hostAddress.port, hostAddress.port));
+    url = url.replaceAll("jdbc:mariadb:", "jdbc:mariadb:replication:");
+    if (conf.sslMode() == SslMode.VERIFY_FULL) {
+      url = url.replaceAll("sslMode=verify-full", "sslMode=verify-ca");
+    }
+    try {
+      int replica1 = 0;
+      int replica2 = 0;
+      for (int i = 0; i < 100; i++) {
+        try (Connection con =
+            (Connection)
+                DriverManager.getConnection(
+                    url + "&waitReconnectTimeout=30&deniedListTimeout=300")) {
+          assertTrue(con.__test_host().contains("primary"));
+          con.setReadOnly(true);
+          assertTrue(con.__test_host().contains("replica"));
+          if (con.__test_host().contains("mariadb2")) {
+            replica1++;
+          }
+          if (con.__test_host().contains("mariadb3")) {
+            replica2++;
+          }
+        }
+      }
+
+      assertTrue(
+          replica1 > 35, "value replica1/replicat2 aren't right : " + replica1 + "/" + replica2);
+      assertTrue(
+          replica2 > 35, "value replica1/replicat2 aren't right : " + replica1 + "/" + replica2);
+    } catch (SQLNonTransientConnectionException e) {
+      // eat
+    }
+  }
+
+  @Test
+  public void readOnly() throws SQLException {
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv"))
+            && !isXpand());
+    try (Connection con = createProxyConKeep("&waitReconnectTimeout=300&deniedListTimeout=300")) {
+      Statement stmt = con.createStatement();
+      stmt.execute("DROP TABLE IF EXISTS testReadOnly");
+      stmt.execute("CREATE TABLE testReadOnly(id int)");
+      con.setAutoCommit(false);
+      con.setReadOnly(true);
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> stmt.execute("INSERT INTO testReadOnly values (2)"),
+          "Cannot execute statement in a READ ONLY transaction");
+      con.setReadOnly(false);
+      stmt.execute("DROP TABLE testReadOnly");
+    }
+  }
+
+  @Test
+  public void syncState() throws Exception {
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+    try (Connection con = createProxyConKeep("")) {
+      Statement stmt = con.createStatement();
+      stmt.execute("CREATE DATABASE IF NOT EXISTS sync");
+      con.setCatalog("sync");
+      con.setTransactionIsolation(java.sql.Connection.TRANSACTION_SERIALIZABLE);
+      con.setReadOnly(true);
+      assertEquals("sync", con.getCatalog());
+      assertEquals(java.sql.Connection.TRANSACTION_SERIALIZABLE, con.getTransactionIsolation());
+      con.setReadOnly(true);
+      con.setReadOnly(false);
+      assertEquals(java.sql.Connection.TRANSACTION_SERIALIZABLE, con.getTransactionIsolation());
+      con.setTransactionIsolation(java.sql.Connection.TRANSACTION_READ_COMMITTED);
+      con.setReadOnly(true);
+      assertEquals(java.sql.Connection.TRANSACTION_READ_COMMITTED, con.getTransactionIsolation());
+      con.setTransactionIsolation(java.sql.Connection.TRANSACTION_READ_UNCOMMITTED);
+      con.setReadOnly(false);
+      assertEquals(java.sql.Connection.TRANSACTION_READ_UNCOMMITTED, con.getTransactionIsolation());
+      con.setTransactionIsolation(java.sql.Connection.TRANSACTION_REPEATABLE_READ);
+      con.setReadOnly(true);
+      assertEquals(java.sql.Connection.TRANSACTION_REPEATABLE_READ, con.getTransactionIsolation());
+    } finally {
+      sharedConn.createStatement().execute("DROP DATABASE IF EXISTS sync");
+    }
+  }
+
+  @Test
+  public void replicaNotSet() throws Exception {
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+
+    String url = mDefUrl.replaceAll("jdbc:mariadb:", "jdbc:mariadb:replication:");
+    try (java.sql.Connection con = DriverManager.getConnection(url + "&waitReconnectTimeout=20")) {
+      con.isValid(1);
+      con.setReadOnly(true);
+      con.isValid(1);
+      // force reconnection try
+      Thread.sleep(50);
+      con.isValid(1);
+    }
+  }
+
+  @Test
+  public void closedConnectionMulti() throws Exception {
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+
+    Configuration conf = Configuration.parse(mDefUrl);
+    HostAddress hostAddress = conf.addresses().get(0);
+    String url =
+        mDefUrl.replaceAll(
+            "//([^/]*)/",
+            String.format(
+                "//address=(host=localhost)(port=9999)(type=master),address=(host=%s)(port=%s)(type=master)/",
+                hostAddress.host, hostAddress.port));
+    url = url.replaceAll("jdbc:mariadb:", "jdbc:mariadb:sequential:");
+    if (conf.sslMode() == SslMode.VERIFY_FULL) {
+      url = url.replaceAll("sslMode=verify-full", "sslMode=verify-ca");
+    }
+
+    Connection con =
+        (Connection)
+            DriverManager.getConnection(
+                url
+                    + "&waitReconnectTimeout=300&deniedListTimeout=300&retriesAllDown=4&connectTimeout=500&useServerPrepStmts&cachePrepStmts=false");
+    testClosedConn(con);
+
+    url =
+        mDefUrl.replaceAll(
+            "//([^/]*)/",
+            String.format(
+                "//%s:%s,%s,%s/",
+                hostAddress.host, hostAddress.port, hostAddress.host, hostAddress.port));
+    url = url.replaceAll("jdbc:mariadb:", "jdbc:mariadb:replication:");
+    if (conf.sslMode() == SslMode.VERIFY_FULL) {
+      url = url.replaceAll("sslMode=verify-full", "sslMode=verify-ca");
+    }
+
+    con =
+        (Connection)
+            DriverManager.getConnection(
+                url
+                    + "&waitReconnectTimeout=300&deniedListTimeout=300&retriesAllDown=4&connectTimeout=500&useServerPrepStmts&cachePrepStmts=false");
+    testClosedConn(con);
+  }
+
+  private void testClosedConn(Connection con) throws SQLException {
+    PreparedStatement prep = con.prepareStatement("SELECT ?");
+    PreparedStatement prep2 = con.prepareStatement("SELECT 1, ?");
+    prep2.setString(1, "1");
+    prep2.execute();
+    Statement stmt = con.createStatement();
+    stmt.setFetchSize(1);
+    ResultSet rs = stmt.executeQuery("SELECT * FROM sequence_1_to_10");
+    rs.next();
+
+    con.close();
+
+    prep.setString(1, "1");
+    assertThrowsContains(SQLException.class, () -> prep.execute(), "Connection is closed");
+    assertThrowsContains(SQLException.class, () -> prep2.execute(), "Connection is closed");
+    assertThrowsContains(
+        SQLException.class, () -> rs.next(), "Error while streaming resultSet data");
+    assertThrowsContains(SQLException.class, () -> prep2.close(), "Connection is closed");
+    con.close();
+    assertThrowsContains(SQLException.class, () -> con.abort(null), "Connection is closed");
+    assertNotNull(con.getClient().getHostAddress());
+    assertThrowsContains(
+        SQLException.class,
+        () -> con.getClient().readStreamingResults(null, 0, 0, 0, 0, true),
+        "Connection is closed");
+    con.getClient().reset();
+  }
+
+  @Test
+  public void masterFailover() throws Exception {
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+
+    Configuration conf = Configuration.parse(mDefUrl);
+    HostAddress hostAddress = conf.addresses().get(0);
+    try {
+      proxy = new TcpProxy(hostAddress.host, hostAddress.port);
+    } catch (IOException i) {
+      throw new SQLException("proxy error", i);
+    }
+
+    String url =
+        mDefUrl.replaceAll(
+            "//([^/]*)/",
+            String.format(
+                "//address=(host=localhost)(port=9999)(type=master),address=(host=localhost)(port=%s)(type=master),address=(host=%s)(port=%s)(type=master)/",
+                proxy.getLocalPort(), hostAddress.host, hostAddress.port));
+    url = url.replaceAll("jdbc:mariadb:", "jdbc:mariadb:sequential:");
+    if (conf.sslMode() == SslMode.VERIFY_FULL) {
+      url = url.replaceAll("sslMode=verify-full", "sslMode=verify-ca");
+    }
+
+    try (Connection con =
+        (Connection)
+            DriverManager.getConnection(
+                url
+                    + "&deniedListTimeout=300&retriesAllDown=4&connectTimeout=20&deniedListTimeout=20")) {
+      Statement stmt = con.createStatement();
+      stmt.execute("SET @con=1");
+      proxy.restart(50);
+      con.isValid(1000);
+    }
+
+    Thread.sleep(100);
+    // same in transaction
+    try (Connection con =
+        (Connection)
+            DriverManager.getConnection(
+                url
+                    + "&waitReconnectTimeout=300&retriesAllDown=10&connectTimeout=20&deniedListTimeout=20")) {
+      Statement stmt = con.createStatement();
+      stmt.execute("START TRANSACTION");
+      stmt.execute("SET @con=1");
+
+      proxy.restart(100);
+      try {
+        ResultSet rs = stmt.executeQuery("SELECT @con");
+        if (rs.next()) {
+          System.out.println("Resultset res:" + rs.getString(1));
+        }
+        fail("must have thrown exception");
+      } catch (SQLTransientConnectionException e) {
+        assertTrue(e.getMessage().contains("In progress transaction was lost"));
+      }
+    }
+
+    Thread.sleep(50);
+    // testing blacklisted
+    try (Connection con =
+        (Connection)
+            DriverManager.getConnection(
+                url + "&retriesAllDown=4&connectTimeout=20&deniedListTimeout=20")) {
+      Statement stmt = con.createStatement();
+      con.setAutoCommit(false);
+      stmt.execute("START TRANSACTION");
+      stmt.execute("SET @con=1");
+
+      proxy.restart(50);
+      try {
+        ResultSet rs = stmt.executeQuery("SELECT @con");
+        rs.next();
+        assertEquals(1, rs.getInt(1));
+      } catch (SQLException e) {
+        assertTrue(e.getMessage().contains("In progress transaction was lost"));
+      }
+    }
+    Thread.sleep(50);
+    // with transaction replay
+    try (Connection con =
+        (Connection)
+            DriverManager.getConnection(
+                url
+                    + "&transactionReplay=true&waitReconnectTimeout=300&deniedListTimeout=300&retriesAllDown=4&connectTimeout=20")) {
+      Statement stmt = con.createStatement();
+      stmt.execute("DROP TABLE IF EXISTS testReplay");
+      stmt.execute("CREATE TABLE testReplay(id INT)");
+      stmt.execute("INSERT INTO testReplay VALUE (1)");
+      con.setAutoCommit(false);
+      stmt.execute("START TRANSACTION");
+      stmt.execute("INSERT INTO testReplay VALUE (2)");
+      try (PreparedStatement prep = con.prepareStatement("INSERT INTO testReplay VALUE (?)")) {
+        prep.setInt(1, 3);
+        prep.execute();
+      }
+
+      try (PreparedStatement prep = con.prepareStatement("INSERT INTO testReplay VALUE (?)")) {
+        prep.setInt(1, 4);
+        prep.execute();
+        proxy.restart(50);
+        prep.setInt(1, 5);
+        prep.execute();
+      }
+
+      ResultSet rs = stmt.executeQuery("SELECT * from testReplay");
+      rs.next();
+      assertEquals(1, rs.getInt(1));
+      rs.next();
+      assertEquals(2, rs.getInt(1));
+      rs.next();
+      assertEquals(3, rs.getInt(1));
+      rs.next();
+      assertEquals(4, rs.getInt(1));
+      rs.next();
+      assertEquals(5, rs.getInt(1));
+      assertFalse(rs.next());
+      stmt.execute("DROP TABLE IF EXISTS testReplay");
+    }
+  }
+
+  @Test
+  public void masterStreamingFailover() throws Exception {
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+
+    Configuration conf = Configuration.parse(mDefUrl);
+    HostAddress hostAddress = conf.addresses().get(0);
+    try {
+      proxy = new TcpProxy(hostAddress.host, hostAddress.port);
+    } catch (IOException i) {
+      throw new SQLException("proxy error", i);
+    }
+
+    String url =
+        mDefUrl.replaceAll(
+            "//([^/]*)/",
+            String.format(
+                "//address=(host=localhost)(port=%s)(type=master)/", proxy.getLocalPort()));
+    url = url.replaceAll("jdbc:mariadb:", "jdbc:mariadb:sequential:");
+    if (conf.sslMode() == SslMode.VERIFY_FULL) {
+      url = url.replaceAll("sslMode=verify-full", "sslMode=verify-ca");
+    }
+
+    Connection con =
+        (Connection)
+            DriverManager.getConnection(
+                url
+                    + "&allowMultiQueries&transactionReplay=true&waitReconnectTimeout=300&deniedListTimeout=300&retriesAllDown=40&connectTimeout=500&useReadAheadInput=false");
+    long threadId = con.getThreadId();
+    Statement stmt = con.createStatement();
+    stmt.setFetchSize(2);
+    ResultSet rs =
+        stmt.executeQuery("SELECT * FROM sequence_1_to_10; SELECT * FROM sequence_1_to_10000");
+    rs.next();
+    assertEquals(1, rs.getInt(1));
+    proxy.restart(50);
+    Statement stmt2 = con.createStatement();
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> stmt2.executeQuery("SELECT * from sequence_1_to_10"),
+        "Socket error during result streaming");
+    assertNotEquals(threadId, con.getThreadId());
+
+    // additional small test
+    assertEquals(0, con.getNetworkTimeout());
+    con.setNetworkTimeout(Runnable::run, 10);
+    assertEquals(10, con.getNetworkTimeout());
+
+    con.setReadOnly(true);
+    con.close();
+    Common.assertThrowsContains(
+        SQLNonTransientConnectionException.class,
+        () -> con.setReadOnly(false),
+        "Connection is closed");
+    Common.assertThrowsContains(
+        SQLNonTransientConnectionException.class,
+        () -> con.abort(Runnable::run),
+        "Connection is closed");
+
+    Connection con2 =
+        (Connection)
+            DriverManager.getConnection(
+                url
+                    + "&allowMultiQueries&transactionReplay=true&waitReconnectTimeout=300&deniedListTimeout=300&retriesAllDown=40&connectTimeout=500&useReadAheadInput=false");
+    con2.abort(Runnable::run);
+  }
+
+  @Test
+  public void masterReplicationFailover() throws Exception {
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+
+    Configuration conf = Configuration.parse(mDefUrl);
+    HostAddress hostAddress = conf.addresses().get(0);
+    try {
+      proxy = new TcpProxy(hostAddress.host, hostAddress.port);
+    } catch (IOException i) {
+      throw new SQLException("proxy error", i);
+    }
+
+    String url =
+        mDefUrl.replaceAll(
+            "//([^/]*)/",
+            String.format(
+                "//localhost:%s,%s:%s/", proxy.getLocalPort(), hostAddress.host, hostAddress.port));
+    url = url.replaceAll("jdbc:mariadb:", "jdbc:mariadb:replication:");
+    if (conf.sslMode() == SslMode.VERIFY_FULL) {
+      url = url.replaceAll("sslMode=verify-full", "sslMode=verify-ca");
+    }
+
+    try (Connection con =
+        (Connection)
+            DriverManager.getConnection(
+                url
+                    + "&waitReconnectTimeout=300&deniedListTimeout=300&retriesAllDown=4&connectTimeout=500")) {
+      Statement stmt = con.createStatement();
+      stmt.execute("SET @con=1");
+      con.setReadOnly(true);
+      con.isValid(1);
+      proxy.restart(50);
+      Thread.sleep(20);
+      con.setReadOnly(false);
+
+      assertThrowsContains(
+          SQLTransientConnectionException.class,
+          () -> stmt.executeQuery("SELECT @con"),
+          "Driver has reconnect connection after a communications link failure with");
+    }
+
+    // never reconnect
+    try (Connection con =
+        (Connection)
+            DriverManager.getConnection(
+                url
+                    + "&waitReconnectTimeout=300&deniedListTimeout=300&retriesAllDown=4&connectTimeout=500")) {
+      Statement stmt = con.createStatement();
+      stmt.execute("SET @con=1");
+      con.setReadOnly(true);
+      con.isValid(1);
+      proxy.stop();
+      Thread.sleep(20);
+      con.setReadOnly(false);
+      assertFalse(con.isValid(1));
+      assertThrows(SQLException.class, () -> stmt.execute("SELECT 1"));
+    }
+  }
+
+  @Test
+  public void masterReplicationStreamingFailover() throws Exception {
+    Assumptions.assumeTrue(
+        isMariaDBServer()
+            && !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv")));
+
+    Configuration conf = Configuration.parse(mDefUrl);
+    HostAddress hostAddress = conf.addresses().get(0);
+    try {
+      proxy = new TcpProxy(hostAddress.host, hostAddress.port);
+    } catch (IOException i) {
+      throw new SQLException("proxy error", i);
+    }
+
+    String url =
+        mDefUrl.replaceAll(
+            "//([^/]*)/",
+            String.format(
+                "//address=(host=localhost)(port=%s)(type=primary),address=(host=%s)(port=%s)(type=replica)/",
+                proxy.getLocalPort(), hostAddress.host, hostAddress.port));
+    url = url.replaceAll("jdbc:mariadb:", "jdbc:mariadb:replication:");
+    if (conf.sslMode() == SslMode.VERIFY_FULL) {
+      url = url.replaceAll("sslMode=verify-full", "sslMode=verify-ca");
+    }
+
+    Connection con =
+        (Connection)
+            DriverManager.getConnection(
+                url
+                    + "&allowMultiQueries&transactionReplay=true&waitReconnectTimeout=300&deniedListTimeout=300&retriesAllDown=40&connectTimeout=500&useReadAheadInput=false");
+    long threadId = con.getThreadId();
+    Statement stmt = con.createStatement();
+    stmt.setFetchSize(2);
+    ResultSet rs =
+        stmt.executeQuery("SELECT * FROM sequence_1_to_10; SELECT * FROM sequence_1_to_10000");
+    rs.next();
+    assertEquals(1, rs.getInt(1));
+    proxy.restart(50);
+    Statement stmt2 = con.createStatement();
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> stmt2.executeQuery("SELECT * from sequence_1_to_10"),
+        "Socket error during result streaming");
+    assertNotEquals(threadId, con.getThreadId());
+
+    // additional small test
+    assertEquals(0, con.getNetworkTimeout());
+    con.setNetworkTimeout(Runnable::run, 10);
+    assertEquals(10, con.getNetworkTimeout());
+
+    con.setReadOnly(true);
+    con.close();
+    Common.assertThrowsContains(
+        SQLNonTransientConnectionException.class,
+        () -> con.setReadOnly(false),
+        "Connection is closed");
+    Common.assertThrowsContains(
+        SQLNonTransientConnectionException.class,
+        () -> con.abort(Runnable::run),
+        "Connection is closed");
+
+    Connection con2 =
+        (Connection)
+            DriverManager.getConnection(
+                url
+                    + "&allowMultiQueries&transactionReplay=true&waitReconnectTimeout=300&deniedListTimeout=300&retriesAllDown=40&connectTimeout=500&useReadAheadInput=false");
+    con2.abort(Runnable::run);
+  }
+
+  public Connection createProxyConKeep(String opts) throws SQLException {
+    Configuration conf = Configuration.parse(mDefUrl);
+    HostAddress hostAddress = conf.addresses().get(0);
+    try {
+      proxy = new TcpProxy(hostAddress.host, hostAddress.port);
+    } catch (IOException i) {
+      throw new SQLException("proxy error", i);
+    }
+
+    String url =
+        mDefUrl.replaceAll(
+            "//([^/]*)/",
+            String.format(
+                "//%s:%s,localhost:%s/", hostAddress.host, hostAddress.port, proxy.getLocalPort()));
+    url = url.replaceAll("jdbc:mariadb:", "jdbc:mariadb:replication:");
+    if (conf.sslMode() == SslMode.VERIFY_FULL) {
+      url = url.replaceAll("sslMode=verify-full", "sslMode=verify-ca");
+    }
+
+    return (Connection) DriverManager.getConnection(url + opts);
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/MultiPacketTest.java b/src/test/java/org/mariadb/jdbc/integration/MultiPacketTest.java
new file mode 100644
index 0000000..08a4fd2
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/MultiPacketTest.java
@@ -0,0 +1,155 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+
+public class MultiPacketTest extends Common {
+  private static final char[] arr2 = new char[17 * 1024 * 1024];
+
+  static {
+    for (int pos = 0; pos < arr2.length; pos++) {
+      arr2[pos] = (char) ('A' + (pos % 60));
+    }
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    Assumptions.assumeTrue(getMaxAllowedPacket() > 19 * 1024 * 1024);
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS multiPacketTest");
+    stmt.execute("CREATE TABLE multiPacketTest (t1 MEDIUMTEXT, t2 LONGTEXT)");
+  }
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    sharedConn.createStatement().execute("DROP TABLE IF EXISTS multiPacketTest");
+  }
+
+  @Test
+  public void bigByteSend() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("TRUNCATE multiPacketTest");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep =
+        sharedConnBinary.prepareStatement("INSERT INTO multiPacketTest VALUE (?,?)")) {
+      prep.setString(1, new String(arr2, 0, 128 * 1024 - 24));
+      prep.setByte(2, (byte) 2);
+      prep.execute();
+    }
+    ResultSet rs = stmt.executeQuery("SELECT t2 FROM multiPacketTest");
+    assertTrue(rs.next(), "expected rows");
+    assertEquals("2", rs.getString(1));
+  }
+
+  @Test
+  public void bigByte2Send() throws Throwable {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("TRUNCATE multiPacketTest");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep =
+        sharedConnBinary.prepareStatement("INSERT INTO multiPacketTest VALUE (?,?)")) {
+      prep.setString(1, "test1");
+      prep.setByte(2, (byte) 1);
+      prep.execute();
+      prep.setString(1, new String(arr2, 0, 16 * 1024 * 1024 - 21));
+      prep.setByte(2, (byte) 2);
+      prep.execute();
+    }
+    ResultSet rs = stmt.executeQuery("SELECT t2 FROM multiPacketTest");
+    assertTrue(rs.next(), "expected rows");
+    assertTrue(rs.next());
+    assertEquals("2", rs.getString(1));
+  }
+
+  @Test
+  public void bigShortSend() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("TRUNCATE multiPacketTest");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep =
+        sharedConnBinary.prepareStatement("INSERT INTO multiPacketTest VALUE (?,?)")) {
+      prep.setString(1, new String(arr2, 0, 16 * 1024 * 1024 - 21));
+      prep.setShort(2, (short) 2);
+      prep.execute();
+    }
+    ResultSet rs = stmt.executeQuery("SELECT t2 FROM multiPacketTest");
+    assertTrue(rs.next(), "expected rows");
+    assertEquals("2", rs.getString(1));
+  }
+
+  @Test
+  public void bigIntSend() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("TRUNCATE multiPacketTest");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep =
+        sharedConnBinary.prepareStatement("INSERT INTO multiPacketTest VALUE (?,?)")) {
+      prep.setString(1, new String(arr2, 0, 16 * 1024 * 1024 - 21));
+      prep.setInt(2, 2);
+      prep.execute();
+    }
+    ResultSet rs = stmt.executeQuery("SELECT t2 FROM multiPacketTest");
+    assertTrue(rs.next(), "expected rows");
+    assertEquals("2", rs.getString(1));
+  }
+
+  @Test
+  public void bigLongSend() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("TRUNCATE multiPacketTest");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep =
+        sharedConnBinary.prepareStatement("INSERT INTO multiPacketTest VALUE (?,?)")) {
+      prep.setString(1, new String(arr2, 0, 16 * 1024 * 1024 - 21));
+      prep.setLong(2, 2L);
+      prep.execute();
+    }
+    ResultSet rs = stmt.executeQuery("SELECT t2 FROM multiPacketTest");
+    assertTrue(rs.next(), "expected rows");
+    assertEquals("2", rs.getString(1));
+  }
+
+  @Test
+  public void bigStringSend() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("TRUNCATE multiPacketTest");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep =
+        sharedConnBinary.prepareStatement("INSERT INTO multiPacketTest VALUE (?,?)")) {
+      prep.setString(1, new String(arr2, 0, 16 * 1024 * 1024 - 21));
+      prep.setString(2, new String(arr2, 0, 30_000));
+      prep.execute();
+
+      prep.setString(1, new String(arr2, 0, 16 * 1024 * 1024 - 21));
+      prep.setString(2, new String(arr2, 0, 70_000));
+      prep.execute();
+      if (getMaxAllowedPacket() > 34 * 1024 * 1024) {
+        prep.setString(1, new String(arr2, 0, 16 * 1024 * 1024 - 21));
+        prep.setString(2, new String(arr2, 0, 17 * 1024 * 1024));
+        prep.execute();
+      }
+    }
+    ResultSet rs = stmt.executeQuery("SELECT t2 FROM multiPacketTest");
+    assertTrue(rs.next(), "expected rows");
+    assertEquals(new String(arr2, 0, 30_000), rs.getString(1));
+    rs.next();
+    assertEquals(new String(arr2, 0, 70_000), rs.getString(1));
+    rs.next();
+    if (getMaxAllowedPacket() > 34 * 1024 * 1024) {
+      assertEquals(new String(arr2, 0, 17 * 1024 * 1024), rs.getString(1));
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/MultiQueriesTest.java b/src/test/java/org/mariadb/jdbc/integration/MultiQueriesTest.java
new file mode 100644
index 0000000..66f0eeb
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/MultiQueriesTest.java
@@ -0,0 +1,158 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Connection;
+import org.mariadb.jdbc.Statement;
+
+public class MultiQueriesTest extends Common {
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    after2();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE AllowMultiQueriesTest(id int not null primary key auto_increment, "
+            + "test varchar(10))");
+    stmt.execute("INSERT INTO AllowMultiQueriesTest(test) VALUES ('a'), ('b')");
+  }
+
+  @AfterAll
+  public static void after2() throws SQLException {
+    sharedConn.createStatement().execute("DROP TABLE IF EXISTS AllowMultiQueriesTest");
+  }
+
+  @Test
+  public void allowMultiQueriesSingleTest() throws SQLException {
+    try (Connection connection = createCon("&allowMultiQueries=true")) {
+      try (Statement statement = connection.createStatement()) {
+        statement.execute("SELECT 1; SELECT 2; SELECT 3;");
+        int counter = 1;
+        do {
+          ResultSet resultSet = statement.getResultSet();
+          assertEquals(-1, statement.getUpdateCount());
+          assertTrue(resultSet.next());
+          assertEquals(counter++, resultSet.getInt(1));
+        } while (statement.getMoreResults());
+        assertEquals(4, counter);
+      }
+    }
+  }
+
+  @Test
+  public void checkMultiGeneratedKeys() throws SQLException {
+    try (Connection connection = createCon("&allowMultiQueries=true")) {
+      Statement stmt = connection.createStatement();
+      stmt.execute("SELECT 1; SET @TOTO=3; SELECT 2", java.sql.Statement.RETURN_GENERATED_KEYS);
+      ResultSet rs = stmt.getResultSet();
+      assertTrue(rs.next());
+      assertEquals(1, rs.getInt(1));
+      assertFalse(stmt.getMoreResults());
+      stmt.getGeneratedKeys();
+      assertTrue(stmt.getMoreResults());
+      rs = stmt.getResultSet();
+      assertTrue(rs.next());
+      assertEquals(2, rs.getInt(1));
+    }
+  }
+
+  @Test
+  public void allowMultiQueriesFetchTest() throws SQLException {
+    try (Connection connection = createCon("&allowMultiQueries=true")) {
+      try (Statement stmt = connection.createStatement()) {
+        stmt.setFetchSize(1);
+        stmt.execute("SELECT * from AllowMultiQueriesTest;SELECT * from AllowMultiQueriesTest;");
+        do {
+          ResultSet resultSet = stmt.getResultSet();
+          assertEquals(-1, stmt.getUpdateCount());
+          assertTrue(resultSet.next());
+          assertEquals("a", resultSet.getString(2));
+        } while (stmt.getMoreResults());
+
+        stmt.executeQuery(
+            "SELECT * from AllowMultiQueriesTest;SELECT * from AllowMultiQueriesTest;");
+        ResultSet rs = stmt.executeQuery("SELECT 1");
+        rs.next();
+        assertEquals(1, rs.getInt(1));
+      }
+      try (Statement statement = connection.createStatement()) {
+        statement.execute("SELECT 1");
+      }
+    }
+  }
+
+  @Test
+  public void quitWhileStreaming() throws SQLException {
+    // XPAND doesn't support DO command
+    Assumptions.assumeFalse(isXpand());
+
+    Connection connection = createCon("&allowMultiQueries=true");
+    Statement stmt = connection.createStatement();
+    stmt.setFetchSize(1);
+    stmt.executeQuery(
+        "DO 2;SELECT * from AllowMultiQueriesTest;SELECT * from AllowMultiQueriesTest; DO 1; SELECT"
+            + " 2");
+    connection.abort(Runnable::run);
+
+    connection = createCon("&allowMultiQueries=true");
+    stmt = connection.createStatement();
+    stmt.setFetchSize(1);
+    stmt.executeQuery("DO 2;DO 1;SELECT * from AllowMultiQueriesTest");
+    connection.abort(Runnable::run);
+  }
+
+  @Test
+  public void allowMultiQueriesFetchKeepTest() throws SQLException {
+    try (Connection connection = createCon("&allowMultiQueries=true")) {
+      try (Statement stmt = connection.createStatement()) {
+        stmt.setFetchSize(1);
+        stmt.execute("SELECT * from AllowMultiQueriesTest;SELECT 3;");
+        ResultSet rs1 = stmt.getResultSet();
+        assertTrue(stmt.getMoreResults(java.sql.Statement.KEEP_CURRENT_RESULT));
+        assertTrue(rs1.next());
+        assertEquals("a", rs1.getString(2));
+
+        ResultSet rs = stmt.getResultSet();
+        assertTrue(rs.next());
+        assertEquals(3, rs.getInt(1));
+      }
+    }
+  }
+
+  @Test
+  public void allowMultiQueriesFetchCloseTest() throws SQLException {
+    try (Connection connection = createCon("&allowMultiQueries=true")) {
+      try (Statement statement = connection.createStatement()) {
+        statement.setFetchSize(1);
+        statement.execute(
+            "SELECT * from AllowMultiQueriesTest;SELECT * from AllowMultiQueriesTest;SELECT 3;");
+        ResultSet rs1 = statement.getResultSet();
+        assertTrue(statement.getMoreResults(Statement.CLOSE_CURRENT_RESULT));
+        try {
+          rs1.next();
+          fail("Must have thrown exception, since closed");
+        } catch (SQLException sqle) {
+          assertTrue(sqle.getMessage().contains("Operation not permit on a closed resultSet"));
+        }
+
+        rs1 = statement.getResultSet();
+        assertTrue(statement.getMoreResults(java.sql.Statement.KEEP_CURRENT_RESULT));
+        assertTrue(rs1.next());
+        assertEquals("a", rs1.getString(2));
+
+        ResultSet rs = statement.getResultSet();
+        assertTrue(rs.next());
+        assertEquals(3, rs.getInt(1));
+      }
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/ParameterMetaDataTest.java b/src/test/java/org/mariadb/jdbc/integration/ParameterMetaDataTest.java
new file mode 100644
index 0000000..3c5fd24
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/ParameterMetaDataTest.java
@@ -0,0 +1,205 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+
+public class ParameterMetaDataTest extends Common {
+
+  @AfterAll
+  public static void after2() throws SQLException {
+    sharedConn.createStatement().execute("DROP TABLE IF EXISTS parameter_meta");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    after2();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE parameter_meta ("
+            + "id int not null primary key auto_increment, "
+            + "test varchar(20), "
+            + "t2 DECIMAL(10,3))");
+  }
+
+  @Test
+  public void parameterMetaDataTypeNotAvailable() throws Exception {
+    try (org.mariadb.jdbc.Connection con = createCon("&useServerPrepStmts=false")) {
+      parameterMetaDataTypeNotAvailable(con);
+    }
+    try (org.mariadb.jdbc.Connection con = createCon("&useServerPrepStmts")) {
+      parameterMetaDataTypeNotAvailable(con);
+    }
+  }
+
+  private void parameterMetaDataTypeNotAvailable(org.mariadb.jdbc.Connection con)
+      throws SQLException {
+    String query = "SELECT * FROM parameter_meta WHERE test = ? and id = ?";
+    try (PreparedStatement prepStmt = con.prepareStatement(query)) {
+      ParameterMetaData parameterMetaData = prepStmt.getParameterMetaData();
+      assertEquals(2, parameterMetaData.getParameterCount());
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> parameterMetaData.getParameterType(1),
+          "Getting parameter type metadata are not supported");
+      Common.assertThrowsContains(
+          SQLException.class, () -> parameterMetaData.isNullable(-1), "Wrong index position");
+      Common.assertThrowsContains(
+          SQLException.class, () -> parameterMetaData.isNullable(3), "Wrong index position");
+    }
+  }
+
+  @Test
+  public void parameterMetaDataNotPreparable() throws Exception {
+    try (org.mariadb.jdbc.Connection con = createCon("&useServerPrepStmts=false")) {
+      // statement that cannot be prepared
+      try (PreparedStatement pstmt =
+          con.prepareStatement("select  TMP.field1 from (select ? from dual) TMP")) {
+        ParameterMetaData meta = pstmt.getParameterMetaData();
+        assertEquals(1, meta.getParameterCount());
+        assertEquals(ParameterMetaData.parameterModeIn, meta.getParameterMode(1));
+        Common.assertThrowsContains(
+            SQLSyntaxErrorException.class,
+            () -> meta.getParameterTypeName(1),
+            "Unknown parameter metadata type name");
+        Common.assertThrowsContains(
+            SQLFeatureNotSupportedException.class,
+            () -> meta.getParameterClassName(1),
+            "Unknown parameter metadata class name");
+        Common.assertThrowsContains(
+            SQLFeatureNotSupportedException.class,
+            () -> meta.getParameterType(1),
+            "Getting parameter type metadata is not supported");
+        Common.assertThrowsContains(
+            SQLSyntaxErrorException.class,
+            () -> meta.getPrecision(1),
+            "Unknown parameter metadata precision");
+        Common.assertThrowsContains(
+            SQLSyntaxErrorException.class,
+            () -> meta.getScale(1),
+            "Unknown parameter metadata scale");
+
+        Common.assertThrowsContains(
+            SQLSyntaxErrorException.class,
+            () -> meta.getParameterMode(0),
+            "Wrong index position. Is 0 but must be in 1-1 range");
+        Common.assertThrowsContains(
+            SQLSyntaxErrorException.class,
+            () -> meta.getParameterTypeName(0),
+            "Wrong index position. Is 0 but must be in 1-1 range");
+        Common.assertThrowsContains(
+            SQLSyntaxErrorException.class,
+            () -> meta.getParameterMode(0),
+            "Wrong index position. Is 0 but must be in 1-1 range");
+        Common.assertThrowsContains(
+            SQLSyntaxErrorException.class,
+            () -> meta.getParameterClassName(0),
+            "Wrong index position. Is 0 but must be in 1-1 range");
+        Common.assertThrowsContains(
+            SQLSyntaxErrorException.class,
+            () -> meta.getParameterType(2),
+            "Wrong index position. Is 2 but must be in 1-1 range");
+        Common.assertThrowsContains(
+            SQLSyntaxErrorException.class,
+            () -> meta.getPrecision(0),
+            "Wrong index position. Is 0 but must be in 1-1 range");
+        Common.assertThrowsContains(
+            SQLSyntaxErrorException.class,
+            () -> meta.getScale(0),
+            "Wrong index position. Is 0 but must be in 1-1 range");
+      }
+    }
+    try (org.mariadb.jdbc.Connection con = createCon("&useServerPrepStmts")) {
+      // statement that cannot be prepared
+      try (PreparedStatement pstmt =
+          con.prepareStatement("select  TMP.field1 from (select ? from dual) TMP")) {
+        try {
+          pstmt.getParameterMetaData();
+          fail();
+        } catch (SQLSyntaxErrorException e) {
+          // eat
+        }
+      } catch (SQLSyntaxErrorException e) {
+        // eat
+      }
+    }
+  }
+
+  @Test
+  public void parameterMetaDataBasic() throws SQLException {
+    String query = "SELECT * FROM parameter_meta WHERE test = ? and id = ? and t2 = ?";
+    // Parameter type are not sent by server.
+    // See https://jira.mariadb.org/browse/CONJ-568 and https://jira.mariadb.org/browse/MDEV-15031
+    // so only basic info like parameter number are retrieved.
+    try (PreparedStatement prepStmt = sharedConnBinary.prepareStatement(query)) {
+      prepStmt.setString(1, "");
+      prepStmt.setInt(2, 1);
+      prepStmt.setInt(3, 1);
+      prepStmt.executeQuery();
+      ParameterMetaData meta = prepStmt.getParameterMetaData();
+      assertEquals(3, meta.getParameterCount());
+      if (!isMariaDBServer() && (minVersion(8, 0, 22) && !minVersion(8, 0, 30))) {
+        assertEquals(16383, meta.getPrecision(1));
+        assertEquals(31, meta.getScale(1));
+      } else {
+        // Xpand datatype as issue https://jira.mariadb.org/browse/XPT-279
+        if (!isXpand()) {
+          assertEquals(0, meta.getPrecision(1));
+          assertEquals(0, meta.getScale(1));
+        }
+      }
+
+      assertTrue(meta.isSigned(1));
+      assertEquals(ParameterMetaData.parameterNullable, meta.isNullable(1));
+      assertEquals(ParameterMetaData.parameterModeIn, meta.getParameterMode(1));
+
+      Common.assertThrowsContains(
+          SQLFeatureNotSupportedException.class,
+          () -> meta.getParameterType(1),
+          "Getting parameter type " + "metadata are not supported");
+      Common.assertThrowsContains(
+          SQLFeatureNotSupportedException.class,
+          () -> meta.getParameterClassName(1),
+          "Unknown parameter metadata class name");
+
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> meta.getScale(20),
+          "Wrong index position. Is 20 but must be in 1-3 range");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> meta.getPrecision(20),
+          "Wrong index position. Is 20 but must be in 1-3 range");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> meta.isSigned(20),
+          "Wrong index position. Is 20 but must be in 1-3 range");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> meta.isNullable(20),
+          "Wrong index position. Is 20 but must be in 1-3 range");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> meta.getParameterTypeName(20),
+          "Wrong index position. Is 20 but must be in 1-3 range");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> meta.getParameterMode(20),
+          "Wrong index position. Is 20 but must be in 1-3 range");
+
+      meta.unwrap(java.sql.ParameterMetaData.class);
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> meta.unwrap(String.class),
+          "The receiver is not a wrapper for java.lang.String");
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/PoolDataSourceTest.java b/src/test/java/org/mariadb/jdbc/integration/PoolDataSourceTest.java
new file mode 100644
index 0000000..c014ec6
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/PoolDataSourceTest.java
@@ -0,0 +1,750 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.lang.management.ManagementFactory;
+import java.sql.*;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import javax.management.MBeanInfo;
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+import javax.sql.ConnectionPoolDataSource;
+import javax.sql.PooledConnection;
+import javax.sql.XAConnection;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.MariaDbPoolDataSource;
+import org.mariadb.jdbc.pool.PoolThreadFactory;
+import org.mariadb.jdbc.pool.Pools;
+
+public class PoolDataSourceTest extends Common {
+
+  /** Initialisation. */
+  @BeforeAll
+  public static void beforeClassDataSourceTest() throws SQLException {
+    drop();
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+    boolean useOldNotation =
+        (!isMariaDBServer() || !minVersion(10, 2, 0))
+            && (isMariaDBServer() || !minVersion(8, 0, 0));
+    Statement stmt = sharedConn.createStatement();
+    if (useOldNotation) {
+      stmt.execute("CREATE USER IF NOT EXISTS 'poolUser'@'%'");
+      stmt.execute(
+          "GRANT SELECT ON "
+              + sharedConn.getCatalog()
+              + ".* TO 'poolUser'@'%' IDENTIFIED BY '!Passw0rd3Works'");
+    } else {
+      stmt.execute("CREATE USER IF NOT EXISTS 'poolUser'@'%' IDENTIFIED BY '!Passw0rd3Works'");
+      stmt.execute("GRANT SELECT ON " + sharedConn.getCatalog() + ".* TO 'poolUser'@'%'");
+    }
+    stmt.execute(
+        "CREATE TABLE testResetRollback(id int not null primary key auto_increment, test"
+            + " varchar(20))");
+    stmt.execute("FLUSH TABLES");
+    // mysql 8.0.31 broken public key retrieval, so avoid FLUSHING for now
+    Assumptions.assumeTrue(!isMariaDBServer() && !exactVersion(8, 0, 31));
+    stmt.execute("FLUSH PRIVILEGES");
+  }
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    try (Statement stmt = sharedConn.createStatement()) {
+      stmt.execute("DROP TABLE IF EXISTS testResetRollback");
+    }
+  }
+
+  @Test
+  public void basic() throws SQLException {
+    MariaDbPoolDataSource ds = new MariaDbPoolDataSource(mDefUrl + "&maxPoolSize=2");
+    testDs(ds);
+    ds.close();
+
+    ds = new MariaDbPoolDataSource();
+    ds.setUrl(mDefUrl + "&maxPoolSize=2");
+    testDs(ds);
+    ds.close();
+  }
+
+  private void testDs(MariaDbPoolDataSource ds) throws SQLException {
+    try (Connection con1 = ds.getConnection()) {
+      long threadId;
+      try (org.mariadb.jdbc.Connection con2 = (org.mariadb.jdbc.Connection) ds.getConnection()) {
+        threadId = con2.getThreadId();
+        ResultSet rs1 = con1.createStatement().executeQuery("SELECT 1");
+        ResultSet rs2 = con2.createStatement().executeQuery("SELECT 2");
+        while (rs1.next()) {
+          assertEquals(1, rs1.getInt(1));
+        }
+        while (rs2.next()) {
+          assertEquals(2, rs2.getInt(1));
+        }
+      }
+      try (org.mariadb.jdbc.Connection con2 = (org.mariadb.jdbc.Connection) ds.getConnection()) {
+        assertEquals(threadId, con2.getThreadId());
+      }
+    }
+
+    PooledConnection con1 = null;
+    PooledConnection con2 = null;
+    try {
+      con1 = ds.getPooledConnection();
+      con2 = ds.getPooledConnection();
+
+      ResultSet rs1 = con1.getConnection().createStatement().executeQuery("SELECT 1");
+      ResultSet rs2 = con2.getConnection().createStatement().executeQuery("SELECT 2");
+      while (rs1.next()) {
+        assertEquals(1, rs1.getInt(1));
+      }
+      while (rs2.next()) {
+        assertEquals(2, rs2.getInt(1));
+      }
+      long threadId = ((org.mariadb.jdbc.Connection) con2.getConnection()).getThreadId();
+      if (con2 != null) con2.getConnection().close();
+      con2 = ds.getPooledConnection();
+      assertEquals(threadId, ((org.mariadb.jdbc.Connection) con2.getConnection()).getThreadId());
+    } finally {
+      if (con1 != null) con1.getConnection().close();
+      if (con2 != null) con2.getConnection().close();
+    }
+
+    XAConnection conx1 = null;
+    XAConnection conx2 = null;
+    try {
+      conx1 = ds.getXAConnection();
+      conx2 = ds.getXAConnection();
+
+      ResultSet rs1 = conx1.getConnection().createStatement().executeQuery("SELECT 1");
+      ResultSet rs2 = conx2.getConnection().createStatement().executeQuery("SELECT 2");
+      while (rs1.next()) {
+        assertEquals(1, rs1.getInt(1));
+      }
+      while (rs2.next()) {
+        assertEquals(2, rs2.getInt(1));
+      }
+
+    } finally {
+      if (conx1 != null) con1.close();
+      if (conx2 != null) con2.close();
+    }
+  }
+
+  @Test
+  public void basic2() throws SQLException {
+    MariaDbPoolDataSource ds = new MariaDbPoolDataSource();
+    assertNull(ds.getUrl());
+    assertNull(ds.getUser());
+    assertEquals(30, ds.getLoginTimeout());
+    DriverManager.setLoginTimeout(40);
+    assertEquals(40, ds.getLoginTimeout());
+    DriverManager.setLoginTimeout(0);
+    ds.setLoginTimeout(50);
+    assertEquals(50, ds.getLoginTimeout());
+
+    assertThrows(SQLException.class, () -> ds.getConnection());
+    assertThrows(SQLException.class, () -> ds.getConnection("user", "password"));
+    assertThrows(SQLException.class, () -> ds.getPooledConnection());
+    assertThrows(SQLException.class, () -> ds.getPooledConnection("user", "password"));
+    assertThrows(SQLException.class, () -> ds.getXAConnection());
+    assertThrows(SQLException.class, () -> ds.getXAConnection("user", "password"));
+
+    ds.setUser("dd");
+    assertEquals("dd", ds.getUser());
+
+    ds.setPassword("pwd");
+    assertThrows(SQLException.class, () -> ds.getConnection());
+    assertThrows(SQLException.class, () -> ds.getPooledConnection());
+
+    assertThrows(SQLException.class, () -> ds.setUrl("jdbc:wrong://d"));
+
+    ds.setUrl("jdbc:mariadb://myhost:5500/db?someOption=val");
+    assertEquals(
+        "jdbc:mariadb://myhost:5500/db?user=dd&password=***&someOption=val&connectTimeout=50000",
+        ds.getUrl());
+    ds.close();
+  }
+
+  @Test
+  public void testDataSource() throws SQLException {
+    try (MariaDbPoolDataSource ds =
+        new MariaDbPoolDataSource(mDefUrl + "&allowPublicKeyRetrieval")) {
+      try (Connection connection = ds.getConnection()) {
+        assertEquals(connection.isValid(0), true);
+      }
+
+      try (Connection connection = ds.getConnection("poolUser", "!Passw0rd3Works")) {
+        assertEquals(connection.isValid(0), true);
+      }
+
+      PooledConnection poolCon = ds.getPooledConnection();
+      assertEquals(poolCon.getConnection().isValid(0), true);
+      poolCon.close();
+      poolCon = ds.getPooledConnection("poolUser", "!Passw0rd3Works");
+      assertEquals(poolCon.getConnection().isValid(0), true);
+      poolCon.close();
+    }
+  }
+
+  @Test
+  public void testResetDatabase() throws SQLException {
+    try (MariaDbPoolDataSource pool = new MariaDbPoolDataSource(mDefUrl + "&maxPoolSize=1")) {
+      try (Connection connection = pool.getConnection()) {
+        Statement statement = connection.createStatement();
+        statement.execute("CREATE DATABASE IF NOT EXISTS testingReset");
+        connection.setCatalog("testingReset");
+      }
+
+      try (Connection connection = pool.getConnection()) {
+        assertEquals(sharedConn.getCatalog(), connection.getCatalog());
+        Statement statement = connection.createStatement();
+        statement.execute("DROP DATABASE testingReset");
+      }
+    }
+  }
+
+  @Test
+  public void testResetSessionVariable() throws SQLException {
+    testResetSessionVariable(false);
+    if (isMariaDBServer() && minVersion(10, 2, 0)) {
+      testResetSessionVariable(true);
+    }
+  }
+
+  private void testResetSessionVariable(boolean useResetConnection) throws SQLException {
+    try (MariaDbPoolDataSource pool =
+        new MariaDbPoolDataSource(
+            mDefUrl + "&maxPoolSize=1&useResetConnection=" + useResetConnection)) {
+
+      long nowMillis;
+      int initialWaitTimeout;
+
+      try (Connection connection = pool.getConnection()) {
+        Statement statement = connection.createStatement();
+
+        nowMillis = getNowTime(statement);
+        initialWaitTimeout = getWaitTimeout(statement);
+
+        statement.execute(
+            "SET @@timestamp=UNIX_TIMESTAMP('1970-10-01 01:00:00'), @@wait_timeout=2000");
+        long newNowMillis = getNowTime(statement);
+        int waitTimeout = getWaitTimeout(statement);
+
+        assertTrue(nowMillis - newNowMillis > 23_587_200_000L);
+        assertEquals(2_000, waitTimeout);
+      }
+
+      try (Connection connection = pool.getConnection()) {
+        Statement statement = connection.createStatement();
+
+        long newNowMillis = getNowTime(statement);
+        int waitTimeout = getWaitTimeout(statement);
+
+        if (useResetConnection) {
+          assertTrue(nowMillis - newNowMillis < 10L);
+          assertEquals(initialWaitTimeout, waitTimeout);
+        } else {
+          assertTrue(nowMillis - newNowMillis > 23_587_200_000L);
+          assertEquals(2_000, waitTimeout);
+        }
+      }
+    }
+  }
+
+  private long getNowTime(Statement statement) throws SQLException {
+    ResultSet rs = statement.executeQuery("SELECT NOW()");
+    assertTrue(rs.next());
+    return rs.getTimestamp(1).getTime();
+  }
+
+  private int getWaitTimeout(Statement statement) throws SQLException {
+    ResultSet rs = statement.executeQuery("SELECT @@wait_timeout");
+    assertTrue(rs.next());
+    return rs.getInt(1);
+  }
+
+  @Test
+  public void testResetUserVariable() throws SQLException {
+    testResetUserVariable(false);
+    testResetUserVariable(false);
+    if (isMariaDBServer() && minVersion(10, 2, 0)) {
+      testResetUserVariable(true);
+      testResetUserVariable(true);
+    }
+  }
+
+  private void testResetUserVariable(boolean useResetConnection) throws SQLException {
+    try (MariaDbPoolDataSource pool =
+        new MariaDbPoolDataSource(
+            mDefUrl
+                + "&maxPoolSize=1&useResetConnection="
+                + useResetConnection
+                + "&allowPublicKeyRetrieval")) {
+      try (Connection connection = pool.getConnection()) {
+        Statement statement = connection.createStatement();
+        assertNull(getUserVariableStr(statement));
+
+        statement.execute("SET @str = '123'");
+
+        assertEquals("123", getUserVariableStr(statement));
+      }
+
+      try (Connection connection = pool.getConnection()) {
+        Statement statement = connection.createStatement();
+        if (useResetConnection) {
+          assertNull(getUserVariableStr(statement));
+        } else {
+          assertEquals("123", getUserVariableStr(statement));
+        }
+      }
+    }
+  }
+
+  private String getUserVariableStr(Statement statement) throws SQLException {
+    ResultSet rs = statement.executeQuery("SELECT @str");
+    assertTrue(rs.next());
+    return rs.getString(1);
+  }
+
+  @Test
+  public void testNetworkTimeout() throws SQLException {
+    try (MariaDbPoolDataSource pool =
+        new MariaDbPoolDataSource(mDefUrl + "&maxPoolSize=1&socketTimeout=10000")) {
+      try (Connection connection = pool.getConnection()) {
+        assertEquals(10_000, connection.getNetworkTimeout());
+        connection.setNetworkTimeout(null, 5_000);
+      }
+
+      try (Connection connection = pool.getConnection()) {
+        assertEquals(10_000, connection.getNetworkTimeout());
+      }
+    }
+  }
+
+  @Test
+  public void testResetReadOnly() throws SQLException {
+    try (MariaDbPoolDataSource pool = new MariaDbPoolDataSource(mDefUrl + "&maxPoolSize=1")) {
+      try (Connection connection = pool.getConnection()) {
+        assertFalse(connection.isReadOnly());
+        connection.setReadOnly(true);
+        assertTrue(connection.isReadOnly());
+      }
+
+      try (Connection connection = pool.getConnection()) {
+        assertFalse(connection.isReadOnly());
+      }
+    }
+  }
+
+  @Test
+  public void testResetAutoCommit() throws SQLException {
+    try (MariaDbPoolDataSource pool = new MariaDbPoolDataSource(mDefUrl + "&maxPoolSize=1")) {
+      try (Connection connection = pool.getConnection()) {
+        assertTrue(connection.getAutoCommit());
+        connection.setAutoCommit(false);
+        assertFalse(connection.getAutoCommit());
+      }
+
+      try (Connection connection = pool.getConnection()) {
+        assertTrue(connection.getAutoCommit());
+      }
+    }
+  }
+
+  @Test
+  public void testResetAutoCommitOption() throws SQLException {
+    try (MariaDbPoolDataSource pool =
+        new MariaDbPoolDataSource(mDefUrl + "&maxPoolSize=1&autocommit=false&poolName=PoolTest")) {
+      assertTrue(pool.getPoolName().startsWith("PoolTest-"));
+      try (Connection connection = pool.getConnection()) {
+        assertFalse(connection.getAutoCommit());
+        connection.setAutoCommit(true);
+        assertTrue(connection.getAutoCommit());
+      }
+
+      try (Connection connection = pool.getConnection()) {
+        assertFalse(connection.getAutoCommit());
+      }
+    }
+  }
+
+  @Test
+  public void testResetTransactionIsolation() throws SQLException {
+    try (MariaDbPoolDataSource pool = new MariaDbPoolDataSource(mDefUrl + "&maxPoolSize=1")) {
+
+      try (Connection connection = pool.getConnection()) {
+        assertEquals(Connection.TRANSACTION_REPEATABLE_READ, connection.getTransactionIsolation());
+        connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
+        assertEquals(Connection.TRANSACTION_SERIALIZABLE, connection.getTransactionIsolation());
+      }
+
+      try (Connection connection = pool.getConnection()) {
+        assertEquals(Connection.TRANSACTION_REPEATABLE_READ, connection.getTransactionIsolation());
+      }
+    }
+  }
+
+  @Test
+  public void testJmx() throws Exception {
+    MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+    ObjectName filter = new ObjectName("org.mariadb.jdbc.pool:type=PoolTestJmx-*");
+    try (MariaDbPoolDataSource pool =
+        new MariaDbPoolDataSource(mDefUrl + "&maxPoolSize=5&minPoolSize=0&poolName=PoolTestJmx")) {
+      try (Connection connection = pool.getConnection()) {
+        connection.isValid(1);
+        Set<ObjectName> objectNames = server.queryNames(filter, null);
+        assertEquals(1, objectNames.size());
+        ObjectName name = objectNames.iterator().next();
+
+        MBeanInfo info = server.getMBeanInfo(name);
+        assertEquals(4, info.getAttributes().length);
+
+        checkJmxInfo(server, name, 1, 1, 0);
+
+        try (Connection connection2 = pool.getConnection()) {
+          connection2.isValid(1);
+          checkJmxInfo(server, name, 2, 2, 0);
+        }
+        checkJmxInfo(server, name, 1, 2, 1);
+      }
+    }
+  }
+
+  @Test
+  public void testNoMinConnection() throws Exception {
+    MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+    ObjectName filter = new ObjectName("org.mariadb.jdbc.pool:type=testNoMinConnection-*");
+    try (MariaDbPoolDataSource pool =
+        new MariaDbPoolDataSource(mDefUrl + "&maxPoolSize=5&poolName=testNoMinConnection")) {
+      try (Connection connection = pool.getConnection()) {
+        connection.isValid(1);
+        Set<ObjectName> objectNames = server.queryNames(filter, null);
+        assertEquals(1, objectNames.size());
+        ObjectName name = objectNames.iterator().next();
+
+        MBeanInfo info = server.getMBeanInfo(name);
+        assertEquals(4, info.getAttributes().length);
+
+        // wait to ensure pool has time to create 5 connections
+        try {
+          Thread.sleep(500);
+        } catch (InterruptedException interruptEx) {
+          // eat
+        }
+
+        checkJmxInfo(server, name, 1, 5, 4);
+
+        try (Connection connection2 = pool.getConnection()) {
+          connection2.isValid(1);
+          checkJmxInfo(server, name, 2, 5, 3);
+        }
+        checkJmxInfo(server, name, 1, 5, 4);
+      }
+    }
+  }
+
+  @Test
+  public void testIdleTimeout() throws Throwable {
+    // appveyor is so slow wait time are not relevant.
+    Assumptions.assumeTrue(System.getenv("APPVEYOR_BUILD_WORKER_IMAGE") == null);
+
+    MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+    ObjectName filter = new ObjectName("org.mariadb.jdbc.pool:type=testIdleTimeout-*");
+    try (MariaDbPoolDataSource pool =
+        new MariaDbPoolDataSource(
+            mDefUrl
+                + "&maxPoolSize=5&minPoolSize=3&poolName=testIdleTimeout&testMinRemovalDelay=50&maxIdleTime=100")) {
+      // wait to ensure pool has time to create 3 connections
+      Thread.sleep(1_000);
+
+      Set<ObjectName> objectNames = server.queryNames(filter, null);
+      ObjectName name = objectNames.iterator().next();
+      checkJmxInfo(server, name, 0, 3, 3);
+
+      List<Long> initialThreadIds = pool.testGetConnectionIdleThreadIds();
+      Thread.sleep(200);
+
+      // must still have 3 connections, but must be other ones
+      checkJmxInfo(server, name, 0, 3, 3);
+    }
+  }
+
+  @Test
+  public void testMinConnection() throws Throwable {
+    MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+    ObjectName filter = new ObjectName("org.mariadb.jdbc.pool:type=testMinConnection-*");
+    try (MariaDbPoolDataSource pool =
+        new MariaDbPoolDataSource(
+            mDefUrl
+                + "&maxPoolSize=5&minPoolSize=3&poolName=testMinConnection&testMinRemovalDelay=30&maxIdleTime=100")) {
+      try (Connection connection = pool.getConnection()) {
+        connection.isValid(1);
+        Set<ObjectName> objectNames = server.queryNames(filter, null);
+        assertEquals(1, objectNames.size());
+        ObjectName name = objectNames.iterator().next();
+
+        MBeanInfo info = server.getMBeanInfo(name);
+        assertEquals(4, info.getAttributes().length);
+
+        // to ensure pool has time to create minimal connection number
+        Thread.sleep(200);
+
+        checkJmxInfo(server, name, 1, 3, 2);
+
+        try (Connection connection2 = pool.getConnection()) {
+          connection2.isValid(1);
+          checkJmxInfo(server, name, 2, 3, 1);
+        }
+        checkJmxInfo(server, name, 1, 3, 2);
+      }
+    }
+  }
+
+  private void checkJmxInfo(
+      MBeanServer server,
+      ObjectName name,
+      long expectedActive,
+      long expectedTotal,
+      long expectedIdle)
+      throws Exception {
+
+    assertEquals(
+        expectedActive, ((Long) server.getAttribute(name, "ActiveConnections")).longValue());
+    assertEquals(expectedTotal, ((Long) server.getAttribute(name, "TotalConnections")).longValue());
+    assertEquals(expectedIdle, ((Long) server.getAttribute(name, "IdleConnections")).longValue());
+    assertEquals(0, ((Long) server.getAttribute(name, "ConnectionRequests")).longValue());
+  }
+
+  @Test
+  public void testJmxDisable() throws Exception {
+    MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+    ObjectName filter = new ObjectName("org.mariadb.jdbc.pool:type=PoolTest-*");
+    try (MariaDbPoolDataSource pool =
+        new MariaDbPoolDataSource(
+            mDefUrl + "&maxPoolSize=2&registerJmxPool=false&poolName=PoolTest")) {
+      try (Connection connection = pool.getConnection()) {
+        connection.isValid(1);
+        Set<ObjectName> objectNames = server.queryNames(filter, null);
+        assertEquals(0, objectNames.size());
+      }
+    }
+  }
+
+  @Test
+  public void testResetRollback() throws SQLException {
+    sharedConn.createStatement().execute("FLUSH TABLES");
+    try (MariaDbPoolDataSource pool = new MariaDbPoolDataSource(mDefUrl + "&maxPoolSize=1")) {
+      try (Connection connection = pool.getConnection()) {
+        Statement stmt = connection.createStatement();
+        stmt.executeUpdate("INSERT INTO testResetRollback (test) VALUES ('heja')");
+        connection.setAutoCommit(false);
+        stmt.executeUpdate("INSERT INTO testResetRollback (test) VALUES ('japp')");
+      }
+
+      try (Connection connection = pool.getConnection()) {
+        Statement stmt = connection.createStatement();
+        ResultSet rs = stmt.executeQuery("SELECT count(*) FROM testResetRollback");
+        assertTrue(rs.next());
+        assertEquals(1, rs.getInt(1));
+      }
+    }
+  }
+
+  @Test
+  public void ensureUsingPool() throws Exception {
+    ThreadPoolExecutor connectionAppender =
+        new ThreadPoolExecutor(
+            50,
+            5000,
+            10,
+            TimeUnit.SECONDS,
+            new LinkedBlockingQueue<>(5000),
+            new PoolThreadFactory("testPool"));
+
+    final long start = System.currentTimeMillis();
+    Set<Integer> threadIds = new HashSet<>();
+    for (int i = 0; i < 500; i++) {
+      connectionAppender.execute(
+          () -> {
+            try (Connection connection =
+                DriverManager.getConnection(
+                    mDefUrl + "&pool&staticGlobal&poolName=PoolEnsureUsingPool&log=true")) {
+              Statement stmt = connection.createStatement();
+              ResultSet rs = stmt.executeQuery("SELECT CONNECTION_ID()");
+              rs.next();
+              Integer connectionId = rs.getInt(1);
+              threadIds.add(connectionId);
+              stmt.execute("SELECT 1");
+
+            } catch (SQLException e) {
+              e.printStackTrace();
+            }
+          });
+    }
+    connectionAppender.shutdown();
+    connectionAppender.awaitTermination(30, TimeUnit.SECONDS);
+    assertTrue(threadIds.size() <= 9, "connection ids must be less than 9 : " + threadIds.size());
+    Pools.close("PoolTest");
+  }
+
+  @Test
+  public void ensureClosed() throws Throwable {
+    Thread.sleep(500); // ensure that previous close are effective
+    int initialConnection = getCurrentConnections();
+    Assumptions.assumeFalse(initialConnection == -1);
+
+    try (MariaDbPoolDataSource pool =
+        new MariaDbPoolDataSource(mDefUrl + "&maxPoolSize=10&minPoolSize=1")) {
+
+      try (Connection connection = pool.getConnection()) {
+        connection.isValid(10_000);
+      }
+
+      assertTrue(getCurrentConnections() > initialConnection);
+
+      // reuse IdleConnection
+      try (Connection connection = pool.getConnection()) {
+        connection.isValid(10_000);
+      }
+
+      Thread.sleep(500);
+      assertTrue(getCurrentConnections() > initialConnection);
+    }
+    Thread.sleep(2000); // ensure that previous close are effective
+    assertEquals(initialConnection, getCurrentConnections());
+  }
+
+  @Test
+  public void wrongUrlHandling() throws SQLException {
+    try (MariaDbPoolDataSource pool =
+        new MariaDbPoolDataSource(
+            "jdbc:mariadb://unknownHost/db?user=wrong&maxPoolSize=10&connectTimeout=500")) {
+      long start = System.currentTimeMillis();
+      try {
+        pool.getConnection();
+        fail();
+      } catch (SQLException sqle) {
+        // ensure more time for windows
+        assertTrue(
+            (System.currentTimeMillis() - start) >= 500
+                && (System.currentTimeMillis() - start) < 850,
+            "timeout does not correspond to option. Elapsed time:"
+                + (System.currentTimeMillis() - start));
+        assertTrue(
+            sqle.getMessage()
+                .contains(
+                    "No connection available within the specified time (option 'connectTimeout':"
+                        + " 500 ms)"));
+      }
+    }
+  }
+
+  @Test
+  public void testPrepareReset() throws SQLException {
+    try (MariaDbPoolDataSource pool =
+        new MariaDbPoolDataSource(
+            mDefUrl + "&maxPoolSize=1&useServerPrepStmts=true&useResetConnection")) {
+      try (Connection connection = pool.getConnection()) {
+        PreparedStatement preparedStatement = connection.prepareStatement("SELECT ?");
+        preparedStatement.setString(1, "1");
+        preparedStatement.execute();
+      }
+
+      try (Connection connection = pool.getConnection()) {
+        // must re-prepare
+        PreparedStatement preparedStatement = connection.prepareStatement("SELECT ?");
+        preparedStatement.setString(1, "1");
+        preparedStatement.execute();
+      }
+    }
+  }
+
+  /**
+   * List current connections to server.
+   *
+   * @return number of thread connected.
+   */
+  public static int getCurrentConnections() {
+    try {
+      Statement stmt = sharedConn.createStatement();
+      ResultSet rs = stmt.executeQuery("show status where `variable_name` = 'Threads_connected'");
+      if (rs.next()) {
+        System.out.println("threads : " + rs.getInt(2));
+        return rs.getInt(2);
+      }
+      return -1;
+    } catch (SQLException e) {
+      return -1;
+    }
+  }
+
+  @Test
+  public void poolWithUser() throws SQLException {
+    try (MariaDbPoolDataSource pool =
+        new MariaDbPoolDataSource(
+            mDefUrl + "&maxPoolSize=1&poolName=myPool&allowPublicKeyRetrieval")) {
+      long threadId = 0;
+      try (Connection conn = pool.getConnection()) {
+        conn.isValid(1);
+        threadId = ((org.mariadb.jdbc.Connection) conn).getThreadId();
+      }
+
+      try (Connection conn = pool.getConnection(user, password)) {
+        conn.isValid(1);
+        assertEquals(threadId, ((org.mariadb.jdbc.Connection) conn).getThreadId());
+      }
+      try (Connection conn = pool.getConnection("poolUser", "!Passw0rd3Works")) {
+        conn.isValid(1);
+        assertNotEquals(threadId, ((org.mariadb.jdbc.Connection) conn).getThreadId());
+      }
+    }
+  }
+
+  @Test
+  public void various() throws SQLException {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> new MariaDbPoolDataSource("jdbc:notMariadb"),
+        "Wrong mariaDB url");
+    try (MariaDbPoolDataSource pool =
+        new MariaDbPoolDataSource(mDefUrl + "&maxPoolSize=1&poolName=myPool&connectTimeout=2000")) {
+      assertNotNull(pool.unwrap(org.mariadb.jdbc.MariaDbPoolDataSource.class));
+      assertNotNull(pool.unwrap(ConnectionPoolDataSource.class));
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> pool.unwrap(String.class),
+          "Datasource is not a wrapper for java.lang.String");
+      assertTrue(pool.isWrapperFor(org.mariadb.jdbc.MariaDbPoolDataSource.class));
+      assertTrue(pool.isWrapperFor(ConnectionPoolDataSource.class));
+      assertFalse(pool.isWrapperFor(String.class));
+      pool.setLogWriter(null);
+      assertNull(pool.getLogWriter());
+      assertNull(pool.getParentLogger());
+      assertEquals(2, pool.getLoginTimeout());
+      pool.setLoginTimeout(4);
+      assertEquals(4, pool.getLoginTimeout());
+    }
+  }
+
+  @Test
+  public void pools() throws SQLException {
+    // ensure all are closed
+    Pools.close();
+    Pools.close(null);
+    new MariaDbPoolDataSource(mDefUrl + "&maxPoolSize=1&poolName=myPool");
+    Pools.close("myPool");
+    new MariaDbPoolDataSource(mDefUrl + "&maxPoolSize=1&poolName=myPool");
+    Pools.close();
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/PooledConnectionTest.java b/src/test/java/org/mariadb/jdbc/integration/PooledConnectionTest.java
new file mode 100644
index 0000000..a5da9c4
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/PooledConnectionTest.java
@@ -0,0 +1,297 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.sql.*;
+import java.sql.Connection;
+import java.sql.Statement;
+import javax.sql.*;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.*;
+import org.mariadb.jdbc.export.SslMode;
+import org.mariadb.jdbc.integration.tools.TcpProxy;
+import org.mariadb.jdbc.pool.MariaDbInnerPoolConnection;
+import org.mariadb.jdbc.pool.Pool;
+import org.mariadb.jdbc.pool.Pools;
+
+public class PooledConnectionTest extends Common {
+
+  @Test
+  public void testPooledConnectionClosed() throws Exception {
+    ConnectionPoolDataSource ds = new MariaDbDataSource(mDefUrl);
+    PooledConnection pc = ds.getPooledConnection();
+    Connection connection = pc.getConnection();
+    MyEventListener listener = new MyEventListener();
+    pc.addConnectionEventListener(listener);
+    pc.addStatementEventListener(listener);
+    pc.close();
+    assertTrue(listener.closed);
+    assertThrows(SQLException.class, () -> connection.createStatement().execute("select 1"));
+    pc.removeConnectionEventListener(listener);
+    pc.removeStatementEventListener(listener);
+  }
+
+  @Test
+  public void testPoolWait() throws Exception {
+    try (MariaDbPoolDataSource ds =
+        new MariaDbPoolDataSource(
+            mDefUrl + "&sessionVariables=wait_timeout=1&maxIdleTime=2&testMinRemovalDelay=2")) {
+      Thread.sleep(4000);
+      PooledConnection pc = ds.getPooledConnection();
+      pc.getConnection().isValid(1);
+      pc.close();
+    }
+  }
+
+  @Test
+  public void testPoolWaitWithValidation() throws Exception {
+    try (MariaDbPoolDataSource ds = new MariaDbPoolDataSource(mDefUrl + "&poolValidMinDelay=1")) {
+      Thread.sleep(100);
+      PooledConnection pc = ds.getPooledConnection();
+      pc.getConnection().isValid(1);
+      pc.close();
+    }
+  }
+
+  @Test
+  public void testPoolFailover() throws Exception {
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+
+    Configuration conf = Configuration.parse(mDefUrl);
+    HostAddress hostAddress = conf.addresses().get(0);
+    try {
+      proxy = new TcpProxy(hostAddress.host, hostAddress.port);
+    } catch (IOException i) {
+      throw new SQLException("proxy error", i);
+    }
+
+    String url = mDefUrl.replaceAll("//([^/]*)/", "//localhost:" + proxy.getLocalPort() + "/");
+    if (conf.sslMode() == SslMode.VERIFY_FULL) {
+      url = url.replaceAll("sslMode=verify-full", "sslMode=verify-ca");
+    }
+
+    try (MariaDbPoolDataSource ds =
+        new MariaDbPoolDataSource(url + "&poolValidMinDelay=1&connectTimeout=10&maxPoolSize=1")) {
+
+      PooledConnection pc = ds.getPooledConnection();
+      pc.getConnection().isValid(1);
+      pc.close();
+      Thread.sleep(200);
+      proxy.stop();
+      Common.assertThrowsContains(
+          SQLException.class,
+          ds::getPooledConnection,
+          "No connection available within the specified time");
+    }
+  }
+
+  @Test
+  public void testPoolKillConnection() throws Exception {
+    Assumptions.assumeTrue(
+        !"maxscale".equals(System.getenv("srv"))
+            && !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv"))
+            && !"galera".equals(System.getenv("srv"))
+            && !isXpand());
+
+    File tempFile = File.createTempFile("log", ".tmp");
+    //
+    //    Logger logger = (Logger) LoggerFactory.getLogger("org.mariadb.jdbc");
+    //    Level initialLevel = logger.getLevel();
+    //    logger.setLevel(Level.TRACE);
+    //    logger.setAdditive(false);
+    //    logger.detachAndStopAllAppenders();
+    //
+    //    LoggerContext context = new LoggerContext();
+    //    FileAppender<ILoggingEvent> fa = new FileAppender<>();
+    //    fa.setName("FILE");
+    //    fa.setImmediateFlush(true);
+    //    PatternLayoutEncoder pa = new PatternLayoutEncoder();
+    //    pa.setPattern("%r %5p %c [%t] - %m%n");
+    //    pa.setContext(context);
+    //    pa.start();
+    //    fa.setEncoder(pa);
+    //
+    //    fa.setFile(tempFile.getPath());
+    //    fa.setAppend(true);
+    //    fa.setContext(context);
+    //    fa.start();
+    //
+    //    logger.addAppender(fa);
+
+    try (MariaDbPoolDataSource ds =
+        new MariaDbPoolDataSource(mDefUrl + "&maxPoolSize=1&allowPublicKeyRetrieval")) {
+      Thread.sleep(100);
+      MariaDbInnerPoolConnection pc = (MariaDbInnerPoolConnection) ds.getPooledConnection();
+      org.mariadb.jdbc.Connection conn = pc.getConnection();
+      long threadId = conn.getThreadId();
+      try {
+        conn.createStatement().execute("KILL " + threadId);
+      } catch (SQLException e) {
+        // eat "Connection was killed" message
+      }
+      pc.close();
+      pc = (MariaDbInnerPoolConnection) ds.getPooledConnection();
+      conn = pc.getConnection();
+      assertNotEquals(threadId, conn.getThreadId());
+      pc.close();
+    } finally {
+
+      //      String contents = new String(Files.readAllBytes(Paths.get(tempFile.getPath())));
+      //      assertTrue(
+      //          contents.contains(
+      //              "removed from pool MariaDB-pool due to error during reset (total:0, active:0,
+      // pending:0)"),
+      //          contents);
+      //      assertTrue(contents.contains("pool MariaDB-pool new physical connection created"),
+      // contents);
+      //
+      //      assertTrue(
+      //          contents.contains("closing pool MariaDB-pool (total:1, active:0, pending:0)"),
+      // contents);
+      //      logger.setLevel(initialLevel);
+      //      logger.detachAppender(fa);
+    }
+  }
+
+  @Test
+  public void testPooledConnectionException() throws Exception {
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv"))
+            && !"galera".equals(System.getenv("srv"))
+            && !isXpand());
+
+    ConnectionPoolDataSource ds = new MariaDbDataSource(mDefUrl);
+    PooledConnection pc = null;
+    try {
+      pc = ds.getPooledConnection();
+      MyEventListener listener = new MyEventListener();
+      pc.addConnectionEventListener(listener);
+      Connection connection = pc.getConnection();
+
+      /* Ask server to abort the connection */
+      try {
+        connection.createStatement().execute("KILL CONNECTION_ID()");
+      } catch (Exception e) {
+        /* exception is expected here, server sends query aborted */
+      }
+
+      /* Try to read  after server side closed the connection */
+      assertThrows(SQLException.class, () -> connection.createStatement().execute("SELECT 1"));
+    } finally {
+      if (pc != null) {
+        pc.close();
+      }
+    }
+  }
+
+  @Test
+  public void testPooledConnectionException2() throws Exception {
+    Assumptions.assumeTrue(
+        !"maxscale".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+    try (Pool pool = Pools.retrievePool(Configuration.parse(mDefUrl + "&maxPoolSize=2"))) {
+      MariaDbInnerPoolConnection pooledConnection = pool.getPoolConnection();
+      org.mariadb.jdbc.Connection con = pooledConnection.getConnection();
+      con.setAutoCommit(false);
+      con.createStatement().execute("START TRANSACTION ");
+
+      Connection con2 = pool.getPoolConnection().getConnection();
+      con2.createStatement().execute("KILL " + con.getThreadId());
+      con2.close();
+      Thread.sleep(10);
+      assertThrows(SQLException.class, con::commit);
+      pooledConnection.close();
+    }
+  }
+
+  @Test
+  public void testPooledConnectionStatementError() throws Exception {
+    Assumptions.assumeTrue(
+        !"maxscale".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+    Statement stmt = sharedConn.createStatement();
+    try {
+      stmt.execute("DROP USER 'dsUser'");
+    } catch (SQLException e) {
+      // eat
+    }
+
+    if (minVersion(8, 0, 0)) {
+      if (isMariaDBServer()) {
+        stmt.execute("CREATE USER 'dsUser'@'%' IDENTIFIED BY 'MySup8%rPassw@ord'");
+        stmt.execute("GRANT SELECT ON " + sharedConn.getCatalog() + ".* TO 'dsUser'@'%'");
+      } else {
+        stmt.execute(
+            "CREATE USER 'dsUser'@'%' IDENTIFIED WITH mysql_native_password BY"
+                + " 'MySup8%rPassw@ord'");
+        stmt.execute("GRANT SELECT ON " + sharedConn.getCatalog() + ".* TO 'dsUser'@'%'");
+      }
+    } else {
+      stmt.execute("CREATE USER 'dsUser'@'%'");
+      stmt.execute(
+          "GRANT SELECT ON "
+              + sharedConn.getCatalog()
+              + ".* TO 'dsUser'@'%' IDENTIFIED BY 'MySup8%rPassw@ord'");
+    }
+    // mysql 8.0.31 broken public key retrieval, so avoid FLUSHING for now
+    Assumptions.assumeTrue(!isMariaDBServer() && !exactVersion(8, 0, 31));
+    stmt.execute("FLUSH PRIVILEGES");
+
+    ConnectionPoolDataSource ds = new MariaDbDataSource(mDefUrl);
+    PooledConnection pc = ds.getPooledConnection("dsUser", "MySup8%rPassw@ord");
+    MyEventListener listener = new MyEventListener();
+    pc.addStatementEventListener(listener);
+    Connection connection = pc.getConnection();
+    try (PreparedStatement ps = connection.prepareStatement("SELECT ?")) {
+      ps.execute();
+      fail("should never get there");
+    } catch (Exception e) {
+      assertTrue(listener.statementErrorOccurred);
+    }
+    assertTrue(listener.statementClosed);
+    pc.close();
+  }
+
+  public static class MyEventListener implements ConnectionEventListener, StatementEventListener {
+
+    public SQLException sqlException;
+    public boolean closed;
+    public boolean connectionErrorOccurred;
+    public boolean statementClosed;
+    public boolean statementErrorOccurred;
+
+    /** MyEventListener initialisation. */
+    public MyEventListener() {
+      sqlException = null;
+      closed = false;
+      connectionErrorOccurred = false;
+    }
+
+    public void connectionClosed(ConnectionEvent event) {
+      sqlException = event.getSQLException();
+      closed = true;
+    }
+
+    public void connectionErrorOccurred(ConnectionEvent event) {
+      sqlException = event.getSQLException();
+      connectionErrorOccurred = true;
+    }
+
+    public void statementClosed(StatementEvent event) {
+      statementClosed = true;
+    }
+
+    public void statementErrorOccurred(StatementEvent event) {
+      sqlException = event.getSQLException();
+      statementErrorOccurred = true;
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/PreparedStatementMetadataTest.java b/src/test/java/org/mariadb/jdbc/integration/PreparedStatementMetadataTest.java
new file mode 100644
index 0000000..acd4942
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/PreparedStatementMetadataTest.java
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.*;
+import org.junit.jupiter.api.*;
+import org.mariadb.jdbc.Connection;
+import org.mariadb.jdbc.Statement;
+
+public class PreparedStatementMetadataTest extends Common {
+
+  @Test
+  public void execute() throws SQLException {
+    try (Connection con = createCon("&useServerPrepStmts=false")) {
+      execute(con);
+    }
+    try (Connection con = createCon("&useServerPrepStmts")) {
+      execute(con);
+    }
+  }
+
+  private void execute(Connection conn) throws SQLException {
+    Statement stmt = conn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS prepareMeta");
+    stmt.execute("CREATE TABLE prepareMeta(id int, val varchar(256))");
+    try (PreparedStatement prep = conn.prepareStatement("SELECT * FROM prepareMeta")) {
+      ResultSetMetaData meta = prep.getMetaData();
+      assertEquals(2, meta.getColumnCount());
+      meta = prep.getMetaData();
+      assertEquals(2, meta.getColumnCount());
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/PreparedStatementParametersTest.java b/src/test/java/org/mariadb/jdbc/integration/PreparedStatementParametersTest.java
new file mode 100644
index 0000000..f723f25
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/PreparedStatementParametersTest.java
@@ -0,0 +1,554 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.StringReader;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.sql.*;
+import java.util.Calendar;
+import java.util.TimeZone;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Connection;
+import org.mariadb.jdbc.MariaDbBlob;
+import org.mariadb.jdbc.MariaDbClob;
+import org.mariadb.jdbc.Statement;
+
+public class PreparedStatementParametersTest extends Common {
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE bigTest (t1 int not null primary key auto_increment, t2 LONGTEXT)");
+    stmt.execute("CREATE TABLE prepareParam (t1 BLOB(20))");
+    stmt.execute("CREATE TABLE prepareParam2 (t1 BIGINT)");
+    stmt.execute("CREATE TABLE prepareParam3 (t1 DOUBLE)");
+    stmt.execute(
+        "CREATE TABLE prepareParam4 (t1 VARCHAR(30)) "
+            + "CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
+    stmt.execute("CREATE TABLE prepareParam5 (t1 TIMESTAMP(6))");
+    stmt.execute("CREATE TABLE prepareParam6 (t1 BIGINT)");
+    stmt.execute("CREATE TABLE prepareParam7 (t1 TIME(6))");
+    stmt.execute("FLUSH TABLES");
+  }
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS bigTest");
+    stmt.execute("DROP TABLE IF EXISTS prepareParam");
+    stmt.execute("DROP TABLE IF EXISTS prepareParam2");
+    stmt.execute("DROP TABLE IF EXISTS prepareParam3");
+    stmt.execute("DROP TABLE IF EXISTS prepareParam4");
+    stmt.execute("DROP TABLE IF EXISTS prepareParam5");
+    stmt.execute("DROP TABLE IF EXISTS prepareParam6");
+    stmt.execute("DROP TABLE IF EXISTS prepareParam7");
+  }
+
+  @Test
+  public void validateParameters() throws Exception {
+    // error crashing maxscale 6.1.x
+    Assumptions.assumeTrue(
+        !sharedConn.getMetaData().getDatabaseProductVersion().contains("maxScale-6.1.")
+            && !"skysql-ha".equals(System.getenv("srv")));
+    validateParameters(sharedConn);
+    validateParameters(sharedConnBinary);
+  }
+
+  public void validateParameters(Connection con) throws Exception {
+    try (PreparedStatement prep = con.prepareStatement("INSERT INTO prepareParam6 VALUES (?)")) {
+      try {
+        prep.execute();
+        fail();
+      } catch (SQLException sqle) {
+        // eat
+      }
+    }
+    try (PreparedStatement prep = con.prepareStatement("INSERT INTO prepareParam6 VALUES (?)")) {
+      prep.setInt(1, 1);
+      prep.execute();
+      prep.clearParameters();
+      Common.assertThrowsContains(
+          SQLTransientConnectionException.class,
+          prep::execute,
+          "Parameter at position 1 is not set");
+    }
+  }
+
+  @Test
+  public void checkParameters() throws Exception {
+    checkParameters(sharedConn, true);
+    checkParameters(sharedConnBinary, false);
+  }
+
+  @SuppressWarnings("deprecation")
+  public void checkParameters(org.mariadb.jdbc.Connection con, boolean text) throws Exception {
+    Calendar utcCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+    checkSendBlob(
+        ps -> ps.setBlob(1, new MariaDbBlob("0123".getBytes(), 1, 2)),
+        rs -> assertArrayEquals("12".getBytes(), rs.getBytes(1)),
+        con);
+    checkSendBlob(
+        ps -> ps.setBlob(1, new MariaDbBlob("0123".getBytes(), 1, 2).getBinaryStream()),
+        rs -> assertArrayEquals("12".getBytes(), rs.getBytes(1)),
+        con);
+    checkSendBlob(
+        ps -> ps.setBlob(1, new MariaDbBlob("01234".getBytes(), 1, 3).getBinaryStream(), 2),
+        rs -> assertArrayEquals("12".getBytes(), rs.getBytes(1)),
+        con);
+    checkSendBlob(
+        ps -> ps.setBlob(1, new MariaDbBlob("01234".getBytes(), 1, 3).getBinaryStream(), 2L),
+        rs -> assertArrayEquals("12".getBytes(), rs.getBytes(1)),
+        con);
+    checkSendBlob(ps -> ps.setNull(1, Types.VARBINARY), rs -> assertNull(rs.getObject(1)), con);
+    checkSendBlob(
+        ps -> ps.setNull(1, Types.VARBINARY, String.class.getName()),
+        rs -> assertNull(rs.getObject(1)),
+        con);
+    checkSendLong(ps -> ps.setBoolean(1, true), rs -> assertTrue(rs.getBoolean(1)), con);
+    checkSendLong(ps -> ps.setBoolean(1, false), rs -> assertFalse(rs.getBoolean(1)), con);
+    checkSendLong(
+        ps -> ps.setByte(1, (byte) 0x58), rs -> assertEquals((byte) 0x58, rs.getByte(1)), con);
+    checkSendLong(
+        ps -> ps.setShort(1, (short) 127), rs -> assertEquals((short) 127, rs.getShort(1)), con);
+    checkSendLong(ps -> ps.setInt(1, -555), rs -> assertEquals(-555, rs.getInt(1)), con);
+    checkSendLong(ps -> ps.setLong(1, -999L), rs -> assertEquals(-999L, rs.getLong(1)), con);
+    checkSendDouble(
+        ps -> ps.setFloat(1, -56.59F), rs -> assertEquals(-56.59F, rs.getFloat(1), 0.01F), con);
+    checkSendDouble(
+        ps -> ps.setDouble(1, -56.59D), rs -> assertEquals(-56.59D, rs.getDouble(1), 0.01D), con);
+    checkSendDouble(
+        ps -> ps.setBigDecimal(1, new BigDecimal("-156.59")),
+        rs ->
+            assertEquals(
+                new BigDecimal("-156.59"), rs.getBigDecimal(1).setScale(2, RoundingMode.HALF_DOWN)),
+        con);
+    checkSendString(
+        ps -> ps.setString(1, "你好(hello in Chinese)"),
+        rs -> assertEquals("你好(hello in Chinese)", rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setNString(1, "你好(hello in Chinese)"),
+        rs -> assertEquals("你好(hello in Chinese)", rs.getNString(1)),
+        con);
+    checkSendBlob(
+        ps -> ps.setBytes(1, "01234".getBytes()),
+        rs -> assertArrayEquals("01234".getBytes(), rs.getBytes(1)),
+        con);
+    checkSendTimestamp(
+        ps -> ps.setDate(1, Date.valueOf("2010-05-25")),
+        rs -> assertEquals(Date.valueOf("2010-05-25"), rs.getDate(1)),
+        con);
+    boolean minus = TimeZone.getDefault().getOffset(System.currentTimeMillis()) > 0;
+    checkSendTimestamp(
+        ps -> ps.setDate(1, Date.valueOf("2010-01-12"), utcCal),
+        rs ->
+            assertEquals(minus ? 1263164400000L : 1263254400000L, rs.getDate(1, utcCal).getTime()),
+        con);
+    checkSendTimestamp(
+        ps -> ps.setDate(1, Date.valueOf("2010-01-12"), utcCal),
+        rs -> assertEquals(minus ? "2010-01-11" : "2010-01-12", rs.getDate(1, utcCal).toString()),
+        con);
+    checkSendTimestamp(
+        ps -> ps.setDate(1, Date.valueOf("2010-05-25")),
+        rs -> assertEquals(Date.valueOf("2010-05-25").getTime(), rs.getDate(1).getTime()),
+        con);
+    if (isXpand()) {
+      // skip due to error https://jira.mariadb.org/browse/XPT-265
+    } else {
+      if (text) {
+        Common.assertThrowsContains(
+            SQLException.class,
+            () ->
+                checkSendTimestamp(
+                    ps -> ps.setTime(1, new Time(Time.valueOf("18:16:01").getTime() + 123)),
+                    rs ->
+                        assertEquals(
+                            Time.valueOf("18:16:01").getTime() + 123, rs.getTime(1).getTime()),
+                    con),
+            "Incorrect datetime value: '18:16:01.123'");
+      } else {
+        checkSendTimestamp(
+            ps -> ps.setTime(1, new Time(Time.valueOf("18:16:01").getTime() + 123)),
+            rs -> assertEquals(Time.valueOf("18:16:01").getTime() + 123, rs.getTime(1).getTime()),
+            con);
+      }
+    }
+
+    checkSendTimestamp(
+        ps -> ps.setTimestamp(1, Timestamp.valueOf("2010-05-25 18:16:01.987")),
+        rs ->
+            assertEquals(
+                Timestamp.valueOf("2010-05-25 18:16:01.987").getTime(),
+                rs.getTimestamp(1).getTime()),
+        con);
+    checkSendTimestamp(
+        ps -> ps.setTimestamp(1, Timestamp.valueOf("2010-05-25 18:16:01.987")),
+        rs -> assertEquals("2010-05-25 18:16:01.987", rs.getTimestamp(1).toString()),
+        con);
+    checkSendTimestamp(
+        ps -> ps.setTimestamp(1, Timestamp.valueOf("2010-05-25 18:16:01.987"), utcCal),
+        rs ->
+            assertEquals(
+                Timestamp.valueOf("2010-05-25 18:16:01.987").getTime(),
+                rs.getTimestamp(1, utcCal).getTime()),
+        con);
+
+    checkSendTimestamp(
+        ps -> ps.setTimestamp(1, Timestamp.valueOf("2010-05-25 18:16:01.987"), utcCal),
+        rs ->
+            assertEquals(
+                Timestamp.valueOf("2010-05-25 18:16:01.987").getTime()
+                    - TimeZone.getDefault().getOffset(1, 2010, 5, 25, 1, 987),
+                rs.getTimestamp(1).getTime()),
+        con);
+    checkSendString(
+        ps -> ps.setAsciiStream(1, new ByteArrayInputStream("abcdef".getBytes())),
+        rs -> assertEquals("abcdef", rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setAsciiStream(1, new ByteArrayInputStream("abcdef".getBytes()), 5),
+        rs -> assertEquals("abcde", rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setAsciiStream(1, new ByteArrayInputStream("abcdef".getBytes()), 5L),
+        rs -> assertEquals("abcde", rs.getString(1)),
+        con);
+    final String unicodeString =
+        ""
+            + "\uD83D\uDE0E" // 😎 unicode 6 smiling face with sunglasses
+            + "\uD83C\uDF36" // 🌶 unicode 7 hot pepper
+            + "\uD83C\uDFA4" // 🎤 unicode 8 no microphones
+            + "\uD83E\uDD42 "; // 🥂 unicode 9 clinking glasses
+    final byte[] unicodeBytes = unicodeString.getBytes(StandardCharsets.UTF_8);
+    checkSendString(
+        ps -> ps.setUnicodeStream(1, new ByteArrayInputStream(unicodeBytes), 16),
+        rs ->
+            assertEquals(
+                unicodeString.substring(0, 8),
+                rs.getString(1),
+                "expected " + unicodeString.substring(0, 8) + " but is " + rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setBinaryStream(1, new ByteArrayInputStream(unicodeBytes)),
+        rs -> assertEquals(unicodeString, rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setBinaryStream(1, new ByteArrayInputStream(unicodeBytes), 16),
+        rs -> assertEquals(unicodeString.substring(0, 8), rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setBinaryStream(1, new ByteArrayInputStream(unicodeBytes), 16L),
+        rs -> assertEquals(unicodeString.substring(0, 8), rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setCharacterStream(1, new StringReader(unicodeString)),
+        rs -> assertEquals(unicodeString, rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setCharacterStream(1, new StringReader(unicodeString), 8),
+        rs -> assertEquals(unicodeString.substring(0, 8), rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setCharacterStream(1, new StringReader(unicodeString), 8L),
+        rs -> assertEquals(unicodeString.substring(0, 8), rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setNCharacterStream(1, new StringReader(unicodeString)),
+        rs -> assertEquals(unicodeString, rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setNCharacterStream(1, new StringReader(unicodeString), 8),
+        rs -> assertEquals(unicodeString.substring(0, 8), rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setNCharacterStream(1, new StringReader(unicodeString), 8L),
+        rs -> assertEquals(unicodeString.substring(0, 8), rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setClob(1, new MariaDbClob(unicodeBytes, 0, 16)),
+        rs -> assertEquals(unicodeString.substring(0, 8), rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setClob(1, new StringReader(unicodeString)),
+        rs -> assertEquals(unicodeString, rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setClob(1, new StringReader(unicodeString), 8),
+        rs -> assertEquals(unicodeString.substring(0, 8), rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setClob(1, new StringReader(unicodeString), 8L),
+        rs -> assertEquals(unicodeString.substring(0, 8), rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setNClob(1, new MariaDbClob(unicodeBytes, 0, 16)),
+        rs -> assertEquals(unicodeString.substring(0, 8), rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setNClob(1, new StringReader(unicodeString)),
+        rs -> assertEquals(unicodeString, rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setNClob(1, new StringReader(unicodeString), 8),
+        rs -> assertEquals(unicodeString.substring(0, 8), rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setNClob(1, new StringReader(unicodeString), 8L),
+        rs -> assertEquals(unicodeString.substring(0, 8), rs.getString(1)),
+        con);
+    checkSendString(
+        ps -> ps.setURL(1, new URL("https://www.someUrl.com")),
+        rs -> assertEquals("https://www.someUrl.com", rs.getString(1)),
+        con);
+    // TODO SET OBJECT
+  }
+
+  @Test
+  public void checkTimeParameters() throws Exception {
+    checkTimeParameters(sharedConn);
+    checkTimeParameters(sharedConnBinary);
+  }
+
+  @SuppressWarnings("deprecation")
+  public void checkTimeParameters(Connection con) throws Exception {
+    Calendar utcCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+    checkSendTime(
+        ps -> ps.setTime(1, new Time(Time.valueOf("18:16:01").getTime())),
+        rs -> assertEquals("18:16:01", rs.getTime(1).toString()),
+        con);
+    checkSendTime(
+        ps -> ps.setTime(1, new Time(Time.valueOf("18:16:01").getTime()), utcCal),
+        rs -> assertEquals("18:16:01", rs.getTime(1, utcCal).toString()),
+        con);
+    checkSendTime(
+        ps -> ps.setTime(1, new Time(Time.valueOf("18:16:01").getTime() + 123), utcCal),
+        rs ->
+            assertEquals(
+                Time.valueOf("18:16:01").getTime() + 123 - TimeZone.getDefault().getOffset(0),
+                rs.getTime(1).getTime()),
+        con);
+  }
+
+  @Test
+  public void checkNotSupported() throws Exception {
+    checkNotSupported(sharedConn);
+    checkNotSupported(sharedConnBinary);
+  }
+
+  private void checkNotSupported(Connection con) throws SQLException {
+    try (PreparedStatement preparedStatement = con.prepareStatement("SELECT * FROM prepareParam")) {
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> preparedStatement.setRef(1, null),
+          "REF parameter are not supported");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> preparedStatement.setArray(1, null),
+          "Array parameter are not supported");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> preparedStatement.setRowId(1, null),
+          "RowId parameter are not supported");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> preparedStatement.setSQLXML(1, null),
+          "SQLXML parameter are not supported");
+    }
+  }
+
+  private void checkSendBlob(
+      ThrowingConsumer<PreparedStatement, Exception> consumer,
+      ThrowingConsumer<ResultSet, Exception> check,
+      Connection con)
+      throws Exception {
+    checkSend(consumer, check, "prepareParam", con);
+  }
+
+  private void checkSendLong(
+      ThrowingConsumer<PreparedStatement, Exception> consumer,
+      ThrowingConsumer<ResultSet, Exception> check,
+      Connection con)
+      throws Exception {
+    checkSend(consumer, check, "prepareParam2", con);
+  }
+
+  private void checkSendDouble(
+      ThrowingConsumer<PreparedStatement, Exception> consumer,
+      ThrowingConsumer<ResultSet, Exception> check,
+      Connection con)
+      throws Exception {
+    checkSend(consumer, check, "prepareParam3", con);
+  }
+
+  private void checkSendString(
+      ThrowingConsumer<PreparedStatement, Exception> consumer,
+      ThrowingConsumer<ResultSet, Exception> check,
+      Connection con)
+      throws Exception {
+    checkSend(consumer, check, "prepareParam4", con);
+  }
+
+  private void checkSendTimestamp(
+      ThrowingConsumer<PreparedStatement, Exception> consumer,
+      ThrowingConsumer<ResultSet, Exception> check,
+      Connection con)
+      throws Exception {
+    checkSend(consumer, check, "prepareParam5", con);
+  }
+
+  private void checkSendTime(
+      ThrowingConsumer<PreparedStatement, Exception> consumer,
+      ThrowingConsumer<ResultSet, Exception> check,
+      Connection con)
+      throws Exception {
+    checkSend(consumer, check, "prepareParam7", con);
+  }
+
+  private void checkSend(
+      ThrowingConsumer<PreparedStatement, Exception> consumer,
+      ThrowingConsumer<ResultSet, Exception> check,
+      String table,
+      Connection con)
+      throws Exception {
+    Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION");
+    try (PreparedStatement preparedStatement =
+        con.prepareStatement("INSERT INTO " + table + " VALUES (?)")) {
+      consumer.accept(preparedStatement);
+      preparedStatement.execute();
+    }
+    ResultSet rs = stmt.executeQuery("SELECT * FROM " + table);
+    assertTrue(rs.next());
+    con.rollback();
+    check.accept(rs);
+  }
+
+  @Test
+  public void bigSend() throws SQLException {
+    int maxAllowedPacket = getMaxAllowedPacket();
+    Assumptions.assumeTrue(maxAllowedPacket > 21 * 1024 * 1024);
+    char[] arr = new char[20 * 1024 * 1024];
+    for (int pos = 0; pos < arr.length; pos++) {
+      arr[pos] = (char) ('A' + (pos % 60));
+    }
+    String st = new String(arr);
+    bigSend(sharedConn, st);
+    bigSend(sharedConnBinary, st);
+    try (Connection con = createCon("useEof=false")) {
+      bigSend(con, st);
+    }
+  }
+
+  public void bigSend(Connection con, String st) throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE bigTest");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep = con.prepareStatement("INSERT INTO bigTest VALUES (?, ?)")) {
+      prep.setInt(1, 1);
+      prep.setString(2, st);
+      prep.execute();
+    }
+
+    ResultSet rs = stmt.executeQuery("SELECT t2 from bigTest WHERE t1 = 1");
+    assertTrue(rs.next());
+    assertEquals(st, rs.getString(1));
+    con.commit();
+  }
+
+  @Test
+  public void bigSendError() throws SQLException {
+    int maxAllowedPacket = getMaxAllowedPacket();
+    Assumptions.assumeTrue(maxAllowedPacket < 32 * 1024 * 1024);
+    char[] arr = new char[maxAllowedPacket];
+    for (int pos = 0; pos < arr.length; pos++) {
+      arr[pos] = (char) ('A' + (pos % 60));
+    }
+    boolean expectClosed = maxAllowedPacket >= 16 * 1024 * 1024;
+    String st = new String(arr);
+    try (Connection con = createCon("maxAllowedPacket=" + maxAllowedPacket)) {
+      bigSendError(con, st, expectClosed);
+    }
+    try (Connection con =
+        createCon("useServerPrepStmts=true&maxAllowedPacket=" + maxAllowedPacket)) {
+      bigSendError(con, st, expectClosed);
+    }
+    try (Connection con = createCon("transactionReplay&maxAllowedPacket=" + maxAllowedPacket)) {
+      bigSendError(con, st, expectClosed);
+    }
+  }
+
+  public void bigSendError(Connection con, String st, boolean expectClose) throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE bigTest");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep = con.prepareStatement("INSERT INTO bigTest VALUES (?, ?)")) {
+      prep.setInt(1, 1);
+      prep.setString(2, st);
+      Common.assertThrowsContains(
+          SQLException.class,
+          prep::execute,
+          "Packet too big for current server max_allowed_packet value");
+      assertEquals(expectClose, con.isClosed());
+    }
+    if (!con.isClosed()) con.commit();
+  }
+
+  @Test
+  public void bigSendErrorMax() throws SQLException {
+    Assumptions.assumeTrue(
+        !"maxscale".equals(System.getenv("srv"))
+            && !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv")));
+
+    int maxAllowedPacket = getMaxAllowedPacket();
+    Assumptions.assumeTrue(
+        maxAllowedPacket > 16 * 1024 * 1024 && maxAllowedPacket < 100 * 1024 * 1024);
+    char[] arr = new char[maxAllowedPacket + 100];
+    for (int pos = 0; pos < arr.length; pos++) {
+      arr[pos] = (char) ('A' + (pos % 60));
+    }
+    String st = new String(arr);
+    try (Connection con = createCon("maxAllowedPacket=" + maxAllowedPacket)) {
+      bigSendErrorMax(con, st, true);
+    }
+    try (Connection con =
+        createCon("useServerPrepStmts=true&maxAllowedPacket=" + maxAllowedPacket)) {
+      bigSendErrorMax(con, st, true);
+    }
+    try (Connection con = createCon("transactionReplay&maxAllowedPacket=" + maxAllowedPacket)) {
+      bigSendError(con, st, true);
+    }
+  }
+
+  public void bigSendErrorMax(Connection con, String st, boolean expectClose) throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE bigTest");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep = con.prepareStatement("INSERT INTO bigTest VALUES (?, ?)")) {
+      prep.setInt(1, 1);
+      prep.setString(2, st);
+      Common.assertThrowsContains(
+          SQLNonTransientConnectionException.class,
+          prep::execute,
+          "Packet too big for current server max_allowed_packet value");
+      assertEquals(expectClose, con.isClosed());
+    }
+  }
+
+  @FunctionalInterface
+  public interface ThrowingConsumer<T, E extends Exception> {
+    void accept(T t) throws E;
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/PreparedStatementTest.java b/src/test/java/org/mariadb/jdbc/integration/PreparedStatementTest.java
new file mode 100644
index 0000000..038957f
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/PreparedStatementTest.java
@@ -0,0 +1,1278 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.ByteArrayInputStream;
+import java.math.BigDecimal;
+import java.sql.*;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.Random;
+import org.junit.jupiter.api.*;
+import org.mariadb.jdbc.Connection;
+import org.mariadb.jdbc.MariaDbBlob;
+import org.mariadb.jdbc.Statement;
+
+public class PreparedStatementTest extends Common {
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS prepare1");
+    stmt.execute("DROP TABLE IF EXISTS prepare2");
+    stmt.execute("DROP TABLE IF EXISTS prepare3");
+    stmt.execute("DROP TABLE IF EXISTS prepare4");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE prepare1 (t1 int not null primary key auto_increment, t2 int)");
+    stmt.execute("CREATE TABLE prepare2 (t1 int not null primary key auto_increment, t2 int)");
+    stmt.execute("CREATE TABLE prepare3 (t1 LONGTEXT, t2 LONGTEXT, t3 LONGTEXT, t4 LONGTEXT)");
+    stmt.execute("CREATE TABLE prepare4 (t1 int)");
+    stmt.execute("INSERT INTO prepare4 VALUES (1),(2),(3),(4),(5)");
+    createSequenceTables();
+  }
+
+  @Test
+  public void prep() throws SQLException {
+    try (PreparedStatement stmt = sharedConn.prepareStatement("SELECT ?")) {
+      assertEquals(ResultSet.TYPE_FORWARD_ONLY, stmt.getResultSetType());
+      assertEquals(ResultSet.CONCUR_READ_ONLY, stmt.getResultSetConcurrency());
+      assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, stmt.getResultSetHoldability());
+      assertEquals(sharedConn, stmt.getConnection());
+    }
+
+    try (PreparedStatement stmt =
+        sharedConn.prepareStatement(
+            "SELECT ?", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE)) {
+      assertEquals(ResultSet.TYPE_SCROLL_INSENSITIVE, stmt.getResultSetType());
+      assertEquals(ResultSet.CONCUR_UPDATABLE, stmt.getResultSetConcurrency());
+      assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, stmt.getResultSetHoldability());
+      assertEquals(sharedConn, stmt.getConnection());
+    }
+
+    try (PreparedStatement stmt =
+        sharedConn.prepareStatement(
+            "SELECT ?",
+            ResultSet.TYPE_SCROLL_INSENSITIVE,
+            ResultSet.CONCUR_UPDATABLE,
+            ResultSet.CLOSE_CURSORS_AT_COMMIT)) {
+      assertEquals(ResultSet.TYPE_SCROLL_INSENSITIVE, stmt.getResultSetType());
+      assertEquals(ResultSet.CONCUR_UPDATABLE, stmt.getResultSetConcurrency());
+      // not supported
+      assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, stmt.getResultSetHoldability());
+      assertEquals(sharedConn, stmt.getConnection());
+    }
+  }
+
+  @Test
+  public void execute() throws SQLException {
+    try (Connection con = createCon("&useServerPrepStmts=false")) {
+      execute(con);
+    }
+    try (Connection con = createCon("&useServerPrepStmts")) {
+      execute(con);
+    }
+    try (Connection con = createCon("&useServerPrepStmts&cachePrepStmts=false")) {
+      execute(con);
+    }
+  }
+
+  private void execute(Connection conn) throws SQLException {
+    Statement stmt = conn.createStatement();
+    stmt.execute("TRUNCATE prepare1");
+    try (PreparedStatement preparedStatement =
+        conn.prepareStatement("INSERT INTO prepare1(t1, t2) VALUES (?,?)")) {
+      preparedStatement.setInt(1, 5);
+      preparedStatement.setInt(2, 10);
+
+      assertFalse(preparedStatement.execute());
+
+      ParameterMetaData paramMeta = preparedStatement.getParameterMetaData();
+      paramMeta.getParameterTypeName(1);
+      paramMeta = preparedStatement.getParameterMetaData();
+      paramMeta.getParameterTypeName(1);
+
+      // verification
+      ResultSet rs = stmt.executeQuery("SELECT * FROM prepare1");
+      assertTrue(rs.next());
+      assertEquals(5, rs.getInt(1));
+      assertEquals(10, rs.getInt(2));
+      assertFalse(rs.next());
+
+      // prepare is already done. must only execute.
+      preparedStatement.setInt(1, 7);
+      preparedStatement.setInt(2, 12);
+      assertFalse(preparedStatement.execute());
+
+      rs = stmt.executeQuery("SELECT * FROM prepare1 WHERE t1 > 5");
+      assertTrue(rs.next());
+      assertEquals(7, rs.getInt(1));
+      assertEquals(12, rs.getInt(2));
+      assertFalse(rs.next());
+    }
+
+    try (PreparedStatement preparedStatement =
+        conn.prepareStatement("SELECT * FROM prepare1 WHERE t1 > ?")) {
+      preparedStatement.setInt(1, 4);
+      assertTrue(preparedStatement.execute());
+      ResultSet rs = preparedStatement.getResultSet();
+      assertTrue(rs.next());
+      assertEquals(5, rs.getInt(1));
+      assertEquals(10, rs.getInt(2));
+      assertTrue(rs.next());
+      assertEquals(7, rs.getInt(1));
+      assertEquals(12, rs.getInt(2));
+      assertFalse(rs.next());
+
+      preparedStatement.setMaxRows(1);
+      preparedStatement.setInt(1, 4);
+      assertTrue(preparedStatement.execute());
+      rs = preparedStatement.getResultSet();
+      assertTrue(rs.next());
+      assertEquals(5, rs.getInt(1));
+      assertEquals(10, rs.getInt(2));
+      if (isMariaDBServer()) {
+        // setMaxRows() has no effect for mysql, since not supporting SET STATEMENT SQL_SELECT_LIMIT
+        assertFalse(rs.next());
+      }
+    }
+
+    // prepare not cached (length > 8K)
+    StringBuilder sql = new StringBuilder("INSERT INTO prepare1(t1, t2) VALUES (?,?) /*");
+    for (int i = 0; i < 900; i++) {
+      sql.append("1234567890");
+    }
+    sql.append("*/");
+    stmt.execute("TRUNCATE prepare1");
+    try (PreparedStatement preparedStatement = conn.prepareStatement(sql.toString())) {
+      preparedStatement.setInt(1, 5);
+      preparedStatement.setInt(2, 10);
+      assertFalse(preparedStatement.execute());
+
+      ParameterMetaData paramMeta = preparedStatement.getParameterMetaData();
+      paramMeta.getParameterTypeName(1);
+      paramMeta = preparedStatement.getParameterMetaData();
+      paramMeta.getParameterTypeName(1);
+
+      // verification
+      ResultSet rs = stmt.executeQuery("SELECT * FROM prepare1");
+      assertTrue(rs.next());
+      assertEquals(5, rs.getInt(1));
+      assertEquals(10, rs.getInt(2));
+      assertFalse(rs.next());
+
+      // prepare is already done. must only execute.
+      preparedStatement.setInt(1, 7);
+      preparedStatement.setInt(2, 12);
+      assertFalse(preparedStatement.execute());
+
+      rs = stmt.executeQuery("SELECT * FROM prepare1 WHERE t1 > 5");
+      assertTrue(rs.next());
+      assertEquals(7, rs.getInt(1));
+      assertEquals(12, rs.getInt(2));
+      assertFalse(rs.next());
+    }
+  }
+
+  @Test
+  public void executeWithoutAllParameters() throws SQLException {
+    executeWithoutAllParameters(sharedConn);
+    executeWithoutAllParameters(sharedConnBinary);
+  }
+
+  public void executeWithoutAllParameters(Connection con) throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE prepare1");
+    try (PreparedStatement preparedStatement =
+        con.prepareStatement("INSERT INTO prepare1(t1, t2) VALUES (?,?)")) {
+      preparedStatement.setInt(2, 10);
+      Common.assertThrowsContains(
+          SQLException.class,
+          preparedStatement::executeUpdate,
+          "Parameter at position 1 is not " + "set");
+
+      preparedStatement.setNull(1, Types.VARBINARY);
+      preparedStatement.executeUpdate();
+      ResultSet rs = stmt.executeQuery("SELECT * FROM prepare1");
+      assertTrue(rs.next());
+      assertEquals(1, rs.getInt(1));
+      assertEquals(10, rs.getInt(2));
+      assertFalse(rs.next());
+    }
+  }
+
+  @Test
+  public void executeUpdate() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("TRUNCATE prepare1");
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement("INSERT INTO prepare1(t1, t2) VALUES (?,?)")) {
+      preparedStatement.setInt(1, 5);
+      preparedStatement.setInt(2, 10);
+      assertEquals(1, preparedStatement.executeUpdate());
+
+      // verification that query without resultset return an empty resultset
+      preparedStatement.clearParameters();
+      Common.assertThrowsContains(
+          SQLException.class,
+          preparedStatement::executeQuery,
+          "Parameter at position 1 is not set");
+      preparedStatement.setInt(2, 11);
+      preparedStatement.setInt(1, 6);
+      ResultSet rs0 = preparedStatement.executeQuery();
+      assertFalse(rs0.next());
+
+      // verification
+      ResultSet rs = stmt.executeQuery("SELECT * FROM prepare1");
+      assertTrue(rs.next());
+      assertEquals(5, rs.getInt(1));
+      assertEquals(10, rs.getInt(2));
+      assertTrue(rs.next());
+      assertEquals(6, rs.getInt(1));
+      assertEquals(11, rs.getInt(2));
+      assertFalse(rs.next());
+    }
+
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement("SELECT * FROM prepare1")) {
+      Common.assertThrowsContains(
+          SQLException.class,
+          preparedStatement::executeUpdate,
+          "the given SQL statement produces an unexpected ResultSet object");
+    }
+  }
+
+  @Test
+  public void executeQuery() throws SQLException {
+    executeQuery(sharedConn);
+    executeQuery(sharedConnBinary);
+    try (Connection con = createCon("useServerPrepStmts=true&enableSkipMeta=false")) {
+      executeQuery(con);
+    }
+    try (Connection con = createCon("useServerPrepStmts=true&enableSkipMeta=true")) {
+      executeQuery(con);
+    }
+  }
+
+  @Test
+  public void tryMaybeNotPreparable() throws SQLException {
+    try (Connection con = createCon("useServerPrepStmts")) {
+      try (PreparedStatement prep = con.prepareStatement("CREATE TABLE maybeCreate(id int)")) {
+        prep.execute();
+      }
+    } finally {
+      sharedConn.createStatement().execute("DROP TABLE IF EXISTS maybeCreate");
+    }
+    try (Connection con = createCon("useServerPrepStmts")) {
+      try (PreparedStatement prep =
+          con.prepareStatement("CREATE PROCEDURE maybeProc(IN  I date) BEGIN SELECT I; END")) {
+        prep.execute();
+      }
+    } finally {
+      sharedConn.createStatement().execute("DROP PROCEDURE IF EXISTS maybeProc");
+    }
+  }
+
+  private void executeQuery(Connection con) throws SQLException {
+    // https://jira.mariadb.org/browse/XPT-282
+    Assumptions.assumeFalse(isXpand());
+    Statement stmt = con.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS prepare10");
+    stmt.execute("CREATE TABLE prepare10 (t1 int not null primary key auto_increment, t2 int)");
+    stmt.execute("INSERT INTO prepare10(t1, t2) VALUES (5,10), (40,20), (127,45)");
+    try (PreparedStatement preparedStatement =
+        con.prepareStatement("SELECT * FROM prepare10 WHERE t1 > ?")) {
+      preparedStatement.setInt(1, 20);
+      ResultSet rs = preparedStatement.executeQuery();
+      assertTrue(rs.next());
+      assertEquals(40, rs.getInt(1));
+      assertEquals(20, rs.getInt(2));
+      assertTrue(rs.next());
+      assertEquals(127, rs.getInt(1));
+      assertEquals(45, rs.getInt(2));
+      assertFalse(rs.next());
+      Common.assertThrowsContains(
+          SQLException.class, () -> preparedStatement.setInt(-20, 2), "wrong parameter index -20");
+
+      preparedStatement.setInt(1, 50);
+      rs = preparedStatement.executeQuery();
+      assertTrue(rs.next());
+      assertEquals(127, rs.getInt(1));
+      assertEquals(45, rs.getInt(2));
+      assertFalse(rs.next());
+      Common.assertThrowsContains(
+          SQLException.class, () -> preparedStatement.setInt(-20, 2), "wrong parameter index -20");
+      stmt.execute("ALTER TABLE prepare10 ADD COLUMN t3 varchar(20) default 'tt'");
+      preparedStatement.setInt(1, 20);
+      rs = preparedStatement.executeQuery();
+      assertTrue(rs.next());
+      assertEquals(40, rs.getInt(1));
+      assertEquals(20, rs.getInt(2));
+      assertEquals("tt", rs.getString(3));
+    }
+
+    try (PreparedStatement preparedStatement =
+        con.prepareStatement("SELECT * FROM prepare10 WHERE t1 > ?")) {
+      preparedStatement.setInt(1, 20);
+      preparedStatement.executeQuery();
+    }
+  }
+
+  @Test
+  public void clearParameters() throws Exception {
+    // error crashing maxscale 6.1.x
+    Assumptions.assumeTrue(
+        !sharedConn.getMetaData().getDatabaseProductVersion().contains("maxScale-6.1.")
+            && !"skysql-ha".equals(System.getenv("srv")));
+    try (org.mariadb.jdbc.Connection con = createCon("&useServerPrepStmts=false")) {
+      clearParameters(con);
+    }
+    try (org.mariadb.jdbc.Connection con = createCon("&useServerPrepStmts")) {
+      clearParameters(con);
+    }
+  }
+
+  public void clearParameters(Connection con) throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE prepare1");
+    try (PreparedStatement preparedStatement =
+        con.prepareStatement("INSERT INTO prepare1(t1, t2) VALUES (?,?)")) {
+      preparedStatement.setInt(1, 5);
+      preparedStatement.setInt(2, 10);
+      preparedStatement.clearParameters();
+      assertThrows(SQLException.class, preparedStatement::execute);
+    }
+  }
+
+  @Test
+  public void closeOnCompletion() throws SQLException {
+    PreparedStatement preparedStatement =
+        sharedConn.prepareStatement("SELECT * FROM prepare1 WHERE t1 > ?");
+    Assertions.assertFalse(preparedStatement.isCloseOnCompletion());
+    preparedStatement.closeOnCompletion();
+    Assertions.assertTrue(preparedStatement.isCloseOnCompletion());
+    Assertions.assertFalse(preparedStatement.isClosed());
+    preparedStatement.setInt(1, 0);
+    ResultSet rs = preparedStatement.executeQuery();
+    Assertions.assertFalse(rs.isClosed());
+    Assertions.assertFalse(preparedStatement.isClosed());
+    rs.close();
+    Assertions.assertTrue(rs.isClosed());
+    Assertions.assertTrue(preparedStatement.isClosed());
+  }
+
+  @Test
+  public void executeBatch() throws SQLException {
+    executeBatch(sharedConn);
+    executeBatch(sharedConnBinary);
+    try (Connection con = createCon("allowLocalInfile=true")) {
+      executeBatch(con);
+    }
+    try (Connection con = createCon("allowLocalInfile=true&useServerPrepStmts=true")) {
+      executeBatch(con);
+    }
+  }
+
+  private void executeBatch(Connection con) throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE prepare1");
+    stmt.execute("START TRANSACTION");
+    try (PreparedStatement preparedStatement =
+        con.prepareStatement("INSERT INTO prepare1(t1, t2) VALUES (?,?)")) {
+
+      try (PreparedStatement preparedStatement2 =
+          con.prepareStatement("INSERT INTO prepare1(t1, t2) VALUES (?,?)")) {
+        preparedStatement2.setInt(1, 15);
+        preparedStatement2.setInt(2, 110);
+        preparedStatement2.addBatch();
+        preparedStatement2.executeBatch();
+      }
+
+      int[] res = preparedStatement.executeBatch();
+      assertEquals(0, res.length);
+      preparedStatement.setInt(1, 5);
+      preparedStatement.setInt(2, 10);
+      preparedStatement.addBatch();
+      res = preparedStatement.executeBatch();
+      assertEquals(1, res.length);
+      res = preparedStatement.executeBatch();
+      assertEquals(0, res.length);
+    }
+
+    try (PreparedStatement preparedStatement =
+        con.prepareStatement("INSERT INTO prepare1(t1, t2) VALUES (?,?)")) {
+      preparedStatement.setInt(1, 40);
+      preparedStatement.setInt(2, 20);
+      preparedStatement.addBatch();
+      preparedStatement.setInt(1, 127);
+      preparedStatement.setInt(2, 45);
+      preparedStatement.addBatch();
+      int[] res = preparedStatement.executeBatch();
+      assertEquals(2, res.length);
+    }
+
+    try (PreparedStatement preparedStatement =
+        con.prepareStatement("SELECT * FROM prepare1 WHERE t1 > ?")) {
+      preparedStatement.setInt(1, 20);
+      ResultSet rs = preparedStatement.executeQuery();
+      assertTrue(rs.next());
+      assertEquals(40, rs.getInt(1));
+      assertEquals(20, rs.getInt(2));
+      assertTrue(rs.next());
+      assertEquals(127, rs.getInt(1));
+      assertEquals(45, rs.getInt(2));
+      assertFalse(rs.next());
+    }
+    con.commit();
+  }
+
+  @Test
+  public void executeWrongBatch() throws SQLException {
+    executeWrongBatch(sharedConn);
+    executeWrongBatch(sharedConnBinary);
+    try (Connection con = createCon("useBulkStmts=false&useServerPrepStmts=true")) {
+      executeWrongBatch(con);
+    }
+  }
+
+  private void executeWrongBatch(Connection con) throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE prepare1");
+    stmt.execute("SET sql_mode = concat(@@sql_mode,',ERROR_FOR_DIVISION_BY_ZERO')");
+    try (PreparedStatement preparedStatement = con.prepareStatement("SELECT 5/?")) {
+      preparedStatement.setInt(1, 5);
+      preparedStatement.addBatch();
+      preparedStatement.executeBatch();
+
+      preparedStatement.setInt(1, 5);
+      preparedStatement.addBatch();
+      preparedStatement.setInt(1, 0);
+      preparedStatement.addBatch();
+      try {
+        preparedStatement.executeBatch();
+      } catch (BatchUpdateException e) {
+        // eat
+      }
+    }
+  }
+
+  @Test
+  public void executeBatchMultiple() throws SQLException {
+    // xpand don't support DO command
+    Assumptions.assumeFalse(isXpand());
+    try (Connection con = createCon("allowMultiQueries&useBulkStmts=false")) {
+      executeBatchMultiple(con);
+    }
+  }
+
+  private void executeBatchMultiple(Connection con) throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE prepare1");
+    try (PreparedStatement preparedStatement =
+        con.prepareStatement("INSERT INTO prepare1(t1, t2) VALUES (?,?);DO 1")) {
+      int[] res = preparedStatement.executeBatch();
+      assertEquals(0, res.length);
+      preparedStatement.setInt(1, 5);
+      preparedStatement.setInt(2, 10);
+      preparedStatement.addBatch();
+      res = preparedStatement.executeBatch();
+      assertEquals(1, res.length);
+      res = preparedStatement.executeBatch();
+      assertEquals(0, res.length);
+    }
+
+    try (PreparedStatement preparedStatement =
+        con.prepareStatement("INSERT INTO prepare1(t1, t2) VALUES (?,?);DO 1")) {
+      preparedStatement.setInt(1, 40);
+      preparedStatement.setInt(2, 20);
+      preparedStatement.addBatch();
+      preparedStatement.setInt(1, 127);
+      preparedStatement.setInt(2, 45);
+      preparedStatement.addBatch();
+      int[] res = preparedStatement.executeBatch();
+      assertEquals(2, res.length);
+    }
+
+    try (PreparedStatement preparedStatement =
+        con.prepareStatement("SELECT * FROM prepare1 WHERE t1 > ?")) {
+      preparedStatement.setInt(1, 20);
+      ResultSet rs = preparedStatement.executeQuery();
+      assertTrue(rs.next());
+      assertEquals(40, rs.getInt(1));
+      assertEquals(20, rs.getInt(2));
+      assertTrue(rs.next());
+      assertEquals(127, rs.getInt(1));
+      assertEquals(45, rs.getInt(2));
+      assertFalse(rs.next());
+    }
+  }
+
+  @Test
+  public void executeLargeBatch() throws SQLException {
+    executeLargeBatch(sharedConn);
+    executeLargeBatch(sharedConnBinary);
+    try (Connection con = createCon("allowLocalInfile=true")) {
+      executeLargeBatch(con);
+    }
+    try (Connection con = createCon("allowLocalInfile=true&useServerPrepStmts=true")) {
+      executeLargeBatch(con);
+    }
+  }
+
+  private void executeLargeBatch(Connection con) throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE prepare1");
+    try (PreparedStatement preparedStatement =
+        con.prepareStatement("INSERT INTO prepare1(t1, t2) VALUES (?,?)")) {
+      preparedStatement.executeLargeBatch();
+      preparedStatement.setInt(1, 5);
+      preparedStatement.setInt(2, 10);
+      preparedStatement.addBatch();
+      preparedStatement.executeLargeBatch();
+      preparedStatement.executeLargeBatch();
+    }
+
+    try (PreparedStatement preparedStatement =
+        con.prepareStatement("INSERT INTO prepare1(t1, t2) VALUES (?,?)")) {
+      preparedStatement.setInt(1, 40);
+      preparedStatement.setInt(2, 20);
+      preparedStatement.addBatch();
+      preparedStatement.setInt(1, 127);
+      preparedStatement.setInt(2, 45);
+      preparedStatement.addBatch();
+      preparedStatement.executeLargeBatch();
+    }
+
+    try (PreparedStatement preparedStatement =
+        con.prepareStatement("SELECT * FROM prepare1 WHERE t1 > ?")) {
+      preparedStatement.setInt(1, 20);
+      ResultSet rs = preparedStatement.executeQuery();
+      assertTrue(rs.next());
+      assertEquals(40, rs.getInt(1));
+      assertEquals(20, rs.getInt(2));
+      assertTrue(rs.next());
+      assertEquals(127, rs.getInt(1));
+      assertEquals(45, rs.getInt(2));
+      assertFalse(rs.next());
+    }
+  }
+
+  @Test
+  public void executeBatchGenerated() throws SQLException {
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement(
+            "INSERT INTO prepare2(t2) VALUES (?)", java.sql.Statement.RETURN_GENERATED_KEYS)) {
+      preparedStatement.setInt(1, 10);
+      preparedStatement.addBatch();
+      preparedStatement.setInt(1, 20);
+      preparedStatement.addBatch();
+      preparedStatement.executeBatch();
+      ResultSet rs = preparedStatement.getGeneratedKeys();
+      assertTrue(rs.next());
+      assertEquals(1, rs.getInt(1));
+      assertTrue(rs.next());
+      assertEquals(2, rs.getInt(1));
+      assertFalse(rs.next());
+    }
+
+    try (PreparedStatement preparedStatement =
+        sharedConnBinary.prepareStatement(
+            "INSERT INTO prepare2(t2) VALUES (?)", java.sql.Statement.RETURN_GENERATED_KEYS)) {
+      preparedStatement.setInt(1, 10);
+      preparedStatement.addBatch();
+      preparedStatement.setInt(1, 20);
+      preparedStatement.addBatch();
+      preparedStatement.executeBatch();
+      ResultSet rs = preparedStatement.getGeneratedKeys();
+      assertTrue(rs.next());
+      assertEquals(3, rs.getInt(1));
+      assertTrue(rs.next());
+      assertEquals(4, rs.getInt(1));
+      assertFalse(rs.next());
+    }
+
+    try (Connection con = createCon("allowMultiQueries")) {
+      try (PreparedStatement preparedStatement =
+          con.prepareStatement(
+              "INSERT INTO prepare2(t2) VALUES (?);INSERT INTO prepare2(t2) VALUES (?)",
+              java.sql.Statement.RETURN_GENERATED_KEYS)) {
+        preparedStatement.setInt(1, 30);
+        preparedStatement.setInt(2, 50);
+        preparedStatement.execute();
+        ResultSet rs = preparedStatement.getGeneratedKeys();
+        assertTrue(rs.next());
+        assertEquals(5, rs.getInt(1));
+        assertTrue(rs.next());
+        assertEquals(6, rs.getInt(1));
+        assertFalse(rs.next());
+
+        preparedStatement.setInt(1, 210);
+        preparedStatement.setInt(2, 110);
+        preparedStatement.addBatch();
+        preparedStatement.setInt(1, 220);
+        preparedStatement.setInt(2, 220);
+        preparedStatement.addBatch();
+        preparedStatement.executeBatch();
+
+        rs = preparedStatement.getGeneratedKeys();
+        assertTrue(rs.next());
+        assertEquals(7, rs.getInt(1));
+        assertTrue(rs.next());
+        assertEquals(8, rs.getInt(1));
+        assertTrue(rs.next());
+        assertEquals(9, rs.getInt(1));
+        assertTrue(rs.next());
+        assertEquals(10, rs.getInt(1));
+        assertFalse(rs.next());
+      }
+    }
+  }
+
+  @Test
+  public void emptyExecuteBatch() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("TRUNCATE prepare1");
+    stmt.execute("INSERT INTO prepare1(t1, t2) VALUES (5,10), (40,20), (127,45)");
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement("SELECT * FROM prepare1 WHERE t1 > ?")) {
+      assertEquals(0, preparedStatement.executeBatch().length);
+    }
+    try (PreparedStatement preparedStatement =
+        sharedConnBinary.prepareStatement("SELECT * FROM prepare1 WHERE t1 > ?")) {
+      assertEquals(0, preparedStatement.executeBatch().length);
+    }
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement("SELECT * FROM prepare1 WHERE t1 > ?")) {
+      assertEquals(0, preparedStatement.executeLargeBatch().length);
+    }
+    try (PreparedStatement preparedStatement =
+        sharedConnBinary.prepareStatement("SELECT * FROM prepare1 WHERE t1 > ?")) {
+      assertEquals(0, preparedStatement.executeLargeBatch().length);
+    }
+  }
+
+  @Test
+  public void moreResults() throws SQLException {
+    try (Connection con = createCon("&useServerPrepStmts=false")) {
+      moreResults(con);
+    }
+    try (Connection con = createCon("&useServerPrepStmts")) {
+      moreResults(con);
+    }
+  }
+
+  private void moreResults(Connection con) throws SQLException {
+    // error MXS-3929 for maxscale 6.2.0
+    Assumptions.assumeTrue(
+        !sharedConn.getMetaData().getDatabaseProductVersion().contains("maxScale-6.2.0"));
+
+    Statement stmt = con.createStatement();
+    stmt.execute("DROP PROCEDURE IF EXISTS multi");
+    stmt.setFetchSize(3);
+    stmt.execute(
+        "CREATE PROCEDURE multi() BEGIN SELECT * from sequence_1_to_10; SELECT * FROM"
+            + " sequence_1_to_10;SELECT 2; END");
+    stmt.execute("CALL multi()");
+    Assertions.assertTrue(stmt.getMoreResults());
+    ResultSet rs = stmt.getResultSet();
+    int i = 1;
+    while (rs.next()) {
+      Assertions.assertEquals(i++, rs.getInt(1));
+    }
+    Assertions.assertEquals(11, i);
+    stmt.setFetchSize(3);
+    PreparedStatement prep = con.prepareStatement("CALL multi()");
+    rs = prep.executeQuery();
+    Assertions.assertFalse(rs.isClosed());
+    prep.setFetchSize(0); // force more result to load all remaining result-set
+    Assertions.assertTrue(prep.getMoreResults());
+    Assertions.assertTrue(rs.isClosed());
+    rs = prep.getResultSet();
+    i = 1;
+    while (rs.next()) {
+      Assertions.assertEquals(i++, rs.getInt(1));
+    }
+
+    prep.setFetchSize(3);
+    rs = prep.executeQuery();
+    Assertions.assertFalse(rs.isClosed());
+    prep.setFetchSize(0); // force more result to load all remaining result-set
+    Assertions.assertTrue(prep.getMoreResults(java.sql.Statement.KEEP_CURRENT_RESULT));
+    Assertions.assertFalse(rs.isClosed());
+    i = 1;
+    while (rs.next()) {
+      Assertions.assertEquals(i++, rs.getInt(1));
+    }
+    Assertions.assertEquals(11, i);
+    rs = prep.getResultSet();
+    i = 1;
+    while (rs.next()) {
+      Assertions.assertEquals(i++, rs.getInt(1));
+    }
+    Assertions.assertEquals(11, i);
+
+    rs = prep.executeQuery();
+    prep.close();
+    assertTrue(rs.isClosed());
+  }
+
+  @Test
+  public void moreRowLimitedResults() throws SQLException {
+    try (Connection con = createCon("&useServerPrepStmts=false")) {
+      moreRowLimitedResults(con);
+    }
+    try (Connection con = createCon("&useServerPrepStmts")) {
+      moreRowLimitedResults(con);
+    }
+  }
+
+  private void moreRowLimitedResults(Connection con) throws SQLException {
+    // error MXS-3929 for maxscale 6.2.0
+    Assumptions.assumeTrue(
+        !sharedConn.getMetaData().getDatabaseProductVersion().contains("maxScale-6.2.0"));
+
+    Statement stmt = con.createStatement();
+    stmt.execute("DROP PROCEDURE IF EXISTS multi");
+    stmt.setFetchSize(3);
+    stmt.setMaxRows(5);
+    stmt.execute(
+        "CREATE PROCEDURE multi() BEGIN SELECT * from prepare4; SELECT * FROM prepare4;SELECT 2;"
+            + " END");
+    stmt.execute("CALL multi()");
+    Assertions.assertTrue(stmt.getMoreResults());
+    ResultSet rs = stmt.getResultSet();
+    int i = 1;
+    while (rs.next()) {
+      Assertions.assertEquals(i++, rs.getInt(1));
+    }
+    Assertions.assertEquals(6, i);
+    stmt.setFetchSize(3);
+    PreparedStatement prep = con.prepareStatement("CALL multi()");
+    prep.setMaxRows(20);
+    rs = prep.executeQuery();
+    Assertions.assertFalse(rs.isClosed());
+    prep.setFetchSize(0); // force more result to load all remaining result-set
+    Assertions.assertTrue(prep.getMoreResults());
+    Assertions.assertTrue(rs.isClosed());
+    rs = prep.getResultSet();
+    i = 1;
+    while (rs.next()) {
+      Assertions.assertEquals(i++, rs.getInt(1));
+    }
+
+    prep.setFetchSize(3);
+    prep.setMaxRows(5);
+    rs = prep.executeQuery();
+    Assertions.assertFalse(rs.isClosed());
+    prep.setFetchSize(0); // force more result to load all remaining result-set
+    Assertions.assertTrue(prep.getMoreResults(java.sql.Statement.KEEP_CURRENT_RESULT));
+    Assertions.assertFalse(rs.isClosed());
+    i = 1;
+    while (rs.next()) {
+      Assertions.assertEquals(i++, rs.getInt(1));
+    }
+    Assertions.assertEquals(6, i);
+    rs = prep.getResultSet();
+    i = 1;
+    while (rs.next()) {
+      Assertions.assertEquals(i++, rs.getInt(1));
+    }
+    Assertions.assertEquals(6, i);
+
+    rs = prep.executeQuery();
+    prep.close();
+    assertTrue(rs.isClosed());
+  }
+
+  @Test
+  public void prepareWithError() throws SQLException {
+    try (Connection con = createCon("&useServerPrepStmts=false")) {
+      prepareWithError(con);
+    }
+    try (Connection con = createCon("&useServerPrepStmts")) {
+      prepareWithError(con);
+    }
+  }
+
+  private void prepareWithError(Connection con) throws SQLException {
+    Assumptions.assumeTrue(isMariaDBServer());
+    Statement stmt = con.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS prepareError");
+    stmt.setFetchSize(3);
+    stmt.execute("CREATE TABLE prepareError(id int primary key, val varchar(10))");
+    stmt.execute("INSERT INTO prepareError(id, val) values (1, 'val1')");
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO prepareError(id, val) VALUES (?,?)")) {
+      prep.setInt(1, 1);
+      prep.setString(2, "val2");
+      try {
+        prep.execute();
+        fail();
+      } catch (SQLException e) {
+        assertTrue(
+            e.getMessage()
+                    .contains("Duplicate key in container: `testj`.`prepareError` Primary key:")
+                || e.getMessage().contains("Duplicate entry '1' for key 'PRIMARY'"));
+      }
+    }
+    try (PreparedStatement prep = con.prepareStatement("Wrong command")) {
+      try {
+        prep.execute();
+        fail();
+      } catch (SQLException e) {
+        assertTrue(
+            e.getMessage().contains("syntax error: syntax error near")
+                || e.getMessage().contains("You have an error in your SQL syntax"));
+      }
+    }
+  }
+
+  @Test
+  public void streamNotFinished() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.setFetchSize(2);
+    ResultSet rs = stmt.executeQuery("SELECT * FROM sequence_1_to_10");
+
+    Statement stmt2 = sharedConn.createStatement();
+    ResultSet rs2 = stmt2.executeQuery("SELECT 1");
+    rs2.next();
+    assertEquals(1, rs2.getInt(1));
+    for (int i = 1; i <= 10; i++) {
+      rs.next();
+      assertEquals(i, rs.getInt(1));
+    }
+  }
+
+  @Test
+  public void expectedError() throws SQLException {
+    try (PreparedStatement prep = sharedConn.prepareStatement("SELECT ?")) {
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> prep.addBatch("SELECT 1"),
+          "addBatch(String sql) cannot be called on preparedStatement");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> prep.execute("SELECT 1"),
+          "execute(String sql) cannot be called on preparedStatement");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> prep.execute("SELECT 1", Statement.NO_GENERATED_KEYS),
+          "execute(String sql, int autoGeneratedKeys) cannot be called on preparedStatement");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> prep.execute("SELECT 1", new int[] {}),
+          "execute(String sql, int[] columnIndexes) cannot be called on preparedStatement");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> prep.execute("SELECT 1", new String[] {}),
+          "execute(String sql, String[] columnNames) cannot be called on preparedStatement");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> prep.executeQuery("SELECT 1"),
+          "executeQuery(String sql) cannot be called on preparedStatement");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> prep.executeUpdate("SELECT 1"),
+          "executeUpdate(String sql) cannot be called on preparedStatement");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> prep.executeUpdate("SELECT 1", Statement.NO_GENERATED_KEYS),
+          "executeUpdate(String sql, int autoGeneratedKeys) cannot be called on preparedStatement");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> prep.executeUpdate("SELECT 1", new int[] {}),
+          "executeUpdate(String sql, int[] columnIndexes) cannot be called on preparedStatement");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> prep.executeUpdate("SELECT 1", new String[] {}),
+          "executeUpdate(String sql, String[] columnNames) cannot be called on preparedStatement");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> prep.executeLargeUpdate("SELECT 1"),
+          "executeLargeUpdate(String sql) cannot be called on preparedStatement");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> prep.executeLargeUpdate("SELECT 1", Statement.NO_GENERATED_KEYS),
+          "executeLargeUpdate(String sql, int autoGeneratedKeys) cannot be called on"
+              + " preparedStatement");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> prep.executeLargeUpdate("SELECT 1", new int[] {}),
+          "executeLargeUpdate(String sql, int[] columnIndexes) cannot be called on"
+              + " preparedStatement");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> prep.executeLargeUpdate("SELECT 1", new String[] {}),
+          "executeLargeUpdate(String sql, String[] columnNames) cannot be called on"
+              + " preparedStatement");
+    }
+  }
+
+  @Test
+  public void largeMaxRows() throws SQLException {
+    try (Connection con = createCon("&useServerPrepStmts=false")) {
+      largeMaxRows(con);
+    }
+    try (Connection con = createCon("&useServerPrepStmts")) {
+      largeMaxRows(con);
+    }
+  }
+
+  private void largeMaxRows(Connection con) throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS largeMaxRows");
+    stmt.setFetchSize(3);
+    stmt.execute("CREATE TABLE largeMaxRows(id int)");
+    try (PreparedStatement prep = con.prepareStatement("INSERT INTO largeMaxRows(id) VALUE (?)")) {
+      for (int i = 1; i < 51; i++) {
+        prep.setInt(1, i);
+        prep.execute();
+      }
+    }
+
+    try (PreparedStatement prep = con.prepareStatement("SELECT * FROM largeMaxRows")) {
+      assertEquals(0L, prep.getLargeMaxRows());
+      ResultSet rs = prep.executeQuery();
+      int i = 0;
+      while (rs.next()) {
+        i++;
+        assertEquals(i, rs.getInt(1));
+      }
+      assertEquals(50, i);
+
+      try {
+        prep.setLargeMaxRows(-1);
+        Assertions.fail();
+      } catch (SQLException e) {
+        assertTrue(e.getMessage().contains("max rows cannot be negative"));
+      }
+      prep.setLargeMaxRows(10);
+      assertEquals(10L, prep.getLargeMaxRows());
+
+      rs = prep.executeQuery();
+      i = 0;
+      while (rs.next()) {
+        i++;
+        assertEquals(i, rs.getInt(1));
+      }
+      assertEquals(10, i);
+
+      prep.setQueryTimeout(2);
+      rs = prep.executeQuery();
+      i = 0;
+      while (rs.next()) {
+        i++;
+        assertEquals(i, rs.getInt(1));
+      }
+      assertEquals(10, i);
+
+      prep.setQueryTimeout(20);
+      prep.setLargeMaxRows(0);
+      rs = prep.executeQuery();
+      i = 0;
+      while (rs.next()) {
+        i++;
+        assertEquals(i, rs.getInt(1));
+      }
+      assertEquals(50, i);
+      prep.setQueryTimeout(0);
+      prep.setQueryTimeout(0);
+    }
+  }
+
+  @Test
+  public void largeMaxRowsBatch() throws SQLException {
+    try (Connection con = createCon("&useServerPrepStmts=false")) {
+      largeMaxRowsBatch(con);
+    }
+    try (Connection con = createCon("&useServerPrepStmts")) {
+      largeMaxRowsBatch(con);
+    }
+  }
+
+  private void prepareInsert(PreparedStatement prep) throws SQLException {
+    prep.setInt(1, 0);
+    prep.addBatch();
+    prep.setInt(1, 1);
+    prep.addBatch();
+    prep.executeBatch();
+  }
+
+  private void largeMaxRowsBatch(Connection con) throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS large_max_rows_batch");
+    stmt.setFetchSize(3);
+    stmt.execute("CREATE TABLE large_max_rows_batch(id int)");
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO large_max_rows_batch(id) VALUE (?)")) {
+      prepareInsert(prep);
+
+      prep.setMaxRows(1);
+      prepareInsert(prep);
+
+      prep.setQueryTimeout(1);
+      prepareInsert(prep);
+
+      prep.setMaxRows(0);
+      prepareInsert(prep);
+
+      prep.setLargeMaxRows(2);
+      prepareInsert(prep);
+
+      prep.setQueryTimeout(0);
+      prepareInsert(prep);
+    }
+    ResultSet rs = stmt.executeQuery("SELECT count(*) FROM large_max_rows_batch");
+    rs.next();
+    assertEquals(12, rs.getInt(1));
+  }
+
+  @Test
+  public void decrementCache() throws SQLException {
+    decrementCache("&useServerPrepStmts=true&prepStmtCacheSize=5");
+    decrementCache("&useServerPrepStmts=true&useServerPrepStmts=false");
+  }
+
+  public void decrementCache(String connString) throws SQLException {
+    try (Connection con = createCon(connString)) {
+      PreparedStatement prep = con.prepareStatement("SELECT 1");
+      prep.execute();
+      PreparedStatement prep2 = con.prepareStatement("SELECT 1");
+      prep2.execute();
+
+      for (int i = 1; i < 10; i++) {
+        try (PreparedStatement prep1 = con.prepareStatement("SELECT " + i)) {
+          prep1.execute();
+          prep1.setQueryTimeout(1);
+        }
+      }
+
+      prep.setQueryTimeout(1); // will close prepare
+      prep.setQueryTimeout(2); // will close prepare
+      prep.execute();
+      prep.close();
+      prep2.close();
+    }
+  }
+
+  @Test
+  public void prepareStatementConcur() throws SQLException {
+    try (Connection con = createCon("&useServerPrepStmts=false")) {
+      prepareStatementConcur(con);
+    }
+    try (Connection con = createCon("&useServerPrepStmts")) {
+      prepareStatementConcur(con);
+    }
+    try (Connection con = createCon("&useServerPrepStmts=false&disablePipeline")) {
+      prepareStatementConcur(con);
+    }
+  }
+
+  private void prepareStatementConcur(Connection con) throws SQLException {
+    try (PreparedStatement prep = con.prepareStatement("SELECT 1", new int[] {})) {
+      prep.execute();
+    }
+
+    try (PreparedStatement prep = con.prepareStatement("SELECT 1", new String[] {})) {
+      prep.execute();
+    }
+
+    try (PreparedStatement prep =
+        con.prepareStatement("SELECT 1", Statement.RETURN_GENERATED_KEYS)) {
+      assertEquals(ResultSet.CONCUR_READ_ONLY, prep.getResultSetConcurrency());
+      assertEquals(ResultSet.TYPE_FORWARD_ONLY, prep.getResultSetType());
+      prep.execute();
+    }
+    try (PreparedStatement prep =
+        con.prepareStatement(
+            "SELECT 1", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE)) {
+      assertEquals(ResultSet.CONCUR_UPDATABLE, prep.getResultSetConcurrency());
+      assertEquals(ResultSet.TYPE_SCROLL_INSENSITIVE, prep.getResultSetType());
+
+      prep.execute();
+    }
+  }
+
+  @Test
+  public void more2BytesParameters() throws Throwable {
+    int[] rnds = new int[100000];
+    StringBuilder sb = new StringBuilder("select ?");
+    for (int i = 0; i < 100000; i++) {
+      rnds[i] = (int) (Math.random() * 1000);
+    }
+    for (int i = 1; i < 100000; i++) {
+      sb.append(",?");
+    }
+    String sql = sb.toString();
+
+    try (PreparedStatement st = sharedConnBinary.prepareStatement(sql)) {
+      for (int i = 1; i <= 100000; i++) {
+        st.setInt(i, rnds[i - 1]);
+      }
+      try {
+        st.executeQuery();
+        fail();
+      } catch (SQLException e) {
+        assertTrue(
+            e.getMessage().contains("memory exhausted near \",\"")
+                || e.getMessage().contains("Prepared statement contains too many placeholders"));
+      }
+    }
+    assertTrue(sharedConnBinary.isValid(1));
+  }
+
+  private String generateLongText(int len) {
+    int leftLimit = 97; // letter 'a'
+    int rightLimit = 122; // letter 'z'
+    Random random = new Random();
+    return random
+        .ints(leftLimit, rightLimit)
+        .limit(len)
+        .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
+        .toString();
+  }
+
+  @Test
+  public void skippingRes() throws SQLException {
+    int maxAllowedPacket = getMaxAllowedPacket();
+    Assumptions.assumeTrue(maxAllowedPacket > 35_000_000);
+    skippingRes(sharedConn);
+    skippingRes(sharedConnBinary);
+    try (Connection compressText =
+        createCon("useCompression&maxAllowedPacket=" + maxAllowedPacket)) {
+      skippingRes(compressText);
+    }
+    try (Connection compressBinary =
+        createCon("useCompression&useServerPrepStmts&maxAllowedPacket=" + maxAllowedPacket)) {
+      skippingRes(compressBinary);
+    }
+  }
+
+  private void skippingRes(java.sql.Connection con) throws SQLException {
+    con.createStatement().execute("TRUNCATE prepare3");
+    String longText = generateLongText(20_000_000);
+    String mediumText = generateLongText(10_000_000);
+    String smallIntText = generateLongText(60_000);
+
+    try (PreparedStatement prep = con.prepareStatement("INSERT INTO prepare3 values (?,?,?,?)")) {
+      prep.setString(1, longText);
+      prep.setString(2, mediumText);
+      prep.setString(3, smallIntText);
+      prep.setString(4, "expected");
+      prep.execute();
+    }
+
+    try (PreparedStatement prep = con.prepareStatement("SELECT * FROM prepare3")) {
+      ResultSet rs = prep.executeQuery();
+      rs.next();
+      assertEquals("expected", rs.getString(4));
+      assertEquals(smallIntText, rs.getString(3));
+      assertEquals(mediumText, rs.getString(2));
+      assertEquals(longText, rs.getString(1));
+    }
+  }
+
+  @Test
+  public void wrongPosition() throws SQLException {
+    try (PreparedStatement prep = sharedConn.prepareStatement("SELECT 1 FROM DUAL WHERE 0=1")) {
+      ResultSet rs = prep.executeQuery();
+      Common.assertThrowsContains(SQLException.class, () -> rs.getString(1), "wrong row position");
+      Common.assertThrowsContains(
+          SQLException.class, () -> rs.getString("1"), "wrong row position");
+      Common.assertThrowsContains(
+          SQLException.class, () -> rs.getObject(1, String.class), "wrong row position");
+    }
+    try (PreparedStatement prep = sharedConn.prepareStatement("SELECT 1 FROM DUAL")) {
+      ResultSet rs = prep.executeQuery();
+      rs.next();
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> rs.getString(-1),
+          "Wrong index position. Is -1 but must be in 1-1 range");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> rs.getString(10),
+          "Wrong index position. Is 10 but must be in 1-1 range");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> rs.getObject(-1, String.class),
+          "Wrong index position. Is -1 but must be in 1-1 range");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> rs.getObject(10, String.class),
+          "Wrong index position. Is 10 but must be in 1-1 range");
+    }
+  }
+
+  @Test
+  public void prepareStmtToString() throws SQLException {
+    prepareStmtToString(sharedConn, "ClientPreparedStatement");
+    prepareStmtToString(sharedConnBinary, "ServerPreparedStatement");
+  }
+
+  public void prepareStmtToString(java.sql.Connection conn, String prefix) throws SQLException {
+    try (PreparedStatement preparedStatement =
+        conn.prepareStatement("INSERT INTO prepare1(t1, t2) VALUES (?,?)")) {
+
+      assertEquals(
+          prefix + "{sql:'INSERT INTO prepare1(t1, t2) VALUES (?,?)', parameters:[]}",
+          preparedStatement.toString());
+
+      preparedStatement.setInt(1, 5);
+      preparedStatement.setLong(2, 10L);
+      assertEquals(
+          prefix + "{sql:'INSERT INTO prepare1(t1, t2) VALUES (?,?)', parameters:[5,10]}",
+          preparedStatement.toString());
+
+      preparedStatement.setNull(2, Types.VARBINARY);
+      assertEquals(
+          prefix + "{sql:'INSERT INTO prepare1(t1, t2) VALUES (?,?)', parameters:[5,null]}",
+          preparedStatement.toString());
+      preparedStatement.setNull(1, Types.VARBINARY);
+      preparedStatement.setBytes(2, new byte[] {(byte) 'a', (byte) 'b'});
+      assertEquals(
+          prefix
+              + "{sql:'INSERT INTO prepare1(t1, t2) VALUES (?,?)', parameters:[null,_binary 'ab']}",
+          preparedStatement.toString());
+
+      preparedStatement.setBlob(1, new MariaDbBlob(new byte[] {0, 1, 2, 3, 4, 5}));
+      preparedStatement.setAsciiStream(2, new ByteArrayInputStream("test3".getBytes()));
+      assertEquals(
+          prefix
+              + "{sql:'INSERT INTO prepare1(t1, t2) VALUES (?,?)', parameters:[<interface"
+              + " java.sql.Blob>,<class java.io.InputStream>]}",
+          preparedStatement.toString());
+
+      preparedStatement.setObject(1, LocalDateTime.parse("2010-01-12T01:55:12"));
+      preparedStatement.setObject(2, LocalDate.parse("2011-01-01"));
+      assertEquals(
+          prefix
+              + "{sql:'INSERT INTO prepare1(t1, t2) VALUES (?,?)', parameters:['2010-01-12"
+              + " 01:55:12','2011-01-01']}",
+          preparedStatement.toString());
+
+      preparedStatement.setBigDecimal(1, new BigDecimal("158.9876543210"));
+      preparedStatement.setString(2, "test");
+      assertEquals(
+          prefix
+              + "{sql:'INSERT INTO prepare1(t1, t2) VALUES (?,?)',"
+              + " parameters:[158.9876543210,'test']}",
+          preparedStatement.toString());
+
+      preparedStatement.setString(3, "unusedParam");
+      assertEquals(
+          prefix
+              + "{sql:'INSERT INTO prepare1(t1, t2) VALUES (?,?)',"
+              + " parameters:[158.9876543210,'test','unusedParam']}",
+          preparedStatement.toString());
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/ProcedureParameterTest.java b/src/test/java/org/mariadb/jdbc/integration/ProcedureParameterTest.java
new file mode 100644
index 0000000..7382bfb
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/ProcedureParameterTest.java
@@ -0,0 +1,312 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.math.BigDecimal;
+import java.sql.*;
+import java.util.BitSet;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+
+public class ProcedureParameterTest extends Common {
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE PROCEDURE useParameterName(a int) begin select a; end");
+    stmt.execute(
+        "CREATE PROCEDURE withStrangeParameter(IN a DECIMAL(10,2)) begin select a as b; end");
+    stmt.execute("FLUSH TABLES");
+  }
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP PROCEDURE IF EXISTS useParameterName");
+    stmt.execute("DROP PROCEDURE IF EXISTS withStrangeParameter");
+  }
+
+  @Test
+  public void callUseParameterName() throws Exception {
+    // error MXS-3929 for maxscale 6.2.0
+    Assumptions.assumeTrue(
+        !sharedConn.getMetaData().getDatabaseProductVersion().contains("maxScale-6.2.0"));
+    // https://jira.mariadb.org/browse/XPT-267
+    Assumptions.assumeFalse(isXpand());
+
+    CallableStatement stmt = sharedConn.prepareCall("{call useParameterName(?)}");
+    stmt.setInt("a", 1);
+    ResultSet rs = stmt.executeQuery();
+    assertTrue(rs.next());
+    int res = rs.getInt(1);
+    assertEquals(res, 1);
+  }
+
+  @Test
+  public void callWithStrangeParameter() throws SQLException {
+    // error MXS-3929 for maxscale 6.2.0
+    Assumptions.assumeTrue(
+        !sharedConn.getMetaData().getDatabaseProductVersion().contains("maxScale-6.2.0"));
+
+    // https://jira.mariadb.org/browse/XPT-267
+    Assumptions.assumeFalse(isXpand());
+
+    try (CallableStatement call = sharedConn.prepareCall("{call withStrangeParameter(?)}")) {
+      double expected = 5.43;
+      call.setDouble("a", expected);
+      try (ResultSet rs = call.executeQuery()) {
+        assertTrue(rs.next());
+        double res = rs.getDouble("b");
+        assertEquals(expected, res, 0);
+        // now fail due to three decimals
+        double tooMuch = 34.987;
+        call.setDouble("a", tooMuch);
+        try (ResultSet rs2 = call.executeQuery()) {
+          assertTrue(rs2.next());
+          assertNotEquals(rs2.getDouble("b"), tooMuch);
+        }
+      }
+    }
+  }
+
+  @Test
+  public void basicProcedure() throws SQLException {
+    // error MXS-3929 for maxscale 6.2.0
+    Assumptions.assumeTrue(
+        !sharedConn.getMetaData().getDatabaseProductVersion().contains("maxScale-6.2.0"));
+    // https://jira.mariadb.org/browse/XPT-267
+    Assumptions.assumeFalse(isXpand());
+
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP PROCEDURE IF EXISTS basic_proc");
+    stmt.execute(
+        "CREATE PROCEDURE basic_proc (INOUT t1 INT, IN t2 MEDIUMINT unsigned, OUT t3 DECIMAL(8,3),"
+            + " OUT t4 VARCHAR(20), IN t5 SMALLINT) BEGIN \n"
+            + "set t3 = t1 * t5;\n"
+            + "set t1 = t2 * t1;\n"
+            + "set t4 = 'return data';\n"
+            + "END");
+    try (CallableStatement callableStatement =
+        sharedConn.prepareCall("{call basic_proc(?,?,?,?)}")) {
+      ParameterMetaData meta = callableStatement.getParameterMetaData();
+      assertEquals(5, meta.getParameterCount());
+      assertEquals("int", meta.getParameterClassName(1));
+      assertEquals("int", meta.getParameterClassName(2));
+      assertEquals("java.math.BigDecimal", meta.getParameterClassName(3));
+      assertEquals("java.lang.String", meta.getParameterClassName(4));
+      assertEquals("short", meta.getParameterClassName(5));
+      Common.assertThrowsContains(
+          SQLException.class, () -> meta.getParameterClassName(6), "invalid parameter index 6");
+
+      assertEquals("INT", meta.getParameterTypeName(1));
+      assertEquals("MEDIUMINT", meta.getParameterTypeName(2));
+      assertEquals("DECIMAL", meta.getParameterTypeName(3));
+      assertEquals("VARCHAR", meta.getParameterTypeName(4));
+      assertEquals("SMALLINT", meta.getParameterTypeName(5));
+      Common.assertThrowsContains(
+          SQLException.class, () -> meta.getParameterTypeName(0), "invalid parameter index 0");
+
+      assertEquals(Types.INTEGER, meta.getParameterType(1));
+      assertEquals(Types.INTEGER, meta.getParameterType(2));
+      assertEquals(Types.DECIMAL, meta.getParameterType(3));
+      assertEquals(Types.VARCHAR, meta.getParameterType(4));
+      assertEquals(Types.SMALLINT, meta.getParameterType(5));
+      Common.assertThrowsContains(
+          SQLException.class, () -> meta.getParameterType(0), "invalid parameter index 0");
+
+      assertEquals(ParameterMetaData.parameterModeInOut, meta.getParameterMode(1));
+      assertEquals(ParameterMetaData.parameterModeIn, meta.getParameterMode(2));
+      assertEquals(ParameterMetaData.parameterModeOut, meta.getParameterMode(3));
+      assertEquals(ParameterMetaData.parameterModeOut, meta.getParameterMode(4));
+      assertEquals(ParameterMetaData.parameterModeIn, meta.getParameterMode(5));
+      Common.assertThrowsContains(
+          SQLException.class, () -> meta.getParameterMode(10), "invalid parameter index 10");
+
+      assertEquals(10, meta.getPrecision(1));
+      assertEquals(8, meta.getPrecision(3));
+      assertEquals(20, meta.getPrecision(4));
+      assertEquals(5, meta.getPrecision(5));
+      Common.assertThrowsContains(
+          SQLException.class, () -> meta.getPrecision(10), "invalid parameter index 10");
+
+      assertEquals(0, meta.getScale(1));
+      assertEquals(0, meta.getScale(2));
+      assertEquals(3, meta.getScale(3));
+      assertEquals(0, meta.getScale(4));
+      assertEquals(0, meta.getScale(5));
+      Common.assertThrowsContains(
+          SQLException.class, () -> meta.getScale(10), "invalid parameter index 10");
+
+      assertTrue(meta.isSigned(1));
+      assertFalse(meta.isSigned(2));
+
+      assertEquals(ParameterMetaData.parameterNullableUnknown, meta.isNullable(1));
+      assertEquals(ParameterMetaData.parameterNullableUnknown, meta.isNullable(2));
+
+      assertNotNull(meta.unwrap(org.mariadb.jdbc.CallableParameterMetaData.class));
+      assertNotNull(meta.unwrap(ParameterMetaData.class));
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> meta.unwrap(String.class),
+          "The receiver is not a wrapper for java.lang.String");
+      assertTrue(meta.isWrapperFor(org.mariadb.jdbc.CallableParameterMetaData.class));
+      assertTrue(meta.isWrapperFor(ParameterMetaData.class));
+      assertFalse(meta.isWrapperFor(String.class));
+    }
+  }
+
+  @Test
+  public void getParameterTypeProcedure() throws SQLException {
+    // https://jira.mariadb.org/browse/XPT-267
+    Assumptions.assumeFalse(isXpand());
+
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP PROCEDURE IF EXISTS procType");
+    stmt.execute(
+        "CREATE PROCEDURE procType ("
+            + "IN t1 TINYINT, "
+            + "IN t2 SMALLINT, "
+            + "IN t3 MEDIUMINT, "
+            + "IN t4 INT, "
+            + "IN t5 BIGINT, "
+            + "IN t6 DECIMAL(6,3), "
+            + "IN t7 FLOAT, "
+            + "IN t8 DOUBLE, "
+            + "IN t9 BIT, "
+            + "IN t10 CHAR(5), "
+            + "IN t11 VARCHAR(6), "
+            + "IN t12 BINARY(6), "
+            + "IN t13 VARBINARY(6), "
+            + "IN t14 TINYBLOB, "
+            + "IN t15 BLOB, "
+            + "IN t16 MEDIUMBLOB, "
+            + "IN t17 LONGBLOB, "
+            + "IN t18 TINYTEXT, "
+            + "IN t19 TEXT, "
+            + "IN t20 MEDIUMTEXT, "
+            + "IN t21 LONGTEXT, "
+            + "IN t22 ENUM('value1','value2'), "
+            + "IN t23 DATE, "
+            + "IN t24 TIME, "
+            + "IN t25 TIMESTAMP, "
+            + "IN t26 DATETIME, "
+            + "IN t27 YEAR, "
+            + "IN t28 NUMERIC "
+            + ")\n BEGIN \n"
+            + "SELECT 1;\n"
+            + "END");
+    try (CallableStatement callableStatement =
+        sharedConn.prepareCall(
+            "{call procType(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)}")) {
+      ParameterMetaData meta = callableStatement.getParameterMetaData();
+
+      String[] expectedTypeName =
+          new String[] {
+            "TINYINT",
+            "SMALLINT",
+            "MEDIUMINT",
+            "INT",
+            "BIGINT",
+            "DECIMAL",
+            "FLOAT",
+            "DOUBLE",
+            "BIT",
+            "CHAR",
+            "VARCHAR",
+            "BINARY",
+            "VARBINARY",
+            "TINYBLOB",
+            "BLOB",
+            "MEDIUMBLOB",
+            "LONGBLOB",
+            "TINYTEXT",
+            "TEXT",
+            "MEDIUMTEXT",
+            "LONGTEXT",
+            "ENUM",
+            "DATE",
+            "TIME",
+            "TIMESTAMP",
+            "DATETIME",
+            "YEAR",
+            "DECIMAL"
+          };
+      int[] expectedType =
+          new int[] {
+            Types.TINYINT,
+            Types.SMALLINT,
+            Types.INTEGER,
+            Types.INTEGER,
+            Types.BIGINT,
+            Types.DECIMAL,
+            Types.FLOAT,
+            Types.DOUBLE,
+            Types.BIT,
+            Types.CHAR,
+            Types.VARCHAR,
+            Types.BINARY,
+            Types.VARBINARY,
+            Types.BLOB,
+            Types.BLOB,
+            Types.BLOB,
+            Types.BLOB,
+            Types.VARCHAR,
+            Types.CLOB,
+            Types.CLOB,
+            Types.CLOB,
+            Types.VARCHAR,
+            Types.DATE,
+            Types.TIME,
+            Types.TIMESTAMP,
+            Types.TIMESTAMP,
+            Types.SMALLINT,
+            Types.DECIMAL
+          };
+
+      String[] expectedClass =
+          new String[] {
+            byte.class.getName(),
+            short.class.getName(),
+            int.class.getName(),
+            int.class.getName(),
+            long.class.getName(),
+            BigDecimal.class.getName(),
+            float.class.getName(),
+            double.class.getName(),
+            BitSet.class.getName(),
+            String.class.getName(),
+            String.class.getName(),
+            byte[].class.getName(),
+            byte[].class.getName(),
+            byte[].class.getName(),
+            Blob.class.getName(),
+            Blob.class.getName(),
+            Blob.class.getName(),
+            String.class.getName(),
+            Clob.class.getName(),
+            Clob.class.getName(),
+            Clob.class.getName(),
+            String.class.getName(),
+            Date.class.getName(),
+            Time.class.getName(),
+            Timestamp.class.getName(),
+            Timestamp.class.getName(),
+            short.class.getName(),
+            BigDecimal.class.getName()
+          };
+      for (int i = 1; i < 29; i++) {
+        assertEquals(expectedTypeName[i - 1], meta.getParameterTypeName(i));
+        assertEquals(expectedType[i - 1], meta.getParameterType(i));
+        assertEquals(expectedClass[i - 1], meta.getParameterClassName(i));
+      }
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/ProcedureTest.java b/src/test/java/org/mariadb/jdbc/integration/ProcedureTest.java
new file mode 100644
index 0000000..53d8568
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/ProcedureTest.java
@@ -0,0 +1,1129 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.StringReader;
+import java.math.BigDecimal;
+import java.net.URL;
+import java.sql.*;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.MariaDbBlob;
+import org.mariadb.jdbc.MariaDbClob;
+import org.mariadb.jdbc.Statement;
+
+public class ProcedureTest extends Common {
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS procedure_test");
+    stmt.execute("DROP PROCEDURE IF EXISTS multiply_by_2");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE procedure_test (t0 int)");
+    stmt.execute("CREATE PROCEDURE multiply_by_2 (INOUT t1 INT) BEGIN \nset t1 = t1 * 2;\nEND");
+  }
+
+  @Test
+  public void settingParameterBeforeOutRegistration() throws SQLException {
+    try (CallableStatement cstmt = sharedConn.prepareCall("{ CALL multiply_by_2(?) }")) {
+      cstmt.setLong(1, 42L);
+      cstmt.registerOutParameter(1, Types.NUMERIC);
+      cstmt.executeQuery();
+      assertEquals(84, cstmt.getLong(1));
+    }
+  }
+
+  @Test
+  public void wrongCall() {
+    Common.assertThrowsContains(
+        SQLException.class, () -> sharedConn.prepareCall("SELECT ?"), "invalid callable syntax");
+  }
+
+  @Test
+  public void prepInsert() throws SQLException {
+    // https://jira.mariadb.org/browse/XPT-267
+    Assumptions.assumeFalse(isXpand());
+
+    Statement st = sharedConn.createStatement();
+    st.execute("DROP PROCEDURE IF EXISTS prep_proc2");
+    st.execute(
+        "CREATE PROCEDURE prep_proc2 (IN t1 INT) BEGIN \n"
+            + "INSERT INTO procedure_test(t0) VALUE (t1);\n"
+            + "END");
+
+    try (PreparedStatement stmt = sharedConn.prepareCall("CALL prep_proc2(?)")) {
+      stmt.setInt(1, 1);
+      stmt.execute();
+    }
+  }
+
+  @Test
+  public void prep() throws SQLException {
+    Statement st = sharedConn.createStatement();
+    st.execute("DROP PROCEDURE IF EXISTS prep_proc");
+    st.execute("CREATE PROCEDURE prep_proc (IN t1 INT) BEGIN \n" + "SELECT t1;\n" + "END");
+
+    try (PreparedStatement stmt = sharedConn.prepareCall("CALL prep_proc(?)")) {
+      assertEquals(ResultSet.TYPE_FORWARD_ONLY, stmt.getResultSetType());
+      assertEquals(ResultSet.CONCUR_READ_ONLY, stmt.getResultSetConcurrency());
+      assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, stmt.getResultSetHoldability());
+      assertEquals(sharedConn, stmt.getConnection());
+    }
+
+    try (PreparedStatement stmt =
+        sharedConn.prepareCall(
+            "CALL prep_proc(?)", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE)) {
+      assertEquals(ResultSet.TYPE_SCROLL_INSENSITIVE, stmt.getResultSetType());
+      assertEquals(ResultSet.CONCUR_UPDATABLE, stmt.getResultSetConcurrency());
+      assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, stmt.getResultSetHoldability());
+      assertEquals(sharedConn, stmt.getConnection());
+    }
+
+    try (PreparedStatement stmt =
+        sharedConn.prepareCall(
+            "CALL prep_proc(?)",
+            ResultSet.TYPE_SCROLL_INSENSITIVE,
+            ResultSet.CONCUR_UPDATABLE,
+            ResultSet.CLOSE_CURSORS_AT_COMMIT)) {
+      assertEquals(ResultSet.TYPE_SCROLL_INSENSITIVE, stmt.getResultSetType());
+      assertEquals(ResultSet.CONCUR_UPDATABLE, stmt.getResultSetConcurrency());
+      // not supported
+      assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, stmt.getResultSetHoldability());
+      assertEquals(sharedConn, stmt.getConnection());
+    }
+  }
+
+  @Test
+  @SuppressWarnings("deprecated")
+  public void basicProcedure() throws Throwable {
+    // error MXS-3929 for maxscale 6.2.0
+    Assumptions.assumeTrue(
+        !sharedConn.getMetaData().getDatabaseProductVersion().contains("maxScale-6.2.0"));
+    // https://jira.mariadb.org/browse/XPT-267
+    Assumptions.assumeFalse(isXpand());
+
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP PROCEDURE IF EXISTS basic_proc");
+    stmt.execute(
+        "CREATE PROCEDURE basic_proc (IN t1 INT, INOUT t2 INT unsigned, OUT t3 INT, IN t4 INT, OUT"
+            + " t5 VARCHAR(20), OUT t6 TIMESTAMP, OUT t7 blob) BEGIN \n"
+            + "SELECT 1;\n"
+            + "set t3 = t1 * t4;\n"
+            + "set t2 = t2 * t1;\n"
+            + "set t5 = 'http://test';\n"
+            + "set t6 = TIMESTAMP('2003-12-31 12:00:00');\n"
+            + "set t7 = 'test';\n"
+            + "END");
+    try (CallableStatement callableStatement =
+        sharedConn.prepareCall("{call basic_proc(?,?,?,?,?,?,?)}")) {
+      Common.assertThrowsContains(
+          SQLException.class, () -> callableStatement.getString(1), "No output result");
+      callableStatement.getParameterMetaData();
+      Common.assertThrowsContains(
+          SQLSyntaxErrorException.class,
+          () -> callableStatement.registerOutParameter(20, JDBCType.INTEGER),
+          "wrong parameter index 20");
+
+      callableStatement.registerOutParameter(2, JDBCType.INTEGER);
+      callableStatement.registerOutParameter(3, JDBCType.INTEGER);
+      callableStatement.registerOutParameter(5, JDBCType.VARCHAR);
+      callableStatement.registerOutParameter(6, JDBCType.TIMESTAMP);
+      callableStatement.registerOutParameter(7, JDBCType.TIMESTAMP);
+      checkResults(callableStatement);
+
+      callableStatement.clearParameters();
+      checkResults(callableStatement);
+
+      ParameterMetaData meta = callableStatement.getParameterMetaData();
+
+      assertEquals("INT", meta.getParameterTypeName(1));
+      assertEquals("VARCHAR", meta.getParameterTypeName(5));
+
+      callableStatement.registerOutParameter(2, Types.INTEGER);
+      callableStatement.registerOutParameter(3, Types.INTEGER);
+      callableStatement.registerOutParameter(5, Types.VARCHAR);
+      callableStatement.registerOutParameter(6, Types.TIMESTAMP);
+      callableStatement.registerOutParameter(7, Types.TIMESTAMP);
+      checkResults(callableStatement);
+
+      callableStatement.registerOutParameter(2, Types.INTEGER, 10);
+      callableStatement.registerOutParameter(3, Types.INTEGER, 10);
+      callableStatement.registerOutParameter(5, Types.VARCHAR, 10);
+      callableStatement.registerOutParameter(6, Types.TIMESTAMP, 10);
+      callableStatement.registerOutParameter(7, Types.TIMESTAMP, 10);
+      checkResults(callableStatement);
+
+      callableStatement.registerOutParameter(2, JDBCType.INTEGER, 10);
+      callableStatement.registerOutParameter(3, JDBCType.INTEGER, 10);
+      callableStatement.registerOutParameter(5, JDBCType.VARCHAR, 10);
+      callableStatement.registerOutParameter(6, JDBCType.TIMESTAMP, 10);
+      callableStatement.registerOutParameter(7, JDBCType.TIMESTAMP, 10);
+      checkResults(callableStatement);
+
+      callableStatement.registerOutParameter(2, Types.INTEGER, "typeName");
+      callableStatement.registerOutParameter(3, Types.INTEGER, "typeName");
+      callableStatement.registerOutParameter(5, Types.VARCHAR, "typeName");
+      callableStatement.registerOutParameter(6, Types.TIMESTAMP, "typeName");
+      callableStatement.registerOutParameter(7, Types.BLOB, "typeName");
+      checkResults(callableStatement);
+
+      callableStatement.registerOutParameter(2, JDBCType.INTEGER, "typeName");
+      callableStatement.registerOutParameter(3, JDBCType.INTEGER, "typeName");
+      callableStatement.registerOutParameter(5, JDBCType.VARCHAR, "typeName");
+      callableStatement.registerOutParameter(6, JDBCType.TIMESTAMP, "typeName");
+      callableStatement.registerOutParameter(7, JDBCType.BLOB, "typeName");
+      checkResults(callableStatement);
+
+      callableStatement.registerOutParameter("t2", JDBCType.INTEGER);
+      callableStatement.registerOutParameter("t3", JDBCType.INTEGER);
+      callableStatement.registerOutParameter("t5", JDBCType.VARCHAR);
+      callableStatement.registerOutParameter("t6", JDBCType.TIMESTAMP);
+      callableStatement.registerOutParameter("t7", JDBCType.BLOB);
+      checkResults(callableStatement);
+
+      callableStatement.registerOutParameter("t2", Types.INTEGER);
+      callableStatement.registerOutParameter("t3", Types.INTEGER);
+      callableStatement.registerOutParameter("t5", Types.VARCHAR);
+      callableStatement.registerOutParameter("t6", Types.TIMESTAMP);
+      callableStatement.registerOutParameter("t7", Types.BLOB);
+      checkResults(callableStatement);
+
+      callableStatement.registerOutParameter("t2", Types.INTEGER, 10);
+      callableStatement.registerOutParameter("t3", Types.INTEGER, 10);
+      callableStatement.registerOutParameter("t5", Types.VARCHAR, 10);
+      callableStatement.registerOutParameter("t6", Types.TIMESTAMP, 10);
+      callableStatement.registerOutParameter("t7", Types.BLOB, 10);
+      checkResults(callableStatement);
+
+      callableStatement.registerOutParameter("t2", JDBCType.INTEGER, 10);
+      callableStatement.registerOutParameter("t3", JDBCType.INTEGER, 10);
+      callableStatement.registerOutParameter("t5", JDBCType.VARCHAR, 10);
+      callableStatement.registerOutParameter("t6", JDBCType.TIMESTAMP, 10);
+      callableStatement.registerOutParameter("t7", JDBCType.BLOB, 10);
+      checkResults(callableStatement);
+
+      callableStatement.registerOutParameter("t2", Types.INTEGER, "typeName");
+      callableStatement.registerOutParameter("t3", Types.INTEGER, "typeName");
+      callableStatement.registerOutParameter("t5", Types.VARCHAR, "typeName");
+      callableStatement.registerOutParameter("t6", Types.TIMESTAMP, "typeName");
+      callableStatement.registerOutParameter("t7", Types.BLOB, "typeName");
+      checkResults(callableStatement);
+
+      callableStatement.registerOutParameter("t2", JDBCType.INTEGER, "typeName");
+      callableStatement.registerOutParameter("t3", JDBCType.INTEGER, "typeName");
+      callableStatement.registerOutParameter("t5", JDBCType.VARCHAR, "typeName");
+      callableStatement.registerOutParameter("t6", JDBCType.TIMESTAMP, "typeName");
+      callableStatement.registerOutParameter("t7", JDBCType.BLOB, "typeName");
+      checkResults(callableStatement);
+
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> callableStatement.registerOutParameter(100, JDBCType.BINARY),
+          "wrong parameter index 100");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> callableStatement.registerOutParameter("unknown", JDBCType.INTEGER),
+          "parameter name unknown not found");
+    }
+  }
+
+  @SuppressWarnings("deprecation")
+  private void checkResults(CallableStatement callableStatement) throws SQLException, IOException {
+    callableStatement.setInt(1, 2);
+    callableStatement.setInt(2, 3);
+    callableStatement.setInt(4, 10);
+    callableStatement.execute();
+
+    assertEquals(6, callableStatement.getInt(2));
+    assertEquals("6", callableStatement.getString(2));
+    assertTrue(callableStatement.getBoolean(2));
+    assertEquals((byte) 6, callableStatement.getByte(2));
+    assertEquals((short) 6, callableStatement.getShort(2));
+    assertEquals(6L, callableStatement.getLong(2));
+    assertEquals(6F, callableStatement.getFloat(2));
+    assertEquals(6D, callableStatement.getDouble(2));
+    assertEquals(6, callableStatement.getBigDecimal(2).intValue());
+    assertEquals(6, callableStatement.getBigDecimal(2, 5).intValue());
+    assertEquals(6L, callableStatement.getObject(2, (Map<String, Class<?>>) null));
+    assertEquals(6L, callableStatement.getObject(2, new HashMap<>()));
+    assertArrayEquals(new byte[] {116, 101, 115, 116}, callableStatement.getBlob(7).getBytes(1, 4));
+    assertArrayEquals(
+        new byte[] {104, 116, 116, 112, 58, 47, 47, 116, 101, 115, 116},
+        callableStatement.getBytes(5));
+    assertEquals("http://test", callableStatement.getClob(5).toString());
+    assertEquals("http://test", callableStatement.getNClob(5).toString());
+    assertEquals("http://test", callableStatement.getURL(5).toString());
+
+    assertEquals("2003-12-31 12:00:00.0", callableStatement.getTimestamp(6).toString());
+    assertEquals(
+        "2003-12-31 12:00:00.0",
+        callableStatement.getTimestamp(6, Calendar.getInstance()).toString());
+    assertEquals("12:00:00", callableStatement.getTime(6).toString());
+    assertEquals("12:00:00", callableStatement.getTime(6, Calendar.getInstance()).toString());
+    assertEquals("2003-12-31", callableStatement.getDate(6).toString());
+    char[] res = new char[4];
+    callableStatement.getCharacterStream(5).read(res);
+    assertEquals("http", new String(res));
+    callableStatement.getNCharacterStream(5).read(res);
+    assertEquals("http", new String(res));
+
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getBytes(2),
+        "Data type INTEGER cannot be decoded as byte[]");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getDate(2),
+        "Data type INTEGER cannot be decoded as Date");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getBytes(2),
+        "Data type INTEGER cannot be decoded as byte[]");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getDate(2, (Calendar) null),
+        "Data type INTEGER cannot be decoded as Date");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getTime(2),
+        "Data type INTEGER cannot be decoded as Time");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getTime(2, (Calendar) null),
+        "Data type INTEGER cannot be decoded as Time");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getTimestamp(2),
+        "Data type INTEGER cannot be decoded as Timestamp");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getTimestamp(2, (Calendar) null),
+        "Data type INTEGER cannot be decoded as Timestamp");
+    assertEquals(6L, callableStatement.getObject(2));
+    Map<String, Class<?>> map = new HashMap<>();
+    map.put("f", Integer.class);
+    Common.assertThrowsContains(
+        SQLFeatureNotSupportedException.class,
+        () -> callableStatement.getObject(2, map),
+        " Method ResultSet.getObject(int columnIndex, Map<String, Class<?>> map) not supported");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getRef(2),
+        "Method ResultSet.getRef not supported");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getBlob(2),
+        "Data type INTEGER cannot be decoded as Blob");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getClob(2),
+        "Data type INTEGER cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLFeatureNotSupportedException.class,
+        () -> callableStatement.getArray(2),
+        "Method ResultSet.getArray not supported");
+    Common.assertThrowsContains(
+        SQLException.class, () -> callableStatement.getURL(2), "Could not parse '6' as URL");
+
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getNClob(2),
+        "Data type INTEGER cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLFeatureNotSupportedException.class,
+        () -> callableStatement.getSQLXML(2),
+        "SQLXML are not supported");
+    assertEquals("6", callableStatement.getNString(2));
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> callableStatement.getNCharacterStream(2),
+        "Data type INTEGER cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> callableStatement.getCharacterStream(2),
+        "Data type INTEGER cannot be decoded as Reader");
+    assertEquals("6", callableStatement.getObject(2, String.class));
+    Common.assertThrowsContains(
+        SQLException.class, () -> callableStatement.getRowId(2), "RowId are not supported");
+
+    assertEquals(6, callableStatement.getInt("t2"));
+    assertEquals("6", callableStatement.getString("t2"));
+    assertTrue(callableStatement.getBoolean("t2"));
+    assertEquals((byte) 6, callableStatement.getByte("t2"));
+    assertEquals((short) 6, callableStatement.getShort("t2"));
+    assertEquals(6L, callableStatement.getLong("t2"));
+    assertEquals(6F, callableStatement.getFloat("t2"));
+    assertEquals(6D, callableStatement.getDouble("t2"));
+    assertEquals(6, callableStatement.getBigDecimal("t2").intValue());
+    assertEquals(6L, callableStatement.getObject("t2", (Map<String, Class<?>>) null));
+    assertEquals(6L, callableStatement.getObject("t2", new HashMap<>()));
+    assertArrayEquals(
+        new byte[] {116, 101, 115, 116}, callableStatement.getBlob("t7").getBytes(1, 4));
+    assertArrayEquals(
+        new byte[] {104, 116, 116, 112, 58, 47, 47, 116, 101, 115, 116},
+        callableStatement.getBytes("t5"));
+    assertEquals("http://test", callableStatement.getClob("t5").toString());
+    assertEquals("http://test", callableStatement.getNClob("t5").toString());
+    assertEquals("http://test", callableStatement.getURL("t5").toString());
+    callableStatement.getCharacterStream("t5").read(res);
+    assertEquals("http", new String(res));
+    callableStatement.getNCharacterStream("t5").read(res);
+    assertEquals("http", new String(res));
+
+    assertEquals("2003-12-31 12:00:00.0", callableStatement.getTimestamp("t6").toString());
+    assertEquals(
+        "2003-12-31 12:00:00.0",
+        callableStatement.getTimestamp("t6", Calendar.getInstance()).toString());
+    assertEquals("12:00:00", callableStatement.getTime("t6").toString());
+    assertEquals("12:00:00", callableStatement.getTime("t6", Calendar.getInstance()).toString());
+    assertEquals("2003-12-31", callableStatement.getDate("t6").toString());
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getBytes(null),
+        "parameter name cannot be null");
+
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getBytes("t2"),
+        "Data type INTEGER cannot be decoded as byte[]");
+    Common.assertThrowsContains(
+        SQLException.class, () -> callableStatement.getRowId("t2"), "RowId are not supported");
+
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getDate("t2"),
+        "Data type INTEGER cannot be decoded as Date");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getDate("t2", (Calendar) null),
+        "Data type INTEGER cannot be decoded as Date");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getTime("t2"),
+        "Data type INTEGER cannot be decoded as Time");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getTime("t2", (Calendar) null),
+        "Data type INTEGER cannot be decoded as Time");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getTimestamp("t2"),
+        "Data type INTEGER cannot be decoded as Timestamp");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getTimestamp("t2", (Calendar) null),
+        "Data type INTEGER cannot be decoded as Timestamp");
+    assertEquals(6L, callableStatement.getObject("t2"));
+    Common.assertThrowsContains(
+        SQLFeatureNotSupportedException.class,
+        () -> callableStatement.getObject("t2", map),
+        " Method ResultSet.getObject(int columnIndex, Map<String, Class<?>> map) not supported");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getRef("t2"),
+        "Method ResultSet.getRef not supported");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getBlob("t2"),
+        "Data type INTEGER cannot be decoded as Blob");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getClob("t2"),
+        "Data type INTEGER cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLFeatureNotSupportedException.class,
+        () -> callableStatement.getArray("t2"),
+        "Method ResultSet.getArray not supported");
+    Common.assertThrowsContains(
+        SQLException.class, () -> callableStatement.getURL("t2"), "Could not parse '6' as URL");
+
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> callableStatement.getNClob("t2"),
+        "Data type INTEGER cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLFeatureNotSupportedException.class,
+        () -> callableStatement.getSQLXML("t2"),
+        "SQLXML are not supported");
+    assertEquals("6", callableStatement.getNString("t2"));
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> callableStatement.getNCharacterStream("t2"),
+        "Data type INTEGER cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> callableStatement.getCharacterStream("t2"),
+        "Data type INTEGER cannot be decoded as Reader");
+    assertEquals("6", callableStatement.getObject("t2", String.class));
+
+    assertFalse(callableStatement.wasNull());
+    assertEquals(20, callableStatement.getInt(3));
+    assertFalse(callableStatement.wasNull());
+    Common.assertThrowsContains(
+        SQLException.class, () -> callableStatement.getInt(-1), "wrong index");
+
+    Common.assertThrowsContains(
+        SQLException.class, () -> callableStatement.getInt(4), "index 4 not declared as output");
+  }
+
+  @Test
+  public void setProcedureTest() throws Exception {
+    // error MXS-3929 for maxscale 6.2.0
+    Assumptions.assumeTrue(
+        !sharedConn.getMetaData().getDatabaseProductVersion().contains("maxScale-6.2.0"));
+
+    // https://jira.mariadb.org/browse/XPT-268
+    Assumptions.assumeFalse(isXpand());
+
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP PROCEDURE IF EXISTS basic_proc");
+    stmt.execute(
+        "CREATE PROCEDURE basic_proc (IN t1 VARCHAR(20), INOUT t2 VARCHAR(20)) BEGIN \n"
+            + "set t2 = CONCAT(t1, t2);\n"
+            + "END");
+    try (CallableStatement callableStatement = sharedConn.prepareCall("{call basic_proc(?,?)}")) {
+      callableStatement.registerOutParameter(2, JDBCType.VARCHAR);
+      callableStatement.setString(1, "a");
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("ab", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setNString(1, "a");
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("ab", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setURL(1, new URL("http://a"));
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("http://ab", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setNull(1, Types.VARCHAR);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertNull(callableStatement.getString(2));
+      assertTrue(callableStatement.wasNull());
+
+      reset(callableStatement);
+      callableStatement.setNull(1, Types.VARCHAR, "sometype");
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertNull(callableStatement.getString(2));
+      assertTrue(callableStatement.wasNull());
+
+      reset(callableStatement);
+      callableStatement.setBoolean(1, true);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("1b", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setByte(1, (byte) 'a');
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("97b", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setShort(1, (short) 6);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("6b", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setInt(1, 6);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("6b", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setLong(1, 6L);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("6b", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setFloat(1, 6F);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("6b", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setDouble(1, 6D);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("6b", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setBigDecimal(1, BigDecimal.ONE);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("1b", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setTimestamp(1, new Timestamp(0));
+      callableStatement.setString(2, "");
+      callableStatement.execute();
+      callableStatement.getTimestamp(2);
+
+      reset(callableStatement);
+      callableStatement.setTimestamp(1, new Timestamp(0), Calendar.getInstance());
+      callableStatement.setString(2, "");
+      callableStatement.execute();
+      callableStatement.getTimestamp(2);
+
+      reset(callableStatement);
+      callableStatement.setBytes(1, new byte[] {(byte) 'a', (byte) 0x45});
+      callableStatement.setString(2, "c");
+      callableStatement.execute();
+      assertEquals("aEc", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setBlob(1, new MariaDbBlob("0123".getBytes(), 1, 2));
+      callableStatement.setString(2, "c");
+      callableStatement.execute();
+      assertEquals("12c", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setBlob(1, new ByteArrayInputStream("eef".getBytes()));
+      callableStatement.setString(2, "c");
+      callableStatement.execute();
+      assertEquals("eefc", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setDate(1, new Date(0));
+      callableStatement.setString(2, "");
+      callableStatement.execute();
+      callableStatement.getDate(2);
+
+      reset(callableStatement);
+      callableStatement.setDate(1, new Date(0), Calendar.getInstance());
+      callableStatement.setString(2, "");
+      callableStatement.execute();
+      callableStatement.getDate(2, Calendar.getInstance());
+
+      reset(callableStatement);
+      callableStatement.setTime(1, new Time(0));
+      callableStatement.setString(2, "");
+      callableStatement.execute();
+      callableStatement.getTime(2);
+
+      reset(callableStatement);
+      callableStatement.setTime(1, new Time(0), Calendar.getInstance());
+      callableStatement.setString(2, "");
+      callableStatement.execute();
+      callableStatement.getTime(2);
+
+      reset(callableStatement);
+      callableStatement.setAsciiStream(1, new ByteArrayInputStream("dbc".getBytes()));
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("dbcb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setAsciiStream(1, new ByteArrayInputStream("cbc".getBytes()), 2);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("cbb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setBinaryStream(1, new ByteArrayInputStream("bef".getBytes()));
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("befb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setBinaryStream(1, new ByteArrayInputStream("aef".getBytes()), 2);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("aeb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setObject(1, new ByteArrayInputStream("saf".getBytes()));
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("safb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setObject(
+          1, new ByteArrayInputStream("sbf".getBytes()), Types.LONGVARBINARY);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("sbfb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setObject(
+          1, new ByteArrayInputStream("scf".getBytes()), JDBCType.LONGVARBINARY);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("scfb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setObject(1, null);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertNull(callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setObject(
+          1, new ByteArrayInputStream("sdf".getBytes()), Types.LONGVARBINARY, 10);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("sdfb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setObject(
+          1, new ByteArrayInputStream("sea".getBytes()), JDBCType.LONGVARBINARY, 10);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("seab", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setCharacterStream(1, new StringReader("seb"));
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("sebb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setClob(1, new MariaDbClob("sec".getBytes(), 0, 16));
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("secb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setClob(1, new StringReader("sed"));
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("sedb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setClob(1, new StringReader("sef"), 2);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("seb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setNClob(1, new MariaDbClob("seg".getBytes(), 0, 16));
+      callableStatement.setString(2, "c");
+      callableStatement.execute();
+      assertEquals("segc", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setNClob(1, new StringReader("seh"));
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("sehb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setNClob(1, new StringReader("sei"), 2);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("seb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setCharacterStream(1, new StringReader("sej"), 2);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("seb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setNCharacterStream(1, new StringReader("seh"));
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("sehb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setNCharacterStream(1, new StringReader("sek"), 2);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("seb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setDate(1, new Date(0));
+      callableStatement.setString(2, "");
+      callableStatement.execute();
+      callableStatement.getDate(2);
+
+      reset(callableStatement);
+      callableStatement.setDate(1, new Date(0), Calendar.getInstance());
+      callableStatement.setString(2, "");
+      callableStatement.execute();
+      callableStatement.getDate(2, Calendar.getInstance());
+
+      reset(callableStatement);
+      callableStatement.setNull(1, Types.BINARY);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertNull(callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setNull(1, Types.BINARY, "ttt");
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertNull(callableStatement.getString(2));
+
+      // with named params
+
+      callableStatement.registerOutParameter(2, JDBCType.VARCHAR);
+      callableStatement.setString("t1", "a");
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("ab", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setNString("t1", "a");
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("ab", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setURL("t1", new URL("http://a"));
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("http://ab", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setNull("t1", Types.VARCHAR);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertNull(callableStatement.getString(2));
+      assertTrue(callableStatement.wasNull());
+
+      reset(callableStatement);
+      callableStatement.setNull("t1", Types.VARCHAR, "sometype");
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertNull(callableStatement.getString(2));
+      assertTrue(callableStatement.wasNull());
+
+      reset(callableStatement);
+      callableStatement.setBoolean("t1", true);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("1b", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setByte("t1", (byte) 'a');
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("97b", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setShort("t1", (short) 6);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("6b", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setInt("t1", 6);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("6b", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setLong("t1", 6L);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("6b", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setFloat("t1", 6F);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("6b", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setDouble("t1", 6D);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("6b", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setBigDecimal("t1", BigDecimal.ONE);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("1b", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setTimestamp("t1", new Timestamp(0));
+      callableStatement.setString(2, "");
+      callableStatement.execute();
+      callableStatement.getTimestamp("t2");
+
+      reset(callableStatement);
+      callableStatement.setTimestamp("t1", new Timestamp(0), Calendar.getInstance());
+      callableStatement.setString(2, "");
+      callableStatement.execute();
+      callableStatement.getTimestamp("t2", Calendar.getInstance());
+
+      reset(callableStatement);
+      callableStatement.setBytes("t1", new byte[] {(byte) 'a', (byte) 0x45});
+      callableStatement.setString(2, "c");
+      callableStatement.execute();
+      assertEquals("aEc", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setBlob("t1", new MariaDbBlob("0123".getBytes(), 1, 2));
+      callableStatement.setString(2, "c");
+      callableStatement.execute();
+      assertEquals("12c", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setBlob("t1", new ByteArrayInputStream("sef".getBytes()));
+      callableStatement.setString(2, "c");
+      callableStatement.execute();
+      assertEquals("sefc", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setBlob("t1", new ByteArrayInputStream("sef".getBytes()), 2);
+      callableStatement.setString(2, "c");
+      callableStatement.execute();
+      assertEquals("sec", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setDate("t1", new Date(0));
+      callableStatement.setString(2, "");
+      callableStatement.execute();
+      callableStatement.getDate("t2");
+
+      reset(callableStatement);
+      callableStatement.setDate("t1", new Date(0), Calendar.getInstance());
+      callableStatement.setString(2, "");
+      callableStatement.execute();
+      callableStatement.getDate("t2", Calendar.getInstance());
+
+      reset(callableStatement);
+      callableStatement.setTime("t1", new Time(0));
+      callableStatement.setString(2, "");
+      callableStatement.execute();
+      callableStatement.getTime("t2");
+
+      reset(callableStatement);
+      callableStatement.setTime("t1", new Time(0), Calendar.getInstance());
+      callableStatement.setString(2, "");
+      callableStatement.execute();
+      callableStatement.getTime("t2", Calendar.getInstance());
+
+      reset(callableStatement);
+      callableStatement.setAsciiStream("t1", new ByteArrayInputStream("abc".getBytes()));
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("abcb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setAsciiStream("t1", new ByteArrayInputStream("abc".getBytes()), 2);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("abcb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setAsciiStream("t1", new ByteArrayInputStream("abc".getBytes()), 2L);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("abb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setBinaryStream("t1", new ByteArrayInputStream("sef".getBytes()));
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("sefb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setBinaryStream("t1", new ByteArrayInputStream("sef".getBytes()), 2);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("seb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setBinaryStream("t1", new ByteArrayInputStream("sef".getBytes()), 2L);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("seb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setObject("t1", new ByteArrayInputStream("sef".getBytes()));
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("sefb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setObject(
+          "t1", new ByteArrayInputStream("sef".getBytes()), Types.LONGVARBINARY);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("sefb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setObject(
+          "t1", new ByteArrayInputStream("sef".getBytes()), JDBCType.LONGVARBINARY);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("sefb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setObject(
+          "t1", new ByteArrayInputStream("sef".getBytes()), Types.LONGVARBINARY, 10);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("sefb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setObject(
+          "t1", new ByteArrayInputStream("sef".getBytes()), JDBCType.LONGVARBINARY, 10);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("sefb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setCharacterStream("t1", new StringReader("sef"));
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("sefb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setCharacterStream("t1", new StringReader("sef"), 2);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("seb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setCharacterStream("t1", new StringReader("sef"), 2L);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("seb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setNCharacterStream("t1", new StringReader("sef"));
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("sefb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setNCharacterStream("t1", new StringReader("sef"), 2);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("seb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setDate("t1", new Date(0));
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+
+      reset(callableStatement);
+      callableStatement.setNull("t1", Types.BINARY);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertNull(callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setNull("t1", Types.BINARY, "ttt");
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertNull(callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setClob("t1", new MariaDbClob("sef".getBytes(), 0, 16));
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("sefb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setClob("t1", new StringReader("sef"));
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("sefb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setClob("t1", new StringReader("sef"), 2);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("seb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setNClob("t1", new MariaDbClob("sef".getBytes(), 0, 16));
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("sefb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setNClob("t1", new StringReader("sef"));
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("sefb", callableStatement.getString(2));
+
+      reset(callableStatement);
+      callableStatement.setNClob("t1", new StringReader("sef"), 2);
+      callableStatement.setString(2, "b");
+      callableStatement.execute();
+      assertEquals("seb", callableStatement.getString(2));
+
+      Common.assertThrowsContains(
+          SQLFeatureNotSupportedException.class,
+          () -> callableStatement.setRowId(1, null),
+          "RowId parameter are not supported");
+      Common.assertThrowsContains(
+          SQLFeatureNotSupportedException.class,
+          () -> callableStatement.setRowId("t1", null),
+          "RowId parameter are not supported");
+      Common.assertThrowsContains(
+          SQLFeatureNotSupportedException.class,
+          () -> callableStatement.setSQLXML(1, null),
+          "SQLXML parameter are not supported");
+      Common.assertThrowsContains(
+          SQLFeatureNotSupportedException.class,
+          () -> callableStatement.setSQLXML("t1", null),
+          "SQLXML parameter are not supported");
+    }
+  }
+
+  private void reset(CallableStatement callableStatement) throws SQLException {
+    callableStatement.registerOutParameter(2, JDBCType.VARCHAR);
+    callableStatement.setString(1, "a");
+    callableStatement.setString(2, "b");
+    callableStatement.execute();
+    assertEquals("ab", callableStatement.getString(2));
+  }
+
+  @Test
+  public void procedureToString() throws SQLException {
+    try (CallableStatement callableStatement =
+        sharedConn.prepareCall("{call basic_proc(?,?,?,?,?,?,?)}")) {
+
+      assertEquals(
+          "ProcedureStatement{sql:'call basic_proc(?,?,?,?,?,?,?)', parameters:[]}",
+          callableStatement.toString());
+      callableStatement.registerOutParameter(2, JDBCType.INTEGER);
+      callableStatement.registerOutParameter(3, JDBCType.INTEGER);
+      callableStatement.registerOutParameter(5, JDBCType.VARCHAR);
+      assertEquals(
+          "ProcedureStatement{sql:'call basic_proc(?,?,?,?,?,?,?)',"
+              + " parameters:[null,<OUT>null,<OUT>null,null,<OUT>null]}",
+          callableStatement.toString());
+      callableStatement.setBytes(2, new byte[] {(byte) 'a', (byte) 'b'});
+      callableStatement.setLong(1, 10L);
+      assertEquals(
+          "ProcedureStatement{sql:'call basic_proc(?,?,?,?,?,?,?)', parameters:[10,<OUT>_binary"
+              + " 'ab',<OUT>null,null,<OUT>null]}",
+          callableStatement.toString());
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/ResultSetTest.java b/src/test/java/org/mariadb/jdbc/integration/ResultSetTest.java
new file mode 100644
index 0000000..0931148
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/ResultSetTest.java
@@ -0,0 +1,265 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.sql.*;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+
+public class ResultSetTest extends Common {
+
+  private final Class<? extends java.lang.Exception> ns = SQLFeatureNotSupportedException.class;
+
+  @AfterAll
+  public static void after2() throws SQLException {
+    sharedConn.createStatement().execute("DROP TABLE IF EXISTS resultsettest");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    after2();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS resultsettest");
+    stmt.execute("CREATE TABLE resultsettest (t1 int not null primary key auto_increment, t2 int)");
+    stmt.execute("INSERT INTO resultsettest(t2) values (1),(2),(3),(4),(5),(6),(7),(8)");
+  }
+
+  @Test
+  public void nonUpdatableFields() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION");
+    ResultSet rs = stmt.executeQuery("SELECT * FROM resultsettest");
+    Assertions.assertNull(rs.getWarnings());
+    rs.next();
+    Common.assertThrowsContains(ns, () -> rs.updateArray(1, null), "Array are not supported");
+    String NOT_SUPPORTED = "Not supported when using CONCUR_READ_ONLY concurrency";
+    Common.assertThrowsContains(ns, () -> rs.updateNull(1), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBoolean(1, true), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateByte(1, (byte) 0x00), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateShort(1, (short) 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateInt(1, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateLong(1, 0L), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateFloat(1, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateDouble(1, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBigDecimal(1, BigDecimal.ZERO), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateString(1, ""), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNString(1, ""), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBytes(1, new byte[0]), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateDate(1, null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateTime(1, null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateTimestamp(1, null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateAsciiStream(1, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBinaryStream(1, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateCharacterStream(1, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateObject(1, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateObject(1, null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBlob(1, (Blob) null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateClob(1, (Clob) null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBlob(1, (InputStream) null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateClob(1, (Reader) null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNClob(1, (NClob) null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNClob(1, (Reader) null), NOT_SUPPORTED);
+
+    Common.assertThrowsContains(ns, () -> rs.updateArray("t1", null), "Array are not supported");
+
+    Common.assertThrowsContains(ns, () -> rs.updateNull("t1"), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBoolean("t1", true), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateByte("t1", (byte) 0x00), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateShort("t1", (short) 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateInt("t1", 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateLong("t1", 0L), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateFloat("t1", 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateDouble("t1", 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(
+        ns, () -> rs.updateBigDecimal("t1", BigDecimal.ZERO), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateString("t1", ""), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNString("t1", ""), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBytes("t1", new byte[0]), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateDate("t1", null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateTime("t1", null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateTimestamp("t1", null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateAsciiStream("t1", null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBinaryStream("t1", null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateCharacterStream("t1", null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateObject("t1", null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateObject("t1", null), NOT_SUPPORTED);
+
+    Common.assertThrowsContains(ns, () -> rs.updateBlob("t1", (Blob) null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBlob("t1", (InputStream) null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateClob("t1", (Clob) null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateClob("t1", (Reader) null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNClob("t1", (NClob) null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNClob("t1", (Reader) null), NOT_SUPPORTED);
+
+    Common.assertThrowsContains(ns, rs::insertRow, NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, rs::updateRow, NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, rs::deleteRow, NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, rs::refreshRow, NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, rs::cancelRowUpdates, NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, rs::moveToInsertRow, NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, rs::moveToCurrentRow, NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, rs::rowUpdated, NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, rs::rowInserted, NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, rs::rowDeleted, NOT_SUPPORTED);
+
+    Common.assertThrowsContains(
+        ns, () -> rs.updateRef(1, null), "Method ResultSet.updateRef not supported");
+    Common.assertThrowsContains(
+        ns, () -> rs.updateRef("t1", null), "Method ResultSet.updateRef not supported");
+    Common.assertThrowsContains(ns, () -> rs.updateArray(1, null), "Array are not supported");
+    Common.assertThrowsContains(ns, () -> rs.updateArray("t1", null), "Array are not supported");
+    Common.assertThrowsContains(ns, () -> rs.updateRowId(1, null), "RowId are not supported");
+    Common.assertThrowsContains(ns, () -> rs.updateRowId("t1", null), "RowId are not supported");
+    Common.assertThrowsContains(ns, () -> rs.updateSQLXML(1, null), "SQLXML not supported");
+    Common.assertThrowsContains(ns, () -> rs.updateSQLXML("t1", null), "SQLXML not supported");
+
+    Common.assertThrowsContains(ns, () -> rs.updateNCharacterStream(1, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNCharacterStream("t1", null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateAsciiStream(1, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateAsciiStream("t1", null, 0), NOT_SUPPORTED);
+
+    Common.assertThrowsContains(ns, () -> rs.updateBinaryStream(1, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBinaryStream("t1", null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBlob(1, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBlob("t1", null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateClob(1, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateClob("t1", null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNClob(1, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNClob("t1", null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateCharacterStream(1, null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateCharacterStream("t1", null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNCharacterStream(1, null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNCharacterStream("t1", null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateAsciiStream(1, null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateAsciiStream("t1", null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBinaryStream(1, null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBinaryStream("t1", null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateObject(1, null, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateObject("t1", null, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateObject(1, null, null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateObject("t1", null, null), NOT_SUPPORTED);
+    sharedConn.rollback();
+  }
+
+  @Test
+  public void notSupported() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION");
+    ResultSet rs = stmt.executeQuery("SELECT * FROM resultsettest");
+    Common.assertThrowsContains(ns, () -> rs.getRowId(1), "RowId are not supported");
+    Common.assertThrowsContains(ns, () -> rs.getRowId("t1"), "RowId are not supported");
+    Map<String, Class<?>> map = new HashMap<>();
+    map.put("f", Integer.class);
+    Common.assertThrowsContains(
+        ns,
+        () -> rs.getObject(1, map),
+        "Method ResultSet.getObject(int columnIndex, Map<String, Class<?>> map) not supported");
+    Common.assertThrowsContains(
+        ns,
+        () -> rs.getObject("t1", map),
+        "Method ResultSet.getObject(String columnLabel, Map<String, Class<?>> map) not supported");
+    Common.assertThrowsContains(ns, rs::getCursorName, "Cursors are not supported");
+    sharedConn.rollback();
+  }
+
+  @Test
+  public void staticMethod() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION");
+    ResultSet rs = stmt.executeQuery("SELECT * FROM resultsettest");
+    Assertions.assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, rs.getHoldability());
+    rs.unwrap(java.sql.ResultSet.class);
+
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.unwrap(String.class),
+        "The receiver is not a wrapper for java.lang.String");
+    sharedConn.rollback();
+  }
+
+  @Test
+  public void wrongIndex() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION");
+    ResultSet rs = stmt.executeQuery("SELECT * FROM resultsettest");
+    rs.next();
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(-1),
+        "Wrong index position. Is -1 but must be in 1-2 range");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(10),
+        "Wrong index position. Is 10 but must be in 1-2 range");
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.findColumn(null), "null is not a valid label value");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.findColumn("yy"),
+        "Unknown label 'yy'. Possible value [resultsettest.t1, t1, resultsettest.t2, t2]");
+    sharedConn.rollback();
+  }
+
+  @Test
+  public void isBeforeFirstFetchTest() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION");
+    stmt.setFetchSize(1);
+    ResultSet rs = stmt.executeQuery("SELECT * FROM resultsettest");
+    assertTrue(rs.isBeforeFirst());
+    while (rs.next()) {
+      assertFalse(rs.isBeforeFirst());
+    }
+    assertFalse(rs.isBeforeFirst());
+    rs.close();
+    Common.assertThrowsContains(
+        SQLException.class, rs::isBeforeFirst, "Operation not permit on a closed resultSet");
+    sharedConn.rollback();
+  }
+
+  @Test
+  public void testAliases() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION");
+    ResultSet rs =
+        stmt.executeQuery("SELECT t1 as t1alias, t2 as t2alias FROM resultsettest as tablealias");
+    rs.next();
+    assertEquals(1, rs.getInt(1));
+    assertEquals(1, rs.getInt("t1alias"));
+    assertEquals(1, rs.getInt("tablealias.t1alias"));
+    assertEquals(1, rs.getInt(1));
+    assertEquals(1, rs.getInt("t2alias"));
+    assertEquals(1, rs.getInt("tablealias.t2alias"));
+
+    rs = stmt.executeQuery("SELECT t1 as t1alias, t2 as t2alias FROM resultsettest");
+    rs.next();
+    assertEquals(1, rs.getInt(1));
+    assertEquals(1, rs.getInt("t1alias"));
+    assertEquals(1, rs.getInt("resultsettest.t1alias"));
+    assertEquals(1, rs.getInt(1));
+    assertEquals(1, rs.getInt("t2alias"));
+    assertEquals(1, rs.getInt("resultsettest.t2alias"));
+
+    rs = stmt.executeQuery("SELECT t1, t2 FROM resultsettest");
+    rs.next();
+    assertEquals(1, rs.getInt(1));
+    assertEquals(1, rs.getInt("t1"));
+    assertEquals(1, rs.getInt("resultsettest.t1"));
+    assertEquals(1, rs.getInt(1));
+    assertEquals(1, rs.getInt("t2"));
+    assertEquals(1, rs.getInt("resultsettest.t2"));
+    sharedConn.rollback();
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/Sha256AuthenticationTest.java b/src/test/java/org/mariadb/jdbc/integration/Sha256AuthenticationTest.java
new file mode 100644
index 0000000..a45eca2
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/Sha256AuthenticationTest.java
@@ -0,0 +1,242 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import java.io.File;
+import java.io.IOException;
+import java.sql.*;
+import org.junit.jupiter.api.*;
+
+public class Sha256AuthenticationTest extends Common {
+
+  private static String rsaPublicKey;
+
+  private static void dropUserWithoutError(org.mariadb.jdbc.Statement stmt, String user) {
+    try {
+      stmt.execute("DROP USER IF EXISTS " + user);
+    } catch (SQLException e) {
+      // eat
+    }
+  }
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    if (sharedConn != null) {
+      org.mariadb.jdbc.Statement stmt = sharedConn.createStatement();
+      dropUserWithoutError(stmt, "'cachingSha256User'@'%'");
+      dropUserWithoutError(stmt, "'cachingSha256User2'@'%'");
+      dropUserWithoutError(stmt, "'cachingSha256User3'@'%'");
+      dropUserWithoutError(stmt, "'cachingSha256User4'@'%'");
+    }
+    // reason is that after nativePassword test, it sometime always return wrong authentication id
+    // not cached
+    // !? strange, but mysql server error.
+    if (haveSsl() && !isMariaDBServer() && minVersion(8, 0, 0)) {
+      try (Connection con = createCon("sslMode=trust")) {
+        con.createStatement().execute("DO 1");
+      }
+    }
+  }
+
+  @BeforeAll
+  public static void init() throws Exception {
+    Assumptions.assumeTrue(!isMariaDBServer() && minVersion(8, 0, 0));
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    rsaPublicKey = checkFileExists(System.getProperty("rsaPublicKey"));
+    if (rsaPublicKey == null) {
+      ResultSet rs = stmt.executeQuery("SELECT @@caching_sha2_password_public_key_path, @@datadir");
+      rs.next();
+      rsaPublicKey = checkFileExists(rs.getString(1));
+
+      if (rsaPublicKey == null) {
+        rsaPublicKey = checkFileExists(rs.getString(2) + rs.getString(1));
+        if (rsaPublicKey == null) {
+          rsaPublicKey = checkFileExists(System.getenv("TEST_DB_RSA_PUBLIC_KEY"));
+          if (rsaPublicKey == null && System.getenv("TEST_DB_RSA_PUBLIC_KEY") != null) {
+            rsaPublicKey = checkFileExists(System.getenv("TEST_DB_RSA_PUBLIC_KEY"));
+          }
+        }
+      }
+    }
+    if (rsaPublicKey == null) {
+      rsaPublicKey = checkFileExists("../../ssl/public.key");
+    }
+
+    if (rsaPublicKey == null) {
+      ResultSet rs = stmt.executeQuery("SHOW STATUS like 'Caching_sha2_password_rsa_public_key'");
+      rs.next();
+      rsaPublicKey = rs.getString(2);
+      if ("".equals(rsaPublicKey)) rsaPublicKey = null;
+      if (rsaPublicKey != null) {
+        System.out.println(
+            "rsaPublicKey set from @@Caching_sha2_password_rsa_public_key:" + rsaPublicKey);
+      }
+    }
+
+    stmt.execute(
+        "CREATE USER 'cachingSha256User'@'%' IDENTIFIED WITH caching_sha2_password BY"
+            + " '!Passw0rd3Works'");
+    stmt.execute(
+        "CREATE USER 'cachingSha256User2'@'%' IDENTIFIED WITH caching_sha2_password BY ''");
+    stmt.execute(
+        "CREATE USER 'cachingSha256User3'@'%' IDENTIFIED WITH caching_sha2_password BY"
+            + " '!Passw0rd3Works'");
+    stmt.execute(
+        "CREATE USER 'cachingSha256User4'@'%' IDENTIFIED WITH caching_sha2_password BY"
+            + " '!Passw0rd3Works'");
+    stmt.execute("GRANT ALL PRIVILEGES ON *.* TO 'cachingSha256User'@'%'");
+    stmt.execute("GRANT ALL PRIVILEGES ON *.* TO 'cachingSha256User2'@'%'");
+    stmt.execute("GRANT ALL PRIVILEGES ON *.* TO 'cachingSha256User3'@'%'");
+    stmt.execute("GRANT ALL PRIVILEGES ON *.* TO 'cachingSha256User4'@'%'");
+    // mysql 8.0.31 broken public key retrieval, so avoid FLUSHING for now
+    Assumptions.assumeTrue(!isMariaDBServer() && !exactVersion(8, 0, 31));
+    stmt.execute("FLUSH PRIVILEGES");
+  }
+
+  private static String checkFileExists(String path) throws IOException {
+    if (path == null) return null;
+    System.out.println("check path:" + path);
+    File f = new File(path);
+    if (f.exists()) {
+      System.out.println("path exist :" + path);
+      return f.getCanonicalPath().replace("\\", "/");
+    }
+    return null;
+  }
+
+  @Test
+  public void nativePassword() throws Exception {
+    Assumptions.assumeTrue(haveSsl());
+    Assumptions.assumeTrue(
+        !isWindows() && !isMariaDBServer() && rsaPublicKey != null && minVersion(8, 0, 0));
+    Statement stmt = sharedConn.createStatement();
+    try {
+      stmt.execute("DROP USER tmpUser@'%'");
+    } catch (SQLException e) {
+      // eat
+    }
+
+    stmt.execute(
+        "CREATE USER tmpUser@'%' IDENTIFIED WITH mysql_native_password BY '!Passw0rd3Works'");
+    stmt.execute("grant all on `" + sharedConn.getCatalog() + "`.* TO tmpUser@'%'");
+    // mysql 8.0.31 broken public key retrieval, so avoid FLUSHING for now
+    Assumptions.assumeTrue(!isMariaDBServer() && !exactVersion(8, 0, 31));
+    stmt.execute("FLUSH PRIVILEGES"); // reset cache
+    try (Connection con = createCon("user=tmpUser&password=!Passw0rd3Works")) {
+      con.isValid(1);
+    }
+    try {
+      stmt.execute("DROP USER tmpUser@'%' ");
+    } catch (SQLException e) {
+      // eat
+    }
+  }
+
+  @Test
+  public void cachingSha256Empty() throws Exception {
+    Assumptions.assumeTrue(
+        !isWindows() && !isMariaDBServer() && rsaPublicKey != null && minVersion(8, 0, 0));
+    // mysql 8.0.31 broken public key retrieval, so avoid FLUSHING for now
+    Assumptions.assumeTrue(!isMariaDBServer() && !minVersion(8, 0, 31));
+    sharedConn.createStatement().execute("FLUSH PRIVILEGES"); // reset cache
+    try (Connection con = createCon("user=cachingSha256User2&allowPublicKeyRetrieval&password=")) {
+      con.isValid(1);
+    }
+  }
+
+  @Test
+  public void wrongRsaPath() throws Exception {
+    Assumptions.assumeTrue(
+        !isWindows() && !isMariaDBServer() && rsaPublicKey != null && minVersion(8, 0, 0));
+    // mysql 8.0.31 broken public key retrieval, so avoid FLUSHING for now
+    Assumptions.assumeTrue(!isMariaDBServer() && !exactVersion(8, 0, 31));
+    sharedConn.createStatement().execute("FLUSH PRIVILEGES"); // reset cache
+    File tempFile = File.createTempFile("log", ".tmp");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () ->
+            createCon(
+                "user=cachingSha256User4&serverRsaPublicKeyFile="
+                    + tempFile.getPath()
+                    + "2&password=!Passw0rd3Works"),
+        "Could not read server RSA public key from file");
+  }
+
+  @Test
+  public void cachingSha256Allow() throws Exception {
+    Assumptions.assumeTrue(!isMariaDBServer() && rsaPublicKey != null && minVersion(8, 0, 0));
+    // mysql 8.0.31 broken public key retrieval, so avoid FLUSHING for now
+    Assumptions.assumeTrue(!isMariaDBServer() && !minVersion(8, 0, 31));
+    sharedConn.createStatement().execute("FLUSH PRIVILEGES"); // reset cache
+    try (Connection con =
+        createCon("user=cachingSha256User3&allowPublicKeyRetrieval&password=!Passw0rd3Works")) {
+      con.isValid(1);
+    }
+  }
+
+  @Test
+  public void cachingSha256PluginTest() throws Exception {
+    Assumptions.assumeTrue(!isMariaDBServer() && rsaPublicKey != null && minVersion(8, 0, 0));
+    // mysql 8.0.31 broken public key retrieval, so avoid FLUSHING for now
+    Assumptions.assumeTrue(!isMariaDBServer() && !exactVersion(8, 0, 31));
+    sharedConn.createStatement().execute("FLUSH PRIVILEGES"); // reset cache
+
+    try (Connection con =
+        createCon(
+            "user=cachingSha256User&password=!Passw0rd3Works&serverRsaPublicKeyFile="
+                + rsaPublicKey)) {
+      con.isValid(1);
+    }
+
+    try (Connection con =
+        createCon("user=cachingSha256User&password=!Passw0rd3Works&allowPublicKeyRetrieval")) {
+      con.isValid(1);
+    }
+
+    Assumptions.assumeTrue(haveSsl());
+    try (Connection con =
+        createCon("user=cachingSha256User&password=!Passw0rd3Works&sslMode=trust")) {
+      con.isValid(1);
+    }
+
+    try (Connection con =
+        createCon("user=cachingSha256User&password=!Passw0rd3Works&allowPublicKeyRetrieval")) {
+      con.isValid(1);
+    }
+
+    try (Connection con =
+        createCon(
+            "user=cachingSha256User&password=!Passw0rd3Works&serverRsaPublicKeyFile="
+                + rsaPublicKey)) {
+      con.isValid(1);
+    }
+  }
+
+  @Test
+  public void cachingSha256PluginTestWithoutServerRsaKey() throws Exception {
+    Assumptions.assumeTrue(!isWindows() && minVersion(8, 0, 0));
+    // mysql 8.0.31 broken public key retrieval, so avoid FLUSHING for now
+    Assumptions.assumeTrue(!isMariaDBServer() && !minVersion(8, 0, 31));
+    sharedConn.createStatement().execute("FLUSH PRIVILEGES"); // reset cache
+    try (Connection con =
+        createCon("user=cachingSha256User&password=!Passw0rd3Works&allowPublicKeyRetrieval")) {
+      con.isValid(1);
+    }
+  }
+
+  @Test
+  public void cachingSha256PluginTestException() throws Exception {
+    Assumptions.assumeTrue(!isMariaDBServer() && minVersion(8, 0, 0));
+    // mysql 8.0.31 broken public key retrieval, so avoid FLUSHING for now
+    Assumptions.assumeTrue(!isMariaDBServer() && !exactVersion(8, 0, 31));
+    sharedConn.createStatement().execute("FLUSH PRIVILEGES"); // reset cache
+
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> createCon("user=cachingSha256User&password=!Passw0rd3Works"),
+        "RSA public key is not available client side");
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/SslTest.java b/src/test/java/org/mariadb/jdbc/integration/SslTest.java
new file mode 100644
index 0000000..5d3e892
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/SslTest.java
@@ -0,0 +1,366 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.nio.file.Paths;
+import java.security.NoSuchAlgorithmException;
+import java.sql.*;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import javax.net.ssl.SSLContext;
+import org.junit.jupiter.api.*;
+import org.mariadb.jdbc.*;
+import org.mariadb.jdbc.Connection;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.integration.tools.TcpProxy;
+
+@DisplayName("SSL tests")
+public class SslTest extends Common {
+  private static Integer sslPort;
+  private static final String baseOptions = "&user=serverAuthUser&password=!Passw0rd3Works";
+  private static final String baseMutualOptions = "&user=mutualAuthUser&password=!Passw0rd3Works";
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    try {
+      stmt.execute("DROP USER serverAuthUser");
+    } catch (SQLException e) {
+      // eat
+    }
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Assumptions.assumeTrue(haveSsl());
+    createSslUser("serverAuthUser", "REQUIRE SSL");
+    createSslUser("mutualAuthUser", "REQUIRE X509");
+
+    Statement stmt = sharedConn.createStatement();
+    // mysql 8.0.31 broken public key retrieval, so avoid FLUSHING for now
+    Assumptions.assumeTrue(isMariaDBServer() || (!isMariaDBServer() && !exactVersion(8, 0, 31)));
+    stmt.execute("FLUSH PRIVILEGES");
+    sslPort =
+        System.getenv("TEST_MAXSCALE_TLS_PORT") == null
+                || System.getenv("TEST_MAXSCALE_TLS_PORT").isEmpty()
+            ? null
+            : Integer.valueOf(System.getenv("TEST_MAXSCALE_TLS_PORT"));
+  }
+
+  private static void createSslUser(String user, String requirement) throws SQLException {
+    boolean useOldNotation =
+        (!isMariaDBServer() || !minVersion(10, 2, 0))
+            && (isMariaDBServer() || !minVersion(8, 0, 0));
+    Statement stmt = sharedConn.createStatement();
+    if (useOldNotation) {
+      stmt.execute("CREATE USER IF NOT EXISTS '" + user + "'@'%' " + requirement);
+      stmt.execute(
+          "GRANT SELECT ON *.* TO '"
+              + user
+              + "'@'%' IDENTIFIED BY '!Passw0rd3Works' "
+              + requirement);
+    } else {
+      if (!isMariaDBServer() && minVersion(8, 0, 0)) {
+        stmt.execute(
+            "CREATE USER IF NOT EXISTS '"
+                + user
+                + "'@'%' IDENTIFIED WITH mysql_native_password BY '!Passw0rd3Works' "
+                + requirement);
+      } else {
+        stmt.execute(
+            "CREATE USER IF NOT EXISTS '"
+                + user
+                + "'@'%' IDENTIFIED BY '!Passw0rd3Works' "
+                + requirement);
+      }
+      stmt.execute("GRANT SELECT ON " + sharedConn.getCatalog() + ".* TO '" + user + "'@'%' ");
+    }
+  }
+
+  private String getSslVersion(Connection con) throws SQLException {
+    Statement stmt = con.createStatement();
+    ResultSet rs = stmt.executeQuery("show STATUS  LIKE 'Ssl_version'");
+    if (rs.next()) {
+      return rs.getString(2);
+    }
+    return null;
+  }
+
+  @Test
+  public void simpleSsl() throws SQLException {
+    try (Connection con = createCon("sslMode=trust", sslPort)) {
+      assertNotNull(getSslVersion(con));
+    }
+    try (Connection con = createCon("sslMode=trust&useReadAheadInput=false", sslPort)) {
+      assertNotNull(getSslVersion(con));
+    }
+  }
+
+  @Test
+  public void mandatorySsl() throws SQLException {
+    Assumptions.assumeTrue(
+        !"maxscale".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+    try (Connection con = createCon(baseOptions + "&sslMode=trust", sslPort)) {
+      assertNotNull(getSslVersion(con));
+    }
+    assertThrows(SQLException.class, () -> createCon(baseOptions + "&sslMode=disable"));
+    assertThrows(
+        SQLInvalidAuthorizationSpecException.class,
+        () -> createCon(baseMutualOptions + "&sslMode=trust", sslPort));
+  }
+
+  @Test
+  public void enabledSslProtocolSuites() throws SQLException {
+    Assumptions.assumeTrue(
+        !"maxscale".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+    try {
+      List<String> protocols =
+          Arrays.asList(SSLContext.getDefault().getSupportedSSLParameters().getProtocols());
+      Assumptions.assumeTrue(protocols.contains("TLSv1.3") && protocols.contains("TLSv1.2"));
+    } catch (NoSuchAlgorithmException e) {
+      // eat
+    }
+    try (Connection con =
+        createCon(
+            baseOptions + "&sslMode=trust&enabledSslProtocolSuites=TLSv1.2,TLSv1.3", sslPort)) {
+      assertNotNull(getSslVersion(con));
+    }
+    Common.assertThrowsContains(
+        SQLNonTransientConnectionException.class,
+        () ->
+            createCon(baseMutualOptions + "&sslMode=trust&enabledSslProtocolSuites=SSLv3", sslPort),
+        "No appropriate protocol");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () ->
+            createCon(
+                baseMutualOptions + "&sslMode=trust&enabledSslProtocolSuites=unknown", sslPort),
+        "Unsupported SSL protocol 'unknown'");
+  }
+
+  @Test
+  public void enabledSslCipherSuites() throws SQLException {
+    Assumptions.assumeTrue(
+        !"maxscale".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+    try (Connection con =
+        createCon(
+            baseOptions
+                + "&sslMode=trust&enabledSslCipherSuites=TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
+            sslPort)) {
+      assertNotNull(getSslVersion(con));
+    }
+    Common.assertThrowsContains(
+        SQLException.class,
+        () ->
+            createCon(
+                baseMutualOptions + "&sslMode=trust&enabledSslCipherSuites=UNKNOWN_CIPHER",
+                sslPort),
+        "Unsupported SSL cipher");
+  }
+
+  @Test
+  public void mutualAuthSsl() throws SQLException {
+    Assumptions.assumeTrue(
+        !"maxscale".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+    Assumptions.assumeTrue(System.getenv("TEST_DB_CLIENT_PKCS") != null);
+
+    // without password
+    assertThrows(
+        SQLInvalidAuthorizationSpecException.class,
+        () ->
+            createCon(
+                baseMutualOptions
+                    + "&sslMode=trust&keyStore="
+                    + System.getenv("TEST_DB_CLIENT_PKCS"),
+                sslPort));
+    // with password
+    try (Connection con =
+        createCon(
+            baseMutualOptions
+                + "&sslMode=trust&keyStore="
+                + System.getenv("TEST_DB_CLIENT_PKCS")
+                + "&keyStorePassword=kspass",
+            sslPort)) {
+      assertNotNull(getSslVersion(con));
+    }
+
+    // with URL
+    boolean isWin = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win");
+    try (Connection con =
+        createCon(
+            baseMutualOptions
+                + "&sslMode=trust&keyStore="
+                + "file://"
+                + (isWin ? "/" : "")
+                + System.getenv("TEST_DB_CLIENT_PKCS")
+                + "&keyStorePassword=kspass",
+            sslPort)) {
+      assertNotNull(getSslVersion(con));
+    }
+
+    // wrong keystore type
+    assertThrows(
+        SQLInvalidAuthorizationSpecException.class,
+        () ->
+            createCon(
+                baseMutualOptions
+                    + "&sslMode=trust&keyStoreType=JKS&keyStore="
+                    + System.getenv("TEST_DB_CLIENT_PKCS"),
+                sslPort));
+    // good keystore type
+    try (Connection con =
+        createCon(
+            baseMutualOptions
+                + "&sslMode=trust&keyStoreType=pkcs12&keyStore="
+                + System.getenv("TEST_DB_CLIENT_PKCS")
+                + "&keyStorePassword=kspass",
+            sslPort)) {
+      assertNotNull(getSslVersion(con));
+    }
+
+    // with system properties
+    System.setProperty("javax.net.ssl.keyStore", System.getenv("TEST_DB_CLIENT_PKCS"));
+    System.setProperty("javax.net.ssl.keyStorePassword", "kspass");
+    try (Connection con = createCon(baseMutualOptions + "&sslMode=trust", sslPort)) {
+      assertNotNull(getSslVersion(con));
+    }
+
+    // wrong keystore type
+    System.setProperty("javax.net.ssl.keyStoreType", "JKS");
+    try (Connection con = createCon(baseMutualOptions + "&sslMode=trust", sslPort)) {
+      assertNotNull(getSslVersion(con));
+    }
+    try (Connection con = createCon(baseMutualOptions + "&sslMode=trust", sslPort)) {
+      assertNotNull(getSslVersion(con));
+    }
+
+    System.clearProperty("javax.net.ssl.keyStoreType");
+    try (Connection con =
+        createCon(baseMutualOptions + "&sslMode=trust&keyStoreType=JKS", sslPort)) {
+      assertNotNull(getSslVersion(con));
+    }
+
+    // without password
+    System.clearProperty("javax.net.ssl.keyStorePassword");
+    assertThrows(
+        SQLInvalidAuthorizationSpecException.class,
+        () -> createCon(baseMutualOptions + "&sslMode=trust", sslPort));
+  }
+
+  @Test
+  public void certificateMandatorySsl() throws Throwable {
+
+    String serverCertPath = retrieveCertificatePath();
+    Assumptions.assumeTrue(serverCertPath != null, "Canceled, server certificate not provided");
+
+    // certificate path, like  /path/certificate.crt
+    try (Connection con =
+        createCon(baseOptions + "&sslMode=VERIFY_CA&serverSslCert=" + serverCertPath, sslPort)) {
+      assertNotNull(getSslVersion(con));
+    }
+
+    try (Connection con =
+        createCon(baseOptions + "&sslMode=VERIFY_CA&serverSslCert=file:///wrongPath", sslPort)) {
+      assertNotNull(getSslVersion(con));
+    } catch (Exception e) {
+      assertTrue(e.getCause() instanceof FileNotFoundException);
+    }
+
+    if (!"localhost".equals(hostname)) {
+      try (Connection con =
+          createCon(
+              baseOptions + "&sslMode=VERIFY_FULL&serverSslCert=" + serverCertPath, sslPort)) {
+        assertNotNull(getSslVersion(con));
+      }
+
+      Configuration conf = Configuration.parse(mDefUrl);
+      HostAddress hostAddress = conf.addresses().get(0);
+      try {
+        proxy = new TcpProxy(hostAddress.host, sslPort == null ? hostAddress.port : sslPort);
+      } catch (IOException i) {
+        throw new SQLException("proxy error", i);
+      }
+
+      String url = mDefUrl.replaceAll("//([^/]*)/", "//localhost:" + proxy.getLocalPort() + "/");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () ->
+              DriverManager.getConnection(
+                  url + "&sslMode=VERIFY_FULL&serverSslCert=" + serverCertPath),
+          "DNS host \"localhost\" doesn't correspond to certificate");
+    }
+
+    String urlPath = Paths.get(serverCertPath).toUri().toURL().toString();
+    // file certificate path, like  file:/path/certificate.crt
+    try (Connection con =
+        createCon(baseOptions + "&sslMode=VERIFY_CA&serverSslCert=" + urlPath, sslPort)) {
+      assertNotNull(getSslVersion(con));
+    }
+
+    String certificateString = getServerCertificate(serverCertPath);
+    // file certificate, like  -----BEGIN CERTIFICATE-----...
+    try (Connection con =
+        createCon(baseOptions + "&sslMode=VERIFY_CA&serverSslCert=" + certificateString, sslPort)) {
+      assertNotNull(getSslVersion(con));
+    }
+
+    assertThrows(
+        SQLNonTransientConnectionException.class,
+        () -> createCon(baseOptions + "&sslMode=VERIFY_CA", sslPort));
+    assertThrows(
+        SQLInvalidAuthorizationSpecException.class,
+        () ->
+            createCon(
+                baseMutualOptions + "&sslMode=VERIFY_CA&serverSslCert=" + serverCertPath, sslPort));
+  }
+
+  private String getServerCertificate(String serverCertPath) throws SQLException {
+    try (BufferedReader br =
+        new BufferedReader(new InputStreamReader(new FileInputStream(serverCertPath)))) {
+      StringBuilder sb = new StringBuilder();
+      String line;
+      while ((line = br.readLine()) != null) {
+        sb.append(line);
+        sb.append("\n");
+      }
+      return sb.toString();
+    } catch (Exception e) {
+      throw new SQLException("abnormal exception", e);
+    }
+  }
+
+  public static String retrieveCertificatePath() throws Exception {
+    String serverCertificatePath = checkFileExists(System.getProperty("serverCertificatePath"));
+
+    // try local server
+    if (serverCertificatePath == null
+        && !"skysql".equals(System.getenv("srv"))
+        && !"skysql-ha".equals(System.getenv("srv"))) {
+
+      try (ResultSet rs = sharedConn.createStatement().executeQuery("select @@ssl_cert")) {
+        assertTrue(rs.next());
+        serverCertificatePath = checkFileExists(rs.getString(1));
+      }
+    }
+    if (serverCertificatePath == null) {
+      serverCertificatePath = checkFileExists("../../ssl/server.crt");
+    }
+    return serverCertificatePath;
+  }
+
+  private static String checkFileExists(String path) throws IOException {
+    if (path == null) return null;
+    File f = new File(path);
+    if (f.exists()) {
+      return f.getCanonicalPath().replace("\\", "/");
+    }
+    return null;
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/StatementTest.java b/src/test/java/org/mariadb/jdbc/integration/StatementTest.java
new file mode 100644
index 0000000..d8de05e
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/StatementTest.java
@@ -0,0 +1,966 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.junit.jupiter.api.*;
+import org.mariadb.jdbc.Connection;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.plugin.Codec;
+
+public class StatementTest extends Common {
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS StatementTest");
+    stmt.execute("DROP TABLE IF EXISTS executeGenerated");
+    stmt.execute("DROP TABLE IF EXISTS executeGenerated2");
+    stmt.execute("DROP TABLE IF EXISTS testAffectedRow");
+    stmt.execute("DROP TABLE IF EXISTS bigIntId");
+    stmt.execute("DROP TABLE IF EXISTS testCONJ956");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE testCONJ956 (field varchar(300) NOT NULL)");
+    stmt.execute("CREATE TABLE StatementTest (t1 int not null primary key auto_increment, t2 int)");
+    stmt.execute(
+        "CREATE TABLE executeGenerated (t1 int not null primary key auto_increment, t2 int)");
+    stmt.execute(
+        "CREATE TABLE executeGenerated2 (t1 int not null primary key auto_increment, t2 int)");
+    stmt.execute("CREATE TABLE testAffectedRow(id int)");
+    stmt.execute(
+        "CREATE TABLE bigIntId(`id` bigint(20) unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, val"
+            + " VARCHAR(256))");
+    createSequenceTables();
+    stmt.execute("FLUSH TABLES");
+  }
+
+  @Test
+  public void ensureGetGeneratedKeysReturnsEmptyResult() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE IF NOT EXISTS key_test (id INT(11) NOT NULL)");
+    try (PreparedStatement ps =
+        sharedConn.prepareStatement(
+            "INSERT INTO key_test(id) VALUES(5)", Statement.RETURN_GENERATED_KEYS)) {
+      ps.execute();
+      ResultSet rs = ps.getGeneratedKeys();
+      assertFalse(rs.next());
+    }
+    try (PreparedStatement ps =
+        sharedConn.prepareStatement(
+            "UPDATE key_test set id=7 WHERE id=5", Statement.RETURN_GENERATED_KEYS)) {
+      ps.execute();
+      ResultSet rs = ps.getGeneratedKeys();
+      assertFalse(rs.next());
+    }
+
+    stmt.execute("DROP TABLE key_test");
+  }
+
+  @Test
+  public void longGeneratedId() throws SQLException {
+    longGeneratedId(BigInteger.ONE);
+    longGeneratedId(BigInteger.valueOf(Integer.MAX_VALUE));
+    longGeneratedId(BigInteger.valueOf(4294967295L));
+    longGeneratedId(BigInteger.valueOf(Long.MAX_VALUE));
+  }
+
+  public void longGeneratedId(BigInteger expected) throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("ALTER TABLE bigIntId AUTO_INCREMENT=" + expected.toString());
+    stmt.execute(
+        "INSERT INTO bigIntId(val) value ('est')", java.sql.Statement.RETURN_GENERATED_KEYS);
+    ResultSet rs = stmt.getGeneratedKeys();
+
+    ResultSetMetaData rmeta = rs.getMetaData();
+    assertFalse(rmeta.isSigned(1));
+    assertTrue(rs.next());
+    if (expected.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) >= 1) {
+      assertThrowsContains(SQLDataException.class, () -> rs.getInt(1), "integer overflow");
+    } else {
+      assertEquals(expected.intValueExact(), rs.getInt(1));
+    }
+
+    if (expected.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) >= 1) {
+      assertThrowsContains(
+          SQLDataException.class, () -> rs.getLong(1), "cannot be decoded as Long");
+    } else {
+      assertEquals(expected.longValueExact(), rs.getLong(1));
+    }
+    assertTrue(expected.compareTo(((CompleteResult) rs).getBigInteger(1)) == 0);
+    assertTrue(new BigDecimal(expected).compareTo(rs.getBigDecimal(1)) == 0);
+  }
+
+  @Test
+  public void getConnection() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    assertEquals(ResultSet.TYPE_FORWARD_ONLY, stmt.getResultSetType());
+    assertEquals(ResultSet.CONCUR_READ_ONLY, stmt.getResultSetConcurrency());
+    assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, stmt.getResultSetHoldability());
+    assertEquals(sharedConn, stmt.getConnection());
+
+    stmt =
+        sharedConn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
+    assertEquals(ResultSet.TYPE_SCROLL_INSENSITIVE, stmt.getResultSetType());
+    assertEquals(ResultSet.CONCUR_UPDATABLE, stmt.getResultSetConcurrency());
+    assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, stmt.getResultSetHoldability());
+
+    stmt =
+        sharedConn.createStatement(
+            ResultSet.TYPE_SCROLL_INSENSITIVE,
+            ResultSet.CONCUR_UPDATABLE,
+            ResultSet.CLOSE_CURSORS_AT_COMMIT);
+    assertEquals(ResultSet.TYPE_SCROLL_INSENSITIVE, stmt.getResultSetType());
+    assertEquals(ResultSet.CONCUR_UPDATABLE, stmt.getResultSetConcurrency());
+
+    // not supported
+    assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, stmt.getResultSetHoldability());
+  }
+
+  @Test
+  public void setObjectError() throws SQLException {
+    try (PreparedStatement prep = sharedConn.prepareStatement("SELECT ?")) {
+      assertThrowsContains(
+          SQLException.class, () -> prep.setObject(1, "", Types.ARRAY), "Type not supported");
+      assertThrowsContains(
+          SQLException.class, () -> prep.setObject(1, "", JDBCType.ARRAY), "Type not supported");
+      assertThrowsContains(
+          SQLException.class,
+          () -> prep.setObject(1, "a", JDBCType.BLOB),
+          "Cannot convert a string to a Blob");
+      assertThrowsContains(
+          SQLException.class,
+          () -> prep.setObject(1, 'a', JDBCType.BLOB),
+          "Cannot convert a character to a Blob");
+    }
+  }
+
+  @Test
+  public void conj956() throws SQLException {
+    StringBuilder sb = new StringBuilder();
+    String sQuery = "SELECT EXISTS (SELECT 1 FROM testCONJ956 WHERE ((field=?)))";
+    for (int i = 1; i <= 300; i++) {
+      sb.append("a");
+      if (i < 204) {
+        continue;
+      }
+      PreparedStatement stmt = sharedConn.prepareStatement(sQuery);
+      stmt.setString(1, sb.toString());
+      stmt.executeQuery();
+    }
+  }
+
+  @Test
+  public void execute() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    assertTrue(stmt.execute("SELECT 1", Statement.RETURN_GENERATED_KEYS));
+    ResultSet rs = stmt.getGeneratedKeys();
+    Assertions.assertNull(rs.getWarnings());
+    assertFalse(rs.next());
+    assertNotNull(stmt.getResultSet());
+    assertEquals(-1, stmt.getUpdateCount());
+    assertFalse(stmt.getMoreResults());
+    assertEquals(-1, stmt.getUpdateCount());
+    if (!isXpand()) {
+      assertFalse(stmt.execute("DO 1"));
+      Assertions.assertNull(stmt.getResultSet());
+      assertEquals(0, stmt.getUpdateCount());
+      assertFalse(stmt.getMoreResults());
+      assertEquals(-1, stmt.getUpdateCount());
+    }
+
+    assertTrue(stmt.execute("SELECT 1", new int[] {1, 2}));
+    rs = stmt.getGeneratedKeys();
+    assertFalse(rs.next());
+
+    assertTrue(stmt.execute("SELECT 1", new String[] {"test", "test2"}));
+    rs = stmt.getGeneratedKeys();
+    assertFalse(rs.next());
+
+    stmt.close();
+  }
+
+  @Test
+  public void executeGenerated() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    assertFalse(stmt.execute("INSERT INTO executeGenerated(t2) values (100)"));
+
+    SQLException e = Assertions.assertThrows(SQLException.class, stmt::getGeneratedKeys);
+    assertTrue(e.getMessage().contains("Cannot return generated keys"));
+
+    assertFalse(
+        stmt.execute(
+            "INSERT INTO executeGenerated(t2) values (100)", Statement.RETURN_GENERATED_KEYS));
+    ResultSet rs = stmt.getGeneratedKeys();
+    assertTrue(rs.next());
+    assertEquals(2, rs.getInt(1));
+  }
+
+  @Test
+  public void executeGeneratedBatch() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.addBatch("INSERT INTO executeGenerated2(t2) values (110)");
+    stmt.addBatch("INSERT INTO executeGenerated2(t2) values (120)");
+    int[] res = stmt.executeBatch();
+    assertArrayEquals(new int[] {1, 1}, res);
+    ResultSet rs = stmt.getGeneratedKeys();
+    assertTrue(rs.next());
+    assertEquals(1, rs.getInt(1));
+    assertTrue(rs.next());
+    assertEquals(2, rs.getInt(1));
+    assertFalse(rs.next());
+  }
+
+  @Test
+  public void executeUpdate() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("INSERT INTO StatementTest(t1, t2) values (1, 110), (2, 120)");
+    assertEquals(
+        2, stmt.executeUpdate("UPDATE StatementTest SET t2 = 130 WHERE t2 > 100 AND t2 < 200"));
+    assertEquals(2, stmt.getUpdateCount());
+    assertFalse(stmt.getMoreResults());
+    assertEquals(-1, stmt.getUpdateCount());
+
+    assertEquals(
+        2,
+        stmt.executeUpdate(
+            "UPDATE StatementTest SET t2 = 150 WHERE t2 > 100 AND t2 < 200", new int[] {1, 2}));
+    assertEquals(2, stmt.getUpdateCount());
+
+    assertEquals(
+        2,
+        stmt.executeUpdate(
+            "UPDATE StatementTest SET t2 = 150 WHERE t2 > 100 AND t2 < 200",
+            new String[] {"test", "test2"}));
+    assertEquals(2, stmt.getUpdateCount());
+
+    try {
+      stmt.executeUpdate("SELECT 1");
+      Assertions.fail();
+    } catch (SQLException sqle) {
+      assertTrue(
+          sqle.getMessage()
+              .contains("the given SQL statement produces an unexpected ResultSet object"));
+    }
+    if (!isXpand()) {
+      assertEquals(0, stmt.executeUpdate("DO 1"));
+    }
+  }
+
+  @Test
+  public void executeLargeUpdate() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("INSERT INTO StatementTest(t1, t2) values (10, 210), (12, 220)");
+    assertEquals(2, stmt.executeLargeUpdate("UPDATE StatementTest SET t2 = 230 WHERE t2 > 200"));
+    assertEquals(2L, stmt.getLargeUpdateCount());
+    assertFalse(stmt.getMoreResults());
+    assertEquals(-1L, stmt.getLargeUpdateCount());
+
+    assertEquals(
+        2,
+        stmt.executeLargeUpdate(
+            "UPDATE StatementTest SET t2 = 250 WHERE t2 > 200", new int[] {1, 2}));
+    assertEquals(2L, stmt.getLargeUpdateCount());
+
+    assertEquals(
+        2,
+        stmt.executeLargeUpdate(
+            "UPDATE StatementTest SET t2 = 250 WHERE t2 > 200", new String[] {"test", "test2"}));
+    assertEquals(2L, stmt.getLargeUpdateCount());
+
+    try {
+      stmt.executeLargeUpdate("SELECT 1");
+      Assertions.fail();
+    } catch (SQLException sqle) {
+      assertTrue(
+          sqle.getMessage()
+              .contains("the given SQL statement produces an unexpected ResultSet object"));
+    }
+    if (!isXpand()) {
+      assertEquals(0, stmt.executeLargeUpdate("DO 1"));
+    }
+  }
+
+  @Test
+  public void executeQuery() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT 1");
+    assertTrue(rs.next());
+    if (!isXpand()) {
+      rs = stmt.executeQuery("DO 1");
+      assertFalse(rs.next());
+    }
+  }
+
+  @Test
+  public void close() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    assertFalse(stmt.isClosed());
+    ResultSet rs = stmt.executeQuery("select * FROM sequence_1_to_10 LIMIT 1");
+    rs.next();
+    rs.getObject(1);
+
+    rs = stmt.executeQuery("SELECT * FROM sequence_1_to_10000");
+    assertFalse(rs.isClosed());
+    stmt.close();
+    assertTrue(stmt.isClosed());
+    assertTrue(rs.isClosed());
+
+    Common.assertThrowsContains(
+        SQLException.class, stmt::clearBatch, "Cannot do an operation on a closed statement");
+    Common.assertThrowsContains(
+        SQLException.class, stmt::isPoolable, "Cannot do an operation on a closed statement");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> stmt.setPoolable(true),
+        "Cannot do an operation on a closed statement");
+    Common.assertThrowsContains(
+        SQLException.class,
+        stmt::closeOnCompletion,
+        "Cannot do an operation on a closed statement");
+    Common.assertThrowsContains(
+        SQLException.class,
+        stmt::isCloseOnCompletion,
+        "Cannot do an operation on a closed statement");
+
+    Common.assertThrowsContains(
+        SQLException.class,
+        stmt::getResultSetConcurrency,
+        "Cannot do an operation on a closed statement");
+    Common.assertThrowsContains(
+        SQLException.class, stmt::getFetchSize, "Cannot do an operation on a closed statement");
+    Common.assertThrowsContains(
+        SQLException.class, stmt::getMoreResults, "Cannot do an operation on a closed statement");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> stmt.execute("ANY"),
+        "Cannot do an operation on a closed statement");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> stmt.executeUpdate("ANY"),
+        "Cannot do an operation on a closed statement");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> stmt.executeQuery("ANY"),
+        "Cannot do an operation on a closed statement");
+    Common.assertThrowsContains(
+        SQLException.class, stmt::executeBatch, "Cannot do an operation on a closed statement");
+    Common.assertThrowsContains(
+        SQLException.class, stmt::getConnection, "Cannot do an operation on a closed statement");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> stmt.getMoreResults(1),
+        "Cannot do an operation on a closed statement");
+    Common.assertThrowsContains(
+        SQLException.class, stmt::cancel, "Cannot do an operation on a closed statement");
+    Common.assertThrowsContains(
+        SQLException.class, stmt::getMaxRows, "Cannot do an operation on a closed statement");
+    Common.assertThrowsContains(
+        SQLException.class, stmt::getLargeMaxRows, "Cannot do an operation on a closed statement");
+
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> stmt.setMaxRows(1),
+        "Cannot do an operation on a closed statement");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> stmt.setEscapeProcessing(true),
+        "Cannot do an operation on a closed statement");
+    Common.assertThrowsContains(
+        SQLException.class, stmt::getQueryTimeout, "Cannot do an operation on a closed statement");
+    Common.assertThrowsContains(
+        SQLException.class, stmt::getUpdateCount, "Cannot do an operation on a closed statement");
+    Common.assertThrowsContains(
+        SQLException.class,
+        stmt::getLargeUpdateCount,
+        "Cannot do an operation on a closed statement");
+  }
+
+  @Test
+  public void maxRows() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    assertEquals(0, stmt.getMaxRows());
+    try {
+      stmt.setMaxRows(-1);
+      Assertions.fail();
+    } catch (SQLException e) {
+      assertTrue(e.getMessage().contains("max rows cannot be negative"));
+    }
+
+    stmt.setMaxRows(10);
+    assertEquals(10, stmt.getMaxRows());
+
+    ResultSet rs = stmt.executeQuery("SELECT * FROM sequence_1_to_10000");
+    int i = 0;
+    while (rs.next()) {
+      i++;
+      assertEquals(i, rs.getInt(1));
+    }
+    assertEquals(10, i);
+
+    stmt.setQueryTimeout(2);
+    rs = stmt.executeQuery("SELECT * FROM sequence_1_to_10000");
+    i = 0;
+    while (rs.next()) {
+      i++;
+      assertEquals(i, rs.getInt(1));
+    }
+    assertEquals(10, i);
+  }
+
+  @Test
+  public void largeMaxRows() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    assertEquals(0L, stmt.getLargeMaxRows());
+    try {
+      stmt.setLargeMaxRows(-1);
+      Assertions.fail();
+    } catch (SQLException e) {
+      assertTrue(e.getMessage().contains("max rows cannot be negative"));
+    }
+
+    stmt.setLargeMaxRows(10);
+    assertEquals(10L, stmt.getLargeMaxRows());
+
+    ResultSet rs = stmt.executeQuery("SELECT * FROM sequence_1_to_10000");
+    int i = 0;
+    while (rs.next()) {
+      i++;
+      assertEquals(i, rs.getInt(1));
+    }
+    assertEquals(10, i);
+
+    stmt.setQueryTimeout(2);
+    rs = stmt.executeQuery("SELECT * FROM sequence_1_to_10000");
+    i = 0;
+    while (rs.next()) {
+      i++;
+      assertEquals(i, rs.getInt(1));
+    }
+    assertEquals(10, i);
+  }
+
+  @Test
+  public void checkFixedData() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    assertFalse(stmt.isPoolable());
+    stmt.setPoolable(true);
+    assertFalse(stmt.isPoolable());
+    assertFalse(stmt.isWrapperFor(String.class));
+    assertFalse(stmt.isWrapperFor(null));
+    assertTrue(stmt.isWrapperFor(Statement.class));
+    stmt.unwrap(java.sql.Statement.class);
+
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> stmt.unwrap(String.class),
+        "he receiver is not a wrapper and does not implement the interface");
+    Common.assertThrowsContains(
+        SQLException.class, () -> stmt.setCursorName(""), "Cursors are not supported");
+
+    assertEquals(ResultSet.FETCH_FORWARD, stmt.getFetchDirection());
+    stmt.setFetchDirection(ResultSet.FETCH_REVERSE);
+    assertEquals(ResultSet.FETCH_FORWARD, stmt.getFetchDirection());
+    assertEquals(ResultSet.CONCUR_READ_ONLY, stmt.getResultSetConcurrency());
+    assertEquals(ResultSet.TYPE_FORWARD_ONLY, stmt.getResultSetType());
+    assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, stmt.getResultSetHoldability());
+    assertEquals(0, stmt.getMaxFieldSize());
+    stmt.setMaxFieldSize(100);
+    assertEquals(0, stmt.getMaxFieldSize());
+  }
+
+  @Test
+  public void getMoreResults() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM sequence_1_to_10000");
+    assertFalse(stmt.getMoreResults(Statement.KEEP_CURRENT_RESULT));
+    assertFalse(rs.isClosed());
+
+    rs = stmt.executeQuery("SELECT * FROM sequence_1_to_10000");
+    stmt.getMoreResults(Statement.CLOSE_CURRENT_RESULT);
+    assertTrue(rs.isClosed());
+    stmt.close();
+  }
+
+  @Test
+  @Timeout(20)
+  public void queryTimeout() {
+    Assumptions.assumeTrue(
+        isMariaDBServer()
+            && !"maxscale".equals(System.getenv("srv"))
+            && !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv"))
+            && !isXpand());
+    Statement stmt = sharedConn.createStatement();
+
+    Common.assertThrowsContains(
+        SQLException.class, () -> stmt.setQueryTimeout(-1), "Query timeout cannot be negative");
+
+    Common.assertThrowsContains(
+        SQLTimeoutException.class,
+        () -> {
+          stmt.setQueryTimeout(1);
+          assertEquals(1, stmt.getQueryTimeout());
+          stmt.execute(
+              "select * from information_schema.columns as c1,  information_schema.tables,"
+                  + " information_schema.tables as t2");
+        },
+        "Query execution was interrupted (max_statement_time exceeded)");
+  }
+
+  @Test
+  public void smallQueryTimeout() throws Exception {
+    Statement stmt = sharedConn.createStatement();
+    stmt.setQueryTimeout(1);
+    stmt.execute("SELECT 1");
+
+    stmt.setMaxRows(1);
+    stmt.execute("SELECT 1");
+
+    stmt.setQueryTimeout(0);
+    stmt.execute("SELECT 1");
+  }
+
+  @Test
+  public void escaping() throws Exception {
+    try (Connection con =
+        (Connection) DriverManager.getConnection(mDefUrl + "&dumpQueriesOnException=true")) {
+      Statement stmt = con.createStatement();
+      Common.assertThrowsContains(
+          SQLException.class,
+          () ->
+              stmt.executeQuery(
+                  "select {fn timestampdiff(SQL_TSI_HOUR, '2003-02-01','2003-05-01')} df df "),
+          "select {fn timestampdiff" + "(SQL_TSI_HOUR, '2003-02-01','2003-05-01')} df df ");
+      stmt.setEscapeProcessing(true);
+      Common.assertThrowsContains(
+          SQLException.class,
+          () ->
+              stmt.executeQuery(
+                  "select {fn timestampdiff(SQL_TSI_HOUR, '2003-02-01','2003-05-01')} df df "),
+          "select timestampdiff(HOUR, '2003-02-01','2003-05-01') df df ");
+    }
+  }
+
+  @Test
+  public void testWarnings() throws SQLException {
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv")));
+    Assumptions.assumeTrue(isMariaDBServer() && !isXpand());
+
+    Statement stmt = sharedConn.createStatement();
+
+    // connection level
+    Assertions.assertNull(sharedConn.getWarnings());
+    stmt.executeQuery("select now() = 1");
+    SQLWarning warning = sharedConn.getWarnings();
+    assertTrue(warning.getMessage().contains("ncorrect datetime value: '1'"));
+    stmt.executeQuery("select now() = 1");
+    sharedConn.clearWarnings();
+    Assertions.assertNull(sharedConn.getWarnings());
+
+    // statement level
+    ResultSet rs = stmt.executeQuery("select now() = 1");
+    warning = rs.getWarnings();
+    assertTrue(warning.getMessage().contains("ncorrect datetime value: '1'"));
+
+    rs = stmt.executeQuery("select now() = 1");
+    rs.clearWarnings();
+    Assertions.assertNull(rs.getWarnings());
+
+    stmt.executeQuery("select now() = 1");
+    warning = stmt.getWarnings();
+    assertTrue(warning.getMessage().contains("ncorrect datetime value: '1'"));
+
+    stmt.executeQuery("select now() = 1");
+    stmt.clearWarnings();
+    Assertions.assertNull(stmt.getWarnings());
+  }
+
+  @Test
+  public void cancel() throws Exception {
+    Assumptions.assumeTrue(
+        isMariaDBServer()
+            && !"maxscale".equals(System.getenv("srv"))
+            && !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv"))
+            && !isXpand());
+    Statement stmt = sharedConn.createStatement();
+    stmt.cancel(); // will do nothing
+
+    ExecutorService exec = Executors.newFixedThreadPool(1);
+
+    Common.assertThrowsContains(
+        SQLTimeoutException.class,
+        () -> {
+          exec.execute(new CancelThread(stmt));
+          stmt.execute(
+              "select * from information_schema.columns as c1,  information_schema.tables,"
+                  + " information_schema.tables as t2");
+          exec.shutdown();
+        },
+        "Query execution was interrupted");
+  }
+
+  @Test
+  public void fetch() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    Statement stmt2 = sharedConn.createStatement();
+    Common.assertThrowsContains(
+        SQLException.class, () -> stmt.setFetchSize(-10), "invalid fetch size");
+
+    stmt.setFetchSize(10);
+    assertEquals(10, stmt.getFetchSize());
+    ResultSet rs = stmt.executeQuery("select * FROM sequence_1_to_10000");
+
+    for (int i = 1; i <= 10000; i++) {
+      assertTrue(rs.next());
+      assertEquals(i, rs.getInt(1));
+    }
+
+    assertFalse(rs.next());
+
+    rs = stmt.executeQuery("select * FROM sequence_1_to_10");
+    ResultSet rs2 = stmt2.executeQuery("SELECT 200");
+    for (int i = 1; i <= 10; i++) {
+      assertTrue(rs.next());
+      assertEquals(i, rs.getInt(1));
+    }
+    assertTrue(rs2.next());
+    assertEquals(200, rs2.getInt(1));
+  }
+
+  @Test
+  public void fetchUnFinishedSameStatement() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.setFetchSize(10);
+    assertEquals(10, stmt.getFetchSize());
+    ResultSet rs = stmt.executeQuery("select * FROM sequence_1_to_10000");
+
+    for (int i = 1; i <= 5000; i++) {
+      assertTrue(rs.next());
+      assertEquals(i, rs.getInt(1));
+    }
+
+    ResultSet rs2 = stmt.executeQuery("select * FROM sequence_1_to_10000");
+
+    for (int i = 5001; i <= 10000; i++) {
+      assertTrue(rs.next());
+      assertEquals(i, rs.getInt(1));
+    }
+    assertFalse(rs.next());
+
+    for (int i = 1; i <= 10000; i++) {
+      assertTrue(rs2.next());
+      assertEquals(i, rs2.getInt(1));
+    }
+    assertFalse(rs2.next());
+  }
+
+  @Test
+  public void fetchUnFinishedOtherStatement() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.setFetchSize(5);
+    assertEquals(5, stmt.getFetchSize());
+    ResultSet rs = stmt.executeQuery("select * FROM sequence_1_to_10000");
+
+    for (int i = 1; i <= 10; i++) {
+      assertTrue(rs.next());
+      assertEquals(i, rs.getInt(1));
+    }
+
+    Statement stmt2 = sharedConn.createStatement();
+    ResultSet rs2 = stmt2.executeQuery("select * FROM sequence_1_to_10000");
+
+    for (int i = 11; i <= 10000; i++) {
+      assertTrue(rs.next(), "val " + i);
+      assertEquals(i, rs.getInt(1));
+    }
+    assertFalse(rs.next());
+
+    for (int i = 1; i <= 10000; i++) {
+      assertTrue(rs2.next());
+      assertEquals(i, rs2.getInt(1));
+    }
+    assertFalse(rs2.next());
+  }
+
+  @Test
+  public void fetchUnfinished() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.setFetchSize(1);
+    stmt.executeQuery("select * FROM sequence_1_to_10");
+    assertFalse(stmt.getMoreResults());
+
+    Statement stmt2 = sharedConn.createStatement();
+    ResultSet rs = stmt2.executeQuery("SELECT 1");
+    rs.next();
+    assertEquals(1, rs.getInt(1));
+  }
+
+  @Test
+  public void fetchClose() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.setFetchSize(10);
+    assertEquals(10, stmt.getFetchSize());
+    ResultSet rs = stmt.executeQuery("select * FROM sequence_1_to_10000");
+
+    for (int i = 1; i <= 5000; i++) {
+      assertTrue(rs.next());
+      assertEquals(i, rs.getInt(1));
+    }
+    stmt.close();
+    assertTrue(rs.isClosed());
+    stmt.close();
+
+    Statement stmt2 = sharedConn.createStatement();
+    ResultSet rs2 = stmt2.executeQuery("select * FROM sequence_1_to_10000");
+    for (int i = 1; i <= 10000; i++) {
+      assertTrue(rs2.next());
+      assertEquals(i, rs2.getInt(1));
+    }
+    assertFalse(rs2.next());
+  }
+
+  @Test
+  public void executeBatchBasic() throws SQLException {
+    executeBatchBasic(sharedConn);
+    try (Connection con = createCon("allowLocalInfile=true")) {
+      executeBatchBasic(con);
+    }
+  }
+
+  private void executeBatchBasic(Connection con) throws SQLException {
+    Statement stmt = con.createStatement();
+    assertArrayEquals(new int[0], stmt.executeBatch());
+    stmt.clearBatch();
+    stmt.execute("DROP TABLE IF EXISTS executeBatchBasic");
+    stmt.execute(
+        "CREATE TABLE executeBatchBasic (t1 int not null primary key auto_increment, t2 int)");
+
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> stmt.addBatch(null),
+        "null cannot be set to addBatch(String sql)");
+
+    stmt.addBatch("INSERT INTO executeBatchBasic(t2) VALUES (55)");
+    stmt.setEscapeProcessing(true);
+    stmt.addBatch("INSERT INTO executeBatchBasic(t2) VALUES (56)");
+    int[] ret = stmt.executeBatch();
+    Assertions.assertArrayEquals(new int[] {1, 1}, ret);
+
+    ret = stmt.executeBatch();
+    Assertions.assertArrayEquals(new int[0], ret);
+
+    stmt.addBatch("INSERT INTO executeLargeBatchBasic(t2) VALUES (57)");
+    stmt.clearBatch();
+    ret = stmt.executeBatch();
+    Assertions.assertArrayEquals(new int[0], ret);
+    assertArrayEquals(new int[0], stmt.executeBatch());
+    stmt.addBatch("INSERT INTO executeLargeBatchBasic(t2) VALUES (57)");
+    stmt.addBatch("WRONG QUERY");
+    try {
+      stmt.executeBatch();
+      fail();
+    } catch (BatchUpdateException e) {
+      assertTrue(
+          e.getMessage().contains("You have an error in your SQL syntax")
+              || e.getMessage().contains("syntax error"));
+      assertNotNull(e.getCause());
+      assertEquals(e.getCause().getMessage(), e.getMessage());
+      assertEquals(((SQLException) e.getCause()).getSQLState(), e.getSQLState());
+      assertEquals(((SQLException) e.getCause()).getErrorCode(), e.getErrorCode());
+    }
+  }
+
+  @Test
+  public void executeLargeBatchBasic() throws SQLException {
+    executeLargeBatchBasic(sharedConn);
+    try (Connection con = createCon("allowLocalInfile=true")) {
+      executeLargeBatchBasic(con);
+    }
+  }
+
+  private void executeLargeBatchBasic(Connection con) throws SQLException {
+    Statement stmt = con.createStatement();
+    assertArrayEquals(new long[0], stmt.executeLargeBatch());
+    stmt.clearBatch();
+    stmt.execute("DROP TABLE IF EXISTS executeLargeBatchBasic");
+    stmt.execute(
+        "CREATE TABLE executeLargeBatchBasic (t1 int not null primary key auto_increment, t2 int)");
+    stmt.addBatch("INSERT INTO executeLargeBatchBasic(t2) VALUES (55)");
+    stmt.addBatch("INSERT INTO executeLargeBatchBasic(t2) VALUES (56)");
+    long[] ret = stmt.executeLargeBatch();
+    Assertions.assertArrayEquals(new long[] {1, 1}, ret);
+
+    ret = stmt.executeLargeBatch();
+    Assertions.assertArrayEquals(new long[0], ret);
+
+    stmt.addBatch("INSERT INTO executeLargeBatchBasic(t2) VALUES (57)");
+    stmt.clearBatch();
+    ret = stmt.executeLargeBatch();
+    Assertions.assertArrayEquals(new long[0], ret);
+    ret = stmt.executeLargeBatch();
+    Assertions.assertArrayEquals(new long[0], ret);
+    stmt.addBatch("INSERT INTO executeLargeBatchBasic(t2) VALUES (57)");
+    stmt.addBatch("WRONG QUERY");
+    try {
+      stmt.executeBatch();
+      fail();
+    } catch (BatchUpdateException e) {
+      assertTrue(
+          e.getMessage().contains("You have an error in your SQL syntax")
+              || e.getMessage().contains("syntax error"));
+      assertNotNull(e.getCause());
+      assertEquals(e.getCause().getMessage(), e.getMessage());
+      assertEquals(((SQLException) e.getCause()).getSQLState(), e.getSQLState());
+      assertEquals(((SQLException) e.getCause()).getErrorCode(), e.getErrorCode());
+    }
+  }
+
+  @Test
+  public void fetchSize() throws SQLException {
+    assertEquals(0, sharedConn.createStatement().getFetchSize());
+    try (Connection con = createCon("&defaultFetchSize=10")) {
+      assertEquals(10, con.createStatement().getFetchSize());
+      try (PreparedStatement prep = con.prepareStatement("SELECT ?")) {
+        assertEquals(10, prep.getFetchSize());
+      }
+    }
+  }
+
+  @Test
+  public void moreResults() throws SQLException {
+    // error MXS-3929 for maxscale 6.2.0
+    Assumptions.assumeTrue(
+        !sharedConn.getMetaData().getDatabaseProductVersion().contains("maxScale-6.2.0"));
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP PROCEDURE IF EXISTS multi");
+    stmt.setFetchSize(3);
+    stmt.execute(
+        "CREATE PROCEDURE multi() BEGIN SELECT * from sequence_1_to_10; SELECT * FROM"
+            + " sequence_1_to_10000;SELECT 2; END");
+    stmt.execute("CALL multi()");
+    assertTrue(stmt.getMoreResults());
+    ResultSet rs = stmt.getResultSet();
+    int i = 1;
+    while (rs.next()) {
+      assertEquals(i++, rs.getInt(1));
+    }
+    assertEquals(10001, i);
+    stmt.setFetchSize(3);
+
+    rs = stmt.executeQuery("CALL multi()");
+    assertFalse(rs.isClosed());
+    stmt.setFetchSize(0); // force more result to load all remaining result-set
+    assertTrue(stmt.getMoreResults());
+    assertTrue(rs.isClosed());
+    rs = stmt.getResultSet();
+    i = 1;
+    while (rs.next()) {
+      assertEquals(i++, rs.getInt(1));
+    }
+
+    stmt.setFetchSize(3);
+    rs = stmt.executeQuery("CALL multi()");
+    assertFalse(rs.isClosed());
+    stmt.setFetchSize(0); // force more result to load all remaining result-set
+    assertTrue(stmt.getMoreResults(java.sql.Statement.KEEP_CURRENT_RESULT));
+    assertFalse(rs.isClosed());
+    i = 1;
+    while (rs.next()) {
+      assertEquals(i++, rs.getInt(1));
+    }
+    assertEquals(11, i);
+    rs = stmt.getResultSet();
+    i = 1;
+    while (rs.next()) {
+      assertEquals(i++, rs.getInt(1));
+    }
+    assertEquals(10001, i);
+
+    rs = stmt.executeQuery("CALL multi()");
+    stmt.close();
+    assertTrue(rs.isClosed());
+  }
+
+  @Test
+  public void closeOnCompletion() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    assertFalse(stmt.isCloseOnCompletion());
+    stmt.closeOnCompletion();
+    assertTrue(stmt.isCloseOnCompletion());
+    assertFalse(stmt.isClosed());
+    ResultSet rs = stmt.executeQuery("SELECT 1");
+    assertFalse(rs.isClosed());
+    assertFalse(stmt.isClosed());
+    rs.close();
+    assertTrue(rs.isClosed());
+    assertTrue(stmt.isClosed());
+  }
+
+  private static class CancelThread implements Runnable {
+
+    private final java.sql.Statement stmt;
+
+    public CancelThread(java.sql.Statement stmt) {
+      this.stmt = stmt;
+    }
+
+    @Override
+    public void run() {
+      try {
+        Thread.sleep(100);
+        stmt.cancel();
+      } catch (SQLException | InterruptedException e) {
+        e.printStackTrace();
+      }
+    }
+  }
+
+  @Test
+  public void testAffectedRow() throws SQLException {
+    testAffectedRow(false);
+    if (!isXpand()) {
+      testAffectedRow(true);
+    }
+  }
+
+  @Test
+  public void ensureClassDefined() {
+    for (Codec<?> codec : sharedConn.getContext().getConf().codecs()) {
+      Type it = codec.getClass().getGenericInterfaces()[0];
+      ParameterizedType parameterizedType = (ParameterizedType) it;
+      Type typeParameter = parameterizedType.getActualTypeArguments()[0];
+      if (!"byte[]".equals(codec.className()))
+        assertEquals(((Class<?>) typeParameter).getName(), codec.className());
+    }
+  }
+
+  private void testAffectedRow(boolean useAffectedRows) throws SQLException {
+    try (Connection con = createCon("&useAffectedRows=" + useAffectedRows)) {
+      java.sql.Statement stmt = con.createStatement();
+      stmt.execute("TRUNCATE testAffectedRow");
+      stmt.execute("START TRANSACTION");
+      stmt.execute("INSERT INTO testAffectedRow values (1), (1), (2), (3)");
+      int rowCount = stmt.executeUpdate("UPDATE testAffectedRow set id = 1");
+      assertEquals(useAffectedRows ? 2 : 4, rowCount);
+      con.rollback();
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/UnixsocketTest.java b/src/test/java/org/mariadb/jdbc/integration/UnixsocketTest.java
new file mode 100644
index 0000000..d214540
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/UnixsocketTest.java
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.sql.SQLNonTransientConnectionException;
+import java.util.Properties;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+
+public class UnixsocketTest extends Common {
+  @Test
+  public void testConnectWithUnixSocketWhenDBNotUp() throws IOException {
+    Assumptions.assumeTrue(!isWindows());
+    Assumptions.assumeTrue(
+        !"maxscale".equals(System.getenv("srv"))
+            && !"skysql".equals(System.getenv("srv"))
+            && !"xpand".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv")));
+
+    String url = mDefUrl + "&localSocket=/tmp/not_valid_socket&localSocketAddress=localhost";
+
+    java.sql.Driver driver = new org.mariadb.jdbc.Driver();
+
+    Runtime rt = Runtime.getRuntime();
+    // System.out.println("netstat-apnx | grep " + ProcessHandle.current().pid());
+    String[] commands = {"/bin/sh", "-c", "netstat -apnx | grep " + ProcessHandle.current().pid()};
+    Process proc = rt.exec(commands);
+
+    BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));
+    int initialLines = 0;
+    while (stdInput.readLine() != null) {
+      initialLines++;
+    }
+    proc.destroy();
+
+    for (int i = 0; i < 10; i++) {
+      assertThrows(
+          SQLNonTransientConnectionException.class,
+          () -> {
+            driver.connect(url, new Properties());
+          });
+    }
+    proc = rt.exec(commands);
+    stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));
+    int finalLines = 0;
+    while (stdInput.readLine() != null) {
+      finalLines++;
+    }
+    proc.destroy();
+    assertEquals(
+        finalLines,
+        initialLines,
+        "Error Leaking socket file descriptors. initial :"
+            + initialLines
+            + " but ending with "
+            + finalLines);
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/UpdateResultSetTest.java b/src/test/java/org/mariadb/jdbc/integration/UpdateResultSetTest.java
new file mode 100644
index 0000000..18ba2e2
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/UpdateResultSetTest.java
@@ -0,0 +1,1174 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+import java.sql.*;
+import org.junit.jupiter.api.*;
+
+public class UpdateResultSetTest extends Common {
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    org.mariadb.jdbc.Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS testnoprimarykey");
+    stmt.execute("DROP TABLE IF EXISTS testMultipleTable1");
+    stmt.execute("DROP TABLE IF EXISTS testMultipleTable2");
+    stmt.execute("DROP TABLE IF EXISTS testOneNoTable");
+    stmt.execute("DROP TABLE IF EXISTS UpdateWithoutPrimary");
+    stmt.execute("DROP TABLE IF EXISTS testbasicprimarykey");
+    stmt.execute("DROP DATABASE IF EXISTS testConnectorJ");
+    stmt.execute("DROP TABLE IF EXISTS testUpdateWhenFetch");
+    stmt.execute("DROP TABLE IF EXISTS testExpError");
+    stmt.execute("DROP TABLE IF EXISTS `testDefaultUUID`");
+    stmt.execute("DROP TABLE IF EXISTS `test_update_max`");
+    stmt.execute("DROP TABLE IF EXISTS `testAutoIncrement`");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    org.mariadb.jdbc.Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE testnoprimarykey(`id` INT NOT NULL,`t1` VARCHAR(50) NOT NULL)");
+    stmt.execute(
+        "CREATE TABLE testbasicprimarykey(`id` INT NOT NULL,`t1` VARCHAR(50) NOT NULL, CONSTRAINT"
+            + " pk PRIMARY KEY (id))");
+    stmt.execute(
+        "CREATE TABLE testMultipleTable1(`id1` INT NOT NULL AUTO_INCREMENT,`t1` VARCHAR(50)"
+            + " NULL,PRIMARY KEY (`id1`))");
+    stmt.execute(
+        "CREATE TABLE testMultipleTable2(`id2` INT NOT NULL AUTO_INCREMENT,`t1` VARCHAR(50)"
+            + " NULL,PRIMARY KEY (`id2`))");
+    stmt.execute(
+        "CREATE TABLE testOneNoTable(`id1` INT NOT NULL AUTO_INCREMENT,`t1` VARCHAR(50)"
+            + " NULL,PRIMARY KEY (`id1`))");
+    stmt.execute(
+        "CREATE TABLE testAutoIncrement(`id1` INT NOT NULL AUTO_INCREMENT,`t1` VARCHAR(50)"
+            + " NULL,PRIMARY KEY (`id1`))");
+
+    stmt.execute(
+        "CREATE TABLE testUpdateWhenFetch("
+            + "`id` INT NOT NULL AUTO_INCREMENT,"
+            + "`t1` VARCHAR(50) NOT NULL,"
+            + "`t2` VARCHAR(50) NULL default 'default-value',"
+            + "PRIMARY KEY (`id`))"
+            + "DEFAULT CHARSET=utf8");
+    stmt.execute(
+        "CREATE TABLE testExpError ("
+            + " `id1` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,"
+            + "`t1` varchar(100) DEFAULT NULL,"
+            + "`t2` varchar(100) DEFAULT NULL)");
+    if (isMariaDBServer() && minVersion(10, 2, 0)) {
+      stmt.execute(
+          "CREATE TABLE `testDefaultUUID` ("
+              + "`column1` varchar(40) NOT NULL DEFAULT uuid(),"
+              + "`column2` varchar(100) DEFAULT NULL,"
+              + " PRIMARY KEY (`column1`))");
+    }
+    stmt.execute(
+        "CREATE TABLE test_update_max(`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,`t1` VARCHAR(50)"
+            + " NOT NULL)");
+  }
+
+  /**
+   * Test error message when no primary key.
+   *
+   * @throws Exception not expected
+   */
+  @Test
+  public void testNoPrimaryKey() throws Exception {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("INSERT INTO testnoprimarykey VALUES (1, 't1'), (2, 't2')");
+
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement(
+            "SELECT * FROM testnoprimarykey",
+            ResultSet.TYPE_FORWARD_ONLY,
+            ResultSet.CONCUR_UPDATABLE)) {
+      ResultSet rs = preparedStatement.executeQuery();
+      assertTrue(rs.next());
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> rs.updateString(1, "1"),
+          "ResultSet cannot be updated. Cannot update rows, since no primary field is present in"
+              + " query");
+    }
+  }
+
+  @Test
+  public void testBasicPrimaryKey() throws Exception {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("INSERT INTO testbasicprimarykey VALUES (1, 't1'), (2, 't2')");
+
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement(
+            "SELECT t1 FROM testbasicprimarykey",
+            ResultSet.TYPE_FORWARD_ONLY,
+            ResultSet.CONCUR_UPDATABLE)) {
+      ResultSet rs = preparedStatement.executeQuery();
+      assertTrue(rs.next());
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> rs.updateString(1, "val"),
+          "ResultSet cannot be updated. Cannot update rows, since primary field id is not present"
+              + " in query");
+    }
+  }
+
+  @Test
+  public void testNoDatabase() throws Exception {
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement(
+            "SELECT 1", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)) {
+      ResultSet rs = preparedStatement.executeQuery();
+      assertTrue(rs.next());
+      Common.assertThrowsContains(
+          SQLFeatureNotSupportedException.class,
+          () -> rs.updateString(1, "1"),
+          "The result-set contains fields without without any database/table information");
+    }
+  }
+
+  @Test
+  public void testMultipleTable() throws Exception {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    stmt.executeQuery("INSERT INTO testMultipleTable1(t1) values ('1')");
+    stmt.executeQuery("INSERT INTO testMultipleTable2(t1) values ('2')");
+
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement(
+            "SELECT * FROM testMultipleTable1, testMultipleTable2",
+            ResultSet.TYPE_FORWARD_ONLY,
+            ResultSet.CONCUR_UPDATABLE)) {
+      ResultSet rs = preparedStatement.executeQuery();
+      assertTrue(rs.next());
+      Common.assertThrowsContains(
+          SQLFeatureNotSupportedException.class,
+          () -> rs.updateString("t1", "new value"),
+          "ResultSet cannot be updated. The result-set contains fields on different tables");
+    } finally {
+      sharedConn.rollback();
+    }
+  }
+
+  @Test
+  public void testOneNoTable() throws Exception {
+    Statement stmt = sharedConn.createStatement();
+    stmt.executeQuery("INSERT INTO testOneNoTable(t1) values ('1')");
+
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement(
+            "SELECT *, now() FROM testOneNoTable",
+            ResultSet.TYPE_FORWARD_ONLY,
+            ResultSet.CONCUR_UPDATABLE)) {
+      ResultSet rs = preparedStatement.executeQuery();
+      assertTrue(rs.next());
+      Common.assertThrowsContains(
+          SQLFeatureNotSupportedException.class,
+          () -> rs.updateString("t1", "new value"),
+          "ResultSet cannot be updated. "
+              + "The result-set contains fields without without any database/table information");
+    }
+  }
+
+  @Test
+  public void testAutoIncrement() throws Exception {
+    Statement stmt = sharedConn.createStatement();
+    PreparedStatement pstmt =
+        sharedConn.prepareStatement("INSERT INTO testAutoIncrement(t1) values (?)");
+    pstmt.setString(1, "1");
+    pstmt.execute();
+
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement(
+            "SELECT id1, t1 FROM testAutoIncrement",
+            ResultSet.TYPE_FORWARD_ONLY,
+            ResultSet.CONCUR_UPDATABLE)) {
+      ResultSet rs = preparedStatement.executeQuery();
+
+      rs.moveToInsertRow();
+      rs.updateString(2, "0-1");
+      rs.insertRow();
+    }
+
+    ResultSet rs = stmt.executeQuery("SELECT * FROM testAutoIncrement");
+    assertTrue(rs.next());
+    assertEquals(1, rs.getInt(1));
+    assertEquals("1", rs.getString(2));
+    rs.next();
+    assertEquals(2, rs.getInt(1));
+    assertEquals("0-1", rs.getString(2));
+    assertFalse(rs.next());
+  }
+
+  @Test
+  public void testMultipleDatabase() throws Exception {
+    Statement stmt = sharedConn.createStatement();
+    try {
+      stmt.execute("DROP DATABASE testConnectorJ");
+    } catch (SQLException sqle) {
+      // eat
+    }
+    stmt.execute("CREATE DATABASE testConnectorJ");
+    stmt.execute("DROP TABLE IF EXISTS testMultipleDatabase");
+    stmt.execute(
+        "CREATE TABLE testMultipleDatabase(`id1` INT NOT NULL AUTO_INCREMENT,`t1` VARCHAR(50)"
+            + " NULL,PRIMARY KEY (`id1`))");
+    stmt.execute(
+        "CREATE TABLE testConnectorJ.testMultipleDatabase(`id2` INT NOT NULL AUTO_INCREMENT,`t2`"
+            + " VARCHAR(50) NULL,PRIMARY KEY (`id2`))");
+    stmt.executeQuery("INSERT INTO testMultipleDatabase(t1) values ('1')");
+    stmt.executeQuery("INSERT INTO testConnectorJ.testMultipleDatabase(t2) values ('2')");
+
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement(
+            "SELECT * FROM "
+                + sharedConn.getCatalog()
+                + ".testMultipleDatabase, testConnectorJ.testMultipleDatabase",
+            ResultSet.TYPE_FORWARD_ONLY,
+            ResultSet.CONCUR_UPDATABLE)) {
+      ResultSet rs = preparedStatement.executeQuery();
+      assertTrue(rs.next());
+      Common.assertThrowsContains(
+          SQLFeatureNotSupportedException.class,
+          () -> rs.updateString("t1", "new value"),
+          "The result-set contains more than one database");
+    }
+  }
+
+  @Test
+  public void testMeta() throws Exception {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE UpdateWithoutPrimary(`id` INT NOT NULL AUTO_INCREMENT,"
+            + "`t1` VARCHAR(50) NOT NULL,"
+            + "`t2` VARCHAR(50) NULL default 'default-value',"
+            + "PRIMARY KEY (`id`))");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    stmt.executeQuery("INSERT INTO UpdateWithoutPrimary(t1,t2) values ('1-1','1-2')");
+
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement(
+            "SELECT t1, t2 FROM UpdateWithoutPrimary",
+            ResultSet.TYPE_FORWARD_ONLY,
+            ResultSet.CONCUR_UPDATABLE)) {
+      ResultSet rs = preparedStatement.executeQuery();
+      assertTrue(rs.next());
+
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> {
+            rs.updateString(1, "1-1-bis");
+            rs.updateRow();
+          },
+          "ResultSet cannot be updated. Cannot update rows, since primary field id is not present"
+              + " in query");
+      Common.assertThrowsContains(
+          SQLException.class,
+          rs::deleteRow,
+          "ResultSet cannot be updated. Cannot update rows, since primary field id is not present"
+              + " in query");
+      ResultSetMetaData rsmd = rs.getMetaData();
+      assertFalse(rsmd.isReadOnly(1));
+      assertFalse(rsmd.isReadOnly(2));
+      assertTrue(rsmd.isWritable(1));
+      assertTrue(rsmd.isWritable(2));
+      assertTrue(rsmd.isDefinitelyWritable(1));
+      assertTrue(rsmd.isDefinitelyWritable(2));
+
+      Common.assertThrowsContains(
+          SQLException.class, () -> rsmd.isReadOnly(3), "wrong column index 3");
+      Common.assertThrowsContains(
+          SQLException.class, () -> rsmd.isWritable(3), "wrong column index 3");
+      Common.assertThrowsContains(
+          SQLException.class, () -> rsmd.isDefinitelyWritable(3), "wrong column index 3");
+    }
+    ResultSet rs = stmt.executeQuery("SELECT id, t1, t2 FROM UpdateWithoutPrimary");
+    assertTrue(rs.next());
+    assertEquals(1, rs.getInt(1));
+    assertEquals("1-1", rs.getString(2));
+    assertEquals("1-2", rs.getString(3));
+    assertFalse(rs.next());
+    sharedConn.rollback();
+  }
+
+  @Test
+  public void testUpdateWhenFetch() throws Exception {
+    Statement stmt = sharedConn.createStatement();
+    PreparedStatement pstmt =
+        sharedConn.prepareStatement("INSERT INTO testUpdateWhenFetch(t1,t2) values (?, ?)");
+    for (int i = 1; i < 100; i++) {
+      pstmt.setString(1, i + "-1");
+      pstmt.setString(2, i + "-2");
+      pstmt.addBatch();
+    }
+    pstmt.executeBatch();
+
+    String utf8escapeQuote = "你好 '' \" \\";
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement(
+            "SELECT id, t1, t2 FROM testUpdateWhenFetch",
+            ResultSet.TYPE_FORWARD_ONLY,
+            ResultSet.CONCUR_UPDATABLE)) {
+      preparedStatement.setFetchSize(2);
+      ResultSet rs = preparedStatement.executeQuery();
+      assertFalse(rs.rowInserted());
+      assertFalse(rs.rowUpdated());
+
+      rs.moveToInsertRow();
+      rs.updateInt(1, -1);
+      rs.updateString(2, "0-1");
+      rs.updateString(3, "0-2");
+      Common.assertThrowsContains(
+          SQLSyntaxErrorException.class, () -> rs.updateObject(10, "val"), "No such column: 10");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> rs.updateObject(2, new SQLException("dd")),
+          "not supported type");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> rs.updateObject(2, new SQLException("dd"), null, 20),
+          "not supported type");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> rs.updateObject("t2", new SQLException("dd"), null),
+          "not supported type");
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> rs.updateObject("t2", new SQLException("dd"), null, 20),
+          "not supported type");
+
+      Common.assertThrowsContains(
+          SQLSyntaxErrorException.class, () -> rs.updateObject(-10, "val"), "No such column: -10");
+      rs.insertRow();
+      assertTrue(rs.rowInserted());
+
+      rs.next();
+      assertFalse(rs.rowInserted());
+      rs.next();
+      rs.updateString(2, utf8escapeQuote);
+      assertFalse(rs.rowUpdated());
+      assertFalse(rs.rowDeleted());
+      assertFalse(rs.rowInserted());
+      rs.updateRow();
+      assertTrue(rs.rowUpdated());
+    }
+
+    ResultSet rs = stmt.executeQuery("SELECT id, t1, t2 FROM testUpdateWhenFetch");
+    assertTrue(rs.next());
+    assertEquals(-1, rs.getInt(1));
+    assertEquals("0-1", rs.getString(2));
+    assertEquals("0-2", rs.getString(3));
+
+    assertTrue(rs.next());
+    assertEquals(1, rs.getInt(1));
+    assertEquals("1-1", rs.getString(2));
+    assertEquals("1-2", rs.getString(3));
+
+    assertTrue(rs.next());
+    assertEquals(2, rs.getInt(1));
+    assertEquals(utf8escapeQuote, rs.getString(2));
+    assertEquals("2-2", rs.getString(3));
+
+    for (int i = 3; i < 100; i++) {
+      assertTrue(rs.next());
+      assertEquals(i + "-1", rs.getString(2));
+      assertEquals(i + "-2", rs.getString(3));
+    }
+    assertFalse(rs.next());
+  }
+
+  @Test
+  public void testPrimaryGenerated() throws Exception {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS PrimaryGenerated");
+    stmt.execute(
+        "CREATE TABLE PrimaryGenerated("
+            + "`id` INT NOT NULL AUTO_INCREMENT,"
+            + "`t1` VARCHAR(50) NOT NULL,"
+            + "`t2` VARCHAR(50) NULL default 'default-value',"
+            + "PRIMARY KEY (`id`))");
+
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement(
+            "SELECT t1, t2, id FROM PrimaryGenerated",
+            ResultSet.TYPE_FORWARD_ONLY,
+            ResultSet.CONCUR_UPDATABLE)) {
+      ResultSet rs = preparedStatement.executeQuery();
+      assertFalse(rs.next());
+
+      rs.moveToInsertRow();
+      rs.updateString(1, "1-1");
+      rs.updateString(2, "1-2");
+      rs.insertRow();
+
+      rs.moveToInsertRow();
+      rs.updateString(1, "2-1");
+      rs.insertRow();
+
+      rs.moveToInsertRow();
+      rs.updateString(2, "3-2");
+      assertThrows(SQLException.class, rs::insertRow);
+
+      rs.absolute(1);
+      assertEquals("1-1", rs.getString(1));
+      assertEquals("1-2", rs.getString(2));
+      assertEquals(1, rs.getInt(3));
+
+      assertTrue(rs.next());
+      assertEquals("2-1", rs.getString(1));
+      assertEquals("default-value", rs.getString(2));
+      assertEquals(2, rs.getInt(3));
+
+      assertFalse(rs.next());
+    }
+
+    ResultSet rs = stmt.executeQuery("SELECT id, t1, t2 FROM PrimaryGenerated");
+    assertTrue(rs.next());
+    assertEquals(1, rs.getInt(1));
+    assertEquals("1-1", rs.getString(2));
+    assertEquals("1-2", rs.getString(3));
+
+    assertTrue(rs.next());
+    assertEquals(2, rs.getInt(1));
+    assertEquals("2-1", rs.getString(2));
+    assertEquals("default-value", rs.getString(3));
+
+    assertFalse(rs.next());
+  }
+
+  @Test
+  public void testPrimaryGeneratedDefault() throws Exception {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS testPrimaryGeneratedDefault");
+    stmt.execute(
+        "CREATE TABLE testPrimaryGeneratedDefault("
+            + "`id` INT NOT NULL AUTO_INCREMENT,"
+            + "`t1` VARCHAR(50) NOT NULL default 'default-value1',"
+            + "`t2` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,"
+            + "PRIMARY KEY (`id`))");
+
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement(
+            "SELECT id, t1, t2 FROM testPrimaryGeneratedDefault",
+            ResultSet.TYPE_FORWARD_ONLY,
+            ResultSet.CONCUR_UPDATABLE)) {
+      ResultSet rs = preparedStatement.executeQuery();
+      assertFalse(rs.next());
+      rs.moveToInsertRow();
+      rs.insertRow();
+
+      rs.moveToInsertRow();
+      rs.insertRow();
+
+      rs.first();
+      rs.moveToInsertRow();
+      rs.first();
+
+      rs.last();
+      rs.moveToInsertRow();
+      rs.last();
+
+      rs.afterLast();
+      rs.moveToInsertRow();
+      rs.afterLast();
+
+      rs.relative(-1);
+      rs.moveToInsertRow();
+      rs.relative(-1);
+
+      rs.next();
+      rs.moveToInsertRow();
+      rs.next();
+
+      rs.previous();
+      rs.moveToInsertRow();
+      rs.previous();
+
+      rs.beforeFirst();
+      rs.moveToInsertRow();
+      rs.beforeFirst();
+
+      assertTrue(rs.next());
+      assertEquals(1, rs.getInt(1));
+      assertEquals("default-value1", rs.getString(2));
+      assertNotNull(rs.getDate(3));
+
+      assertTrue(rs.next());
+      assertEquals(2, rs.getInt(1));
+      assertEquals("default-value1", rs.getString(2));
+      assertNotNull(rs.getDate(3));
+      assertFalse(rs.next());
+    }
+
+    ResultSet rs = stmt.executeQuery("SELECT id, t1, t2 FROM testPrimaryGeneratedDefault");
+    assertTrue(rs.next());
+    assertEquals(1, rs.getInt(1));
+    assertEquals("default-value1", rs.getString(2));
+    assertNotNull(rs.getDate(3));
+
+    assertTrue(rs.next());
+    assertEquals(2, rs.getInt(1));
+    assertEquals("default-value1", rs.getString(2));
+    assertNotNull(rs.getDate(3));
+
+    assertFalse(rs.next());
+  }
+
+  @Test
+  public void testDelete() throws Exception {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS testDelete");
+    stmt.execute(
+        "CREATE TABLE testDelete("
+            + "`id` INT NOT NULL,"
+            + "`id2` INT NOT NULL,"
+            + "`t1` VARCHAR(50),"
+            + "PRIMARY KEY (`id`,`id2`))");
+
+    stmt = sharedConn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);
+    stmt.execute("INSERT INTO testDelete values (1,-1,'1'), (2,-2,'2'), (3,-3,'3')");
+
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement(
+            "SELECT * FROM testDelete", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)) {
+      ResultSet rs = preparedStatement.executeQuery();
+      assertThrows(SQLException.class, rs::deleteRow);
+      assertTrue(rs.next());
+      assertTrue(rs.next());
+      assertEquals(2, rs.getInt(1));
+      rs.deleteRow();
+      assertEquals(1, rs.getInt(1));
+      assertEquals(-1, rs.getInt(2));
+      assertTrue(rs.next());
+      assertEquals(3, rs.getInt(1));
+      assertEquals(-3, rs.getInt(2));
+    }
+
+    ResultSet rs = stmt.executeQuery("SELECT * FROM testDelete");
+    assertTrue(rs.next());
+    assertEquals(1, rs.getInt(1));
+    assertEquals(-1, rs.getInt(2));
+    assertTrue(rs.next());
+    assertEquals(3, rs.getInt(1));
+    assertEquals(-3, rs.getInt(2));
+    assertFalse(rs.next());
+
+    rs.absolute(1);
+    rs.deleteRow();
+    Common.assertThrowsContains(SQLException.class, () -> rs.getInt(1), "wrong row position");
+  }
+
+  @Test
+  public void testUpdateChangingMultiplePrimaryKey() throws Exception {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS testUpdateChangingMultiplePrimaryKey");
+    stmt.execute(
+        "CREATE TABLE testUpdateChangingMultiplePrimaryKey("
+            + "`id` INT NOT NULL,"
+            + "`id2` INT NOT NULL,"
+            + "`t1` VARCHAR(50),"
+            + "PRIMARY KEY (`id`,`id2`))");
+    stmt.execute(
+        "INSERT INTO testUpdateChangingMultiplePrimaryKey values (1,-1,'1'), (2,-2,'2'),"
+            + " (3,-3,'3')");
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement(
+            "SELECT * FROM testUpdateChangingMultiplePrimaryKey",
+            ResultSet.TYPE_FORWARD_ONLY,
+            ResultSet.CONCUR_UPDATABLE)) {
+      ResultSet rs = preparedStatement.executeQuery();
+
+      assertTrue(rs.next());
+      assertTrue(rs.next());
+      rs.updateInt(1, 4);
+      rs.updateInt(2, -4);
+      rs.updateString(3, "4");
+      rs.updateRow();
+
+      assertEquals(4, rs.getInt(1));
+      assertEquals(-4, rs.getInt(2));
+      assertEquals("4", rs.getString(3));
+    }
+
+    ResultSet rs = stmt.executeQuery("SELECT * FROM testUpdateChangingMultiplePrimaryKey");
+
+    assertTrue(rs.next());
+    assertEquals(1, rs.getInt(1));
+    assertEquals(-1, rs.getInt(2));
+    assertEquals("1", rs.getString(3));
+
+    assertTrue(rs.next());
+    assertEquals(3, rs.getInt(1));
+    assertEquals(-3, rs.getInt(2));
+    assertEquals("3", rs.getString(3));
+
+    assertTrue(rs.next());
+    assertEquals(4, rs.getInt(1));
+    assertEquals(-4, rs.getInt(2));
+    assertEquals("4", rs.getString(3));
+
+    assertFalse(rs.next());
+  }
+
+  @Test
+  public void updateBlobClob() throws SQLException, IOException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS updateBlob");
+    stmt.execute("CREATE TABLE updateBlob(id int not null primary key, strm blob)");
+    stmt.execute("START TRANSACTION ");
+    try {
+      PreparedStatement prep =
+          sharedConn.prepareStatement("insert into updateBlob (id, strm) values (?,?)");
+      byte[] theBlob = {1, 2, 3, 4, 5, 6};
+      InputStream stream = new ByteArrayInputStream(theBlob);
+
+      prep.setInt(1, 1);
+      prep.setBlob(2, stream);
+      prep.execute();
+
+      byte[] updatedBlob = "abcdef".getBytes(StandardCharsets.UTF_8);
+
+      try (PreparedStatement preparedStatement =
+          sharedConn.prepareStatement(
+              "select * from updateBlob",
+              ResultSet.TYPE_FORWARD_ONLY,
+              ResultSet.CONCUR_UPDATABLE)) {
+        ResultSet rs = preparedStatement.executeQuery();
+        assertTrue(rs.next());
+
+        rs.updateBlob(2, new ByteArrayInputStream(updatedBlob));
+        rs.updateRow();
+        checkResult(rs, updatedBlob);
+
+        rs.updateBlob("strm", new ByteArrayInputStream(updatedBlob));
+        rs.updateRow();
+        checkResult(rs, updatedBlob);
+
+        rs.updateBlob(2, new ByteArrayInputStream(updatedBlob), 20L);
+        rs.updateRow();
+        checkResult(rs, updatedBlob);
+
+        rs.updateBlob("strm", new ByteArrayInputStream(updatedBlob), 20L);
+        rs.updateRow();
+        checkResult(rs, updatedBlob);
+
+        rs.updateClob(2, new StringReader("abcdef"));
+        rs.updateRow();
+        checkResult(rs, updatedBlob);
+
+        rs.updateClob("strm", new StringReader("abcdef"));
+        rs.updateRow();
+        checkResult(rs, updatedBlob);
+
+        rs.updateClob(2, new StringReader("abcdef"), 20L);
+        rs.updateRow();
+        checkResult(rs, updatedBlob);
+
+        rs.updateClob("strm", new StringReader("abcdef"), 20L);
+        rs.updateRow();
+        checkResult(rs, updatedBlob);
+
+        rs.updateNClob(2, new StringReader("abcdef"));
+        rs.updateRow();
+        checkResult(rs, updatedBlob);
+
+        rs.updateNClob("strm", new StringReader("abcdef"));
+        rs.updateRow();
+        checkResult(rs, updatedBlob);
+
+        rs.updateNClob(2, new StringReader("abcdef"), 20L);
+        rs.updateRow();
+        checkResult(rs, updatedBlob);
+
+        rs.updateNClob("strm", new StringReader("abcdef"), 20L);
+        rs.updateRow();
+        checkResult(rs, updatedBlob);
+      }
+
+      try (PreparedStatement preparedStatement =
+          sharedConn.prepareStatement(
+              "select * from updateBlob",
+              ResultSet.TYPE_FORWARD_ONLY,
+              ResultSet.CONCUR_UPDATABLE)) {
+        ResultSet rs = preparedStatement.executeQuery();
+        assertTrue(rs.next());
+        checkResult(rs, updatedBlob);
+      }
+    } finally {
+      sharedConn.rollback();
+    }
+  }
+
+  private void checkResult(ResultSet rs, byte[] updatedBlob) throws SQLException, IOException {
+    InputStream readStuff = rs.getBlob("strm").getBinaryStream();
+    int ch;
+    int pos = 0;
+    while ((ch = readStuff.read()) != -1) {
+      assertEquals(updatedBlob[pos++], ch);
+    }
+
+    readStuff = rs.getBinaryStream("strm");
+
+    pos = 0;
+    while ((ch = readStuff.read()) != -1) {
+      assertEquals(updatedBlob[pos++], ch);
+    }
+  }
+
+  @Test
+  public void updateMeta() throws SQLException {
+    DatabaseMetaData meta = sharedConn.getMetaData();
+
+    assertTrue(meta.ownUpdatesAreVisible(ResultSet.TYPE_FORWARD_ONLY));
+    assertTrue(meta.ownDeletesAreVisible(ResultSet.TYPE_FORWARD_ONLY));
+    assertTrue(meta.ownInsertsAreVisible(ResultSet.TYPE_FORWARD_ONLY));
+    assertTrue(
+        meta.supportsResultSetConcurrency(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY));
+    assertTrue(
+        meta.supportsResultSetConcurrency(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE));
+
+    assertTrue(meta.ownUpdatesAreVisible(ResultSet.TYPE_SCROLL_INSENSITIVE));
+    assertTrue(meta.ownDeletesAreVisible(ResultSet.TYPE_SCROLL_INSENSITIVE));
+    assertTrue(meta.ownInsertsAreVisible(ResultSet.TYPE_SCROLL_INSENSITIVE));
+    assertTrue(
+        meta.supportsResultSetConcurrency(
+            ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY));
+    assertTrue(
+        meta.supportsResultSetConcurrency(
+            ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE));
+
+    assertFalse(meta.ownUpdatesAreVisible(ResultSet.TYPE_SCROLL_SENSITIVE));
+    assertFalse(meta.ownDeletesAreVisible(ResultSet.TYPE_SCROLL_SENSITIVE));
+    assertFalse(meta.ownInsertsAreVisible(ResultSet.TYPE_SCROLL_SENSITIVE));
+    assertFalse(
+        meta.supportsResultSetConcurrency(
+            ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY));
+    assertFalse(
+        meta.supportsResultSetConcurrency(
+            ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE));
+  }
+
+  @Test
+  public void updateResultSetMeta() throws SQLException {
+    java.sql.Statement stmt = sharedConn.createStatement();
+    assertEquals(ResultSet.CONCUR_READ_ONLY, stmt.getResultSetConcurrency());
+    ResultSet rs = stmt.executeQuery("SELECT 1");
+    assertEquals(ResultSet.CONCUR_READ_ONLY, rs.getConcurrency());
+
+    stmt = sharedConn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);
+    assertEquals(ResultSet.CONCUR_UPDATABLE, stmt.getResultSetConcurrency());
+    rs = stmt.executeQuery("SELECT 1");
+    assertEquals(ResultSet.CONCUR_UPDATABLE, rs.getConcurrency());
+  }
+
+  @Test
+  public void insertNoRow() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS insertNoRow");
+    stmt.execute("CREATE TABLE insertNoRow(id int not null primary key, strm blob)");
+    java.sql.Statement st =
+        sharedConn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
+    ResultSet rs = st.executeQuery("select * from insertNoRow");
+    assertFalse(rs.next());
+    rs.moveToInsertRow();
+    try {
+      rs.refreshRow();
+      fail("Can't refresh when on the insert row.");
+    } catch (SQLException sqle) {
+      // expected
+    }
+    rs.moveToCurrentRow();
+  }
+
+  @Test
+  public void refreshRow() throws SQLException {
+    try (org.mariadb.jdbc.Connection con = createCon("&useServerPrepStmts=false")) {
+      refreshRow(con);
+    }
+    try (org.mariadb.jdbc.Connection con = createCon("&useServerPrepStmts")) {
+      refreshRow(con);
+    }
+  }
+
+  private void refreshRow(Connection con) throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS refreshRow");
+    stmt.execute("CREATE TABLE refreshRow(id int not null primary key, strm text)");
+
+    java.sql.Statement st =
+        con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
+    st.execute("INSERT INTO refreshRow values (1, '555')");
+    ResultSet rs = st.executeQuery("select * from refreshRow");
+
+    st.execute("UPDATE refreshRow set strm = '666' WHERE id = 1");
+    try {
+      rs.refreshRow();
+      fail("Can't refresh when not on row.");
+    } catch (SQLException sqle) {
+      // expected
+    }
+
+    assertTrue(rs.next());
+    assertEquals(1, rs.getInt(1));
+    assertEquals("555", rs.getString(2));
+    rs.refreshRow();
+    assertEquals("666", rs.getString(2));
+
+    rs.moveToInsertRow();
+    assertThrows(SQLException.class, rs::refreshRow);
+    rs.moveToCurrentRow();
+
+    assertFalse(rs.next());
+    assertThrows(SQLException.class, rs::refreshRow);
+  }
+
+  @Test
+  public void testMoveToInsertRow() throws SQLException {
+    try (org.mariadb.jdbc.Connection con = createCon("&useServerPrepStmts=false")) {
+      testMoveToInsertRow(con);
+    }
+    try (org.mariadb.jdbc.Connection con = createCon("&useServerPrepStmts")) {
+      testMoveToInsertRow(con);
+    }
+  }
+
+  private void testMoveToInsertRow(Connection con) throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS testMoveToInsertRow");
+    stmt.execute("CREATE TABLE testMoveToInsertRow(t2 text, t1 text, id int primary key)");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select id, t1, t2 from testMoveToInsertRow",
+            ResultSet.TYPE_SCROLL_INSENSITIVE,
+            ResultSet.CONCUR_UPDATABLE)) {
+      ResultSet rs = preparedStatement.executeQuery();
+
+      assertNotNull(rs);
+      assertEquals(0, rs.getRow());
+      rs.moveToInsertRow();
+      rs.updateInt(1, 1);
+      rs.updateString(2, "t1-value");
+      rs.updateString(3, "t2-value");
+      rs.insertRow();
+      rs.first();
+      assertEquals(1, rs.getRow());
+
+      rs.updateInt("id", 2);
+      rs.updateString("t1", "t1-bis-value");
+      rs.updateRow();
+      assertEquals(1, rs.getRow());
+
+      assertEquals(2, rs.getInt("id"));
+      assertEquals("t1-bis-value", rs.getString("t1"));
+      assertEquals("t2-value", rs.getString("t2"));
+
+      rs.deleteRow();
+      assertEquals(0, rs.getRow());
+
+      rs.moveToInsertRow();
+      rs.updateInt("id", 3);
+      rs.updateString("t1", "other-t1-value");
+      Common.assertThrowsContains(
+          SQLException.class, rs::refreshRow, "Cannot call refreshRow() when inserting a new row");
+
+      rs.insertRow();
+      assertEquals(0, rs.getRow());
+      rs.next();
+      assertEquals(3, rs.getInt("id"));
+      assertEquals("other-t1-value", rs.getString("t1"));
+      assertNull(rs.getString("t2"));
+    } finally {
+      con.commit();
+    }
+
+    stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement(
+            "select id, t1, t2 from testMoveToInsertRow",
+            ResultSet.TYPE_SCROLL_INSENSITIVE,
+            ResultSet.CONCUR_UPDATABLE)) {
+      ResultSet rs = preparedStatement.executeQuery();
+
+      assertTrue(rs.first());
+      assertEquals(1, rs.getRow());
+      rs.updateInt("id", 3);
+      rs.updateString("t1", "t1-3");
+      rs.updateRow();
+      assertEquals(1, rs.getRow());
+
+      assertEquals(3, rs.getInt("id"));
+      assertEquals("t1-3", rs.getString("t1"));
+
+      rs.moveToInsertRow();
+      rs.updateInt("id", 4);
+      rs.updateString("t1", "t1-4");
+      rs.insertRow();
+      assertEquals(1, rs.getRow());
+
+      rs.updateInt("id", 5);
+      rs.updateString("t1", "t1-5");
+      rs.insertRow();
+      assertEquals(1, rs.getRow());
+
+      rs.moveToCurrentRow();
+      assertEquals(3, rs.getInt("id"));
+      assertEquals("t1-3", rs.getString("t1"));
+
+      assertTrue(rs.next());
+      assertEquals(4, rs.getInt("id"));
+      assertEquals("t1-4", rs.getString("t1"));
+
+      assertTrue(rs.next());
+      assertEquals(5, rs.getInt("id"));
+      assertEquals("t1-5", rs.getString("t1"));
+    } finally {
+      sharedConn.rollback();
+    }
+  }
+
+  @Test
+  public void cancelRowUpdatesTest() throws SQLException {
+    try (org.mariadb.jdbc.Connection con = createCon("&useServerPrepStmts=false")) {
+      cancelRowUpdatesTest(con);
+    }
+    try (org.mariadb.jdbc.Connection con = createCon("&useServerPrepStmts")) {
+      cancelRowUpdatesTest(con);
+    }
+  }
+
+  private void cancelRowUpdatesTest(Connection con) throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS cancelRowUpdatesTest");
+    stmt.execute("CREATE TABLE cancelRowUpdatesTest(c text, id int primary key)");
+    stmt.execute("INSERT INTO cancelRowUpdatesTest(id,c) values (1,'1'), (2,'2'),(3,'3'),(4,'4')");
+    stmt.execute("FLUSH TABLES");
+
+    try (PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select id,c from cancelRowUpdatesTest order by id",
+            ResultSet.TYPE_SCROLL_INSENSITIVE,
+            ResultSet.CONCUR_UPDATABLE)) {
+      ResultSet rs = preparedStatement.executeQuery();
+
+      assertTrue(rs.next());
+      assertTrue(rs.next());
+
+      assertEquals("2", rs.getString("c"));
+      rs.updateString("c", "2bis");
+      rs.cancelRowUpdates();
+      rs.updateRow();
+      assertEquals("2", rs.getString("c"));
+
+      rs.updateString("c", "2bis");
+      rs.updateRow();
+      assertEquals("2bis", rs.getString("c"));
+
+      assertTrue(rs.first());
+      assertTrue(rs.next());
+      assertEquals("2bis", rs.getString("c"));
+    }
+  }
+
+  @Test
+  public void deleteRowsTest() throws SQLException {
+    try (org.mariadb.jdbc.Connection con = createCon("&useServerPrepStmts=false")) {
+      deleteRowsTest(con);
+    }
+    try (org.mariadb.jdbc.Connection con = createCon("&useServerPrepStmts")) {
+      deleteRowsTest(con);
+    }
+  }
+
+  private void deleteRowsTest(Connection con) throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS deleteRows");
+    stmt.execute("CREATE TABLE deleteRows(c text, id int primary key)");
+    stmt.execute("INSERT INTO deleteRows(id,c) values (1,'1'), (2,'2'),(3,'3'),(4,'4')");
+
+    try (PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select id,c from deleteRows order by id",
+            ResultSet.TYPE_SCROLL_INSENSITIVE,
+            ResultSet.CONCUR_UPDATABLE)) {
+      ResultSet rs = preparedStatement.executeQuery();
+
+      assertTrue(rs.next());
+      assertEquals(1, rs.getInt("id"));
+
+      rs.deleteRow();
+
+      assertTrue(rs.isBeforeFirst());
+
+      assertTrue(rs.next());
+      assertTrue(rs.next());
+      assertEquals(3, rs.getInt("id"));
+
+      rs.deleteRow();
+      assertEquals(2, rs.getInt("id"));
+    }
+  }
+
+  @Test
+  public void updatePosTest() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS updatePosTest");
+    stmt.execute("CREATE TABLE updatePosTest(c text, id int primary key)");
+    stmt.execute("INSERT INTO updatePosTest(id,c) values (1,'1')");
+
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement(
+            "select id,c from updatePosTest",
+            ResultSet.TYPE_SCROLL_INSENSITIVE,
+            ResultSet.CONCUR_UPDATABLE)) {
+
+      ResultSet rs = preparedStatement.executeQuery();
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> rs.updateInt(1, 20),
+          "Current position is before the first row");
+      Common.assertThrowsContains(
+          SQLException.class, rs::updateRow, "Current position is before the first row");
+      Common.assertThrowsContains(
+          SQLException.class, rs::deleteRow, "Current position is before the first row");
+
+      assertTrue(rs.next());
+      rs.updateInt(1, 20);
+      rs.updateNull(2);
+      rs.updateRow();
+      rs.deleteRow();
+      assertFalse(rs.next());
+      Common.assertThrowsContains(
+          SQLException.class, () -> rs.updateInt(1, 20), "Current position is after the last row");
+      Common.assertThrowsContains(
+          SQLException.class, rs::updateRow, "Current position is after the last row");
+      Common.assertThrowsContains(
+          SQLException.class, rs::deleteRow, "Current position is after the last row");
+    }
+  }
+
+  /**
+   * CONJ-519 : Updatable result-set possible NPE when same field is repeated.
+   *
+   * @throws SQLException if any exception occur
+   */
+  @Test
+  public void repeatedFieldUpdatable() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS repeatedFieldUpdatable");
+    stmt.execute(
+        "CREATE TABLE repeatedFieldUpdatable(t1 varchar(50) NOT NULL, t2 varchar(50), PRIMARY KEY"
+            + " (t1))");
+    stmt.execute("insert into repeatedFieldUpdatable values ('gg', 'hh'), ('jj', 'll')");
+
+    PreparedStatement preparedStatement =
+        sharedConn.prepareStatement(
+            "SELECT t1, t2, t1 as t3 FROM repeatedFieldUpdatable",
+            ResultSet.TYPE_FORWARD_ONLY,
+            ResultSet.CONCUR_UPDATABLE);
+    ResultSet rs = preparedStatement.executeQuery();
+    while (rs.next()) {
+      rs.getObject(3);
+    }
+  }
+
+  @Test
+  public void updatableDefaultPrimaryField() throws SQLException {
+    Assumptions.assumeTrue(isMariaDBServer() && minVersion(10, 2, 0));
+    String sql = "SELECT t.* FROM testDefaultUUID t WHERE 1 = 2";
+    try (PreparedStatement pstmt =
+        sharedConn.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)) {
+      pstmt.execute();
+      ResultSet rs = pstmt.getResultSet();
+      rs.moveToInsertRow();
+      rs.updateString("column2", "x");
+      try {
+        rs.insertRow();
+        if (!isMariaDBServer() || !minVersion(10, 5, 3)) {
+          fail("Must have thrown exception");
+        }
+        rs.next();
+        assertEquals(36, rs.getString(1).length());
+        assertEquals("x", rs.getString(2));
+      } catch (SQLException e) {
+        if (isMariaDBServer() && minVersion(10, 5, 3)) {
+          fail("Must have succeed");
+        }
+        assertTrue(
+            e.getMessage()
+                .contains(
+                    "Cannot call insertRow() not setting value for primary key column1 with "
+                        + "default value before server 10.5"));
+      }
+      rs.moveToInsertRow();
+      rs.updateString("column1", "de6f7774-e399-11ea-aa68-c8348e0fed44");
+      rs.updateString("column2", "x");
+      rs.insertRow();
+      rs.next();
+      assertEquals("de6f7774-e399-11ea-aa68-c8348e0fed44", rs.getString(1));
+      assertEquals("x", rs.getString(2));
+    }
+  }
+
+  @Test
+  public void expectedErrorField() throws SQLException {
+    Assumptions.assumeTrue(isMariaDBServer() && minVersion(10, 2, 0));
+    String sql = "SELECT * FROM testExpError t WHERE 1 = 2";
+    try (PreparedStatement pstmt =
+        sharedConn.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)) {
+      pstmt.execute();
+      ResultSet rs = pstmt.getResultSet();
+      rs.moveToInsertRow();
+      Common.assertThrowsContains(SQLException.class, () -> rs.updateRef(2, null), "not supported");
+      Common.assertThrowsContains(
+          SQLException.class, () -> rs.updateRef("t1", null), "not supported");
+      Common.assertThrowsContains(
+          SQLException.class, () -> rs.updateArray(2, null), "not supported");
+      Common.assertThrowsContains(
+          SQLException.class, () -> rs.updateArray("t1", null), "not supported");
+      Common.assertThrowsContains(
+          SQLException.class, () -> rs.updateRowId(2, null), "not supported");
+      Common.assertThrowsContains(
+          SQLException.class, () -> rs.updateRowId("t1", null), "not supported");
+      Common.assertThrowsContains(
+          SQLException.class, () -> rs.updateSQLXML(2, null), "not supported");
+      Common.assertThrowsContains(
+          SQLException.class, () -> rs.updateSQLXML("t1", null), "not supported");
+      Common.assertThrowsContains(
+          SQLException.class, rs::deleteRow, "Cannot call deleteRow() when inserting a new row");
+      Common.assertThrowsContains(
+          SQLException.class, rs::updateRow, "Cannot call updateRow() when inserting a new row");
+    }
+  }
+
+  @Test
+  public void addAfterDataFull() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "INSERT INTO test_update_max(t1) value ('1'), ('2'), ('3'), ('4'), ('5'), ('6'), ('7'),"
+            + " ('8'), ('9'), ('10')");
+    try (PreparedStatement preparedStatement =
+        sharedConn.prepareStatement(
+            "SELECT t1, id FROM test_update_max",
+            ResultSet.TYPE_FORWARD_ONLY,
+            ResultSet.CONCUR_UPDATABLE)) {
+      ResultSet rs = preparedStatement.executeQuery();
+      rs.moveToInsertRow();
+      rs.updateString("t1", "11");
+      rs.insertRow();
+      for (int i = 0; i < 11; i++) rs.next();
+      assertEquals("11", rs.getString("t1"));
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/XaTest.java b/src/test/java/org/mariadb/jdbc/integration/XaTest.java
new file mode 100644
index 0000000..af35bc1
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/XaTest.java
@@ -0,0 +1,292 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.*;
+import java.sql.Connection;
+import java.util.UUID;
+import javax.sql.XAConnection;
+import javax.sql.XADataSource;
+import javax.transaction.xa.XAException;
+import javax.transaction.xa.XAResource;
+import javax.transaction.xa.Xid;
+import org.junit.jupiter.api.*;
+import org.mariadb.jdbc.*;
+import org.mariadb.jdbc.Statement;
+
+public class XaTest extends Common {
+
+  private static MariaDbDataSource dataSource;
+  private static MariaDbPoolDataSource poolDataSource;
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS xatable");
+    if (poolDataSource != null) poolDataSource.close();
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    Assumptions.assumeTrue(
+        !"skysql".equals(System.getenv("srv"))
+            && !"skysql-ha".equals(System.getenv("srv"))
+            && !isXpand());
+
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS xatable");
+    stmt.execute("CREATE TABLE xatable(i int)");
+    stmt.execute("FLUSH TABLES");
+    dataSource = new MariaDbDataSource(mDefUrl);
+    poolDataSource = new MariaDbPoolDataSource(mDefUrl);
+  }
+
+  @Test
+  public void xidToString() {
+    MariaDbXid xid = new MariaDbXid(1575, new byte[] {0x00}, new byte[] {0x01});
+    assertEquals("0x00,0x01,0x627", MariaDbPoolConnection.xidToString(xid));
+    assertEquals(
+        "0x00,0x,0x627",
+        MariaDbPoolConnection.xidToString(new MariaDbXid(1575, new byte[] {0x00}, null)));
+    assertEquals(
+        "0x,0x000100,0x400",
+        MariaDbPoolConnection.xidToString(
+            new MariaDbXid(1024, new byte[] {}, new byte[] {0x00, 0x01, 0x00})));
+    assertEquals(
+        "0x00,0x000100,0xc3c20186",
+        MariaDbPoolConnection.xidToString(
+            new MariaDbXid(-1010695802, new byte[] {0x00}, new byte[] {0x00, 0x01, 0x00})));
+    assertEquals(xid, xid);
+    assertEquals(xid, new MariaDbXid(1575, new byte[] {0x00}, new byte[] {0x01}));
+    assertFalse(xid.equals("dd"));
+    assertFalse(xid.equals(null));
+    assertEquals(1544359, xid.hashCode());
+  }
+
+  @Test
+  public void xaRmTest() throws Exception {
+    MariaDbDataSource dataSource1 = new MariaDbDataSource(mDefUrl);
+    MariaDbDataSource dataSource2 = new MariaDbDataSource(mDefUrl + "&test=t");
+    XAConnection con1 = dataSource1.getXAConnection();
+    XAConnection con2 = dataSource1.getXAConnection();
+    XAConnection con3 = dataSource2.getXAConnection();
+    assertTrue(con1.getXAResource().isSameRM(con1.getXAResource()));
+    assertTrue(con1.getXAResource().isSameRM(con2.getXAResource()));
+    assertFalse(con1.getXAResource().isSameRM(con3.getXAResource()));
+
+    assertEquals(0, con1.getXAResource().getTransactionTimeout());
+    con1.getXAResource().setTransactionTimeout(10);
+    assertEquals(0, con1.getXAResource().getTransactionTimeout());
+    Xid xid = new MariaDbXid(1575, new byte[] {0x00}, new byte[] {0x01});
+    con1.getXAResource().forget(xid);
+    assertThrows(XAException.class, () -> con1.getXAResource().end(xid, XAResource.TMENDRSCAN));
+    assertThrows(XAException.class, () -> con1.getXAResource().start(xid, XAResource.TMSUCCESS));
+
+    con1.close();
+    con2.close();
+    con3.close();
+  }
+
+  private Xid newXid() {
+    return new MariaDbXid(
+        1, UUID.randomUUID().toString().getBytes(), UUID.randomUUID().toString().getBytes());
+  }
+
+  private Xid newXid(Xid branchFrom) {
+    return new MariaDbXid(
+        1, branchFrom.getGlobalTransactionId(), UUID.randomUUID().toString().getBytes());
+  }
+
+  /**
+   * 2 phase commit , with either commit or rollback at the end.
+   *
+   * @param doCommit must commit
+   * @throws Exception exception
+   */
+  private int test2PhaseCommit(boolean doCommit, XADataSource dataSource) throws Exception {
+
+    int connectionNumber = 2;
+
+    Xid parentXid = newXid();
+    java.sql.Connection[] connections = new java.sql.Connection[connectionNumber];
+    XAConnection[] xaConnections = new XAConnection[connectionNumber];
+    XAResource[] xaResources = new XAResource[connectionNumber];
+    Xid[] xids = new Xid[connectionNumber];
+
+    try {
+
+      for (int i = 0; i < connectionNumber; i++) {
+        if (i == 0) {
+          xaConnections[i] = dataSource.getXAConnection(user, password);
+        } else xaConnections[i] = dataSource.getXAConnection();
+
+        connections[i] = xaConnections[i].getConnection();
+        xaResources[i] = xaConnections[i].getXAResource();
+        xids[i] = newXid(parentXid);
+      }
+
+      startAllResources(connectionNumber, xaResources, xids);
+      insertDatas(connectionNumber, connections);
+      endAllResources(connectionNumber, xaResources, xids);
+      prepareAllResources(connectionNumber, xaResources, xids);
+
+      for (int i = 0; i < connectionNumber; i++) {
+        if (doCommit) {
+          xaResources[i].commit(xids[i], false);
+        } else {
+          xaResources[i].rollback(xids[i]);
+        }
+      }
+
+    } finally {
+      for (int i = 0; i < connectionNumber; i++) {
+        try {
+          if (xaConnections[i] != null) {
+            xaConnections[i].getConnection().close();
+          }
+        } catch (Exception e) {
+          e.printStackTrace();
+        }
+      }
+    }
+    return connectionNumber;
+  }
+
+  private void startAllResources(int connectionNumber, XAResource[] xaResources, Xid[] xids)
+      throws XAException {
+    for (int i = 0; i < connectionNumber; i++) {
+      xaResources[i].start(xids[i], XAResource.TMNOFLAGS);
+    }
+  }
+
+  private void endAllResources(int connectionNumber, XAResource[] xaResources, Xid[] xids)
+      throws XAException {
+    for (int i = 0; i < connectionNumber; i++) {
+      xaResources[i].end(xids[i], XAResource.TMSUCCESS);
+    }
+  }
+
+  private void prepareAllResources(int connectionNumber, XAResource[] xaResources, Xid[] xids)
+      throws XAException {
+    for (int i = 0; i < connectionNumber; i++) {
+      xaResources[i].prepare(xids[i]);
+    }
+  }
+
+  private void insertDatas(int connectionNumber, java.sql.Connection[] connections)
+      throws SQLException {
+    for (int i = 0; i < connectionNumber; i++) {
+      connections[i].createStatement().executeUpdate("INSERT INTO xatable VALUES (" + i + ")");
+    }
+  }
+
+  @Test
+  public void testCommit() throws Exception {
+    Assumptions.assumeFalse("galera".equals(System.getenv("srv")));
+    testCommit(dataSource);
+    testCommit(poolDataSource);
+  }
+
+  public void testCommit(XADataSource dataSource) throws Exception {
+    java.sql.Statement stmt = sharedConn.createStatement();
+    stmt.execute("TRUNCATE xatable");
+    int connectionNumber = test2PhaseCommit(true, dataSource);
+
+    // check the completion
+    ResultSet rs = stmt.executeQuery("SELECT * from xatable order by i");
+    for (int i = 0; i < connectionNumber; i++) {
+      assertTrue(rs.next());
+      assertEquals(rs.getInt(1), i);
+    }
+  }
+
+  @Test
+  public void testRollback() throws Exception {
+    Assumptions.assumeFalse("galera".equals(System.getenv("srv")));
+    testRollback(dataSource);
+    testRollback(poolDataSource);
+  }
+
+  public void testRollback(XADataSource dataSource) throws Exception {
+    java.sql.Statement stmt = sharedConn.createStatement();
+    stmt.execute("TRUNCATE xatable");
+    test2PhaseCommit(false, dataSource);
+    // check the completion
+    ResultSet rs = stmt.executeQuery("SELECT * from xatable order by i");
+    assertFalse(rs.next());
+  }
+
+  @Test
+  public void testRecover() throws Exception {
+    Assumptions.assumeFalse("galera".equals(System.getenv("srv")));
+    XAConnection xaConnection = dataSource.getXAConnection();
+    try {
+      java.sql.Connection connection = xaConnection.getConnection();
+      Xid xid = newXid();
+      XAResource xaResource = xaConnection.getXAResource();
+      xaResource.start(xid, XAResource.TMNOFLAGS);
+      connection.createStatement().executeQuery("SELECT 1");
+      xaResource.end(xid, XAResource.TMSUCCESS);
+      xaResource.prepare(xid);
+      Xid[] recoveredXids = xaResource.recover(XAResource.TMSTARTRSCAN | XAResource.TMENDRSCAN);
+      assertTrue(recoveredXids != null);
+      assertTrue(recoveredXids.length > 0);
+      boolean found = false;
+
+      for (Xid x : recoveredXids) {
+        if (x != null && x.equals(xid)) {
+          found = true;
+          break;
+        }
+      }
+      assertTrue(found);
+    } finally {
+      xaConnection.close();
+    }
+  }
+
+  @Test
+  public void resumeAndJoinTest() throws Exception {
+    Assumptions.assumeFalse("galera".equals(System.getenv("srv")));
+    Connection conn1;
+    MariaDbDataSource ds = new MariaDbDataSource(mDefUrl);
+
+    XAConnection xaConn1 = null;
+    Xid xid = newXid();
+    try {
+      xaConn1 = ds.getXAConnection();
+      XAResource xaRes1 = xaConn1.getXAResource();
+      conn1 = xaConn1.getConnection();
+      xaRes1.start(xid, XAResource.TMNOFLAGS);
+      conn1.createStatement().executeQuery("SELECT 1");
+      xaRes1.end(xid, XAResource.TMSUCCESS);
+      xaRes1.start(xid, XAResource.TMRESUME);
+      conn1.createStatement().executeQuery("SELECT 1");
+      xaRes1.end(xid, XAResource.TMSUCCESS);
+      xaRes1.commit(xid, true);
+      xaConn1.close();
+
+      xaConn1 = ds.getXAConnection();
+      xaRes1 = xaConn1.getXAResource();
+      conn1 = xaConn1.getConnection();
+      xaRes1.start(xid, XAResource.TMNOFLAGS);
+      conn1.createStatement().executeQuery("SELECT 1");
+      xaRes1.end(xid, XAResource.TMSUCCESS);
+      try {
+        xaRes1.start(xid, XAResource.TMJOIN);
+        fail(); // without pinGlobalTxToPhysicalConnection=true
+      } catch (XAException xaex) {
+        xaConn1.close();
+      }
+
+    } finally {
+      if (xaConn1 != null) {
+        xaConn1.close();
+      }
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/BinaryCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/BinaryCodecTest.java
new file mode 100644
index 0000000..33bdf74
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/BinaryCodecTest.java
@@ -0,0 +1,834 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.sql.*;
+import java.time.*;
+import java.util.Calendar;
+import java.util.TimeZone;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.MariaDbBlob;
+import org.mariadb.jdbc.MariaDbClob;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.integration.Common;
+
+public class BinaryCodecTest extends CommonCodecTest {
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS BinaryCodec");
+    stmt.execute("DROP TABLE IF EXISTS BinaryCodec2");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE BinaryCodec (t1 VARBINARY(20), t2 VARBINARY(30), t3 VARBINARY(20), t4"
+            + " BINARY(20)) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
+    stmt.execute(
+        "INSERT INTO BinaryCodec VALUES ('0', '1', 'some🌟', null), ('2011-01-01', '2010-12-31"
+            + " 23:59:59.152', '23:54:51.840010', null)");
+    stmt.execute(
+        "CREATE TABLE BinaryCodec2 (id int not null primary key auto_increment, t1 VARBINARY(20))");
+    stmt.execute("FLUSH TABLES");
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from BinaryCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getPrepare(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement prepareStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from BinaryCodec"
+                + " WHERE 1 > ?");
+    prepareStatement.closeOnCompletion();
+    prepareStatement.setInt(1, 0);
+    ResultSet rs = prepareStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws SQLException {
+    getObject(get());
+  }
+
+  @Test
+  public void getObjectPrepare() throws SQLException {
+    getObject(getPrepare(sharedConn));
+    getObject(getPrepare(sharedConnBinary));
+  }
+
+  public void getObject(ResultSet rs) throws SQLException {
+    assertArrayEquals("0".getBytes(StandardCharsets.UTF_8), (byte[]) rs.getObject(1));
+    assertFalse(rs.wasNull());
+    assertArrayEquals("1".getBytes(StandardCharsets.UTF_8), (byte[]) rs.getObject(2));
+    assertArrayEquals("1".getBytes(StandardCharsets.UTF_8), (byte[]) rs.getObject("t2alias"));
+    assertFalse(rs.wasNull());
+    assertArrayEquals("some🌟".getBytes(StandardCharsets.UTF_8), (byte[]) rs.getObject(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testObject(rs, Integer.class, 0);
+    testObject(rs, String.class, "0");
+    testObject(rs, Long.class, 0L);
+    testObject(rs, Short.class, (short) 0);
+    testObject(rs, BigDecimal.class, BigDecimal.valueOf(0));
+    testObject(rs, BigInteger.class, BigInteger.valueOf(0));
+    testObject(rs, Double.class, (double) 0);
+    testObject(rs, Float.class, (float) 0);
+    testObject(rs, Byte.class, (byte) 0);
+    testArrObject(rs, new byte[] {(byte) '0'});
+    testObject(rs, Boolean.class, Boolean.FALSE);
+    testObject(rs, Clob.class, new MariaDbClob("0".getBytes()));
+    testObject(rs, NClob.class, new MariaDbClob("0".getBytes()));
+    testObject(rs, InputStream.class, new MariaDbClob("0".getBytes()).getBinaryStream());
+    testObject(rs, Reader.class, new StringReader("0"));
+    rs.next();
+    testObject(rs, LocalDate.class, LocalDate.parse("2011-01-01"));
+    testObject(rs, LocalDateTime.class, LocalDateTime.parse("2011-01-01T00:00"));
+    testObject(rs, LocalTime.class, LocalTime.parse("23:54:51.840010"), 3);
+    Time t = Time.valueOf("23:54:51");
+    testObject(rs, Time.class, new Time(t.getTime() + 840), 3);
+    testObject(rs, Date.class, Date.valueOf("2011-01-01"));
+    Timestamp tt = Timestamp.valueOf("2010-12-31 23:59:59");
+    testObject(rs, Timestamp.class, new Timestamp(tt.getTime() + 152), 2);
+    testObject(
+        rs,
+        ZonedDateTime.class,
+        LocalDateTime.parse("2011-01-01T00:00").atZone(ZoneId.systemDefault()));
+    testErrObject(rs, OffsetDateTime.class);
+    testErrObject(rs, OffsetTime.class);
+  }
+
+  @Test
+  public void getString() throws SQLException {
+    getString(get());
+  }
+
+  @Test
+  public void getStringPrepare() throws SQLException {
+    getString(getPrepare(sharedConn));
+    getString(getPrepare(sharedConnBinary));
+  }
+
+  public void getString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getString(2));
+    assertEquals("1", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("some🌟", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNString() throws SQLException {
+    getNString(get());
+  }
+
+  @Test
+  public void getNStringPrepare() throws SQLException {
+    getNString(getPrepare(sharedConn));
+    getNString(getPrepare(sharedConnBinary));
+  }
+
+  public void getNString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getNString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getNString(2));
+    assertEquals("1", rs.getNString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("some🌟", rs.getNString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBoolean() throws SQLException {
+    getBoolean(get());
+  }
+
+  @Test
+  public void getBooleanPrepare() throws SQLException {
+    getBoolean(getPrepare(sharedConn));
+    getBoolean(getPrepare(sharedConnBinary));
+  }
+
+  public void getBoolean(ResultSet rs) throws SQLException {
+    assertFalse(rs.getBoolean(1));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(2));
+    assertTrue(rs.getBoolean("t2alias"));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(3));
+    assertFalse(rs.wasNull());
+    assertFalse(rs.getBoolean(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getByte() throws SQLException {
+    getByte(get());
+  }
+
+  @Test
+  public void getBytePrepare() throws SQLException {
+    getByte(getPrepare(sharedConn));
+    getByte(getPrepare(sharedConnBinary));
+  }
+
+  public void getByte(ResultSet rs) throws SQLException {
+    assertEquals((byte) 0, rs.getByte(1));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 1, rs.getByte(2));
+    assertEquals((byte) 1, rs.getByte("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getByte(3),
+        "value 'some\uD83C\uDF1F' (VARSTRING) cannot be decoded as Byte");
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 0, rs.getByte(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getShort() throws SQLException {
+    getShort(get());
+  }
+
+  @Test
+  public void getShortPrepare() throws SQLException {
+    getShort(getPrepare(sharedConn));
+    getShort(getPrepare(sharedConnBinary));
+  }
+
+  public void getShort(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getShort(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getShort(2));
+    assertEquals(1, rs.getShort("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getShort(3), "value 'some🌟' cannot be decoded as Short");
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getShort(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(get());
+  }
+
+  @Test
+  public void getIntPrepare() throws SQLException {
+    getInt(getPrepare(sharedConn));
+    getInt(getPrepare(sharedConnBinary));
+  }
+
+  public void getInt(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getInt(2));
+    assertEquals(1, rs.getInt("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getInt(3), "value 'some🌟' cannot be decoded as Integer");
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(get());
+  }
+
+  @Test
+  public void getLongPrepare() throws SQLException {
+    getLong(getPrepare(sharedConn));
+    getLong(getPrepare(sharedConnBinary));
+  }
+
+  public void getLong(ResultSet rs) throws SQLException {
+    assertEquals(0L, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1L, rs.getLong(2));
+    assertEquals(1L, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getLong(3),
+        "value 'some\uD83C\uDF1F' cannot be decoded as Long");
+    assertFalse(rs.wasNull());
+    assertEquals(0L, rs.getLong(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(get());
+  }
+
+  @Test
+  public void getFloatPrepare() throws SQLException {
+    getFloat(getPrepare(sharedConn));
+    getFloat(getPrepare(sharedConnBinary));
+  }
+
+  public void getFloat(ResultSet rs) throws SQLException {
+    assertEquals(0F, rs.getFloat(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1F, rs.getFloat(2));
+    assertEquals(1F, rs.getFloat("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getFloat(3), "value 'some🌟' cannot be decoded as Float");
+    assertFalse(rs.wasNull());
+    assertEquals(0F, rs.getFloat(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(get());
+  }
+
+  @Test
+  public void getDoublePrepare() throws SQLException {
+    getDouble(getPrepare(sharedConn));
+    getDouble(getPrepare(sharedConnBinary));
+  }
+
+  public void getDouble(ResultSet rs) throws SQLException {
+    assertEquals(0D, rs.getDouble(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1D, rs.getDouble(2));
+    assertEquals(1D, rs.getDouble("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDouble(3),
+        "value 'some🌟' cannot be decoded as Double");
+    assertFalse(rs.wasNull());
+    assertEquals(0D, rs.getDouble(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(get());
+  }
+
+  @Test
+  public void getBigDecimalPrepare() throws SQLException {
+    getBigDecimal(getPrepare(sharedConn));
+    getBigDecimal(getPrepare(sharedConnBinary));
+  }
+
+  public void getBigDecimal(ResultSet rs) throws SQLException {
+    assertEquals(BigDecimal.ZERO, rs.getBigDecimal(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal(2));
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBigDecimal(3),
+        "value 'some🌟' cannot be decoded as BigDecimal");
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(get());
+  }
+
+  @Test
+  public void getDatePrepare() throws SQLException {
+    getDate(getPrepare(sharedConn));
+    getDate(getPrepare(sharedConnBinary));
+  }
+
+  public void getDate(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDate(1),
+        "value '0' (VARSTRING) cannot be decoded as Date");
+    rs.next();
+    assertEquals("2011-01-01", rs.getDate(1).toString());
+    assertFalse(rs.wasNull());
+    assertEquals("2010-12-31", rs.getDate(2).toString());
+    assertFalse(rs.wasNull());
+    assertEquals("2010-12-31", rs.getDate("t2alias").toString());
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDate(3),
+        "value '23:54:51.840010' (VARSTRING) cannot be decoded as Date");
+    assertFalse(rs.wasNull());
+    assertNull(rs.getDate(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(get());
+  }
+
+  @Test
+  public void getTimePrepare() throws SQLException {
+    getTime(getPrepare(sharedConn));
+    getTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getTime(ResultSet rs) throws SQLException {
+    rs.next();
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime(1),
+        "value '2011-01-01' cannot be decoded as Time");
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime(2),
+        "value '2010-12-31 23:59:59.152' cannot be decoded as Time");
+    assertFalse(rs.wasNull());
+    assertEquals(Time.valueOf("23:54:51").getTime() + 840, rs.getTime(3).getTime());
+    assertEquals(
+        86091840, rs.getTime(3, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertEquals(Time.valueOf("23:54:51").getTime() + 840, rs.getTime("t3alias").getTime());
+    assertEquals(
+        86091840,
+        rs.getTime("t3alias", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getTimestamp() throws SQLException {
+    getTimestamp(get());
+  }
+
+  @Test
+  public void getTimestampPrepare() throws SQLException {
+    getTimestamp(getPrepare(sharedConn));
+    getTimestamp(getPrepare(sharedConnBinary));
+  }
+
+  public void getTimestamp(ResultSet rs) throws SQLException {
+    rs.next();
+
+    assertEquals(Timestamp.valueOf("2011-01-01 00:00:00").getTime(), rs.getTimestamp(1).getTime());
+    assertEquals(
+        Timestamp.valueOf("2011-01-01 00:00:00").getTime() + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp(1, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertEquals(
+        Timestamp.valueOf("2011-01-01 00:00:00").getTime(), rs.getTimestamp("t1alias").getTime());
+    assertEquals(
+        Timestamp.valueOf("2011-01-01 00:00:00").getTime() + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp("t1alias", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime() + 152, rs.getTimestamp(2).getTime());
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime()
+            + 152
+            + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp(2, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime() + 152,
+        rs.getTimestamp("t2alias").getTime());
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime()
+            + 152
+            + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp("t2alias", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+
+    //
+    //    ('2011-01-01', '2010-12-31 23:59:59.152',"
+    //            + " '23:54:51.840010', null)
+  }
+
+  @Test
+  public void getOffsetDateTime() throws SQLException {
+    getOffsetDateTime(get());
+  }
+
+  @Test
+  public void getOffsetDateTimePrepare() throws SQLException {
+    getOffsetDateTime(getPrepare(sharedConn));
+    getOffsetDateTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getOffsetDateTime(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(1, OffsetDateTime.class),
+        "cannot be decoded as OffsetDateTime");
+  }
+
+  @Test
+  public void getAsciiStream() throws Exception {
+    getAsciiStream(get());
+  }
+
+  @Test
+  public void getAsciiStreamPrepare() throws Exception {
+    getAsciiStream(getPrepare(sharedConn));
+    getAsciiStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getAsciiStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getAsciiStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getAsciiStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getAsciiStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getAsciiStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getAsciiStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getUnicodeStream() throws Exception {
+    getUnicodeStream(get());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepare() throws Exception {
+    getUnicodeStream(getPrepare(sharedConn));
+    getUnicodeStream(getPrepare(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  public void getUnicodeStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getUnicodeStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getUnicodeStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getUnicodeStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)),
+        rs.getUnicodeStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getUnicodeStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBinaryStream() throws Exception {
+    getBinaryStream(get());
+  }
+
+  @Test
+  public void getBinaryStreamPrepare() throws Exception {
+    getBinaryStream(getPrepare(sharedConn));
+    getBinaryStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getBinaryStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getBinaryStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getBinaryStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getBinaryStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getBinaryStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBinaryStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBytes() throws Exception {
+    getBytes(get());
+  }
+
+  @Test
+  public void getBytesPrepare() throws Exception {
+    getBytes(getPrepare(sharedConn));
+    getBytes(getPrepare(sharedConnBinary));
+  }
+
+  public void getBytes(ResultSet rs) throws Exception {
+    assertArrayEquals("0".getBytes(), rs.getBytes(1));
+    assertFalse(rs.wasNull());
+    assertArrayEquals("1".getBytes(), rs.getBytes(2));
+    assertArrayEquals("1".getBytes(), rs.getBytes("t2alias"));
+    assertFalse(rs.wasNull());
+    assertArrayEquals("some🌟".getBytes(StandardCharsets.UTF_8), rs.getBytes(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBytes(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getCharacterStream() throws Exception {
+    getCharacterStream(get());
+  }
+
+  @Test
+  public void getCharacterStreamPrepare() throws Exception {
+    getCharacterStream(getPrepare(sharedConn));
+    getCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getCharacterStream(ResultSet rs) throws Exception {
+    assertReaderEquals(new StringReader("0"), rs.getCharacterStream(1));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("1"), rs.getCharacterStream(2));
+    assertReaderEquals(new StringReader("1"), rs.getCharacterStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("some🌟"), rs.getCharacterStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getCharacterStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNCharacterStream() throws Exception {
+    getNCharacterStream(get());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepare() throws Exception {
+    getNCharacterStream(getPrepare(sharedConn));
+    getNCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getNCharacterStream(ResultSet rs) throws Exception {
+    assertReaderEquals(new StringReader("0"), rs.getNCharacterStream(1));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("1"), rs.getNCharacterStream(2));
+    assertReaderEquals(new StringReader("1"), rs.getNCharacterStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("some🌟"), rs.getNCharacterStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNCharacterStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBlob() throws Exception {
+    getBlob(get());
+  }
+
+  @Test
+  public void getBlobPrepare() throws Exception {
+    getBlob(getPrepare(sharedConn));
+    getBlob(getPrepare(sharedConnBinary));
+  }
+
+  public void getBlob(ResultSet rs) throws Exception {
+    assertStreamEquals(new MariaDbBlob("0".getBytes()), rs.getBlob(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbBlob("1".getBytes()), rs.getBlob(2));
+    assertStreamEquals(new MariaDbBlob("1".getBytes()), rs.getBlob("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbBlob("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getBlob(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBlob(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getClob() throws Exception {
+    getClob(get());
+  }
+
+  @Test
+  public void getClobPrepare() throws Exception {
+    getClob(getPrepare(sharedConn));
+    getClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getClob(ResultSet rs) throws Exception {
+    assertStreamEquals(new MariaDbClob("0".getBytes()), rs.getClob(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getClob(2));
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getClob("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getClob(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getClob(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNClob() throws Exception {
+    getNClob(get());
+  }
+
+  @Test
+  public void getNClobPrepare() throws Exception {
+    getNClob(getPrepare(sharedConn));
+    getNClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getNClob(ResultSet rs) throws Exception {
+    assertStreamEquals(new MariaDbClob("0".getBytes()), rs.getNClob(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getNClob(2));
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getNClob("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getNClob(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNClob(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = get();
+    ResultSetMetaData meta = rs.getMetaData();
+    assertEquals("VARBINARY", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("byte[]", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.VARBINARY, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(20, meta.getPrecision(1));
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    assertEquals(20, meta.getColumnDisplaySize(1));
+    assertEquals(Types.VARBINARY, meta.getColumnType(4));
+  }
+
+  @Test
+  public void sendParam() throws SQLException {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+
+    try (Connection con = createCon("transactionReplay=true&useServerPrepStmts=false")) {
+      sendParam(con);
+    }
+    try (Connection con = createCon("transactionReplay=true&useServerPrepStmts=true")) {
+      sendParam(con);
+    }
+  }
+
+  private void sendParam(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE BinaryCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep = con.prepareStatement("INSERT INTO BinaryCodec2(t1) VALUES (?)")) {
+      prep.setClob(1, new MariaDbClob("e🌟1".getBytes(StandardCharsets.UTF_8)));
+      prep.execute();
+      prep.setClob(1, (Clob) null);
+      prep.execute();
+      prep.setObject(1, new MariaDbClob("e🌟2".getBytes(StandardCharsets.UTF_8)));
+      prep.execute();
+      prep.setObject(1, null);
+      prep.execute();
+      prep.setObject(1, new MariaDbClob("e🌟3".getBytes(StandardCharsets.UTF_8)), Types.CLOB);
+      prep.execute();
+      prep.setObject(1, null, Types.CLOB);
+      prep.execute();
+    }
+
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM BinaryCodec2");
+
+    assertTrue(rs.next());
+    assertEquals("e🌟1", rs.getClob(2).toString());
+    rs.updateClob(2, new MariaDbClob("f🌟1".getBytes(StandardCharsets.UTF_8)));
+    rs.updateRow();
+    assertEquals("f🌟1", rs.getClob(2).toString());
+
+    assertTrue(rs.next());
+    assertNull(rs.getClob(2));
+    rs.updateObject(2, new MariaDbClob("f🌟2".getBytes(StandardCharsets.UTF_8)));
+    rs.updateRow();
+    assertEquals("f🌟2", rs.getClob(2).toString());
+
+    assertTrue(rs.next());
+    assertEquals("e🌟2", rs.getClob(2).toString());
+    rs.updateClob("t1", (Clob) null);
+    rs.updateRow();
+    assertNull(rs.getClob(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getClob(2));
+    rs.updateObject("t1", new MariaDbClob("f🌟3".getBytes(StandardCharsets.UTF_8)), Types.CLOB);
+    rs.updateRow();
+    assertEquals("f🌟3", rs.getClob(2).toString());
+
+    assertTrue(rs.next());
+    assertEquals("e🌟3", rs.getClob(2).toString());
+    rs.updateObject(2, null, Types.CLOB);
+    rs.updateRow();
+    assertNull(rs.getClob(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getClob(2));
+
+    rs = stmt.executeQuery("SELECT * FROM BinaryCodec2");
+    assertTrue(rs.next());
+    assertEquals("f🌟1", rs.getClob(2).toString());
+    assertTrue(rs.next());
+    assertEquals("f🌟2", rs.getClob(2).toString());
+    assertTrue(rs.next());
+    assertNull(rs.getClob(2));
+    assertTrue(rs.next());
+    assertEquals("f🌟3", rs.getClob(2).toString());
+    assertTrue(rs.next());
+    assertNull(rs.getClob(2));
+    assertTrue(rs.next());
+    assertNull(rs.getClob(2));
+    con.commit();
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/BitCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/BitCodecTest.java
new file mode 100644
index 0000000..23299a0
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/BitCodecTest.java
@@ -0,0 +1,746 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import java.time.*;
+import java.util.BitSet;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.MariaDbBlob;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.integration.Common;
+
+public class BitCodecTest extends CommonCodecTest {
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS BitCodec");
+    stmt.execute("DROP TABLE IF EXISTS BitCodec2");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE BitCodec (t1 BIT(1), t2 BIT(4), t3 BIT(16), t4 BIT(24))");
+    stmt.execute("INSERT INTO BitCodec VALUES (b'0000', b'0001', b'0000111100000100', null)");
+    stmt.execute("CREATE TABLE BitCodec2 (id int not null primary key auto_increment, t1 BIT(16))");
+    stmt.execute("FLUSH TABLES");
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from BitCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getPrepare(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from BitCodec"
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    ResultSet rs = preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws SQLException {
+    getObject(get());
+  }
+
+  @Test
+  public void getObjectPrepare() throws SQLException {
+    getObject(getPrepare(sharedConn));
+    getObject(getPrepare(sharedConnBinary));
+  }
+
+  public void getObject(ResultSet rs) throws SQLException {
+    assertEquals(false, rs.getObject(1));
+    assertFalse(rs.wasNull());
+    assertArrayEquals(new byte[] {(byte) 1}, (byte[]) rs.getObject(2));
+    assertArrayEquals(new byte[] {(byte) 1}, (byte[]) rs.getObject("t2alias"));
+    assertFalse(rs.wasNull());
+    assertArrayEquals(new byte[] {(byte) 15, (byte) 4}, (byte[]) rs.getObject(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testObject(rs, Integer.class, 0);
+    testObject(rs, Byte.class, Byte.valueOf("0"));
+    testObject(rs, String.class, "b''");
+    testObject(rs, Long.class, 0L);
+    testObject(rs, Short.class, (short) 0);
+    testObject(rs, BitSet.class, BitSet.valueOf(new byte[] {(byte) 0}));
+    testObject(rs, BigDecimal.class, BigDecimal.valueOf(0));
+    testObject(rs, BigInteger.class, BigInteger.valueOf(0));
+    testObject(rs, Boolean.class, Boolean.FALSE);
+
+    testErrObject(rs, Double.class);
+    testErrObject(rs, Float.class);
+    testObject(rs, Byte.class, (byte) 0);
+    testObject(rs, byte[].class, new byte[] {0});
+    testErrObject(rs, Date.class);
+    testErrObject(rs, Time.class);
+    testErrObject(rs, Timestamp.class);
+    testErrObject(rs, java.util.Date.class);
+    testErrObject(rs, LocalDate.class);
+    testErrObject(rs, ZonedDateTime.class);
+    testErrObject(rs, OffsetDateTime.class);
+    testErrObject(rs, LocalDateTime.class);
+    testErrObject(rs, OffsetTime.class);
+    testErrObject(rs, Clob.class);
+    testErrObject(rs, NClob.class);
+    testErrObject(rs, InputStream.class);
+    testErrObject(rs, Reader.class);
+  }
+
+  @Test
+  public void getString() throws SQLException {
+    getString(get());
+  }
+
+  @Test
+  public void getStringPrepare() throws SQLException {
+    getString(getPrepare(sharedConn));
+    getString(getPrepare(sharedConnBinary));
+  }
+
+  public void getString(ResultSet rs) throws SQLException {
+    assertEquals("b''", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("b'1'", rs.getString(2));
+    assertEquals("b'1'", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("b'111100000100'", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNString() throws SQLException {
+    getNString(get());
+  }
+
+  @Test
+  public void getNStringPrepare() throws SQLException {
+    getNString(getPrepare(sharedConn));
+    getNString(getPrepare(sharedConnBinary));
+  }
+
+  public void getNString(ResultSet rs) throws SQLException {
+    assertEquals("b''", rs.getNString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("b'1'", rs.getNString(2));
+    assertEquals("b'1'", rs.getNString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("b'111100000100'", rs.getNString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBoolean() throws SQLException {
+    getBoolean(get());
+  }
+
+  @Test
+  public void getBooleanPrepare() throws SQLException {
+    getBoolean(getPrepare(sharedConn));
+    getBoolean(getPrepare(sharedConnBinary));
+  }
+
+  public void getBoolean(ResultSet rs) throws SQLException {
+    assertFalse(rs.getBoolean(1));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(2));
+    assertTrue(rs.getBoolean("t2alias"));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(3));
+    assertFalse(rs.wasNull());
+    assertFalse(rs.getBoolean(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getByte() throws SQLException {
+    getByte(get());
+  }
+
+  @Test
+  public void getBytePrepare() throws SQLException {
+    getByte(getPrepare(sharedConn));
+    getByte(getPrepare(sharedConnBinary));
+  }
+
+  public void getByte(ResultSet rs) throws SQLException {
+    assertEquals((byte) 0, rs.getByte(1));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 1, rs.getByte(2));
+    assertEquals((byte) 1, rs.getByte("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 15, rs.getByte(3));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 0, rs.getByte(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getShort() throws SQLException {
+    getShort(get());
+  }
+
+  @Test
+  public void getShortPrepare() throws SQLException {
+    getShort(getPrepare(sharedConn));
+    getShort(getPrepare(sharedConnBinary));
+  }
+
+  public void getShort(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getShort(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getShort(2));
+    assertEquals(1, rs.getShort("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(3844, rs.getShort(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getShort(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(get());
+  }
+
+  @Test
+  public void getIntPrepare() throws SQLException {
+    getInt(getPrepare(sharedConn));
+    getInt(getPrepare(sharedConnBinary));
+  }
+
+  public void getInt(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getInt(2));
+    assertEquals(1, rs.getInt("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(3844, rs.getInt(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(get());
+  }
+
+  @Test
+  public void getLongPrepare() throws SQLException {
+    getLong(getPrepare(sharedConn));
+    getLong(getPrepare(sharedConnBinary));
+  }
+
+  public void getLong(ResultSet rs) throws SQLException {
+    assertEquals(0L, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1L, rs.getLong(2));
+    assertEquals(1L, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(3844L, rs.getLong(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0L, rs.getLong(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(get());
+  }
+
+  @Test
+  public void getFloatPrepare() throws SQLException {
+    getFloat(getPrepare(sharedConn));
+    getFloat(getPrepare(sharedConnBinary));
+  }
+
+  public void getFloat(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getFloat(1), "Data type BIT cannot be decoded as Float");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getFloat("t1alias"),
+        "Data type BIT cannot be decoded as Float");
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(get());
+  }
+
+  @Test
+  public void getDoublePrepare() throws SQLException {
+    getDouble(getPrepare(sharedConn));
+    getDouble(getPrepare(sharedConnBinary));
+  }
+
+  public void getDouble(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getDouble(1), "Data type BIT cannot be decoded as Double");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDouble("t1alias"),
+        "Data type BIT cannot be decoded as Double");
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(get());
+  }
+
+  @Test
+  public void getBigDecimalPrepare() throws SQLException {
+    getBigDecimal(getPrepare(sharedConn));
+    getBigDecimal(getPrepare(sharedConnBinary));
+  }
+
+  public void getBigDecimal(ResultSet rs) throws SQLException {
+    assertEquals(BigDecimal.ZERO, rs.getBigDecimal(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal(2));
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.valueOf(3844), rs.getBigDecimal(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(get());
+  }
+
+  @Test
+  public void getDatePrepare() throws SQLException {
+    getDate(getPrepare(sharedConn));
+    getDate(getPrepare(sharedConnBinary));
+  }
+
+  public void getDate(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getDate(1), "Data type BIT cannot be decoded as Date");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDate("t1alias"),
+        "Data type BIT cannot be decoded as Date");
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(get());
+  }
+
+  @Test
+  public void getTimePrepare() throws SQLException {
+    getTime(getPrepare(sharedConn));
+    getTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getTime(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getTime(1), "Data type BIT cannot be decoded as Time");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime("t1alias"),
+        "Data type BIT cannot be decoded as Time");
+  }
+
+  @Test
+  public void getTimestamp() throws SQLException {
+    getTimestamp(get());
+  }
+
+  @Test
+  public void getTimestampPrepare() throws SQLException {
+    getTimestamp(getPrepare(sharedConn));
+    getTimestamp(getPrepare(sharedConnBinary));
+  }
+
+  public void getTimestamp(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTimestamp(1),
+        "Data type BIT cannot be decoded as Timestamp");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTimestamp("t1alias"),
+        "Data type BIT cannot be decoded as Timestamp");
+  }
+
+  @Test
+  public void getOffsetDateTime() throws SQLException {
+    getOffsetDateTime(get());
+  }
+
+  @Test
+  public void getOffsetDateTimePrepare() throws SQLException {
+    getOffsetDateTime(getPrepare(sharedConn));
+    getOffsetDateTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getOffsetDateTime(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(1, OffsetDateTime.class),
+        "Type class java.time.OffsetDateTime not supported type for BIT type");
+  }
+
+  @Test
+  public void getAsciiStream() throws SQLException {
+    getAsciiStream(get());
+  }
+
+  @Test
+  public void getAsciiStreamPrepare() throws SQLException {
+    getAsciiStream(getPrepare(sharedConn));
+    getAsciiStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getAsciiStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getAsciiStream(1),
+        "Data type BIT cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getAsciiStream("t1alias"),
+        "Data type BIT cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getUnicodeStream() throws SQLException {
+    getUnicodeStream(get());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepare() throws SQLException {
+    getUnicodeStream(getPrepare(sharedConn));
+    getUnicodeStream(getPrepare(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  public void getUnicodeStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getUnicodeStream(1),
+        "Data type BIT cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getUnicodeStream("t1alias"),
+        "Data type BIT cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBinaryStream() throws SQLException {
+    getBinaryStream(get());
+  }
+
+  @Test
+  public void getBinaryStreamPrepare() throws SQLException {
+    getBinaryStream(getPrepare(sharedConn));
+    getBinaryStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getBinaryStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBinaryStream(1),
+        "Data type BIT cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBinaryStream("t1alias"),
+        "Data type BIT cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBytes() throws SQLException {
+    getBytes(get());
+  }
+
+  @Test
+  public void getBytesPrepare() throws SQLException {
+    getBytes(getPrepare(sharedConn));
+    getBytes(getPrepare(sharedConnBinary));
+  }
+
+  public void getBytes(ResultSet rs) throws SQLException {
+    assertArrayEquals(new byte[] {(byte) 0}, rs.getBytes(1));
+    assertFalse(rs.wasNull());
+    assertArrayEquals(new byte[] {(byte) 1}, rs.getBytes(2));
+    assertArrayEquals(new byte[] {(byte) 1}, rs.getBytes("t2alias"));
+    assertFalse(rs.wasNull());
+    assertArrayEquals(new byte[] {(byte) 15, (byte) 4}, rs.getBytes(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getCharacterStream() throws SQLException {
+    getCharacterStream(get());
+  }
+
+  @Test
+  public void getCharacterStreamPrepare() throws SQLException {
+    getCharacterStream(getPrepare(sharedConn));
+    getCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type BIT cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream("t1alias"),
+        "Data type BIT cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getNCharacterStream() throws SQLException {
+    getNCharacterStream(get());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepare() throws SQLException {
+    getNCharacterStream(getPrepare(sharedConn));
+    getNCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getNCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream(1),
+        "Data type BIT cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream("t2alias"),
+        "Data type BIT cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getBlob() throws SQLException {
+    getBlob(get());
+  }
+
+  @Test
+  public void getBlobPrepare() throws SQLException {
+    getBlob(getPrepare(sharedConn));
+    getBlob(getPrepare(sharedConnBinary));
+  }
+
+  public void getBlob(ResultSet rs) throws SQLException {
+
+    assertEquals(new MariaDbBlob(new byte[] {(byte) 0x00}), rs.getBlob(1));
+    assertFalse(rs.wasNull());
+    assertEquals(new MariaDbBlob(new byte[] {(byte) 0x01}), rs.getBlob(2));
+    assertEquals(new MariaDbBlob(new byte[] {0x01}), rs.getBlob("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(new MariaDbBlob(new byte[] {(byte) 15, (byte) 4}), rs.getBlob(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBlob(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getClob() throws SQLException {
+    getClob(get());
+  }
+
+  @Test
+  public void getClobPrepare() throws SQLException {
+    getClob(getPrepare(sharedConn));
+    getClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getClob(1), "Data type BIT cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getClob("t1alias"),
+        "Data type BIT cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getNClob() throws SQLException {
+    getNClob(get());
+  }
+
+  @Test
+  public void getNClobPrepare() throws SQLException {
+    getNClob(getPrepare(sharedConn));
+    getNClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getNClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getNClob(1), "Data type BIT cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNClob("t1alias"),
+        "Data type BIT cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getURL() throws SQLException {
+    getURL(get());
+  }
+
+  @Test
+  public void getURLPrepare() throws SQLException {
+    getURL(getPrepare(sharedConn));
+    getURL(getPrepare(sharedConnBinary));
+  }
+
+  public void getURL(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLSyntaxErrorException.class, () -> rs.getURL(1), "Could not parse 'b''' as URL");
+    Common.assertThrowsContains(
+        SQLSyntaxErrorException.class, () -> rs.getURL("t1alias"), "Could not parse 'b''' as URL");
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = get();
+    ResultSetMetaData meta = rs.getMetaData();
+    assertEquals("BIT", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.lang.Boolean", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.BOOLEAN, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(1, meta.getPrecision(1));
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    assertEquals(1, meta.getColumnDisplaySize(1));
+
+    assertEquals("BIT", meta.getColumnTypeName(2));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(2));
+    assertEquals("byte[]", meta.getColumnClassName(2));
+    assertEquals("t2alias", meta.getColumnLabel(2));
+    assertEquals("t2", meta.getColumnName(2));
+    assertEquals(Types.VARBINARY, meta.getColumnType(2));
+    assertEquals(4, meta.getPrecision(2));
+    assertEquals(0, meta.getScale(2));
+    assertEquals("", meta.getSchemaName(2));
+    assertEquals(4, meta.getColumnDisplaySize(2));
+  }
+
+  @Test
+  public void sendParam() throws SQLException {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE BitCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep = con.prepareStatement("INSERT INTO BitCodec2(t1) VALUES (?)")) {
+      prep.setObject(1, BitSet.valueOf(new byte[] {0x00, 0x01}));
+      prep.execute();
+      prep.setObject(1, null);
+      prep.execute();
+      prep.setObject(1, BitSet.valueOf(new byte[] {0x00, 0x02}), Types.BINARY);
+      prep.execute();
+      prep.setObject(1, null, Types.BINARY);
+      prep.execute();
+      prep.setObject(1, 0, Types.BIT);
+      prep.execute();
+      prep.setObject(1, 1, Types.BIT);
+      prep.execute();
+    }
+
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM BitCodec2");
+    assertTrue(rs.next());
+    assertEquals("b'100000000'", rs.getString(2));
+    rs.updateObject(2, BitSet.valueOf(new byte[] {0x02, 0x00}));
+    rs.updateRow();
+    assertEquals("b'10'", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getBytes(2));
+    rs.updateObject(2, BitSet.valueOf(new byte[] {0x03, 0x00}), Types.BINARY);
+    rs.updateRow();
+    assertEquals("b'11'", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("b'1000000000'", rs.getString(2));
+    rs.updateObject(2, null);
+    rs.updateRow();
+    assertNull(rs.getBytes(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getBytes(2));
+    rs.updateObject("t1", BitSet.valueOf(new byte[] {0x04, 0x00}), Types.BINARY);
+    rs.updateRow();
+    assertEquals("b'100'", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("b''", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("b'1'", rs.getString(2));
+
+    rs = stmt.executeQuery("SELECT * FROM BitCodec2");
+    assertTrue(rs.next());
+    assertEquals("b'10'", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("b'11'", rs.getString(2));
+    assertTrue(rs.next());
+    assertNull(rs.getBytes(2));
+    assertTrue(rs.next());
+    assertEquals("b'100'", rs.getString(2));
+    con.commit();
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/BlobCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/BlobCodecTest.java
new file mode 100644
index 0000000..95ecaa4
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/BlobCodecTest.java
@@ -0,0 +1,1071 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.sql.*;
+import java.time.*;
+import java.util.Arrays;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.MariaDbBlob;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.integration.Common;
+
+public class BlobCodecTest extends CommonCodecTest {
+  private static File tmpFile;
+  private static final byte[] fileContent = new byte[11000];
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS BlobCodec");
+    stmt.execute("DROP TABLE IF EXISTS BlobCodec2");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws Exception {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE BlobCodec (t1 TINYBLOB, t2 TINYBLOB, t3 TINYBLOB, t4 TINYBLOB)");
+    stmt.execute(
+        "INSERT INTO BlobCodec VALUES ('0', '1', 'some🌟', null), "
+            + "('2011-01-01', '2010-12-31 23:59:59.152', '23:54:51.840010', null),"
+            + "('', '2010-12-31 23:59:59.152', '23:54:51.840010', null)");
+    stmt.execute("CREATE TABLE BlobCodec2 (id int not null primary key auto_increment, t1 BLOB)");
+    stmt.execute("FLUSH TABLES");
+
+    tmpFile = File.createTempFile("temp-file-name", ".tmp");
+    for (int i = 0; i < 11_000; i++) {
+      fileContent[i] = (byte) (i % 110 + 40);
+    }
+
+    try (FileOutputStream fos = new FileOutputStream(tmpFile)) {
+      fos.write(fileContent);
+    }
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from BlobCodec");
+    assertTrue(rs.next());
+    return rs;
+  }
+
+  private CompleteResult getPrepare(org.mariadb.jdbc.Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from BlobCodec"
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    CompleteResult rs = (CompleteResult) preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws Exception {
+    getObject(get());
+  }
+
+  @Test
+  public void getObjectPrepare() throws Exception {
+    getObject(getPrepare(sharedConn));
+    getObject(getPrepare(sharedConnBinary));
+  }
+
+  public void getObject(ResultSet rs) throws Exception {
+    assertStreamEquals(new MariaDbBlob("0".getBytes()), (Blob) rs.getObject(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbBlob("1".getBytes()), (Blob) rs.getObject(2));
+    assertStreamEquals(new MariaDbBlob("1".getBytes()), (Blob) rs.getObject("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new MariaDbBlob("some🌟".getBytes(StandardCharsets.UTF_8)), (Blob) rs.getObject(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testErrObject(rs, Integer.class);
+    testObject(rs, String.class, "0");
+    testErrObject(rs, Long.class);
+    testErrObject(rs, Short.class);
+    testErrObject(rs, BigDecimal.class);
+    testErrObject(rs, BigInteger.class);
+    testErrObject(rs, Double.class);
+    testErrObject(rs, Float.class);
+    testObject(rs, Byte.class, (byte) '0');
+    testArrObject(rs, new byte[] {(byte) '0'});
+    testErrObject(rs, Boolean.class);
+    testErrObject(rs, Clob.class);
+    testErrObject(rs, NClob.class);
+    testObject(rs, InputStream.class, new MariaDbBlob("0".getBytes()).getBinaryStream());
+    testErrObject(rs, Reader.class);
+    rs.next();
+    testErrObject(rs, LocalDate.class);
+    testErrObject(rs, LocalTime.class);
+    testErrObject(rs, LocalDateTime.class);
+    testErrObject(rs, Time.class);
+    testErrObject(rs, Date.class);
+    testErrObject(rs, Timestamp.class);
+    testErrObject(rs, ZonedDateTime.class);
+    testErrObject(rs, OffsetDateTime.class);
+    testErrObject(rs, OffsetTime.class);
+    testErrObject(rs, java.util.Date.class);
+  }
+
+  @Test
+  public void getString() throws SQLException {
+    getString(get());
+  }
+
+  @Test
+  public void getStringPrepare() throws SQLException {
+    getString(getPrepare(sharedConn));
+    getString(getPrepare(sharedConnBinary));
+  }
+
+  public void getString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getString(2));
+    assertEquals("1", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("some🌟", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNString() throws SQLException {
+    getNString(get());
+  }
+
+  @Test
+  public void getNStringPrepare() throws SQLException {
+    getNString(getPrepare(sharedConn));
+    getNString(getPrepare(sharedConnBinary));
+  }
+
+  public void getNString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getString(2));
+    assertEquals("1", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("some🌟", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBoolean() throws SQLException {
+    getBoolean(get());
+  }
+
+  @Test
+  public void getBooleanPrepare() throws SQLException {
+    getBoolean(getPrepare(sharedConn));
+    getBoolean(getPrepare(sharedConnBinary));
+  }
+
+  public void getBoolean(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBoolean(1),
+        "Data type BLOB cannot be decoded as Boolean");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getByte() throws SQLException {
+    getByte(get());
+  }
+
+  @Test
+  public void getBytePrepare() throws SQLException {
+    getByte(getPrepare(sharedConn));
+    getByte(getPrepare(sharedConnBinary));
+  }
+
+  public void getByte(ResultSet rs) throws SQLException {
+    assertEquals((byte) 48, rs.getByte(1));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 49, rs.getByte(2));
+    assertEquals((byte) 49, rs.getByte("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 115, rs.getByte(3));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 0, rs.getByte(4));
+    assertTrue(rs.wasNull());
+    rs.next();
+    rs.next();
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getByte(1),
+        "empty String value cannot be decoded as Byte");
+  }
+
+  @Test
+  public void getShort() throws SQLException {
+    getShort(get());
+  }
+
+  @Test
+  public void getShortPrepare() throws SQLException {
+    getShort(getPrepare(sharedConn));
+    getShort(getPrepare(sharedConnBinary));
+  }
+
+  public void getShort(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getShort(1), "Data type BLOB cannot be decoded as Short");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(get());
+  }
+
+  @Test
+  public void getIntPrepare() throws SQLException {
+    getInt(getPrepare(sharedConn));
+    getInt(getPrepare(sharedConnBinary));
+  }
+
+  public void getInt(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getInt(1), "Data type BLOB cannot be decoded as Integer");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(get());
+  }
+
+  @Test
+  public void getLongPrepare() throws SQLException {
+    getLong(getPrepare(sharedConn));
+    getLong(getPrepare(sharedConnBinary));
+  }
+
+  public void getLong(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getLong(1), "Data type BLOB cannot be decoded as Long");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(get());
+  }
+
+  @Test
+  public void getFloatPrepare() throws SQLException {
+    getFloat(getPrepare(sharedConn));
+    getFloat(getPrepare(sharedConnBinary));
+  }
+
+  public void getFloat(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getFloat(1), "Data type BLOB cannot be decoded as Float");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(get());
+  }
+
+  @Test
+  public void getDoublePrepare() throws SQLException {
+    getDouble(getPrepare(sharedConn));
+    getDouble(getPrepare(sharedConnBinary));
+  }
+
+  public void getDouble(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDouble(1),
+        "Data type BLOB cannot be decoded as Double");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(get());
+  }
+
+  @Test
+  public void getBigDecimalPrepare() throws SQLException {
+    getBigDecimal(getPrepare(sharedConn));
+    getBigDecimal(getPrepare(sharedConnBinary));
+  }
+
+  public void getBigDecimal(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBigDecimal(1),
+        "Data type BLOB cannot be decoded as BigDecimal");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getBigIntegerPrepare() throws SQLException {
+    getBigInteger(getPrepare(sharedConn));
+    getBigInteger(getPrepare(sharedConnBinary));
+  }
+
+  public void getBigInteger(CompleteResult rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBigInteger(1),
+        "Data type BLOB cannot be decoded as BigInteger");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getDuration() throws SQLException {
+    getDuration(get());
+  }
+
+  @Test
+  public void getDurationPrepare() throws SQLException {
+    getDuration(getPrepare(sharedConn));
+    getDuration(getPrepare(sharedConnBinary));
+  }
+
+  public void getDuration(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(1, Duration.class),
+        "Data type BLOB cannot be decoded as Duration");
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(get());
+  }
+
+  @Test
+  public void getDatePrepare() throws SQLException {
+    getDate(getPrepare(sharedConn));
+    getDate(getPrepare(sharedConnBinary));
+  }
+
+  public void getDate(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getDate(1), "Data type BLOB cannot be decoded as Date");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(get());
+  }
+
+  @Test
+  public void getTimePrepare() throws SQLException {
+    getTime(getPrepare(sharedConn));
+    getTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getTime(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getTime(1), "Data type BLOB cannot be decoded as Time");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getTimestamp() throws SQLException {
+    getTimestamp(get());
+  }
+
+  @Test
+  public void getTimestampPrepare() throws SQLException {
+    getTimestamp(getPrepare(sharedConn));
+    getTimestamp(getPrepare(sharedConnBinary));
+  }
+
+  public void getTimestamp(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTimestamp(1),
+        "Data type BLOB cannot be decoded as Timestamp");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getOffsetDateTime() throws SQLException {
+    getOffsetDateTime(get());
+  }
+
+  @Test
+  public void getOffsetDateTimePrepare() throws SQLException {
+    getOffsetDateTime(getPrepare(sharedConn));
+    getOffsetDateTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getOffsetDateTime(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(1, OffsetDateTime.class),
+        "cannot be decoded as OffsetDateTime");
+  }
+
+  @Test
+  public void getAsciiStream() throws Exception {
+    getAsciiStream(get());
+  }
+
+  @Test
+  public void getAsciiStreamPrepare() throws Exception {
+    getAsciiStream(getPrepare(sharedConn));
+    getAsciiStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getAsciiStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getAsciiStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getAsciiStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getAsciiStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getAsciiStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getAsciiStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getUnicodeStream() throws Exception {
+    getUnicodeStream(get());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepare() throws Exception {
+    getUnicodeStream(getPrepare(sharedConn));
+    getUnicodeStream(getPrepare(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  public void getUnicodeStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getUnicodeStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getUnicodeStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getUnicodeStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)),
+        rs.getUnicodeStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getUnicodeStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBinaryStream() throws Exception {
+    getBinaryStream(get());
+  }
+
+  @Test
+  public void getBinaryStreamPrepare() throws Exception {
+    getBinaryStream(getPrepare(sharedConn));
+    getBinaryStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getBinaryStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getBinaryStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getBinaryStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getBinaryStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getBinaryStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBinaryStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBytes() throws Exception {
+    getBytes(get());
+  }
+
+  @Test
+  public void getBytesPrepare() throws Exception {
+    getBytes(getPrepare(sharedConn));
+    getBytes(getPrepare(sharedConnBinary));
+  }
+
+  public void getBytes(ResultSet rs) throws Exception {
+    assertArrayEquals("0".getBytes(), rs.getBytes(1));
+    assertFalse(rs.wasNull());
+    assertArrayEquals("1".getBytes(), rs.getBytes(2));
+    assertArrayEquals("1".getBytes(), rs.getBytes("t2alias"));
+    assertFalse(rs.wasNull());
+    assertArrayEquals("some🌟".getBytes(StandardCharsets.UTF_8), rs.getBytes(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBytes(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getCharacterStream() throws Exception {
+    getCharacterStream(get());
+  }
+
+  @Test
+  public void getCharacterStreamPrepare() throws Exception {
+    getCharacterStream(getPrepare(sharedConn));
+    getCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getCharacterStream(ResultSet rs) throws Exception {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type BLOB cannot be decoded as Reader");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getNCharacterStream() throws Exception {
+    getNCharacterStream(get());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepare() throws Exception {
+    getNCharacterStream(getPrepare(sharedConn));
+    getNCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getNCharacterStream(ResultSet rs) throws Exception {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream(1),
+        "Data type BLOB cannot be decoded as Reader");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getBlob() throws Exception {
+    getBlob(get());
+  }
+
+  @Test
+  public void getBlobPrepare() throws Exception {
+    getBlob(getPrepare(sharedConn));
+    getBlob(getPrepare(sharedConnBinary));
+  }
+
+  public void getBlob(ResultSet rs) throws Exception {
+    assertStreamEquals(new MariaDbBlob("0".getBytes()), rs.getBlob(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbBlob("1".getBytes()), rs.getBlob(2));
+    assertStreamEquals(new MariaDbBlob("1".getBytes()), rs.getBlob("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbBlob("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getBlob(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBlob(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getClob() throws Exception {
+    getClob(get());
+  }
+
+  @Test
+  public void getClobPrepare() throws Exception {
+    getClob(getPrepare(sharedConn));
+    getClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getClob(ResultSet rs) throws Exception {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getClob(1), "Data type BLOB cannot be decoded as Clob");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getNClob() throws Exception {
+    getNClob(get());
+  }
+
+  @Test
+  public void getNClobPrepare() throws Exception {
+    getNClob(getPrepare(sharedConn));
+    getNClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getNClob(ResultSet rs) throws Exception {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getNClob(1), "Data type BLOB cannot be decoded as Clob");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = get();
+    ResultSetMetaData meta = rs.getMetaData();
+    assertEquals("TINYBLOB", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.sql.Blob", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.VARBINARY, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(255, meta.getPrecision(1));
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    assertEquals(255, meta.getColumnDisplaySize(1));
+  }
+
+  @Test
+  public void sendParam() throws Exception {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+
+    try (Connection con = createCon("transactionReplay=true&useServerPrepStmts=false")) {
+      sendParam(con);
+    }
+    try (Connection con = createCon("transactionReplay=true&useServerPrepStmts=true")) {
+      sendParam(con);
+    }
+    try (Connection con = createCon()) {
+      java.sql.Statement stmt = con.createStatement();
+      stmt.execute("SET sql_mode = concat(@@sql_mode,',NO_BACKSLASH_ESCAPES')");
+      sendParam(con);
+    }
+  }
+
+  private void sendParam(Connection con) throws Exception {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE BlobCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep = con.prepareStatement("INSERT INTO BlobCodec2(t1) VALUES (?)")) {
+      prep.setBlob(1, (Blob) null);
+      prep.execute();
+      prep.setBlob(1, new MariaDbBlob("e🌟1".getBytes(StandardCharsets.UTF_8)));
+      prep.execute();
+      prep.setBlob(1, (Blob) null);
+      prep.execute();
+      prep.setNull(1, Types.BLOB);
+      prep.execute();
+
+      prep.setObject(1, new MariaDbBlob("e🌟2".getBytes(StandardCharsets.UTF_8)));
+      prep.execute();
+      prep.setObject(1, new MariaDbBlob("e🌟2".getBytes(StandardCharsets.UTF_8)), Types.BLOB, 5);
+      prep.execute();
+      prep.setObject(1, null);
+      prep.execute();
+      prep.setObject(1, new MariaDbBlob("e🌟3".getBytes(StandardCharsets.UTF_8)), Types.BLOB);
+      prep.execute();
+      prep.setObject(1, null, Types.BLOB);
+      prep.execute();
+
+      prep.setObject(1, new MariaDbBlob("e🌟4".getBytes(StandardCharsets.UTF_8)));
+      prep.addBatch();
+      prep.setObject(1, new MariaDbBlob("e🌟56".getBytes(StandardCharsets.UTF_8)), Types.BLOB, 6);
+      prep.addBatch();
+      prep.executeBatch();
+
+      try (FileInputStream fis = new FileInputStream(tmpFile)) {
+        prep.setObject(1, fis, Types.BLOB);
+        prep.execute();
+      }
+      try (FileInputStream fis = new FileInputStream(tmpFile)) {
+        prep.setObject(1, fis, Types.BLOB, 5000);
+        prep.execute();
+      }
+      try (FileInputStream fis = new FileInputStream(tmpFile)) {
+        try (FileInputStream fis2 = new FileInputStream(tmpFile)) {
+          prep.setObject(1, fis, Types.BLOB);
+          prep.addBatch();
+          prep.setObject(1, fis2, Types.BLOB, 5000);
+          prep.addBatch();
+          prep.executeBatch();
+        }
+      }
+      try (FileInputStream fis = new FileInputStream(tmpFile)) {
+        prep.setBlob(1, new BlobInputStream(fis));
+        prep.addBatch();
+        prep.executeBatch();
+      }
+      try (FileInputStream fis = new FileInputStream(tmpFile)) {
+        prep.setObject(1, new BlobInputStream(fis), Types.BLOB, 5000);
+        prep.addBatch();
+        prep.executeBatch();
+      }
+
+      prep.setObject(1, "e🌟6''".getBytes(StandardCharsets.UTF_8));
+      prep.addBatch();
+      prep.setObject(1, "e🌟76".getBytes(StandardCharsets.UTF_8), Types.BLOB, 6);
+      prep.addBatch();
+      prep.executeBatch();
+      prep.setObject(1, "e🌟85".getBytes(StandardCharsets.UTF_8), Types.BLOB, 6);
+      prep.execute();
+      prep.setBytes(1, "e🌟9''\\n".getBytes(StandardCharsets.UTF_8));
+      prep.execute();
+      prep.setBinaryStream(1, new ByteArrayInputStream("e🌟9".getBytes(StandardCharsets.UTF_8)));
+      prep.execute();
+      prep.setBinaryStream(
+          1, new ByteArrayInputStream("e🌟123".getBytes(StandardCharsets.UTF_8)), 6);
+      prep.execute();
+      prep.setBinaryStream(
+          1, new ByteArrayInputStream("e🌟456".getBytes(StandardCharsets.UTF_8)), 6L);
+      prep.execute();
+      prep.setBinaryStream(1, new ByteArrayInputStream("e🌟9".getBytes(StandardCharsets.UTF_8)));
+      prep.execute();
+      prep.setBinaryStream(
+          1, new ByteArrayInputStream("e🌟123".getBytes(StandardCharsets.UTF_8)), 6);
+      prep.execute();
+      prep.setBinaryStream(
+          1, new ByteArrayInputStream("e🌟456".getBytes(StandardCharsets.UTF_8)), 6L);
+      prep.execute();
+    }
+
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM BlobCodec2");
+    assertTrue(rs.next());
+    assertNull(rs.getBlob(2));
+
+    assertTrue(rs.next());
+    assertArrayEquals(
+        "e🌟1".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    rs.updateNull(2);
+    rs.updateRow();
+    assertNull(rs.getBlob(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getBlob(2));
+    rs.updateBlob(2, new MariaDbBlob("g🌟1".getBytes(StandardCharsets.UTF_8)));
+    rs.updateRow();
+    assertArrayEquals(
+        "g🌟1".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+
+    assertTrue(rs.next());
+    assertNull(rs.getBlob(2));
+    rs.updateBlob("t1", new MariaDbBlob("f🌟1".getBytes(StandardCharsets.UTF_8)));
+    rs.updateRow();
+    assertArrayEquals(
+        "f🌟1".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+
+    assertTrue(rs.next());
+    assertArrayEquals(
+        "e🌟2".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    rs.updateNull("t1");
+    rs.updateRow();
+    assertNull(rs.getBlob(2));
+
+    assertTrue(rs.next());
+    assertArrayEquals(
+        "e🌟".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    rs.updateObject(2, new MariaDbBlob("f🌟2".getBytes(StandardCharsets.UTF_8)), 5);
+    rs.updateRow();
+    assertArrayEquals(
+        "f🌟".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+
+    assertTrue(rs.next());
+    assertNull(rs.getBlob(2));
+
+    assertTrue(rs.next());
+    assertArrayEquals(
+        "e🌟3".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertNull(rs.getBlob(2));
+    assertTrue(rs.next());
+    assertArrayEquals(
+        "e🌟4".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertArrayEquals(
+        "e🌟5".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertArrayEquals(fileContent, rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertArrayEquals(
+        Arrays.copyOfRange(fileContent, 0, 5000),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertArrayEquals(fileContent, rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertArrayEquals(
+        Arrays.copyOfRange(fileContent, 0, 5000),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertArrayEquals(fileContent, rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertArrayEquals(
+        Arrays.copyOfRange(fileContent, 0, 5000),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertArrayEquals(
+        "e🌟6''".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertArrayEquals("e🌟7".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+    assertTrue(rs.next());
+    assertArrayEquals("e🌟8".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+    rs.updateBytes("t1", "2g🌟12".getBytes(StandardCharsets.UTF_8));
+    rs.updateRow();
+    assertArrayEquals(
+        "2g🌟12".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertArrayEquals("e🌟9''\\n".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+    rs.updateBytes(2, "2g🌟15".getBytes(StandardCharsets.UTF_8));
+    rs.updateRow();
+    assertArrayEquals(
+        "2g🌟15".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+
+    assertTrue(rs.next());
+    assertArrayEquals("e🌟9".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+    rs.updateBinaryStream(
+        "t1", new ByteArrayInputStream("2g🌟15".getBytes(StandardCharsets.UTF_8)));
+    rs.updateRow();
+    assertArrayEquals("2g🌟15".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+
+    assertTrue(rs.next());
+    assertArrayEquals("e🌟1".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+    rs.updateBinaryStream(
+        "t1", new ByteArrayInputStream("2g🌟15".getBytes(StandardCharsets.UTF_8)), 7);
+    rs.updateRow();
+    assertArrayEquals("2g🌟1".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+
+    assertTrue(rs.next());
+    assertArrayEquals("e🌟4".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+    rs.updateBinaryStream(
+        "t1", new ByteArrayInputStream("2g🌟456".getBytes(StandardCharsets.UTF_8)), 7L);
+    rs.updateRow();
+    assertArrayEquals("2g🌟4".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+
+    assertTrue(rs.next());
+    assertArrayEquals("e🌟9".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+    rs.updateBinaryStream(2, new ByteArrayInputStream("2g🌟15".getBytes(StandardCharsets.UTF_8)));
+    rs.updateRow();
+    assertArrayEquals("2g🌟15".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+
+    assertTrue(rs.next());
+    assertArrayEquals("e🌟1".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+    rs.updateBinaryStream(
+        2, new ByteArrayInputStream("2g🌟15".getBytes(StandardCharsets.UTF_8)), 7);
+    rs.updateRow();
+    assertArrayEquals("2g🌟1".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+
+    assertTrue(rs.next());
+    assertArrayEquals("e🌟4".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+    rs.updateBinaryStream(
+        2, new ByteArrayInputStream("2g🌟456".getBytes(StandardCharsets.UTF_8)), 7L);
+    rs.updateRow();
+    assertArrayEquals("2g🌟4".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+
+    rs = stmt.executeQuery("SELECT * FROM BlobCodec2");
+    assertTrue(rs.next());
+    assertTrue(rs.next());
+    assertNull(rs.getBlob(2));
+
+    assertTrue(rs.next());
+    assertArrayEquals(
+        "g🌟1".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+
+    assertTrue(rs.next());
+    assertArrayEquals(
+        "f🌟1".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+
+    assertTrue(rs.next());
+    assertNull(rs.getBlob(2));
+
+    assertTrue(rs.next());
+    assertArrayEquals(
+        "f🌟".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+
+    assertTrue(rs.next());
+    assertNull(rs.getBlob(2));
+
+    assertTrue(rs.next());
+    assertArrayEquals(
+        "e🌟3".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertNull(rs.getBlob(2));
+    assertTrue(rs.next());
+    assertArrayEquals(
+        "e🌟4".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertArrayEquals(
+        "e🌟5".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertArrayEquals(fileContent, rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertArrayEquals(
+        Arrays.copyOfRange(fileContent, 0, 5000),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertArrayEquals(fileContent, rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertArrayEquals(
+        Arrays.copyOfRange(fileContent, 0, 5000),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertArrayEquals(fileContent, rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertArrayEquals(
+        Arrays.copyOfRange(fileContent, 0, 5000),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertArrayEquals(
+        "e🌟6''".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertArrayEquals("e🌟7".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+    assertTrue(rs.next());
+    assertArrayEquals(
+        "2g🌟12".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+    assertTrue(rs.next());
+    assertArrayEquals(
+        "2g🌟15".getBytes(StandardCharsets.UTF_8),
+        rs.getBlob(2).getBytes(1, (int) rs.getBlob(2).length()));
+
+    assertTrue(rs.next());
+    assertArrayEquals("2g🌟15".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+
+    assertTrue(rs.next());
+    assertArrayEquals("2g🌟1".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+
+    assertTrue(rs.next());
+    assertArrayEquals("2g🌟4".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+
+    assertTrue(rs.next());
+    assertArrayEquals("2g🌟15".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+
+    assertTrue(rs.next());
+    assertArrayEquals("2g🌟1".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+
+    assertTrue(rs.next());
+    assertArrayEquals("2g🌟4".getBytes(StandardCharsets.UTF_8), rs.getBytes(2));
+    con.commit();
+  }
+
+  private static class BlobInputStream implements Blob {
+    private final InputStream data;
+
+    public BlobInputStream(InputStream data) {
+      this.data = data;
+    }
+
+    @Override
+    public long length() throws SQLException {
+      throw new SQLException("Length not available");
+    }
+
+    @Override
+    public byte[] getBytes(final long pos, final int length) throws SQLException {
+
+      // if not have thrown an error
+      byte[] buf = new byte[length];
+      int len;
+      int intpos = 0;
+      try {
+        while ((len = data.read(buf, intpos, length - intpos)) > 0) {
+          intpos += len;
+          if (pos >= len) break;
+        }
+        return buf;
+      } catch (IOException io) {
+        throw new SQLException("Error reading stream");
+      }
+    }
+
+    @Override
+    public InputStream getBinaryStream() {
+      return data;
+    }
+
+    @Override
+    public long position(byte[] bytes, long l) {
+      return 0;
+    }
+
+    @Override
+    public long position(Blob blob, long l) {
+      return 0;
+    }
+
+    @Override
+    public int setBytes(long l, byte[] bytes) {
+      return 0;
+    }
+
+    @Override
+    public int setBytes(long l, byte[] bytes, int i, int i1) {
+      return 0;
+    }
+
+    @Override
+    public OutputStream setBinaryStream(long l) {
+      return null;
+    }
+
+    @Override
+    public void truncate(long l) {}
+
+    @Override
+    public void free() {}
+
+    @Override
+    public InputStream getBinaryStream(long l, long l1) {
+      return null;
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/CharCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/CharCodecTest.java
new file mode 100644
index 0000000..b2290b9
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/CharCodecTest.java
@@ -0,0 +1,732 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.sql.*;
+import java.time.*;
+import java.util.Calendar;
+import java.util.TimeZone;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.MariaDbClob;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.integration.Common;
+
+public class CharCodecTest extends CommonCodecTest {
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS CharCodec");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE CharCodec (t1 CHAR(30), t2 CHAR(30), t3 CHAR(25), t4 CHAR(25)) CHARACTER "
+            + "SET utf8mb4 COLLATE utf8mb4_unicode_ci");
+    stmt.execute(
+        "INSERT INTO CharCodec VALUES ('0', '1', 'some🌟', null), ('2011-01-01', '2010-12-31"
+            + " 23:59:59.152', '23:54:51.840010', null),('2010-12-31T23:59:59.152+01:00',"
+            + " '2010-12-31T23:59:59.152Z', null, null)");
+    stmt.execute("FLUSH TABLES");
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from CharCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getPrepare(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from CharCodec"
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    ResultSet rs = preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws SQLException {
+    getObject(get());
+  }
+
+  @Test
+  public void getObjectPrepare() throws SQLException {
+    getObject(getPrepare(sharedConn));
+    getObject(getPrepare(sharedConnBinary));
+  }
+
+  public void getObject(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getObject(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getObject(2));
+    assertEquals("1", rs.getObject("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("some🌟", rs.getObject(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testObject(rs, Integer.class, 0);
+    testObject(rs, String.class, "0");
+    testObject(rs, Long.class, 0L);
+    testObject(rs, Short.class, (short) 0);
+    testObject(rs, BigDecimal.class, BigDecimal.valueOf(0));
+    testObject(rs, BigInteger.class, BigInteger.valueOf(0));
+    testObject(rs, Double.class, (double) 0);
+    testObject(rs, Float.class, (float) 0);
+    testObject(rs, Byte.class, (byte) 0);
+    testArrObject(rs, new byte[] {(byte) '0'});
+    testObject(rs, Boolean.class, Boolean.FALSE);
+    testObject(rs, Clob.class, new MariaDbClob("0".getBytes()));
+    testObject(rs, NClob.class, new MariaDbClob("0".getBytes()));
+    testObject(rs, InputStream.class, new MariaDbClob("0".getBytes()).getBinaryStream());
+    testObject(rs, Reader.class, new StringReader("0"));
+    rs.next();
+    testObject(rs, LocalDate.class, LocalDate.parse("2011-01-01"));
+    testObject(rs, LocalDateTime.class, LocalDateTime.parse("2011-01-01T00:00"));
+    testObject(rs, LocalTime.class, LocalTime.parse("23:54:51.840010"), 3);
+    Time t = Time.valueOf("23:54:51");
+    testObject(rs, Time.class, new Time(t.getTime() + 840), 3);
+    testObject(rs, Date.class, Date.valueOf("2011-01-01"));
+    Timestamp tt = Timestamp.valueOf("2010-12-31 23:59:59");
+    testObject(rs, Timestamp.class, new Timestamp(tt.getTime() + 152), 2);
+    testObject(
+        rs,
+        ZonedDateTime.class,
+        LocalDateTime.parse("2011-01-01T00:00").atZone(ZoneId.systemDefault()));
+    testErrObject(rs, OffsetDateTime.class);
+    testErrObject(rs, OffsetTime.class);
+    testObject(rs, java.util.Date.class, Date.valueOf("2010-12-31"), 2);
+  }
+
+  @Test
+  public void getString() throws SQLException {
+    getString(get());
+  }
+
+  @Test
+  public void getStringPrepare() throws SQLException {
+    getString(getPrepare(sharedConn));
+    getString(getPrepare(sharedConnBinary));
+  }
+
+  public void getString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getString(2));
+    assertEquals("1", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("some🌟", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNString() throws SQLException {
+    getNString(get());
+  }
+
+  @Test
+  public void getNStringPrepare() throws SQLException {
+    getNString(getPrepare(sharedConn));
+    getNString(getPrepare(sharedConnBinary));
+  }
+
+  public void getNString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getNString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getNString(2));
+    assertEquals("1", rs.getNString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("some🌟", rs.getNString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBoolean() throws SQLException {
+    getBoolean(get());
+  }
+
+  @Test
+  public void getBooleanPrepare() throws SQLException {
+    getBoolean(getPrepare(sharedConn));
+    getBoolean(getPrepare(sharedConnBinary));
+  }
+
+  public void getBoolean(ResultSet rs) throws SQLException {
+    assertFalse(rs.getBoolean(1));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(2));
+    assertTrue(rs.getBoolean("t2alias"));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(3));
+    assertFalse(rs.wasNull());
+    assertFalse(rs.getBoolean(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getByte() throws SQLException {
+    getByte(get());
+  }
+
+  @Test
+  public void getBytePrepare() throws SQLException {
+    getByte(getPrepare(sharedConn));
+    getByte(getPrepare(sharedConnBinary));
+  }
+
+  public void getByte(ResultSet rs) throws SQLException {
+    assertEquals((byte) 0, rs.getByte(1));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 1, rs.getByte(2));
+    assertEquals((byte) 1, rs.getByte("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getByte(3), " cannot be decoded as Byte");
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 0, rs.getByte(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getShort() throws SQLException {
+    getShort(get());
+  }
+
+  @Test
+  public void getShortPrepare() throws SQLException {
+    getShort(getPrepare(sharedConn));
+    getShort(getPrepare(sharedConnBinary));
+  }
+
+  public void getShort(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getShort(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getShort(2));
+    assertEquals(1, rs.getShort("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getShort(3), "value 'some🌟' cannot be decoded as Short");
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getShort(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(get());
+  }
+
+  @Test
+  public void getIntPrepare() throws SQLException {
+    getInt(getPrepare(sharedConn));
+    getInt(getPrepare(sharedConnBinary));
+  }
+
+  public void getInt(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getInt(2));
+    assertEquals(1, rs.getInt("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getInt(3), "value 'some🌟' cannot be decoded as Integer");
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(get());
+  }
+
+  @Test
+  public void getLongPrepare() throws SQLException {
+    getLong(getPrepare(sharedConn));
+    getLong(getPrepare(sharedConnBinary));
+  }
+
+  public void getLong(ResultSet rs) throws SQLException {
+    assertEquals(0L, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1L, rs.getLong(2));
+    assertEquals(1L, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getLong(3),
+        "value 'some\uD83C\uDF1F' cannot be decoded as Long");
+    assertFalse(rs.wasNull());
+    assertEquals(0L, rs.getLong(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(get());
+  }
+
+  @Test
+  public void getFloatPrepare() throws SQLException {
+    getFloat(getPrepare(sharedConn));
+    getFloat(getPrepare(sharedConnBinary));
+  }
+
+  public void getFloat(ResultSet rs) throws SQLException {
+    assertEquals(0F, rs.getFloat(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1F, rs.getFloat(2));
+    assertEquals(1F, rs.getFloat("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getFloat(3), "value 'some🌟' cannot be decoded as Float");
+    assertFalse(rs.wasNull());
+    assertEquals(0F, rs.getFloat(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(get());
+  }
+
+  @Test
+  public void getDoublePrepare() throws SQLException {
+    getDouble(getPrepare(sharedConn));
+    getDouble(getPrepare(sharedConnBinary));
+  }
+
+  public void getDouble(ResultSet rs) throws SQLException {
+    assertEquals(0D, rs.getDouble(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1D, rs.getDouble(2));
+    assertEquals(1D, rs.getDouble("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDouble(3),
+        "value 'some🌟' cannot be decoded as Double");
+    assertFalse(rs.wasNull());
+    assertEquals(0D, rs.getDouble(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(get());
+  }
+
+  @Test
+  public void getBigDecimalPrepare() throws SQLException {
+    getBigDecimal(getPrepare(sharedConn));
+    getBigDecimal(getPrepare(sharedConnBinary));
+  }
+
+  public void getBigDecimal(ResultSet rs) throws SQLException {
+    assertEquals(BigDecimal.ZERO, rs.getBigDecimal(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal(2));
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBigDecimal(3),
+        "value 'some🌟' cannot be decoded as BigDecimal");
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(get());
+  }
+
+  @Test
+  public void getDatePrepare() throws SQLException {
+    getDate(getPrepare(sharedConn));
+    getDate(getPrepare(sharedConnBinary));
+  }
+
+  public void getDate(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getDate(1), " cannot be decoded as Date");
+    rs.next();
+    assertEquals("2011-01-01", rs.getDate(1).toString());
+    assertFalse(rs.wasNull());
+    assertEquals("2010-12-31", rs.getDate(2).toString());
+    assertFalse(rs.wasNull());
+    assertEquals("2010-12-31", rs.getDate("t2alias").toString());
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getDate(3), " cannot be decoded as Date");
+    assertFalse(rs.wasNull());
+    assertNull(rs.getDate(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(get());
+  }
+
+  @Test
+  public void getTimePrepare() throws SQLException {
+    getTime(getPrepare(sharedConn));
+    getTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getTime(ResultSet rs) throws SQLException {
+    rs.next();
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime(1),
+        "value '2011-01-01' cannot be decoded as Time");
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime(2),
+        "value '2010-12-31 23:59:59.152' cannot be decoded as Time");
+    assertFalse(rs.wasNull());
+    assertEquals(Time.valueOf("23:54:51").getTime() + 840, rs.getTime(3).getTime());
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getTimestamp() throws SQLException {
+    getTimestamp(get());
+  }
+
+  @Test
+  public void getTimestampPrepare() throws SQLException {
+    getTimestamp(getPrepare(sharedConn));
+    getTimestamp(getPrepare(sharedConnBinary));
+  }
+
+  public void getTimestamp(ResultSet rs) throws SQLException {
+    rs.next();
+    assertEquals(Timestamp.valueOf("2011-01-01 00:00:00").getTime(), rs.getTimestamp(1).getTime());
+    assertEquals(
+        Timestamp.valueOf("2011-01-01 00:00:00").getTime() + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp(1, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertEquals(
+        Timestamp.valueOf("2011-01-01 00:00:00").getTime(), rs.getTimestamp("t1alias").getTime());
+    assertEquals(
+        Timestamp.valueOf("2011-01-01 00:00:00").getTime() + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp("t1alias", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime() + 152, rs.getTimestamp(2).getTime());
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime()
+            + 152
+            + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp(2, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime() + 152,
+        rs.getTimestamp("t2alias").getTime());
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime()
+            + 152
+            + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp("t2alias", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getOffsetDateTime() throws SQLException {
+    getOffsetDateTime(get());
+  }
+
+  @Test
+  public void getOffsetDateTimePrepare() throws SQLException {
+    getOffsetDateTime(getPrepare(sharedConn));
+    getOffsetDateTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getOffsetDateTime(ResultSet rs) throws SQLException {
+    assertTrue(rs.next());
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(1, OffsetDateTime.class),
+        "cannot be decoded as OffsetDateTime");
+    assertTrue(rs.next());
+    assertEquals(
+        OffsetDateTime.parse("2010-12-31T23:59:59.152+01:00"),
+        rs.getObject(1, OffsetDateTime.class));
+    assertEquals(
+        OffsetDateTime.parse("2010-12-31T23:59:59.152Z"), rs.getObject(2, OffsetDateTime.class));
+  }
+
+  @Test
+  public void getAsciiStream() throws Exception {
+    getAsciiStream(get());
+  }
+
+  @Test
+  public void getAsciiStreamPrepare() throws Exception {
+    getAsciiStream(getPrepare(sharedConn));
+    getAsciiStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getAsciiStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getAsciiStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getAsciiStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getAsciiStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getAsciiStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getAsciiStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getUnicodeStream() throws Exception {
+    getUnicodeStream(get());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepare() throws Exception {
+    getUnicodeStream(getPrepare(sharedConn));
+    getUnicodeStream(getPrepare(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  public void getUnicodeStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getUnicodeStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getUnicodeStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getUnicodeStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)),
+        rs.getUnicodeStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getUnicodeStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBinaryStream() throws Exception {
+    getBinaryStream(get());
+  }
+
+  @Test
+  public void getBinaryStreamPrepare() throws Exception {
+    getBinaryStream(getPrepare(sharedConn));
+    getBinaryStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getBinaryStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getBinaryStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getBinaryStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getBinaryStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getBinaryStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBinaryStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBytes() throws Exception {
+    getBytes(get());
+  }
+
+  @Test
+  public void getBytesPrepare() throws Exception {
+    getBytes(getPrepare(sharedConn));
+    getBytes(getPrepare(sharedConnBinary));
+  }
+
+  public void getBytes(ResultSet rs) throws Exception {
+    assertArrayEquals("0".getBytes(), rs.getBytes(1));
+    assertFalse(rs.wasNull());
+    assertArrayEquals("1".getBytes(), rs.getBytes(2));
+    assertArrayEquals("1".getBytes(), rs.getBytes("t2alias"));
+    assertFalse(rs.wasNull());
+    assertArrayEquals("some🌟".getBytes(StandardCharsets.UTF_8), rs.getBytes(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBytes(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getCharacterStream() throws Exception {
+    getCharacterStream(get());
+  }
+
+  @Test
+  public void getCharacterStreamPrepare() throws Exception {
+    getCharacterStream(getPrepare(sharedConn));
+    getCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getCharacterStream(ResultSet rs) throws Exception {
+    assertReaderEquals(new StringReader("0"), rs.getCharacterStream(1));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("1"), rs.getCharacterStream(2));
+    assertReaderEquals(new StringReader("1"), rs.getCharacterStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("some🌟"), rs.getCharacterStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getCharacterStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNCharacterStream() throws Exception {
+    getNCharacterStream(get());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepare() throws Exception {
+    getNCharacterStream(getPrepare(sharedConn));
+    getNCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getNCharacterStream(ResultSet rs) throws Exception {
+    assertReaderEquals(new StringReader("0"), rs.getNCharacterStream(1));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("1"), rs.getNCharacterStream(2));
+    assertReaderEquals(new StringReader("1"), rs.getNCharacterStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("some🌟"), rs.getNCharacterStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNCharacterStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBlob() throws Exception {
+    getBlob(get());
+  }
+
+  @Test
+  public void getBlobPrepare() throws Exception {
+    getBlob(getPrepare(sharedConn));
+    getBlob(getPrepare(sharedConnBinary));
+  }
+
+  public void getBlob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getBlob(1), "(not binary) cannot be decoded as Blob");
+  }
+
+  @Test
+  public void getClob() throws Exception {
+    getClob(get());
+  }
+
+  @Test
+  public void getClobPrepare() throws Exception {
+    getClob(getPrepare(sharedConn));
+    getClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getClob(ResultSet rs) throws Exception {
+    assertStreamEquals(new MariaDbClob("0".getBytes()), rs.getClob(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getClob(2));
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getClob("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getClob(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getClob(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNClob() throws Exception {
+    getNClob(get());
+  }
+
+  @Test
+  public void getNClobPrepare() throws Exception {
+    getNClob(getPrepare(sharedConn));
+    getNClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getNClob(ResultSet rs) throws Exception {
+    assertStreamEquals(new MariaDbClob("0".getBytes()), rs.getNClob(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getNClob(2));
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getNClob("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getNClob(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNClob(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = get();
+    ResultSetMetaData meta = rs.getMetaData();
+
+    assertTrue(
+        "CHAR".equals(meta.getColumnTypeName(1)) || "VARCHAR".equals(meta.getColumnTypeName(1)));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.lang.String", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertTrue(Types.CHAR == meta.getColumnType(1) || Types.VARCHAR == meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    if (!isXpand()) {
+      assertEquals(30, meta.getColumnDisplaySize(1));
+      assertEquals(30, meta.getPrecision(1));
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/ClobCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/ClobCodecTest.java
new file mode 100644
index 0000000..ccdafd4
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/ClobCodecTest.java
@@ -0,0 +1,1014 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.sql.*;
+import java.time.*;
+import java.util.Calendar;
+import java.util.TimeZone;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.MariaDbClob;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.integration.Common;
+
+public class ClobCodecTest extends CommonCodecTest {
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS ClobCodec");
+    stmt.execute("DROP TABLE IF EXISTS ClobParamCodec");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE ClobCodec (t1 TINYTEXT, t2 TEXT, t3 MEDIUMTEXT, t4 LONGTEXT) CHARACTER "
+            + "SET utf8mb4 COLLATE utf8mb4_unicode_ci");
+    stmt.execute(
+        "INSERT INTO ClobCodec VALUES ('0', '1', 'some🌟', null), ('2011-01-01', '2010-12-31"
+            + " 23:59:59.152', '23:54:51.840010', null)");
+    stmt.execute(
+        "CREATE TABLE ClobParamCodec(id int not null primary key auto_increment, t1 TEXT) "
+            + "CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
+
+    stmt.execute("FLUSH TABLES");
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from ClobCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getPrepare(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from ClobCodec"
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    ResultSet rs = preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws SQLException {
+    getObject(get());
+  }
+
+  @Test
+  public void getObjectPrepare() throws SQLException {
+    getObject(getPrepare(sharedConn));
+    getObject(getPrepare(sharedConnBinary));
+  }
+
+  public void getObject(ResultSet rs) throws SQLException {
+    assertEquals(new MariaDbClob("0".getBytes()), rs.getObject(1, Clob.class));
+    assertFalse(rs.wasNull());
+    assertEquals(new MariaDbClob("1".getBytes()), rs.getObject(2, Clob.class));
+    assertEquals(new MariaDbClob("1".getBytes()), rs.getObject("t2alias", Clob.class));
+    assertFalse(rs.wasNull());
+    assertEquals(
+        new MariaDbClob("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getObject(3, Clob.class));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testObject(rs, Integer.class, 0);
+    testObject(rs, String.class, "0");
+    testObject(rs, Byte.class, Byte.valueOf("0"));
+    testObject(rs, Long.class, 0L);
+    testObject(rs, Short.class, (short) 0);
+    testObject(rs, BigDecimal.class, BigDecimal.valueOf(0));
+    testObject(rs, BigInteger.class, BigInteger.valueOf(0));
+    testObject(rs, Double.class, (double) 0);
+    testObject(rs, Float.class, (float) 0);
+    testObject(rs, Byte.class, (byte) 0);
+    testArrObject(rs, new byte[] {(byte) '0'});
+    testObject(rs, Boolean.class, Boolean.FALSE);
+    testObject(rs, Clob.class, new MariaDbClob("0".getBytes()));
+    testObject(rs, NClob.class, new MariaDbClob("0".getBytes()));
+    testObject(rs, InputStream.class, new MariaDbClob("0".getBytes()).getBinaryStream());
+    testObject(rs, Reader.class, new StringReader("0"));
+    rs.next();
+    testObject(rs, LocalDate.class, LocalDate.parse("2011-01-01"));
+    testObject(rs, LocalDateTime.class, LocalDateTime.parse("2011-01-01T00:00"));
+    testObject(rs, LocalTime.class, LocalTime.parse("23:54:51.840010"), 3);
+    Time t = Time.valueOf("23:54:51");
+    testObject(rs, Time.class, new Time(t.getTime() + 840), 3);
+    testObject(rs, Date.class, Date.valueOf("2011-01-01"));
+    Timestamp tt = Timestamp.valueOf("2010-12-31 23:59:59");
+    testObject(rs, Timestamp.class, new Timestamp(tt.getTime() + 152), 2);
+    testObject(
+        rs,
+        ZonedDateTime.class,
+        LocalDateTime.parse("2011-01-01T00:00").atZone(ZoneId.systemDefault()));
+    testErrObject(rs, BigInteger.class);
+    testErrObject(rs, OffsetDateTime.class);
+    testErrObject(rs, OffsetTime.class);
+    testObject(rs, java.util.Date.class, Date.valueOf("2010-12-31"), 2);
+  }
+
+  @Test
+  public void getString() throws SQLException {
+    getString(get());
+  }
+
+  @Test
+  public void getStringPrepare() throws SQLException {
+    getString(getPrepare(sharedConn));
+    getString(getPrepare(sharedConnBinary));
+  }
+
+  public void getString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getString(2));
+    assertEquals("1", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("some🌟", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNString() throws SQLException {
+    getNString(get());
+  }
+
+  @Test
+  public void getNStringPrepare() throws SQLException {
+    getNString(getPrepare(sharedConn));
+    getNString(getPrepare(sharedConnBinary));
+  }
+
+  public void getNString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getNString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getNString(2));
+    assertEquals("1", rs.getNString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("some🌟", rs.getNString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBoolean() throws SQLException {
+    getBoolean(get());
+  }
+
+  @Test
+  public void getBooleanPrepare() throws SQLException {
+    getBoolean(getPrepare(sharedConn));
+    getBoolean(getPrepare(sharedConnBinary));
+  }
+
+  public void getBoolean(ResultSet rs) throws SQLException {
+    assertFalse(rs.getBoolean(1));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(2));
+    assertTrue(rs.getBoolean("t2alias"));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(3));
+    assertFalse(rs.wasNull());
+    assertFalse(rs.getBoolean(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getByte() throws SQLException {
+    getByte(get());
+  }
+
+  @Test
+  public void getBytePrepare() throws SQLException {
+    getByte(getPrepare(sharedConn));
+    getByte(getPrepare(sharedConnBinary));
+  }
+
+  public void getByte(ResultSet rs) throws SQLException {
+    assertEquals((byte) 0, rs.getByte(1));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 1, rs.getByte(2));
+    assertEquals((byte) 1, rs.getByte("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getByte(3),
+        "value 'some\uD83C\uDF1F' (BLOB) cannot be decoded as Byte");
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 0, rs.getByte(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getShort() throws SQLException {
+    getShort(get());
+  }
+
+  @Test
+  public void getShortPrepare() throws SQLException {
+    getShort(getPrepare(sharedConn));
+    getShort(getPrepare(sharedConnBinary));
+  }
+
+  public void getShort(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getShort(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getShort(2));
+    assertEquals(1, rs.getShort("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getShort(3), "value 'some🌟' cannot be decoded as Short");
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getShort(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(get());
+  }
+
+  @Test
+  public void getIntPrepare() throws SQLException {
+    getInt(getPrepare(sharedConn));
+    getInt(getPrepare(sharedConnBinary));
+  }
+
+  public void getInt(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getInt(2));
+    assertEquals(1, rs.getInt("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getInt(3), "value 'some🌟' cannot be decoded as Integer");
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(get());
+  }
+
+  @Test
+  public void getLongPrepare() throws SQLException {
+    getLong(getPrepare(sharedConn));
+    getLong(getPrepare(sharedConnBinary));
+  }
+
+  public void getLong(ResultSet rs) throws SQLException {
+    assertEquals(0L, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1L, rs.getLong(2));
+    assertEquals(1L, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getLong(3),
+        "value 'some\uD83C\uDF1F' cannot be decoded as Long");
+    assertFalse(rs.wasNull());
+    assertEquals(0L, rs.getLong(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(get());
+  }
+
+  @Test
+  public void getFloatPrepare() throws SQLException {
+    getFloat(getPrepare(sharedConn));
+    getFloat(getPrepare(sharedConnBinary));
+  }
+
+  public void getFloat(ResultSet rs) throws SQLException {
+    assertEquals(0F, rs.getFloat(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1F, rs.getFloat(2));
+    assertEquals(1F, rs.getFloat("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getFloat(3), "value 'some🌟' cannot be decoded as Float");
+    assertFalse(rs.wasNull());
+    assertEquals(0F, rs.getFloat(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(get());
+  }
+
+  @Test
+  public void getDoublePrepare() throws SQLException {
+    getDouble(getPrepare(sharedConn));
+    getDouble(getPrepare(sharedConnBinary));
+  }
+
+  public void getDouble(ResultSet rs) throws SQLException {
+    assertEquals(0D, rs.getDouble(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1D, rs.getDouble(2));
+    assertEquals(1D, rs.getDouble("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDouble(3),
+        "value 'some🌟' cannot be decoded as Double");
+    assertFalse(rs.wasNull());
+    assertEquals(0D, rs.getDouble(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(get());
+  }
+
+  @Test
+  public void getBigDecimalPrepare() throws SQLException {
+    getBigDecimal(getPrepare(sharedConn));
+    getBigDecimal(getPrepare(sharedConnBinary));
+  }
+
+  public void getBigDecimal(ResultSet rs) throws SQLException {
+    assertEquals(BigDecimal.ZERO, rs.getBigDecimal(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal(2));
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBigDecimal(3),
+        "value 'some🌟' cannot be decoded as BigDecimal");
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(get());
+  }
+
+  @Test
+  public void getDatePrepare() throws SQLException {
+    getDate(getPrepare(sharedConn));
+    getDate(getPrepare(sharedConnBinary));
+  }
+
+  public void getDate(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getDate(1), "value '0' (BLOB) cannot be decoded as Date");
+    rs.next();
+    assertEquals("2011-01-01", rs.getDate(1).toString());
+    assertFalse(rs.wasNull());
+    assertEquals("2010-12-31", rs.getDate(2).toString());
+    assertFalse(rs.wasNull());
+    assertEquals("2010-12-31", rs.getDate("t2alias").toString());
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDate(3),
+        "value '23:54:51.840010' (BLOB) cannot be decoded as Date");
+    assertFalse(rs.wasNull());
+    assertNull(rs.getDate(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(get());
+  }
+
+  @Test
+  public void getTimePrepare() throws SQLException {
+    getTime(getPrepare(sharedConn));
+    getTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getTime(ResultSet rs) throws SQLException {
+    rs.next();
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime(1),
+        "BLOB value '2011-01-01' cannot be decoded as Time");
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime(2),
+        "BLOB value '2010-12-31 23:59:59.152' cannot be decoded as Time");
+    assertEquals(Time.valueOf("23:54:51").getTime() + 840, rs.getTime(3).getTime());
+    assertEquals(Time.valueOf("23:54:51").getTime() + 840, rs.getTime("t3alias").getTime());
+    Calendar utc = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+    assertEquals(86091840, rs.getTime(3, utc).getTime());
+    assertEquals(86091840, rs.getTime("t3alias", utc).getTime());
+
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getTimestamp() throws SQLException {
+    getTimestamp(get());
+  }
+
+  @Test
+  public void getTimestampPrepare() throws SQLException {
+    getTimestamp(getPrepare(sharedConn));
+    getTimestamp(getPrepare(sharedConnBinary));
+  }
+
+  public void getTimestamp(ResultSet rs) throws SQLException {
+    rs.next();
+
+    assertEquals(Timestamp.valueOf("2011-01-01 00:00:00").getTime(), rs.getTimestamp(1).getTime());
+    assertEquals(
+        Timestamp.valueOf("2011-01-01 00:00:00").getTime() + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp(1, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertEquals(
+        Timestamp.valueOf("2011-01-01 00:00:00").getTime(), rs.getTimestamp("t1alias").getTime());
+    assertEquals(
+        Timestamp.valueOf("2011-01-01 00:00:00").getTime() + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp("t1alias", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime() + 152, rs.getTimestamp(2).getTime());
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime()
+            + 152
+            + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp(2, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime() + 152,
+        rs.getTimestamp("t2alias").getTime());
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime()
+            + 152
+            + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp("t2alias", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+
+    //
+    //    ('2011-01-01', '2010-12-31 23:59:59.152',"
+    //            + " '23:54:51.840010', null)
+  }
+
+  @Test
+  public void getOffsetDateTime() throws SQLException {
+    getOffsetDateTime(get());
+  }
+
+  @Test
+  public void getOffsetDateTimePrepare() throws SQLException {
+    getOffsetDateTime(getPrepare(sharedConn));
+    getOffsetDateTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getOffsetDateTime(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(1, OffsetDateTime.class),
+        "cannot be decoded as OffsetDateTime");
+  }
+
+  @Test
+  public void getAsciiStream() throws Exception {
+    getAsciiStream(get());
+  }
+
+  @Test
+  public void getAsciiStreamPrepare() throws Exception {
+    getAsciiStream(getPrepare(sharedConn));
+    getAsciiStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getAsciiStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getAsciiStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getAsciiStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getAsciiStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getAsciiStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getAsciiStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getUnicodeStream() throws Exception {
+    getUnicodeStream(get());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepare() throws Exception {
+    getUnicodeStream(getPrepare(sharedConn));
+    getUnicodeStream(getPrepare(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  public void getUnicodeStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getUnicodeStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getUnicodeStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getUnicodeStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)),
+        rs.getUnicodeStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getUnicodeStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBinaryStream() throws Exception {
+    getBinaryStream(get());
+  }
+
+  @Test
+  public void getBinaryStreamPrepare() throws Exception {
+    getBinaryStream(getPrepare(sharedConn));
+    getBinaryStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getBinaryStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getBinaryStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getBinaryStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getBinaryStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getBinaryStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBinaryStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBytes() throws SQLException {
+    getBytes(get());
+  }
+
+  @Test
+  public void getBytesPrepare() throws SQLException {
+    getBytes(getPrepare(sharedConn));
+    getBytes(getPrepare(sharedConnBinary));
+  }
+
+  public void getBytes(ResultSet rs) throws SQLException {
+    assertArrayEquals("0".getBytes(), rs.getBytes(1));
+    assertFalse(rs.wasNull());
+    assertArrayEquals("1".getBytes(), rs.getBytes(2));
+    assertArrayEquals("1".getBytes(), rs.getBytes("t2alias"));
+    assertFalse(rs.wasNull());
+    assertArrayEquals("some🌟".getBytes(StandardCharsets.UTF_8), rs.getBytes(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBytes(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getCharacterStream() throws Exception {
+    getCharacterStream(get());
+  }
+
+  @Test
+  public void getCharacterStreamPrepare() throws Exception {
+    getCharacterStream(getPrepare(sharedConn));
+    getCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getCharacterStream(ResultSet rs) throws Exception {
+    assertReaderEquals(new StringReader("0"), rs.getCharacterStream(1));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("1"), rs.getCharacterStream(2));
+    assertReaderEquals(new StringReader("1"), rs.getCharacterStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("some🌟"), rs.getCharacterStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getCharacterStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNCharacterStream() throws Exception {
+    getNCharacterStream(get());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepare() throws Exception {
+    getNCharacterStream(getPrepare(sharedConn));
+    getNCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getNCharacterStream(ResultSet rs) throws Exception {
+    assertReaderEquals(new StringReader("0"), rs.getNCharacterStream(1));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("1"), rs.getNCharacterStream(2));
+    assertReaderEquals(new StringReader("1"), rs.getNCharacterStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("some🌟"), rs.getNCharacterStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNCharacterStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBlob() throws Exception {
+    getBlob(get());
+  }
+
+  @Test
+  public void getBlobPrepare() throws Exception {
+    getBlob(getPrepare(sharedConn));
+    getBlob(getPrepare(sharedConnBinary));
+  }
+
+  public void getBlob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBlob(1),
+        "Data type BLOB (not binary) cannot be decoded as Blob");
+  }
+
+  @Test
+  public void getClob() throws Exception {
+    getClob(get());
+  }
+
+  @Test
+  public void getClobPrepare() throws Exception {
+    getClob(getPrepare(sharedConn));
+    getClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getClob(ResultSet rs) throws Exception {
+    assertStreamEquals(new MariaDbClob("0".getBytes()), rs.getClob(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getClob(2));
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getClob("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getClob(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getClob(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNClob() throws Exception {
+    getNClob(get());
+  }
+
+  @Test
+  public void getNClobPrepare() throws Exception {
+    getNClob(getPrepare(sharedConn));
+    getNClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getNClob(ResultSet rs) throws Exception {
+    assertStreamEquals(new MariaDbClob("0".getBytes()), rs.getNClob(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getNClob(2));
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getNClob("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getNClob(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNClob(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = get();
+    ResultSetMetaData meta = rs.getMetaData();
+    assertEquals("VARCHAR", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.lang.String", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.VARCHAR, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    if (!isXpand()) {
+      assertEquals(255, meta.getColumnDisplaySize(1));
+      assertEquals(255, meta.getPrecision(1));
+    }
+  }
+
+  @Test
+  public void sendParam() throws SQLException {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+    String urlWithHaMode =
+        mDefUrl.replaceAll("jdbc:mariadb:", "jdbc:mariadb:sequential:")
+            + (mDefUrl.indexOf("?") > 0 ? "&" : "?")
+            + "useServerPrepStmts=true";
+    try (Connection con = DriverManager.getConnection(urlWithHaMode)) {
+      sendParam(con);
+    }
+
+    try (Connection con = createCon("transactionReplay=true&useServerPrepStmts=false")) {
+      sendParam(con);
+    }
+    try (Connection con = createCon("transactionReplay=true&useServerPrepStmts=true")) {
+      sendParam(con);
+    }
+  }
+
+  private void sendParam(Connection con) throws SQLException {
+    StringBuilder longDataSb = new StringBuilder(20000);
+
+    for (int i = 0; i < 20000; i++) {
+      longDataSb.append('0' + i % 16);
+    }
+    Clob longData = new MariaDbClob(longDataSb.toString().getBytes(StandardCharsets.UTF_8));
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE ClobParamCodec");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO ClobParamCodec(t1) VALUES (?)")) {
+      prep.setClob(1, new MariaDbClob("e🌟£1".getBytes(StandardCharsets.UTF_8)));
+      prep.execute();
+      prep.setClob(1, (Clob) null);
+      prep.execute();
+      prep.setObject(1, "e🌟2");
+      prep.execute();
+      prep.setObject(1, null);
+      prep.execute();
+      prep.setObject(1, "e🌟3", Types.VARCHAR);
+      prep.execute();
+      prep.setObject(1, null, Types.VARCHAR);
+      prep.execute();
+      prep.setObject(1, null, Types.VARCHAR);
+      prep.execute();
+      prep.setObject(1, new MariaDbClob("e🌟56".getBytes(StandardCharsets.UTF_8)), Types.CLOB, 4);
+      prep.execute();
+      prep.setObject(1, longData);
+
+      prep.setClob(1, new MariaDbClob("e🌟1".getBytes(StandardCharsets.UTF_8)));
+      prep.addBatch();
+      prep.setClob(1, new MariaDbClob("e🌟1".getBytes(StandardCharsets.UTF_8)));
+      prep.addBatch();
+      prep.setClob(1, longData);
+      prep.addBatch();
+      prep.setClob(1, (Clob) null);
+      prep.addBatch();
+      prep.setObject(1, new MariaDbClob("e🌟56".getBytes(StandardCharsets.UTF_8)), Types.CLOB, 4);
+      prep.addBatch();
+      prep.executeBatch();
+
+      prep.setCharacterStream(1, new StringReader("e🌟789"));
+      prep.execute();
+      prep.setCharacterStream(1, new StringReader("e🌟890"), 4);
+      prep.execute();
+      prep.setObject(1, new StringReader(longDataSb.toString()), Types.CLOB);
+      prep.execute();
+      prep.setObject(1, new StringReader("e🌟568"), Types.CLOB, 4);
+      prep.execute();
+
+      prep.setCharacterStream(1, new StringReader("e🌟789"));
+      prep.addBatch();
+      prep.setCharacterStream(1, new StringReader("e🌟890"), 4);
+      prep.addBatch();
+      prep.executeBatch();
+
+      prep.setNClob(1, new MariaDbClob("e🌟1".getBytes(StandardCharsets.UTF_8)));
+      prep.execute();
+
+      prep.setNClob(1, new MariaDbClob("e🌟145".getBytes(StandardCharsets.UTF_8)));
+      prep.execute();
+
+      prep.setNCharacterStream(1, new StringReader("e🌟789"));
+      prep.execute();
+      prep.setNCharacterStream(1, new StringReader("e🌟890"), 4);
+      prep.execute();
+      prep.setNCharacterStream(1, new StringReader("e🌟789"));
+      prep.execute();
+      prep.setNCharacterStream(1, new StringReader("e🌟890"), 4);
+      prep.execute();
+    }
+
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM ClobParamCodec");
+    assertTrue(rs.next());
+    assertEquals("e🌟£1", rs.getString(2));
+    rs.updateClob(2, new MariaDbClob("f🌟10".getBytes(StandardCharsets.UTF_8)));
+    rs.updateRow();
+    assertEquals("f🌟10", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    rs.updateClob("t1", new MariaDbClob("f🌟15".getBytes(StandardCharsets.UTF_8)));
+    rs.updateRow();
+    assertEquals("f🌟15", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟2", rs.getString(2));
+    rs.updateClob("t1", (Clob) null);
+    rs.updateRow();
+    assertNull(rs.getString(2));
+    assertTrue(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    rs.updateObject("t1", new MariaDbClob("f🌟56".getBytes(StandardCharsets.UTF_8)), 4);
+    rs.updateRow();
+    assertEquals("f🌟5", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟3", rs.getString(2));
+    rs.updateCharacterStream(2, new StringReader("e🌟789"));
+    rs.updateRow();
+    assertEquals("e🌟789", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    rs.updateCharacterStream(2, new StringReader("e🌟789"), 4);
+    rs.updateRow();
+    assertEquals("e🌟7", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    rs.updateCharacterStream(2, new StringReader("e🌟789"), 4L);
+    rs.updateRow();
+    assertEquals("e🌟7", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟5", rs.getString(2));
+    rs.updateCharacterStream("t1", new StringReader("e🌟789"));
+    rs.updateRow();
+    assertEquals("e🌟789", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟1", rs.getString(2));
+    rs.updateCharacterStream("t1", new StringReader("e🌟789"), 4);
+    rs.updateRow();
+    assertEquals("e🌟7", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟1", rs.getString(2));
+    rs.updateCharacterStream("t1", new StringReader("e🌟789"), 4L);
+    rs.updateRow();
+    assertEquals("e🌟7", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals(longDataSb.toString(), rs.getString(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟5", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟789", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟8", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals(longDataSb.toString(), rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟5", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟789", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟8", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟1", rs.getString(2));
+    rs.updateNClob(2, new MariaDbClob("g🌟14".getBytes(StandardCharsets.UTF_8)));
+    rs.updateRow();
+    assertEquals("g🌟14", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟145", rs.getString(2));
+    rs.updateNClob("t1", new MariaDbClob("h🌟14".getBytes(StandardCharsets.UTF_8)));
+    rs.updateRow();
+    assertEquals("h🌟14", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟789", rs.getString(2));
+    rs.updateNCharacterStream("t1", new StringReader("e🌟5789"));
+    rs.updateRow();
+    assertEquals("e🌟5789", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟8", rs.getString(2));
+    rs.updateNCharacterStream("t1", new StringReader("e🌟5789"), 5);
+    rs.updateRow();
+    assertEquals("e🌟57", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟789", rs.getString(2));
+    rs.updateNCharacterStream(2, new StringReader("e🌟5789"));
+    rs.updateRow();
+    assertEquals("e🌟5789", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟8", rs.getString(2));
+    rs.updateNCharacterStream(2, new StringReader("e🌟5789"), 5);
+    rs.updateRow();
+    assertEquals("e🌟57", rs.getString(2));
+
+    rs = stmt.executeQuery("SELECT * FROM ClobParamCodec");
+    assertTrue(rs.next());
+    assertEquals("f🌟10", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("f🌟15", rs.getString(2));
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    assertTrue(rs.wasNull());
+    assertTrue(rs.next());
+    assertEquals("f🌟5", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟789", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟7", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟7", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟789", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟7", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟7", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals(longDataSb.toString(), rs.getString(2));
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟5", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟789", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟8", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals(longDataSb.toString(), rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟5", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟789", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟8", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("g🌟14", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("h🌟14", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟5789", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟57", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟5789", rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("e🌟57", rs.getString(2));
+    con.commit();
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/CommonCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/CommonCodecTest.java
new file mode 100644
index 0000000..99bcad2
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/CommonCodecTest.java
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.sql.*;
+import org.junit.jupiter.api.Assertions;
+import org.mariadb.jdbc.integration.Common;
+
+public class CommonCodecTest extends Common {
+
+  void assertReaderEquals(Reader expected, Reader result) throws IOException {
+    int res1 = 0;
+    int res2 = 0;
+    while (res1 != -1 && res2 != -1) {
+      res1 = expected.read();
+      res2 = result.read();
+      Assertions.assertEquals(res1, res2);
+    }
+  }
+
+  void assertStreamEquals(InputStream expected, InputStream result) throws IOException {
+    int res1 = 0;
+    int res2 = 0;
+    while (res1 != -1 && res2 != -1) {
+      res1 = expected.read();
+      res2 = result.read();
+      Assertions.assertEquals(res1, res2);
+    }
+  }
+
+  void assertStreamEquals(Blob expected, Blob result) throws Exception {
+    assertStreamEquals(expected.getBinaryStream(), result.getBinaryStream());
+  }
+
+  void assertStreamEquals(Clob expected, Clob result) throws Exception {
+    assertReaderEquals(expected.getCharacterStream(), result.getCharacterStream());
+  }
+
+  void testObject(ResultSet rs, Class<?> objClass, Object exp) throws Exception {
+    testObject(rs, objClass, exp, 1);
+  }
+
+  void testObject(ResultSet rs, Class<?> objClass, Object exp, int idx) throws Exception {
+    if (exp instanceof Blob) {
+      assertStreamEquals((Blob) exp, (Blob) rs.getObject(idx, objClass));
+      assertStreamEquals((Blob) exp, (Blob) rs.getObject("t" + idx + "alias", objClass));
+      assertNull(rs.getObject(4, objClass));
+      assertNull(rs.getObject("t4alias", objClass));
+    } else if (exp instanceof Clob) {
+      assertStreamEquals((Clob) exp, (Clob) rs.getObject(idx, objClass));
+      assertStreamEquals((Clob) exp, (Clob) rs.getObject("t" + idx + "alias", objClass));
+      assertNull(rs.getObject(4, objClass));
+      assertNull(rs.getObject("t4alias", objClass));
+    } else if (exp instanceof InputStream) {
+      assertStreamEquals((InputStream) exp, (InputStream) rs.getObject(idx, objClass));
+      assertNull(rs.getObject(4, objClass));
+      assertNull(rs.getObject("t4alias", objClass));
+    } else if (exp instanceof byte[]) {
+      assertArrayEquals((byte[]) exp, (byte[]) rs.getObject(idx, objClass));
+      assertNull(rs.getObject(4, objClass));
+      assertNull(rs.getObject("t4alias", objClass));
+    } else if (exp instanceof Reader) {
+      assertReaderEquals((Reader) exp, (Reader) rs.getObject(idx, objClass));
+      assertNull(rs.getObject(4, objClass));
+      assertNull(rs.getObject("t4alias", objClass));
+    } else if (exp instanceof Time) {
+      assertEquals(((Time) exp).getTime(), ((Time) rs.getObject(idx, objClass)).getTime());
+      assertEquals(
+          ((Time) exp).getTime(), ((Time) rs.getObject("t" + idx + "alias", objClass)).getTime());
+      assertNull(rs.getObject(4, objClass));
+      assertNull(rs.getObject("t4alias", objClass));
+    } else if (exp instanceof Date) {
+      assertEquals(((Date) exp).getTime(), ((Date) rs.getObject(idx, objClass)).getTime());
+      assertEquals(
+          ((Date) exp).getTime(), ((Date) rs.getObject("t" + idx + "alias", objClass)).getTime());
+      assertNull(rs.getObject(4, objClass));
+      assertNull(rs.getObject("t4alias", objClass));
+    } else {
+      assertEquals(exp, rs.getObject(idx, objClass));
+      assertEquals(exp, rs.getObject("t" + idx + "alias", objClass));
+      assertNull(rs.getObject(4, objClass));
+      assertNull(rs.getObject("t4alias", objClass));
+    }
+  }
+
+  void testErrObject(ResultSet rs, Class<?> objClass, int index) throws SQLException {
+    Assertions.assertThrows(SQLException.class, () -> rs.getObject(index, objClass));
+    Assertions.assertThrows(
+        SQLException.class, () -> rs.getObject("t" + index + "alias", objClass));
+  }
+
+  void testErrObject(ResultSet rs, Class<?> objClass) throws SQLException {
+    testErrObject(rs, objClass, 1);
+    assertNull(rs.getObject(4, objClass));
+    assertNull(rs.getObject("t4alias", objClass));
+  }
+
+  void testArrObject(ResultSet rs, byte[] exp, int index) throws SQLException {
+    assertArrayEquals(exp, (byte[]) rs.getObject(index, (Class<?>) byte[].class));
+    assertArrayEquals(exp, (byte[]) rs.getObject("t" + index + "alias", (Class<?>) byte[].class));
+  }
+
+  void testArrObject(ResultSet rs, byte[] exp) throws SQLException {
+    testArrObject(rs, exp, 1);
+    assertNull(rs.getObject(4, (Class<?>) byte[].class));
+    assertNull(rs.getObject("t4alias", (Class<?>) byte[].class));
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/DateCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/DateCodecTest.java
new file mode 100644
index 0000000..cc919c1
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/DateCodecTest.java
@@ -0,0 +1,810 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import java.time.*;
+import java.util.Calendar;
+import java.util.TimeZone;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.integration.Common;
+
+public class DateCodecTest extends CommonCodecTest {
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS DateCodec");
+    stmt.execute("DROP TABLE IF EXISTS DateCodec2");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE DateCodec (t1 DATE, t2 DATE, t3 DATE, t4 DATE)");
+    stmt.execute(
+        "INSERT INTO DateCodec VALUES ('2010-01-12', '1000-01-01', '9999-12-31', null)"
+            + (isMariaDBServer() ? ",('0000-00-00', '1000-01-01', '9999-12-31', null)" : ""));
+    stmt.execute("CREATE TABLE DateCodec2 (id int not null primary key auto_increment, t1 DATE)");
+    stmt.execute("FLUSH TABLES");
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from DateCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getPrepare(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from DateCodec"
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    ResultSet rs = preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws SQLException {
+    getObject(get());
+  }
+
+  @Test
+  public void getObjectPrepare() throws SQLException {
+    getObject(getPrepare(sharedConn));
+    getObject(getPrepare(sharedConnBinary));
+  }
+
+  public void getObject(ResultSet rs) throws SQLException {
+    assertFalse(rs.wasNull());
+    assertEquals(Date.valueOf("2010-01-12").getTime(), ((Date) rs.getObject(1)).getTime());
+    assertFalse(rs.wasNull());
+    assertFalse(rs.wasNull());
+    assertEquals(Date.valueOf("1000-01-01").getTime(), ((Date) rs.getObject(2)).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(Date.valueOf("9999-12-31").getTime(), ((Date) rs.getObject(3)).getTime());
+    assertFalse(rs.wasNull());
+    assertNull(rs.getDate(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testErrObject(rs, Integer.class);
+    testObject(rs, String.class, "2010-01-12");
+    testErrObject(rs, Long.class);
+    testErrObject(rs, Short.class);
+    testErrObject(rs, BigDecimal.class);
+    testErrObject(rs, BigInteger.class);
+    testErrObject(rs, Double.class);
+    testErrObject(rs, Float.class);
+    testErrObject(rs, Byte.class);
+    testErrObject(rs, byte[].class);
+    testErrObject(rs, Boolean.class);
+    testErrObject(rs, Duration.class);
+    testErrObject(rs, Clob.class);
+    testErrObject(rs, NClob.class);
+    testErrObject(rs, InputStream.class);
+    testErrObject(rs, Reader.class);
+    testObject(rs, LocalDate.class, LocalDate.parse("2010-01-12"));
+    testObject(rs, LocalDateTime.class, LocalDateTime.parse("2010-01-12T00:00:00"));
+    testErrObject(rs, LocalTime.class);
+    testErrObject(rs, Time.class);
+    testErrObject(rs, BigInteger.class);
+    testObject(rs, Timestamp.class, Timestamp.valueOf("2010-01-12 00:00:00"));
+    testObject(
+        rs,
+        ZonedDateTime.class,
+        LocalDateTime.parse("2010-01-12T00:00:00").atZone(ZoneId.systemDefault()));
+    testObject(rs, java.util.Date.class, Date.valueOf("2010-01-12"));
+  }
+
+  @Test
+  public void getString() throws SQLException {
+    getString(get());
+  }
+
+  @Test
+  public void getStringPrepare() throws SQLException {
+    getString(getPrepare(sharedConn));
+    getString(getPrepare(sharedConnBinary));
+  }
+
+  public void getString(ResultSet rs) throws SQLException {
+    assertEquals("2010-01-12", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1000-01-01", rs.getString(2));
+    assertEquals("1000-01-01", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("9999-12-31", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+    if (isMariaDBServer()) {
+      rs.next();
+      assertTrue("0000-00-00".equals(rs.getString(1)));
+    }
+  }
+
+  @Test
+  public void getNString() throws SQLException {
+    getNString(get());
+  }
+
+  @Test
+  public void getNStringPrepare() throws SQLException {
+    getNString(getPrepare(sharedConn));
+    getNString(getPrepare(sharedConnBinary));
+  }
+
+  public void getNString(ResultSet rs) throws SQLException {
+    assertEquals("2010-01-12", rs.getNString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1000-01-01", rs.getNString(2));
+    assertEquals("1000-01-01", rs.getNString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("9999-12-31", rs.getNString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBoolean() throws SQLException {
+    getBoolean(get());
+  }
+
+  @Test
+  public void getBooleanPrepare() throws SQLException {
+    getBoolean(getPrepare(sharedConn));
+    getBoolean(getPrepare(sharedConnBinary));
+  }
+
+  public void getBoolean(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getBoolean(1), "Data type DATE cannot be decoded as Boolean");
+  }
+
+  @Test
+  public void getByte() throws SQLException {
+    getByte(get());
+  }
+
+  @Test
+  public void getBytePrepare() throws SQLException {
+    getByte(getPrepare(sharedConn));
+    getByte(getPrepare(sharedConnBinary));
+  }
+
+  public void getByte(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getByte(1), "Data type DATE cannot be decoded as Byte");
+  }
+
+  @Test
+  public void getShort() throws SQLException {
+    getShort(get());
+  }
+
+  @Test
+  public void getShortPrepare() throws SQLException {
+    getShort(getPrepare(sharedConn));
+    getShort(getPrepare(sharedConnBinary));
+  }
+
+  public void getShort(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getShort(1), "Data type DATE cannot be decoded as Short");
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(get());
+  }
+
+  @Test
+  public void getIntPrepare() throws SQLException {
+    getInt(getPrepare(sharedConn));
+    getInt(getPrepare(sharedConnBinary));
+  }
+
+  public void getInt(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getInt(1), "Data type DATE cannot be decoded as Integer");
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(get());
+  }
+
+  @Test
+  public void getLongPrepare() throws SQLException {
+    getLong(getPrepare(sharedConn));
+    getLong(getPrepare(sharedConnBinary));
+  }
+
+  public void getLong(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getLong(1), "Data type DATE cannot be decoded as Long");
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(get());
+  }
+
+  @Test
+  public void getFloatPrepare() throws SQLException {
+    getFloat(getPrepare(sharedConn));
+    getFloat(getPrepare(sharedConnBinary));
+  }
+
+  public void getFloat(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getFloat(1), "Data type DATE cannot be decoded as Float");
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(get());
+  }
+
+  @Test
+  public void getDoublePrepare() throws SQLException {
+    getDouble(getPrepare(sharedConn));
+    getDouble(getPrepare(sharedConnBinary));
+  }
+
+  public void getDouble(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getDouble(1), "Data type DATE cannot be decoded as Double");
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(get());
+  }
+
+  @Test
+  public void getBigDecimalPrepare() throws SQLException {
+    getBigDecimal(getPrepare(sharedConn));
+    getBigDecimal(getPrepare(sharedConnBinary));
+  }
+
+  public void getBigDecimal(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getBigDecimal(1),
+        "Data type DATE cannot be decoded as BigDecimal");
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(get());
+  }
+
+  @Test
+  public void getDatePrepare() throws SQLException {
+    getDate(getPrepare(sharedConn));
+    getDate(getPrepare(sharedConnBinary));
+  }
+
+  public void getDate(ResultSet rs) throws SQLException {
+    assertEquals(
+        1263254400000L, rs.getDate(1, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(Date.valueOf("2010-01-12").getTime(), rs.getDate(1).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(
+        -30609792000000L,
+        rs.getDate(2, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertEquals(
+        -30609792000000L,
+        rs.getDate("t2alias", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(Date.valueOf("1000-01-01").getTime(), rs.getDate(2).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(Date.valueOf("9999-12-31").getTime(), rs.getDate(3).getTime());
+    assertFalse(rs.wasNull());
+    assertNull(rs.getDate(4));
+    assertTrue(rs.wasNull());
+    if (isMariaDBServer()) {
+      rs.next();
+      assertNull(rs.getDate(1));
+    }
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(get());
+  }
+
+  @Test
+  public void getTimePrepare() throws SQLException {
+    getTime(getPrepare(sharedConn));
+    getTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getTime(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getTime(1), "Data type DATE cannot be decoded as Time");
+  }
+
+  @Test
+  public void getDuration() throws SQLException {
+    getDuration(get());
+  }
+
+  @Test
+  public void getDurationPrepare() throws SQLException {
+    getDuration(getPrepare(sharedConn));
+    getDuration(getPrepare(sharedConnBinary));
+  }
+
+  public void getDuration(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(1, Duration.class),
+        "Type class java.time.Duration not supported type for DATE type");
+  }
+
+  @Test
+  public void getLocalTime() throws SQLException {
+    getLocalTime(get());
+  }
+
+  @Test
+  public void getLocalTimePrepare() throws SQLException {
+    getLocalTime(getPrepare(sharedConn));
+    getLocalTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getLocalTime(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(1, LocalTime.class),
+        "Type class java.time.LocalTime not supported type for DATE type");
+  }
+
+  @Test
+  public void getLocalDate() throws SQLException {
+    getLocalDate(get());
+  }
+
+  @Test
+  public void getLocalDatePrepare() throws SQLException {
+    getLocalDate(getPrepare(sharedConn));
+    getLocalDate(getPrepare(sharedConnBinary));
+  }
+
+  public void getLocalDate(ResultSet rs) throws SQLException {
+    assertEquals(LocalDate.parse("2010-01-12"), rs.getObject(1, LocalDate.class));
+    assertFalse(rs.wasNull());
+    assertEquals(LocalDate.parse("1000-01-01"), rs.getObject(2, LocalDate.class));
+    assertEquals(LocalDate.parse("1000-01-01"), rs.getObject("t2alias", LocalDate.class));
+    assertFalse(rs.wasNull());
+    assertEquals(LocalDate.parse("9999-12-31"), rs.getObject(3, LocalDate.class));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4, LocalDate.class));
+    assertTrue(rs.wasNull());
+    if (isMariaDBServer()) {
+      rs.next();
+      assertNull(rs.getObject(1, LocalDate.class));
+    }
+  }
+
+  @Test
+  public void getTimestamp() throws SQLException {
+    getTimestamp(get());
+  }
+
+  @Test
+  public void getTimestampPrepare() throws SQLException {
+    getTimestamp(getPrepare(sharedConn));
+    getTimestamp(getPrepare(sharedConnBinary));
+  }
+
+  public void getTimestamp(ResultSet rs) throws SQLException {
+    assertEquals(Timestamp.valueOf("2010-01-12 00:00:00").getTime(), rs.getTimestamp(1).getTime());
+    assertEquals(
+        1263254400000L,
+        rs.getTimestamp(1, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertEquals(
+        Timestamp.valueOf("2010-01-12 00:00:00").getTime(), rs.getTimestamp("t1alias").getTime());
+    assertEquals(
+        1263254400000L,
+        rs.getTimestamp("t1alias", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(Timestamp.valueOf("1000-01-01 00:00:00").getTime(), rs.getTimestamp(2).getTime());
+    assertEquals(
+        -30609792000000L,
+        rs.getTimestamp(2, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+
+    assertEquals(Timestamp.valueOf("9999-12-31 00:00:00").getTime(), rs.getTimestamp(3).getTime());
+    assertEquals(
+        253402214400000L,
+        rs.getTimestamp(3, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(Timestamp.valueOf("9999-12-31 00:00:00"), rs.getTimestamp(3));
+    assertNull(rs.getTimestamp(4));
+    assertTrue(rs.wasNull());
+    if (isMariaDBServer()) {
+      rs.next();
+      assertNull(rs.getTimestamp(1));
+    }
+  }
+
+  @Test
+  public void getLocalDateTime() throws SQLException {
+    getLocalDateTime(get());
+  }
+
+  @Test
+  public void getLocalDateTimePrepare() throws SQLException {
+    getLocalDateTime(getPrepare(sharedConn));
+    getLocalDateTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getLocalDateTime(ResultSet rs) throws SQLException {
+    assertEquals(LocalDateTime.parse("2010-01-12T00:00:00"), rs.getObject(1, LocalDateTime.class));
+    assertEquals(
+        LocalDateTime.parse("2010-01-12T00:00:00"), rs.getObject("t1alias", LocalDateTime.class));
+    assertNull(rs.getObject(4, LocalDateTime.class));
+    assertTrue(rs.wasNull());
+    if (isMariaDBServer()) {
+      rs.next();
+      assertNull(rs.getObject(1, LocalDateTime.class));
+    }
+  }
+
+  @Test
+  public void getInstant() throws SQLException {
+    getInstant(get());
+  }
+
+  @Test
+  public void getInstantPrepare() throws SQLException {
+    getInstant(getPrepare(sharedConn));
+    getInstant(getPrepare(sharedConnBinary));
+  }
+
+  public void getInstant(ResultSet rs) throws SQLException {
+    assertFalse(rs.wasNull());
+    assertEquals(
+        ZonedDateTime.of(LocalDateTime.parse("2010-01-12T00:00:00"), ZoneId.systemDefault())
+            .toInstant(),
+        rs.getObject(1, Instant.class));
+    assertFalse(rs.wasNull());
+    assertEquals(
+        ZonedDateTime.of(LocalDateTime.parse("1000-01-01T00:00:00"), ZoneId.systemDefault())
+            .toInstant(),
+        rs.getObject(2, Instant.class));
+    assertFalse(rs.wasNull());
+    assertEquals(
+        ZonedDateTime.of(LocalDateTime.parse("9999-12-31T00:00:00"), ZoneId.systemDefault())
+            .toInstant(),
+        rs.getObject(3, Instant.class));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4, Instant.class));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getOffsetDateTime() throws SQLException {
+    getOffsetDateTime(get());
+  }
+
+  @Test
+  public void getOffsetDateTimePrepare() throws SQLException {
+    getOffsetDateTime(getPrepare(sharedConn));
+    getOffsetDateTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getOffsetDateTime(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(1, OffsetDateTime.class),
+        "cannot be decoded as OffsetDateTime");
+  }
+
+  @Test
+  public void getAsciiStream() throws SQLException {
+    getAsciiStream(get());
+  }
+
+  @Test
+  public void getAsciiStreamPrepare() throws SQLException {
+    getAsciiStream(getPrepare(sharedConn));
+    getAsciiStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getAsciiStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getAsciiStream(1),
+        "Data type DATE cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getUnicodeStream() throws SQLException {
+    getUnicodeStream(get());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepare() throws SQLException {
+    getUnicodeStream(getPrepare(sharedConn));
+    getUnicodeStream(getPrepare(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  public void getUnicodeStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getUnicodeStream(1),
+        "Data type DATE cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBinaryStream() throws SQLException {
+    getBinaryStream(get());
+  }
+
+  @Test
+  public void getBinaryStreamPrepare() throws SQLException {
+    getBinaryStream(getPrepare(sharedConn));
+    getBinaryStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getBinaryStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getBinaryStream(1),
+        "Data type DATE cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBytes() throws SQLException {
+    getBytes(get());
+  }
+
+  @Test
+  public void getBytesPrepare() throws SQLException {
+    getBytes(getPrepare(sharedConn));
+    getBytes(getPrepare(sharedConnBinary));
+  }
+
+  public void getBytes(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getBytes(1), "Data type DATE cannot be decoded as byte[]");
+  }
+
+  @Test
+  public void getCharacterStream() throws SQLException {
+    getCharacterStream(get());
+  }
+
+  @Test
+  public void getCharacterStreamPrepare() throws SQLException {
+    getCharacterStream(getPrepare(sharedConn));
+    getCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type DATE cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getNCharacterStream() throws SQLException {
+    getNCharacterStream(get());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepare() throws SQLException {
+    getNCharacterStream(getPrepare(sharedConn));
+    getNCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getNCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream(1),
+        "Data type DATE cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getBlob() throws SQLException {
+    getBlob(get());
+  }
+
+  @Test
+  public void getBlobPrepare() throws SQLException {
+    getBlob(getPrepare(sharedConn));
+    getBlob(getPrepare(sharedConnBinary));
+  }
+
+  public void getBlob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getBlob(1), "Data type DATE cannot be decoded as Blob");
+  }
+
+  @Test
+  public void getClob() throws SQLException {
+    getClob(get());
+  }
+
+  @Test
+  public void getClobPrepare() throws SQLException {
+    getClob(getPrepare(sharedConn));
+    getClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getClob(1), "Data type DATE cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getNClob() throws SQLException {
+    getNClob(get());
+  }
+
+  @Test
+  public void getNClobPrepare() throws SQLException {
+    getNClob(getPrepare(sharedConn));
+    getNClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getNClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getNClob(1), "Data type DATE cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = get();
+    ResultSetMetaData meta = rs.getMetaData();
+    assertEquals("DATE", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.sql.Date", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.DATE, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(10, meta.getPrecision(1));
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    assertEquals(10, meta.getColumnDisplaySize(1));
+  }
+
+  @Test
+  public void sendParam() throws SQLException {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE DateCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep = con.prepareStatement("INSERT INTO DateCodec2(t1) VALUES (?)")) {
+      prep.setDate(1, Date.valueOf("2010-01-12"));
+      prep.execute();
+      prep.setDate(1, null);
+      prep.execute();
+      prep.setObject(1, Date.valueOf("2010-01-13"));
+      prep.execute();
+      prep.setObject(1, null);
+      prep.execute();
+      prep.setObject(1, Date.valueOf("2010-01-14"), Types.DATE);
+      prep.execute();
+      prep.setObject(1, null, Types.DATE);
+      prep.execute();
+      prep.setObject(1, LocalDate.parse("9999-12-31"), Types.DATE);
+      prep.execute();
+      prep.setDate(1, Date.valueOf("2010-01-12"), Calendar.getInstance());
+      prep.execute();
+      prep.setObject(1, new java.util.Date(Date.valueOf("2010-12-13").getTime()));
+      prep.execute();
+    }
+
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM DateCodec2");
+    assertTrue(rs.next());
+    assertEquals(Date.valueOf("2010-01-12"), rs.getDate(2));
+    rs.updateDate(2, Date.valueOf("2021-01-12"));
+    rs.updateRow();
+    assertEquals(Date.valueOf("2021-01-12"), rs.getDate(2));
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    rs.updateDate("t1", Date.valueOf("2021-01-15"));
+    rs.updateRow();
+    assertEquals(Date.valueOf("2021-01-15"), rs.getDate(2));
+    assertTrue(rs.next());
+    assertEquals(Date.valueOf("2010-01-13"), rs.getDate(2));
+    rs.updateDate(2, null);
+    rs.updateRow();
+    assertNull(rs.getString(2));
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    rs.updateObject(2, Date.valueOf("2021-01-14"), Types.DATE);
+    rs.updateRow();
+    assertEquals(Date.valueOf("2021-01-14"), rs.getDate(2));
+    assertTrue(rs.next());
+    assertEquals(Date.valueOf("2010-01-14"), rs.getDate(2));
+    rs.updateObject(2, null, Types.DATE);
+    rs.updateRow();
+    assertNull(rs.getString(2));
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    rs.updateObject(2, LocalDate.parse("9999-12-31"), Types.DATE);
+    rs.updateRow();
+    assertEquals(Date.valueOf("9999-12-31"), rs.getDate(2));
+    assertTrue(rs.next());
+    assertEquals(Date.valueOf("9999-12-31"), rs.getDate(2));
+    assertTrue(rs.next());
+    assertEquals(Date.valueOf("2010-01-12"), rs.getDate(2));
+    assertTrue(rs.next());
+    assertEquals(Date.valueOf("2010-12-13"), rs.getDate(2));
+    rs.updateObject(2, new java.util.Date(Date.valueOf("2010-12-31").getTime()), Types.DATE);
+    rs.updateRow();
+
+    rs = stmt.executeQuery("SELECT * FROM DateCodec2");
+    assertTrue(rs.next());
+    assertEquals(Date.valueOf("2021-01-12"), rs.getDate(2));
+    assertTrue(rs.next());
+    assertEquals(Date.valueOf("2021-01-15"), rs.getDate(2));
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals(Date.valueOf("2021-01-14"), rs.getDate(2));
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals(Date.valueOf("9999-12-31"), rs.getDate(2));
+    assertTrue(rs.next());
+    assertEquals(Date.valueOf("9999-12-31"), rs.getDate(2));
+    assertTrue(rs.next());
+    assertEquals(Date.valueOf("2010-01-12"), rs.getDate(2));
+    assertTrue(rs.next());
+    assertEquals(Date.valueOf("2010-12-31"), rs.getDate(2));
+    con.commit();
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/DateTimeCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/DateTimeCodecTest.java
new file mode 100644
index 0000000..1106fdc
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/DateTimeCodecTest.java
@@ -0,0 +1,1274 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import java.time.*;
+import java.util.Calendar;
+import java.util.TimeZone;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.integration.Common;
+
+public class DateTimeCodecTest extends CommonCodecTest {
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS DateTimeCodec");
+    stmt.execute("DROP TABLE IF EXISTS DateTimeCodec2");
+    stmt.execute("DROP TABLE IF EXISTS DateTimeCodec3");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE DateTimeCodec (t1 DATETIME , t2 DATETIME(6), t3 DATETIME(6), t4"
+            + " DATETIME(6))");
+    stmt.execute(
+        "INSERT INTO DateTimeCodec VALUES ('2010-01-12 01:55:12', '1000-01-01 01:55:13.212345',"
+            + " '9999-12-31 18:30:12.55', null)"
+            + (isMariaDBServer()
+                ? ",('0000-00-00 00:00:00', '0000-00-00 00:00:00', '9999-12-31 00:00:00.00', null)"
+                : ""));
+    stmt.execute(
+        "CREATE TABLE DateTimeCodec2 (id int not null primary key auto_increment, t1 DATETIME(6))");
+    stmt.execute(
+        "CREATE TABLE DateTimeCodec3 (id int not null primary key auto_increment, t1 DATETIME(6))");
+
+    stmt.execute("FLUSH TABLES");
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from DateTimeCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getPrepare(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement prepStmt =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from DateTimeCodec"
+                + " WHERE 1 > ?");
+    prepStmt.closeOnCompletion();
+    prepStmt.setInt(1, 0);
+    ResultSet rs = prepStmt.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws SQLException {
+    getObject(get());
+  }
+
+  @Test
+  public void getObjectPrepare() throws SQLException {
+    getObject(getPrepare(sharedConn));
+    getObject(getPrepare(sharedConnBinary));
+  }
+
+  public void getObject(ResultSet rs) throws SQLException {
+    assertFalse(rs.wasNull());
+    assertEquals(
+        Timestamp.valueOf("2010-01-12 01:55:12").getTime(),
+        ((Timestamp) rs.getObject(1)).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(
+        Timestamp.valueOf("1000-01-01 01:55:13.212345").getTime(),
+        ((Timestamp) rs.getObject(2)).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(
+        Timestamp.valueOf("9999-12-31 18:30:12.55").getTime(),
+        ((Timestamp) rs.getObject(3)).getTime());
+    assertFalse(rs.wasNull());
+    assertNull(rs.getDate(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testErrObject(rs, Integer.class);
+    testObject(rs, String.class, "2010-01-12 01:55:12");
+    testErrObject(rs, Long.class);
+    testErrObject(rs, Short.class);
+    testErrObject(rs, BigDecimal.class);
+    testErrObject(rs, BigInteger.class);
+    testErrObject(rs, Double.class);
+    testErrObject(rs, Float.class);
+    testErrObject(rs, Byte.class);
+    testErrObject(rs, byte[].class);
+    testErrObject(rs, Boolean.class);
+    testErrObject(rs, Clob.class);
+    testErrObject(rs, NClob.class);
+    testErrObject(rs, InputStream.class);
+    testErrObject(rs, Reader.class);
+    testObject(rs, LocalDate.class, LocalDate.parse("2010-01-12"));
+    testObject(rs, LocalDateTime.class, LocalDateTime.parse("2010-01-12T01:55:12"));
+    // get OffsetDateTime for "2010-01-12T01:55:12" corresponding with current zone id:
+    OffsetDateTime expOffsetDateTime =
+        OffsetDateTime.ofInstant(
+            Timestamp.valueOf("2010-01-12 01:55:12").toInstant(), ZoneId.systemDefault());
+    testObject(rs, OffsetDateTime.class, expOffsetDateTime);
+    testObject(
+        rs,
+        Instant.class,
+        ZonedDateTime.of(LocalDateTime.parse("2010-01-12T01:55:12"), ZoneId.systemDefault())
+            .toInstant());
+    testObject(rs, LocalTime.class, LocalTime.parse("01:55:12"));
+    testObject(rs, Time.class, Time.valueOf("01:55:12"));
+    testObject(rs, Timestamp.class, Timestamp.valueOf("2010-01-12 01:55:12"));
+    testObject(
+        rs,
+        ZonedDateTime.class,
+        LocalDateTime.parse("2010-01-12T01:55:12").atZone(ZoneId.systemDefault()));
+    testObject(rs, java.util.Date.class, Date.valueOf("2010-01-12"));
+  }
+
+  @Test
+  public void getString() throws SQLException {
+    getString(get(), true);
+  }
+
+  @Test
+  public void getStringPrepare() throws SQLException {
+    getString(getPrepare(sharedConn), true);
+    getString(getPrepare(sharedConnBinary), false);
+  }
+
+  public void getString(ResultSet rs, boolean text) throws SQLException {
+    assertEquals("2010-01-12 01:55:12", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1000-01-01 01:55:13.212345", rs.getString(2));
+    assertEquals("1000-01-01 01:55:13.212345", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("9999-12-31 18:30:12.550000", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+    if (isMariaDBServer()) {
+      rs.next();
+      assertEquals("0000-00-00 00:00:00", rs.getString(1));
+      if (isXpand() && !text) {
+        // https://jira.mariadb.org/browse/XPT-273
+        assertEquals("0000-00-00 00:00:00", rs.getString(2));
+        assertEquals("9999-12-31 00:00:00", rs.getString(3));
+      } else {
+        assertEquals("0000-00-00 00:00:00.000000", rs.getString(2));
+        assertEquals("9999-12-31 00:00:00.000000", rs.getString(3));
+      }
+    }
+  }
+
+  @Test
+  public void getNString() throws SQLException {
+    getNString(get());
+  }
+
+  @Test
+  public void getNStringPrepare() throws SQLException {
+    getNString(getPrepare(sharedConn));
+    getNString(getPrepare(sharedConnBinary));
+  }
+
+  public void getNString(ResultSet rs) throws SQLException {
+    assertEquals("2010-01-12 01:55:12", rs.getNString(1));
+    assertFalse(rs.wasNull());
+    String s = rs.getNString(2);
+    assertTrue(s.equals("1000-01-01 01:55:13.212345"));
+    s = rs.getNString("t2alias");
+    assertTrue(s.equals("1000-01-01 01:55:13.212345"));
+    assertFalse(rs.wasNull());
+    s = rs.getNString(3);
+    assertTrue(s.equals("9999-12-31 18:30:12.550000") || s.equals("9999-12-31 18:30:12.550"));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBoolean() throws SQLException {
+    getBoolean(get());
+  }
+
+  @Test
+  public void getBooleanPrepare() throws SQLException {
+    getBoolean(getPrepare(sharedConn));
+    getBoolean(getPrepare(sharedConnBinary));
+  }
+
+  public void getBoolean(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getBoolean(1),
+        "Data type DATETIME cannot be decoded as Boolean");
+  }
+
+  @Test
+  public void getByte() throws SQLException {
+    getByte(get());
+  }
+
+  @Test
+  public void getBytePrepare() throws SQLException {
+    getByte(getPrepare(sharedConn));
+    getByte(getPrepare(sharedConnBinary));
+  }
+
+  public void getByte(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getByte(1), "Data type DATETIME cannot be decoded as Byte");
+  }
+
+  @Test
+  public void getShort() throws SQLException {
+    getShort(get());
+  }
+
+  @Test
+  public void getShortPrepare() throws SQLException {
+    getShort(getPrepare(sharedConn));
+    getShort(getPrepare(sharedConnBinary));
+  }
+
+  public void getShort(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getShort(1), "Data type DATETIME cannot be decoded as Short");
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(get());
+  }
+
+  @Test
+  public void getIntPrepare() throws SQLException {
+    getInt(getPrepare(sharedConn));
+    getInt(getPrepare(sharedConnBinary));
+  }
+
+  public void getInt(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getInt(1), "Data type DATETIME cannot be decoded as Integer");
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(get());
+  }
+
+  @Test
+  public void getLongPrepare() throws SQLException {
+    getLong(getPrepare(sharedConn));
+    getLong(getPrepare(sharedConnBinary));
+  }
+
+  public void getLong(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getLong(1), "Data type DATETIME cannot be decoded as Long");
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(get());
+  }
+
+  @Test
+  public void getFloatPrepare() throws SQLException {
+    getFloat(getPrepare(sharedConn));
+    getFloat(getPrepare(sharedConnBinary));
+  }
+
+  public void getFloat(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getFloat(1), "Data type DATETIME cannot be decoded as Float");
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(get());
+  }
+
+  @Test
+  public void getDoublePrepare() throws SQLException {
+    getDouble(getPrepare(sharedConn));
+    getDouble(getPrepare(sharedConnBinary));
+  }
+
+  public void getDouble(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getDouble(1),
+        "Data type DATETIME cannot be decoded as Double");
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(get());
+  }
+
+  @Test
+  public void getBigDecimalPrepare() throws SQLException {
+    getBigDecimal(getPrepare(sharedConn));
+    getBigDecimal(getPrepare(sharedConnBinary));
+  }
+
+  public void getBigDecimal(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getBigDecimal(1),
+        "Data type DATETIME cannot be decoded as BigDecimal");
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(get());
+  }
+
+  @Test
+  public void getDatePrepare() throws SQLException {
+    getDate(getPrepare(sharedConn));
+    getDate(getPrepare(sharedConnBinary));
+  }
+
+  public void getDate(ResultSet rs) throws SQLException {
+    assertEquals(
+        1263254400000L
+            - TimeZone.getDefault().getOffset(Timestamp.valueOf("2010-01-12 01:55:12").getTime()),
+        rs.getDate(1, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(
+        1263254400000L
+            - TimeZone.getDefault()
+                .getOffset(Timestamp.valueOf("2010-01-12 01:55:12.123456").getTime()),
+        rs.getDate(1).getTime());
+    assertFalse(rs.wasNull());
+
+    assertEquals(
+        -30609792000000L
+            - TimeZone.getDefault().getOffset(Timestamp.valueOf("1000-01-01 01:55:13").getTime()),
+        rs.getDate(2, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(
+        -30609792000000L
+            - TimeZone.getDefault().getOffset(Timestamp.valueOf("1000-01-01 01:55:13").getTime()),
+        rs.getDate(2).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(
+        253402214400000L
+            - TimeZone.getDefault().getOffset(Timestamp.valueOf("9999-12-31 18:30:12").getTime()),
+        rs.getDate(3).getTime());
+    assertFalse(rs.wasNull());
+    assertNull(rs.getDate(4));
+    assertTrue(rs.wasNull());
+    if (isMariaDBServer()) {
+      rs.next();
+      assertNull(rs.getTime(1));
+      assertNull(rs.getTime(2));
+    }
+  }
+
+  @Test
+  public void getDateTimezoneTest() throws SQLException {
+    TimeZone initialTz = Calendar.getInstance().getTimeZone();
+
+    TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
+    try (Connection conGmt8 = createCon("timezone=auto")) {
+      getDateTimezoneTestGmt8(conGmt8, getPrepare(conGmt8), TimeZone.getTimeZone("GMT+8"));
+      TimeZone.setDefault(TimeZone.getTimeZone("GMT-8"));
+      try (Connection conGmtm8 = createCon("timezone=auto")) {
+        getDateTimezoneTestGmtm8(conGmtm8, getPrepare(conGmtm8), TimeZone.getTimeZone("GMT-8"));
+      }
+      TimeZone.setDefault(initialTz);
+      try (Connection conAuto = createCon("timezone=auto")) {
+        getDateTimezoneTestNormal(conAuto, getPrepare(conAuto));
+      }
+
+    } finally {
+      TimeZone.setDefault(initialTz);
+    }
+  }
+
+  public void getDateTimezoneTestGmt8(Connection conGmt8, ResultSet rs, TimeZone tz)
+      throws SQLException {
+
+    assertEquals("2010-01-12T01:55:12+08:00", rs.getObject(1, OffsetDateTime.class).toString());
+
+    conGmt8.createStatement().execute("TRUNCATE TABLE DateTimeCodec3");
+    try (PreparedStatement prep =
+        conGmt8.prepareStatement("INSERT INTO DateTimeCodec3 values (?,?)")) {
+      prep.setInt(1, -2);
+      prep.setString(2, "2010-01-12 01:55:12");
+      prep.execute();
+
+      prep.setInt(1, 1);
+      prep.setObject(2, OffsetDateTime.parse("2010-01-12T01:55:12+08:00"));
+      prep.execute();
+
+      prep.setInt(1, 2);
+      prep.setObject(2, OffsetDateTime.parse("2010-01-12T01:55:12+01:00"));
+      prep.execute();
+
+      prep.setInt(1, 3);
+      prep.setObject(2, OffsetDateTime.parse("2010-01-12T01:55:12Z"));
+      prep.execute();
+
+      prep.setInt(1, 4);
+      prep.setObject(2, OffsetDateTime.parse("2010-01-12T17:55:12-04:00"));
+      prep.execute();
+    }
+    conGmt8.commit();
+
+    java.sql.Statement stmt = conGmt8.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prepStmt = conGmt8.prepareStatement("select * from DateTimeCodec3")) {
+      rs = prepStmt.executeQuery();
+      rs.next();
+      assertEquals("2010-01-12T01:55:12+08:00", rs.getObject(2, OffsetDateTime.class).toString());
+      assertEquals("2010-01-12 01:55:12.000000", rs.getString(2));
+
+      rs.next();
+      assertEquals("2010-01-12T01:55:12+08:00", rs.getObject(2, OffsetDateTime.class).toString());
+      assertEquals("2010-01-12 01:55:12.0", rs.getTimestamp(2).toString());
+      assertEquals(1263232512000L, rs.getTimestamp(2).getTime());
+      assertEquals(
+          "2010-01-12 09:55:12.0",
+          rs.getTimestamp(2, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).toString());
+      assertEquals("2010-01-12 01:55:12.000000", rs.getString(2));
+      assertEquals("2010-01-12", rs.getDate(2).toString());
+      assertEquals(
+          "2010-01-12",
+          rs.getDate(2, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).toString());
+      assertEquals("2010-01-12T01:55:12", rs.getObject(2, LocalDateTime.class).toString());
+
+      rs.next();
+      assertEquals("2010-01-12T08:55:12+08:00", rs.getObject(2, OffsetDateTime.class).toString());
+      assertEquals("2010-01-12 08:55:12.0", rs.getTimestamp(2).toString());
+      assertEquals(1263257712000L, rs.getTimestamp(2).getTime());
+      assertEquals("2010-01-12 08:55:12.000000", rs.getString(2));
+      assertEquals("2010-01-12", rs.getDate(2).toString());
+
+      rs.next();
+      assertEquals("2010-01-12T09:55:12+08:00", rs.getObject(2, OffsetDateTime.class).toString());
+      assertEquals("2010-01-12 09:55:12.0", rs.getTimestamp(2).toString());
+      assertEquals(1263261312000L, rs.getTimestamp(2).getTime());
+      assertEquals("2010-01-12 09:55:12.000000", rs.getString(2));
+      assertEquals("2010-01-12", rs.getDate(2).toString());
+
+      rs.next();
+      assertEquals("2010-01-13T05:55:12+08:00", rs.getObject(2, OffsetDateTime.class).toString());
+      assertEquals("2010-01-13 05:55:12.0", rs.getTimestamp(2).toString());
+      assertEquals(1263333312000L, rs.getTimestamp(2).getTime());
+      assertEquals("2010-01-13 05:55:12.000000", rs.getString(2));
+      assertEquals("2010-01-13", rs.getDate(2).toString());
+    }
+    conGmt8.rollback();
+  }
+
+  public void getDateTimezoneTestGmtm8(Connection conGmt8, ResultSet rs, TimeZone tz)
+      throws SQLException {
+
+    assertEquals("2010-01-12T01:55:12-08:00", rs.getObject(1, OffsetDateTime.class).toString());
+
+    conGmt8.createStatement().execute("TRUNCATE TABLE DateTimeCodec3");
+    try (PreparedStatement prep =
+        conGmt8.prepareStatement("INSERT INTO DateTimeCodec3 values (?,?)")) {
+      prep.setInt(1, -2);
+      prep.setString(2, "2010-01-12 01:55:12");
+      prep.execute();
+
+      prep.setInt(1, 1);
+      prep.setObject(2, OffsetDateTime.parse("2010-01-12T01:55:12-08:00"));
+      prep.execute();
+
+      prep.setInt(1, 2);
+      prep.setObject(2, OffsetDateTime.parse("2010-01-12T01:55:12-01:00"));
+      prep.execute();
+
+      prep.setInt(1, 3);
+      prep.setObject(2, OffsetDateTime.parse("2010-01-12T01:55:12Z"));
+      prep.execute();
+
+      prep.setInt(1, 4);
+      prep.setObject(2, OffsetDateTime.parse("2010-01-12T17:55:12+04:00"));
+      prep.execute();
+    }
+    conGmt8.commit();
+
+    java.sql.Statement stmt = conGmt8.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prepStmt = conGmt8.prepareStatement("select * from DateTimeCodec3")) {
+      rs = prepStmt.executeQuery();
+      rs.next();
+      assertEquals("2010-01-12T01:55:12-08:00", rs.getObject(2, OffsetDateTime.class).toString());
+      assertEquals("2010-01-12 01:55:12.000000", rs.getString(2));
+
+      rs.next();
+      assertEquals("2010-01-12T01:55:12-08:00", rs.getObject(2, OffsetDateTime.class).toString());
+      assertEquals("2010-01-12 01:55:12.0", rs.getTimestamp(2).toString());
+      assertEquals(1263290112000L, rs.getTimestamp(2).getTime());
+      assertEquals(
+          "2010-01-11 17:55:12.0",
+          rs.getTimestamp(2, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).toString());
+      assertEquals("2010-01-12 01:55:12.000000", rs.getString(2));
+      assertEquals("2010-01-12", rs.getDate(2).toString());
+      assertEquals(
+          "2010-01-11",
+          rs.getDate(2, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).toString());
+      assertEquals("2010-01-12T01:55:12", rs.getObject(2, LocalDateTime.class).toString());
+
+      rs.next();
+      assertEquals("2010-01-11T18:55:12-08:00", rs.getObject(2, OffsetDateTime.class).toString());
+      assertEquals("2010-01-11 18:55:12.0", rs.getTimestamp(2).toString());
+      assertEquals(1263264912000L, rs.getTimestamp(2).getTime());
+      assertEquals("2010-01-11 18:55:12.000000", rs.getString(2));
+      assertEquals("2010-01-11", rs.getDate(2).toString());
+
+      rs.next();
+      assertEquals("2010-01-11T17:55:12-08:00", rs.getObject(2, OffsetDateTime.class).toString());
+      assertEquals("2010-01-11 17:55:12.0", rs.getTimestamp(2).toString());
+      assertEquals(1263261312000L, rs.getTimestamp(2).getTime());
+      assertEquals("2010-01-11 17:55:12.000000", rs.getString(2));
+      assertEquals("2010-01-11", rs.getDate(2).toString());
+
+      rs.next();
+      assertEquals("2010-01-12T05:55:12-08:00", rs.getObject(2, OffsetDateTime.class).toString());
+      assertEquals("2010-01-12 05:55:12.0", rs.getTimestamp(2).toString());
+      assertEquals(1263304512000L, rs.getTimestamp(2).getTime());
+      assertEquals("2010-01-12 05:55:12.000000", rs.getString(2));
+      assertEquals("2010-01-12", rs.getDate(2).toString());
+    }
+    conGmt8.rollback();
+  }
+
+  public void getDateTimezoneTestNormal(Connection conAuto, ResultSet rs) throws SQLException {
+
+    assertEquals("2010-01-12 01:55:12.0", rs.getObject(1, Timestamp.class).toString());
+
+    conAuto.createStatement().execute("TRUNCATE TABLE DateTimeCodec3");
+    try (PreparedStatement prep =
+        conAuto.prepareStatement("INSERT INTO DateTimeCodec3 values (?,?)")) {
+      prep.setInt(1, 5);
+      prep.setString(2, "2010-01-12 01:55:12");
+      prep.execute();
+
+      prep.setInt(1, 6);
+      prep.setObject(2, "2010-01-12 11:55:12");
+      prep.execute();
+    }
+    conAuto.commit();
+
+    java.sql.Statement stmt = conAuto.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prepStmt =
+        conAuto.prepareStatement("select * from DateTimeCodec3 order by id")) {
+      rs = prepStmt.executeQuery();
+      rs.next();
+      assertEquals(5, rs.getInt(1));
+      assertEquals("2010-01-12T01:55:12", rs.getObject(2, LocalDateTime.class).toString());
+      assertEquals("2010-01-12 01:55:12.000000", rs.getString(2));
+
+      rs.next();
+
+      Timestamp tt = Timestamp.valueOf("2010-01-12 01:55:12");
+      int offset = TimeZone.getDefault().getOffset(tt.getTime());
+      int offsetHour = offset / (3_600_000);
+      if (offsetHour < 0) offsetHour = offsetHour * -1;
+
+      // test might fail if run in timezone with offset not rounded to hours
+      if (offsetHour == 0) {
+        assertEquals("2010-01-12T11:55:12Z", rs.getObject(2, OffsetDateTime.class).toString());
+      } else {
+        assertEquals(
+            "2010-01-12T11:55:12"
+                + ((offset < 0) ? "-" : "+")
+                + ((offsetHour < 10) ? "0" : offsetHour / 10)
+                + (offsetHour % 10)
+                + ":00",
+            rs.getObject(2, OffsetDateTime.class).toString());
+      }
+      assertEquals("2010-01-12 11:55:12.0", rs.getTimestamp(2).toString());
+    }
+    conAuto.rollback();
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(get());
+  }
+
+  @Test
+  public void getTimePrepare() throws SQLException {
+    getTime(getPrepare(sharedConn));
+    getTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getTime(ResultSet rs) throws SQLException {
+    assertEquals(
+        6912000, rs.getTime(1, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+
+    assertEquals(Time.valueOf("01:55:12").getTime(), rs.getTime(1).getTime());
+    assertFalse(rs.wasNull());
+
+    assertEquals(
+        6913212, rs.getTime(2, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(Time.valueOf("01:55:13").getTime() + 212, rs.getTime(2).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(Time.valueOf("18:30:12").getTime() + 550, rs.getTime(3).getTime());
+    assertFalse(rs.wasNull());
+    assertNull(rs.getTime(4));
+    assertTrue(rs.wasNull());
+    if (isMariaDBServer()) {
+      rs.next();
+      assertNull(rs.getTime(1));
+    }
+  }
+
+  @Test
+  public void getDuration() throws SQLException {
+    getDuration(get());
+  }
+
+  @Test
+  public void getDurationPrepare() throws SQLException {
+    getDuration(getPrepare(sharedConn));
+    getDuration(getPrepare(sharedConnBinary));
+  }
+
+  public void getDuration(ResultSet rs) throws SQLException {
+    assertEquals(Duration.parse("PT265H55M12S"), rs.getObject(1, Duration.class));
+    assertEquals(Duration.parse("PT1H55M13.212345S"), rs.getObject(2, Duration.class));
+    assertNull(rs.getObject(4, Duration.class));
+    if (isMariaDBServer()) {
+      rs.next();
+      assertNull(rs.getObject(1, Duration.class));
+    }
+  }
+
+  @Test
+  public void getLocalTime() throws SQLException {
+    getLocalTime(get());
+  }
+
+  @Test
+  public void getLocalTimePrepare() throws SQLException {
+    getLocalTime(getPrepare(sharedConn));
+    getLocalTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getLocalTime(ResultSet rs) throws SQLException {
+    assertEquals(LocalTime.parse("01:55:12"), rs.getObject(1, LocalTime.class));
+    assertFalse(rs.wasNull());
+    assertEquals(LocalTime.parse("01:55:13.212345"), rs.getObject(2, LocalTime.class));
+    assertFalse(rs.wasNull());
+    assertEquals(LocalTime.parse("18:30:12.55"), rs.getObject(3, LocalTime.class));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4, LocalTime.class));
+    assertTrue(rs.wasNull());
+    if (isMariaDBServer()) {
+      rs.next();
+      assertNull(rs.getTime(1));
+    }
+  }
+
+  @Test
+  public void getLocalDate() throws SQLException {
+    getLocalDate(get());
+  }
+
+  @Test
+  public void getLocalDatePrepare() throws SQLException {
+    getLocalDate(getPrepare(sharedConn));
+    getLocalDate(getPrepare(sharedConnBinary));
+  }
+
+  public void getLocalDate(ResultSet rs) throws SQLException {
+    assertEquals(LocalDate.parse("2010-01-12"), rs.getObject(1, LocalDate.class));
+    assertFalse(rs.wasNull());
+    assertEquals(LocalDate.parse("1000-01-01"), rs.getObject(2, LocalDate.class));
+    assertFalse(rs.wasNull());
+    assertEquals(LocalDate.parse("9999-12-31"), rs.getObject(3, LocalDate.class));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4, LocalTime.class));
+    assertTrue(rs.wasNull());
+    if (isMariaDBServer()) {
+      rs.next();
+      assertNull(rs.getObject(1, LocalDate.class));
+    }
+  }
+
+  @Test
+  public void getTimestamp() throws SQLException {
+    getTimestamp(get());
+  }
+
+  @Test
+  public void getTimestampPrepare() throws SQLException {
+    getTimestamp(getPrepare(sharedConn));
+    getTimestamp(getPrepare(sharedConnBinary));
+  }
+
+  public void getTimestamp(ResultSet rs) throws SQLException {
+    assertFalse(rs.wasNull());
+    assertEquals(Timestamp.valueOf("2010-01-12 01:55:12").getTime(), rs.getTimestamp(1).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(
+        Timestamp.valueOf("1000-01-01 01:55:13.212345").getTime(), rs.getTimestamp(2).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(
+        Timestamp.valueOf("9999-12-31 18:30:12.55").getTime(), rs.getTimestamp(3).getTime());
+    assertFalse(rs.wasNull());
+    assertNull(rs.getTimestamp(4));
+    assertTrue(rs.wasNull());
+    if (isMariaDBServer()) {
+      rs.next();
+      assertNull(rs.getTimestamp(1));
+      assertNull(rs.getTimestamp(2));
+      assertEquals(
+          Timestamp.valueOf("9999-12-31 00:00:00.00").getTime(), rs.getTimestamp(3).getTime());
+    }
+  }
+
+  @Test
+  public void getLocalDateTime() throws SQLException {
+    getLocalDateTime(get());
+  }
+
+  @Test
+  public void getLocalDateTimePrepare() throws SQLException {
+    getLocalDateTime(getPrepare(sharedConn));
+    getLocalDateTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getLocalDateTime(ResultSet rs) throws SQLException {
+    assertFalse(rs.wasNull());
+    assertEquals(LocalDateTime.parse("2010-01-12T01:55:12"), rs.getObject(1, LocalDateTime.class));
+    assertFalse(rs.wasNull());
+    assertEquals(
+        LocalDateTime.parse("1000-01-01T01:55:13.212345"), rs.getObject(2, LocalDateTime.class));
+    assertFalse(rs.wasNull());
+    assertEquals(
+        LocalDateTime.parse("9999-12-31T18:30:12.55"), rs.getObject(3, LocalDateTime.class));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4, LocalDateTime.class));
+    assertTrue(rs.wasNull());
+    if (isMariaDBServer()) {
+      rs.next();
+      assertNull(rs.getTimestamp(1));
+      assertNull(rs.getTimestamp(2));
+      assertEquals(
+          LocalDateTime.parse("9999-12-31T00:00:00.00"), rs.getObject(3, LocalDateTime.class));
+    }
+  }
+
+  @Test
+  public void getInstant() throws SQLException {
+    getInstant(get());
+  }
+
+  @Test
+  public void getInstantPrepare() throws SQLException {
+    getInstant(getPrepare(sharedConn));
+    getInstant(getPrepare(sharedConnBinary));
+  }
+
+  public void getInstant(ResultSet rs) throws SQLException {
+    assertFalse(rs.wasNull());
+    assertEquals(
+        ZonedDateTime.of(LocalDateTime.parse("2010-01-12T01:55:12"), ZoneId.systemDefault())
+            .toInstant(),
+        rs.getObject(1, Instant.class));
+    assertFalse(rs.wasNull());
+    assertEquals(
+        ZonedDateTime.of(LocalDateTime.parse("1000-01-01T01:55:13.212345"), ZoneId.systemDefault())
+            .toInstant(),
+        rs.getObject(2, Instant.class));
+    assertFalse(rs.wasNull());
+    assertEquals(
+        ZonedDateTime.of(LocalDateTime.parse("9999-12-31T18:30:12.55"), ZoneId.systemDefault())
+            .toInstant(),
+        rs.getObject(3, Instant.class));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4, Instant.class));
+    assertTrue(rs.wasNull());
+    if (isMariaDBServer()) {
+      rs.next();
+      assertNull(rs.getTimestamp(1));
+      assertNull(rs.getTimestamp(2));
+      assertEquals(
+          ZonedDateTime.of(LocalDateTime.parse("9999-12-31T00:00:00.00"), ZoneId.systemDefault())
+              .toInstant(),
+          rs.getObject(3, Instant.class));
+    }
+  }
+
+  @Test
+  public void getOffsetDateTime() throws SQLException {
+    getOffsetDateTime(get());
+  }
+
+  @Test
+  public void getOffsetDateTimePrepare() throws SQLException {
+    getOffsetDateTime(getPrepare(sharedConn));
+    getOffsetDateTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getOffsetDateTime(ResultSet rs) throws SQLException {
+    assertFalse(rs.wasNull());
+    assertEquals(
+        OffsetDateTime.ofInstant(
+            Timestamp.valueOf("2010-01-12 01:55:12").toInstant(), ZoneId.systemDefault()),
+        rs.getObject(1, OffsetDateTime.class));
+    assertFalse(rs.wasNull());
+
+    LocalDateTime l = LocalDateTime.parse("1000-01-01T01:55:13.212345");
+    assertEquals(
+        OffsetDateTime.of(l, ZoneId.systemDefault().getRules().getOffset(l)),
+        rs.getObject(2, OffsetDateTime.class));
+    assertFalse(rs.wasNull());
+
+    assertEquals(
+        OffsetDateTime.ofInstant(
+            Timestamp.valueOf("9999-12-31 18:30:12.55").toInstant(), ZoneId.systemDefault()),
+        rs.getObject(3, OffsetDateTime.class));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4, OffsetDateTime.class));
+    assertTrue(rs.wasNull());
+    if (isMariaDBServer()) {
+      rs.next();
+      assertNull(rs.getObject(1, OffsetDateTime.class));
+      assertNull(rs.getObject(2, OffsetDateTime.class));
+      assertEquals(
+          OffsetDateTime.ofInstant(
+              Timestamp.valueOf("9999-12-31 00:00:00.00").toInstant(), ZoneId.systemDefault()),
+          rs.getObject(3, OffsetDateTime.class));
+    }
+  }
+
+  @Test
+  public void getAsciiStream() throws SQLException {
+    getAsciiStream(get());
+  }
+
+  @Test
+  public void getAsciiStreamPrepare() throws SQLException {
+    getAsciiStream(getPrepare(sharedConn));
+    getAsciiStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getAsciiStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getAsciiStream(1),
+        "Data type DATETIME cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getUnicodeStream() throws SQLException {
+    getUnicodeStream(get());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepare() throws SQLException {
+    getUnicodeStream(getPrepare(sharedConn));
+    getUnicodeStream(getPrepare(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  public void getUnicodeStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getUnicodeStream(1),
+        "Data type DATETIME cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBinaryStream() throws SQLException {
+    getBinaryStream(get());
+  }
+
+  @Test
+  public void getBinaryStreamPrepare() throws SQLException {
+    getBinaryStream(getPrepare(sharedConn));
+    getBinaryStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getBinaryStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getBinaryStream(1),
+        "Data type DATETIME cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBytes() throws SQLException {
+    getBytes(get());
+  }
+
+  @Test
+  public void getBytesPrepare() throws SQLException {
+    getBytes(getPrepare(sharedConn));
+    getBytes(getPrepare(sharedConnBinary));
+  }
+
+  public void getBytes(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getBytes(1), "Data type DATETIME cannot be decoded as byte[]");
+  }
+
+  @Test
+  public void getCharacterStream() throws SQLException {
+    getCharacterStream(get());
+  }
+
+  @Test
+  public void getCharacterStreamPrepare() throws SQLException {
+    getCharacterStream(getPrepare(sharedConn));
+    getCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type DATETIME cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getNCharacterStream() throws SQLException {
+    getNCharacterStream(get());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepare() throws SQLException {
+    getNCharacterStream(getPrepare(sharedConn));
+    getNCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getNCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream(1),
+        "Data type DATETIME cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getBlob() throws SQLException {
+    getBlob(get());
+  }
+
+  @Test
+  public void getBlobPrepare() throws SQLException {
+    getBlob(getPrepare(sharedConn));
+    getBlob(getPrepare(sharedConnBinary));
+  }
+
+  public void getBlob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getBlob(1), "Data type DATETIME cannot be decoded as Blob");
+  }
+
+  @Test
+  public void getBigInteger() throws SQLException {
+    getBigInteger(get());
+  }
+
+  @Test
+  public void getBigIntegerPrepared() throws SQLException {
+    getBigInteger(getPrepare(sharedConn));
+    getBigInteger(getPrepare(sharedConnBinary));
+  }
+
+  private void getBigInteger(ResultSet res) {
+    CompleteResult rs = (CompleteResult) res;
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBigInteger(1),
+        "Data type DATETIME cannot be decoded as BigInteger");
+  }
+
+  @Test
+  public void getClob() throws SQLException {
+    getClob(get());
+  }
+
+  @Test
+  public void getClobPrepare() throws SQLException {
+    getClob(getPrepare(sharedConn));
+    getClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getClob(1), "Data type DATETIME cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getNClob() throws SQLException {
+    getNClob(get());
+  }
+
+  @Test
+  public void getNClobPrepare() throws SQLException {
+    getNClob(getPrepare(sharedConn));
+    getNClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getNClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getNClob(1), "Data type DATETIME cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = get();
+    ResultSetMetaData meta = rs.getMetaData();
+    assertEquals("DATETIME", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.sql.Timestamp", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.TIMESTAMP, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    // https://jira.mariadb.org/browse/XPT-273
+    if (!isXpand()) {
+      assertEquals(19, meta.getPrecision(1));
+      assertEquals(19, meta.getColumnDisplaySize(1));
+    }
+  }
+
+  @Test
+  public void sendParam() throws SQLException {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE DateTimeCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    LocalDateTime ldtNow = LocalDateTime.parse("2022-04-15T19:49:41.398057");
+    OffsetDateTime offsetDtUtc =
+        OffsetDateTime.of(ldtNow, ZoneId.of("UTC").getRules().getOffset(ldtNow));
+    OffsetDateTime offsetDtCurrent =
+        OffsetDateTime.of(ldtNow, ZoneId.systemDefault().getRules().getOffset(ldtNow));
+
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO DateTimeCodec2(t1) VALUES (?)")) {
+      prep.setDate(1, Date.valueOf("2010-01-12"));
+      prep.execute();
+      prep.setDate(1, null);
+      prep.execute();
+      prep.setObject(1, Date.valueOf("2010-01-13"));
+      prep.execute();
+      prep.setObject(1, null);
+      prep.execute();
+      prep.setObject(1, Date.valueOf("2010-01-14"), Types.DATE);
+      prep.execute();
+      prep.setObject(1, null, Types.DATE);
+      prep.execute();
+      prep.setObject(1, LocalDateTime.parse("2010-01-12T01:55:12.987765"), Types.TIMESTAMP);
+      prep.execute();
+      prep.setObject(1, "2010-01-12 01:55:12.987765", Types.TIMESTAMP);
+      prep.execute();
+      prep.setObject(1, "0000-00-00 00:00:00", Types.TIMESTAMP);
+      prep.execute();
+      prep.setObject(1, LocalDateTime.parse("2010-01-12T01:56:12.456"), Types.TIMESTAMP);
+      prep.execute();
+      prep.setObject(
+          1,
+          LocalDateTime.parse("2011-01-12T01:55:12").atZone(ZoneId.systemDefault()),
+          Types.TIMESTAMP);
+      prep.execute();
+      prep.setObject(
+          1,
+          LocalDateTime.parse("2011-01-12T01:55:12.456").atZone(ZoneId.systemDefault()),
+          Types.TIMESTAMP);
+      prep.execute();
+      prep.setObject(
+          1, LocalDateTime.parse("2012-01-12T01:55:12").atZone(ZoneId.of("UTC")), Types.TIMESTAMP);
+      prep.execute();
+      prep.setObject(
+          1,
+          LocalDateTime.parse("2012-01-12T01:55:12.456").atZone(ZoneId.of("UTC")),
+          Types.TIMESTAMP);
+      prep.execute();
+      prep.setTimestamp(1, Timestamp.valueOf("2015-12-12 01:55:12"));
+      prep.execute();
+      prep.setTimestamp(1, Timestamp.valueOf("2015-12-12 01:55:12.654"));
+      prep.execute();
+      prep.setObject(1, Timestamp.valueOf("2016-12-12 01:55:12"));
+      prep.execute();
+      prep.setObject(1, Timestamp.valueOf("2016-12-12 01:55:12.654"));
+      prep.execute();
+      prep.setObject(1, Instant.ofEpochSecond(10, 654000));
+      prep.execute();
+      prep.setObject(1, Instant.ofEpochSecond(12));
+      prep.execute();
+      prep.setObject(1, offsetDtUtc);
+      prep.execute();
+      prep.setObject(1, offsetDtCurrent);
+      prep.execute();
+      assertThrowsContains(
+          SQLException.class,
+          () -> prep.setObject(1, "2010-aaa", Types.TIMESTAMP),
+          "Could not convert [2010-aaa] to java.sql.Type 93");
+    }
+
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM DateTimeCodec2");
+    assertTrue(rs.next());
+    assertEquals(Date.valueOf("2010-01-12"), rs.getDate(2));
+    rs.updateDate(2, null);
+    rs.updateRow();
+    assertNull(rs.getDate(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    rs.updateDate(2, Date.valueOf("2011-01-12"));
+    rs.updateRow();
+    assertEquals(Date.valueOf("2011-01-12"), rs.getDate(2));
+
+    assertTrue(rs.next());
+    assertEquals(Date.valueOf("2010-01-13"), rs.getDate(2));
+    rs.updateObject(2, null);
+    rs.updateRow();
+    assertNull(rs.getDate(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    rs.updateObject(2, Date.valueOf("2021-01-12"));
+    rs.updateRow();
+    assertEquals(Date.valueOf("2021-01-12"), rs.getDate(2));
+
+    assertTrue(rs.next());
+    assertEquals(Date.valueOf("2010-01-14"), rs.getDate(2));
+    rs.updateObject(2, LocalDateTime.parse("2021-01-12T01:55:12.347654"), Types.TIMESTAMP);
+    rs.updateRow();
+    assertEquals(
+        LocalDateTime.parse("2021-01-12T01:55:12.347654"), rs.getObject(2, LocalDateTime.class));
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    rs.updateTimestamp(2, Timestamp.valueOf("2015-12-12 01:55:12.654"));
+    rs.updateRow();
+    assertEquals(Timestamp.valueOf("2015-12-12 01:55:12.654"), rs.getTimestamp(2));
+    assertTrue(rs.next());
+    assertEquals(
+        LocalDateTime.parse("2010-01-12T01:55:12.987765"), rs.getObject(2, LocalDateTime.class));
+    rs.updateTimestamp("t1", Timestamp.valueOf("2015-12-12 01:55:12.654321"));
+    rs.updateRow();
+    assertEquals(Timestamp.valueOf("2015-12-12 01:55:12.654321"), rs.getTimestamp(2));
+    assertTrue(rs.next());
+    assertEquals(
+        LocalDateTime.parse("2010-01-12T01:55:12.987765"), rs.getObject(2, LocalDateTime.class));
+    assertTrue(rs.next());
+    assertNull(rs.getObject(2, LocalDateTime.class));
+
+    rs = stmt.executeQuery("SELECT * FROM DateTimeCodec2");
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals(Date.valueOf("2011-01-12"), rs.getDate(2));
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals(Date.valueOf("2021-01-12"), rs.getDate(2));
+    assertTrue(rs.next());
+    assertEquals(
+        LocalDateTime.parse("2021-01-12T01:55:12.347654"), rs.getObject(2, LocalDateTime.class));
+    assertTrue(rs.next());
+    assertEquals(Timestamp.valueOf("2015-12-12 01:55:12.654"), rs.getTimestamp(2));
+
+    assertTrue(rs.next());
+    assertEquals(Timestamp.valueOf("2015-12-12 01:55:12.654321"), rs.getTimestamp(2));
+    assertTrue(rs.next());
+    assertEquals(Timestamp.valueOf("2010-01-12 01:55:12.987765"), rs.getTimestamp(2));
+    assertTrue(rs.next());
+    assertTrue(rs.next());
+    assertEquals(
+        LocalDateTime.parse("2010-01-12T01:56:12.456"), rs.getObject(2, LocalDateTime.class));
+
+    assertTrue(rs.next());
+    assertEquals(LocalDateTime.parse("2011-01-12T01:55:12"), rs.getObject(2, LocalDateTime.class));
+    assertTrue(rs.next());
+    assertEquals(
+        LocalDateTime.parse("2011-01-12T01:55:12.456"), rs.getObject(2, LocalDateTime.class));
+    assertTrue(rs.next());
+    assertEquals(
+        LocalDateTime.parse("2012-01-12T01:55:12").atZone(ZoneId.of("UTC")),
+        rs.getObject(2, ZonedDateTime.class).withZoneSameInstant(ZoneId.of("UTC")));
+    assertTrue(rs.next());
+    assertEquals(
+        LocalDateTime.parse("2012-01-12T01:55:12.456").atZone(ZoneId.of("UTC")),
+        rs.getObject(2, ZonedDateTime.class).withZoneSameInstant(ZoneId.of("UTC")));
+
+    assertTrue(rs.next());
+    assertEquals(Timestamp.valueOf("2015-12-12 01:55:12"), rs.getTimestamp(2));
+    assertTrue(rs.next());
+    assertEquals(Timestamp.valueOf("2015-12-12 01:55:12.654"), rs.getTimestamp(2));
+    assertTrue(rs.next());
+    assertEquals(Timestamp.valueOf("2016-12-12 01:55:12"), rs.getTimestamp(2));
+    assertTrue(rs.next());
+    assertEquals(Timestamp.valueOf("2016-12-12 01:55:12.654"), rs.getTimestamp(2));
+    assertTrue(rs.next());
+    assertEquals(Timestamp.from(Instant.ofEpochSecond(10, 654000)), rs.getTimestamp(2));
+    assertTrue(rs.next());
+    assertEquals(Timestamp.from(Instant.ofEpochSecond(12)), rs.getTimestamp(2));
+    assertTrue(rs.next());
+    assertEquals(
+        ldtNow
+            .atZone(ZoneId.of("UTC"))
+            .withZoneSameInstant(ZoneId.systemDefault())
+            .toOffsetDateTime(),
+        rs.getObject(2, OffsetDateTime.class));
+    assertEquals(
+        ldtNow.atZone(ZoneId.of("UTC")).withZoneSameInstant(ZoneId.systemDefault()),
+        rs.getObject(2, ZonedDateTime.class));
+    assertEquals(
+        ldtNow
+            .atZone(ZoneId.of("UTC"))
+            .withZoneSameInstant(ZoneId.systemDefault())
+            .toLocalDateTime(),
+        rs.getObject(2, LocalDateTime.class));
+    assertTrue(rs.next());
+    assertEquals(offsetDtCurrent, rs.getObject(2, OffsetDateTime.class));
+    assertEquals(ldtNow.atZone(ZoneId.systemDefault()), rs.getObject(2, ZonedDateTime.class));
+    assertEquals(ldtNow, rs.getObject(2, LocalDateTime.class));
+    con.commit();
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/DecimalCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/DecimalCodecTest.java
new file mode 100644
index 0000000..3194995
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/DecimalCodecTest.java
@@ -0,0 +1,921 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import java.time.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.integration.Common;
+
+public class DecimalCodecTest extends CommonCodecTest {
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS DecimalCodec");
+    stmt.execute("DROP TABLE IF EXISTS DecimalCodec2");
+    stmt.execute("DROP TABLE IF EXISTS DecimalCodec3");
+    stmt.execute("DROP TABLE IF EXISTS DecimalCodec4");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE DecimalCodec (t1 DECIMAL(10,0), t2 DECIMAL(30,6), t3 DECIMAL(10,3), t4"
+            + " DECIMAL(10,0))");
+    stmt.execute(
+        "CREATE TABLE DecimalCodec2 (t1 DECIMAL(10,0), t2 DECIMAL(10,6), t3 DECIMAL(10,3), t4"
+            + " DECIMAL(10,0))");
+    stmt.execute(
+        "INSERT INTO DecimalCodec VALUES (0, 105.21, -1.6, null), (0, 9223372036854775808, 0,"
+            + " null)");
+    stmt.execute(
+        "CREATE TABLE DecimalCodec3 (id int not null primary key auto_increment, t1"
+            + " DECIMAL(10,0))");
+    stmt.execute(
+        "CREATE TABLE DecimalCodec4 (t1 DECIMAL(10,0) ZEROFILL, t2 DECIMAL(10,6) ZEROFILL, t3"
+            + " DECIMAL(10,3) ZEROFILL, t4 DECIMAL(10,0) ZEROFILL)");
+    stmt.execute("INSERT INTO DecimalCodec4 VALUES (0, 105.21, 1.6, null)");
+
+    stmt.execute("FLUSH TABLES");
+  }
+
+  private ResultSet get() throws SQLException {
+    return get(false);
+  }
+
+  private ResultSet get(boolean zerofill) throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from DecimalCodec"
+                + (zerofill ? "4" : ""));
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getPrepare(Connection con) throws SQLException {
+    return getPrepare(con, false);
+  }
+
+  private ResultSet getPrepare(Connection con, boolean zerofill) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from DecimalCodec"
+                + (zerofill ? "4" : "")
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    ResultSet rs = preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws SQLException {
+    getObject(get());
+  }
+
+  @Test
+  public void getObjectPrepare() throws SQLException {
+    getObject(getPrepare(sharedConn));
+    getObject(getPrepare(sharedConnBinary));
+  }
+
+  public void getObject(ResultSet rs) throws SQLException {
+    assertEquals(BigDecimal.ZERO, rs.getObject(1));
+    assertFalse(rs.wasNull());
+    assertEquals(new BigDecimal("105.210000"), rs.getObject(2));
+    assertEquals(new BigDecimal("105.210000"), rs.getObject("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(new BigDecimal("-1.600"), rs.getObject(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testObject(rs, Integer.class, 0);
+    testObject(rs, String.class, "0");
+    testObject(rs, Long.class, 0L);
+    testObject(rs, Short.class, (short) 0);
+    testObject(rs, BigDecimal.class, BigDecimal.valueOf(0));
+    testObject(rs, BigInteger.class, BigInteger.valueOf(0));
+    testObject(rs, Double.class, (double) 0);
+    testObject(rs, Float.class, (float) 0);
+    testObject(rs, Byte.class, (byte) 0);
+    testErrObject(rs, byte[].class);
+    testErrObject(rs, Date.class);
+    testErrObject(rs, Time.class);
+    testErrObject(rs, Timestamp.class);
+    testErrObject(rs, java.util.Date.class);
+    testErrObject(rs, LocalDate.class);
+    testErrObject(rs, LocalTime.class);
+    testErrObject(rs, ZonedDateTime.class);
+    testErrObject(rs, OffsetDateTime.class);
+    testErrObject(rs, LocalDateTime.class);
+    testErrObject(rs, OffsetTime.class);
+    testObject(rs, Boolean.class, Boolean.FALSE);
+    testErrObject(rs, Clob.class);
+    testErrObject(rs, NClob.class);
+    testErrObject(rs, InputStream.class);
+    testErrObject(rs, Reader.class);
+  }
+
+  @Test
+  public void getString() throws SQLException {
+    getString(get());
+    getStringZerofill(get(true));
+  }
+
+  @Test
+  public void getStringPrepare() throws SQLException {
+    getString(getPrepare(sharedConn));
+    getString(getPrepare(sharedConnBinary));
+    // Xpand doesn't support zerofill https://jira.mariadb.org/browse/XPT-286
+    if (!isXpand()) {
+      getStringZerofill(getPrepare(sharedConn, true));
+      getStringZerofill(getPrepare(sharedConnBinary, true));
+    }
+  }
+
+  public void getString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("105.210000", rs.getString(2));
+    assertEquals("105.210000", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("-1.600", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  public void getStringZerofill(ResultSet rs) throws SQLException {
+    assertEquals("0000000000", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("0105.210000", rs.getString(2));
+    assertEquals("0105.210000", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("0000001.600", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNString() throws SQLException {
+    getNString(get());
+  }
+
+  @Test
+  public void getNStringPrepare() throws SQLException {
+    getNString(getPrepare(sharedConn));
+    getNString(getPrepare(sharedConnBinary));
+  }
+
+  public void getNString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getNString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("105.210000", rs.getNString(2));
+    assertEquals("105.210000", rs.getNString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("-1.600", rs.getNString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBoolean() throws SQLException {
+    getBoolean(get());
+  }
+
+  @Test
+  public void getBooleanPrepare() throws SQLException {
+    getBoolean(getPrepare(sharedConn));
+    getBoolean(getPrepare(sharedConnBinary));
+  }
+
+  public void getBoolean(ResultSet rs) throws SQLException {
+    assertFalse(rs.getBoolean(1));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(2));
+    assertTrue(rs.getBoolean("t2alias"));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(3));
+    assertFalse(rs.wasNull());
+    assertFalse(rs.getBoolean(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getByte() throws SQLException {
+    getByte(get());
+  }
+
+  @Test
+  public void getBytePrepare() throws SQLException {
+    getByte(getPrepare(sharedConn));
+    getByte(getPrepare(sharedConnBinary));
+  }
+
+  public void getByte(ResultSet rs) throws SQLException {
+    assertEquals((byte) 0, rs.getByte(1));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 105, rs.getByte(2));
+    assertEquals((byte) 105, rs.getByte("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) -1, rs.getByte(3));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 0, rs.getByte(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getShort() throws SQLException {
+    getShort(get());
+  }
+
+  @Test
+  public void getShortPrepare() throws SQLException {
+    getShort(getPrepare(sharedConn));
+    getShort(getPrepare(sharedConnBinary));
+  }
+
+  public void getShort(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getShort(1));
+    assertFalse(rs.wasNull());
+    assertEquals(105, rs.getShort(2));
+    assertEquals(105, rs.getShort("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1, rs.getShort(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getShort(4));
+    assertTrue(rs.wasNull());
+    assertTrue(rs.next());
+    assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getShort(2),
+        "value '9223372036854775808.000000' cannot be decoded as Short");
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(get());
+  }
+
+  @Test
+  public void getIntPrepare() throws SQLException {
+    getInt(getPrepare(sharedConn));
+    getInt(getPrepare(sharedConnBinary));
+  }
+
+  public void getInt(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(105, rs.getInt(2));
+    assertEquals(105, rs.getInt("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1, rs.getInt(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+    assertTrue(rs.next());
+    assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getInt(2),
+        "value '9223372036854775808.000000' cannot be decoded as Int");
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(get());
+  }
+
+  @Test
+  public void getLongPrepare() throws SQLException {
+    getLong(getPrepare(sharedConn));
+    getLong(getPrepare(sharedConnBinary));
+  }
+
+  public void getLong(ResultSet rs) throws SQLException {
+    assertEquals(0L, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(105L, rs.getLong(2));
+    assertEquals(105L, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1L, rs.getLong(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0L, rs.getLong(4));
+    assertTrue(rs.wasNull());
+    assertTrue(rs.next());
+    assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getLong(2),
+        "value '9223372036854775808.000000' cannot be decoded as Long");
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(get());
+  }
+
+  @Test
+  public void getFloatPrepare() throws SQLException {
+    getFloat(getPrepare(sharedConn));
+    getFloat(getPrepare(sharedConnBinary));
+  }
+
+  public void getFloat(ResultSet rs) throws SQLException {
+    assertEquals(0F, rs.getFloat(1));
+    assertFalse(rs.wasNull());
+    assertEquals(105.21F, rs.getFloat(2));
+    assertEquals(105.21F, rs.getFloat("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1.6F, rs.getFloat(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0F, rs.getFloat(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(get());
+  }
+
+  @Test
+  public void getDoublePrepare() throws SQLException {
+    getDouble(getPrepare(sharedConn));
+    getDouble(getPrepare(sharedConnBinary));
+  }
+
+  public void getDouble(ResultSet rs) throws SQLException {
+    assertEquals(0D, rs.getDouble(1));
+    assertFalse(rs.wasNull());
+    assertEquals(105.21D, rs.getDouble(2));
+    assertEquals(105.21D, rs.getDouble("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1.6D, rs.getDouble(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0D, rs.getDouble(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(get());
+  }
+
+  @Test
+  public void getBigDecimalPrepare() throws SQLException {
+    getBigDecimal(getPrepare(sharedConn));
+    getBigDecimal(getPrepare(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  public void getBigDecimal(ResultSet rs) throws SQLException {
+    assertEquals(BigDecimal.ZERO, rs.getBigDecimal(1));
+    assertFalse(rs.wasNull());
+    assertEquals(new BigDecimal("105.210000"), rs.getBigDecimal(2));
+    assertEquals(new BigDecimal("105.2"), rs.getBigDecimal(2, BigDecimal.ROUND_DOWN));
+    assertEquals(new BigDecimal("105.210000"), rs.getBigDecimal("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(new BigDecimal("-1.600"), rs.getBigDecimal(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertNull(rs.getBigDecimal(4, BigDecimal.ROUND_CEILING));
+    assertTrue(rs.wasNull());
+    assertTrue(rs.next());
+    assertEquals(new BigDecimal("9223372036854775808.000000"), rs.getBigDecimal(2));
+  }
+
+  @Test
+  public void getBigDecimalScale() throws SQLException {
+    getBigDecimalScale(get());
+  }
+
+  @Test
+  public void getBigDecimalScalePrepare() throws SQLException {
+    getBigDecimalScale(getPrepare(sharedConn));
+    getBigDecimalScale(getPrepare(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  public void getBigDecimalScale(ResultSet rs) throws SQLException {
+    assertEquals(new BigDecimal("0.0"), rs.getBigDecimal(1, 1));
+    assertFalse(rs.wasNull());
+    assertEquals(new BigDecimal("0.0"), rs.getBigDecimal("t1alias", 1));
+    assertFalse(rs.wasNull());
+    assertEquals(new BigDecimal("105.2"), rs.getBigDecimal(2, 1));
+    assertEquals(new BigDecimal("105.2"), rs.getBigDecimal("t2alias", 1));
+    assertFalse(rs.wasNull());
+    assertEquals(new BigDecimal("-1.6"), rs.getBigDecimal(3, 1));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4, 1));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(get());
+  }
+
+  @Test
+  public void getDatePrepare() throws SQLException {
+    getDate(getPrepare(sharedConn));
+    getDate(getPrepare(sharedConnBinary));
+  }
+
+  public void getDate(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getDate(1), "Data type DECIMAL cannot be decoded as Date");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDate("t1alias"),
+        "Data type DECIMAL cannot be decoded as Date");
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(get());
+  }
+
+  @Test
+  public void getTimePrepare() throws SQLException {
+    getTime(getPrepare(sharedConn));
+    getTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getTime(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getTime(1), "Data type DECIMAL cannot be decoded as Time");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime("t1alias"),
+        "Data type DECIMAL cannot be decoded as Time");
+  }
+
+  @Test
+  public void getTimestamp() throws SQLException {
+    getTimestamp(get());
+  }
+
+  @Test
+  public void getTimestampPrepare() throws SQLException {
+    getTimestamp(getPrepare(sharedConn));
+    getTimestamp(getPrepare(sharedConnBinary));
+  }
+
+  public void getTimestamp(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTimestamp(1),
+        "Data type DECIMAL cannot be decoded as Timestamp");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTimestamp("t1alias"),
+        "Data type DECIMAL cannot be decoded as Timestamp");
+  }
+
+  @Test
+  public void getOffsetDateTime() throws SQLException {
+    getOffsetDateTime(get());
+  }
+
+  @Test
+  public void getOffsetDateTimePrepare() throws SQLException {
+    getOffsetDateTime(getPrepare(sharedConn));
+    getOffsetDateTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getOffsetDateTime(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(1, OffsetDateTime.class),
+        "Type class java.time.OffsetDateTime not supported type for DECIMAL type");
+  }
+
+  @Test
+  public void getAsciiStream() throws SQLException {
+    getAsciiStream(get());
+  }
+
+  @Test
+  public void getAsciiStreamPrepare() throws SQLException {
+    getAsciiStream(getPrepare(sharedConn));
+    getAsciiStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getAsciiStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getAsciiStream(1),
+        "Data type DECIMAL cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getAsciiStream("t1alias"),
+        "Data type DECIMAL cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getUnicodeStream() throws SQLException {
+    getUnicodeStream(get());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepare() throws SQLException {
+    getUnicodeStream(getPrepare(sharedConn));
+    getUnicodeStream(getPrepare(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  public void getUnicodeStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getUnicodeStream(1),
+        "Data type DECIMAL cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getUnicodeStream("t1alias"),
+        "Data type DECIMAL cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBinaryStream() throws SQLException {
+    getBinaryStream(get());
+  }
+
+  @Test
+  public void getBinaryStreamPrepare() throws SQLException {
+    getBinaryStream(getPrepare(sharedConn));
+    getBinaryStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getBinaryStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBinaryStream(1),
+        "Data type DECIMAL cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBinaryStream("t1alias"),
+        "Data type DECIMAL cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBytes() throws SQLException {
+    getBytes(get());
+  }
+
+  @Test
+  public void getBytesPrepare() throws SQLException {
+    getBytes(getPrepare(sharedConn));
+    getBytes(getPrepare(sharedConnBinary));
+  }
+
+  public void getBytes(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBytes(1),
+        "Data type DECIMAL cannot be decoded as byte[]");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBytes("t1alias"),
+        "Data type DECIMAL cannot be decoded as byte[]");
+  }
+
+  @Test
+  public void getCharacterStream() throws SQLException {
+    getCharacterStream(get());
+  }
+
+  @Test
+  public void getCharacterStreamPrepare() throws SQLException {
+    getCharacterStream(getPrepare(sharedConn));
+    getCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type DECIMAL cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream("t1alias"),
+        "Data type DECIMAL cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getNCharacterStream() throws SQLException {
+    getNCharacterStream(get());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepare() throws SQLException {
+    getNCharacterStream(getPrepare(sharedConn));
+    getNCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getNCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream(1),
+        "Data type DECIMAL cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream("t2alias"),
+        "Data type DECIMAL cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getRef() throws SQLException {
+    getRef(get());
+  }
+
+  @Test
+  public void getRefPrepare() throws SQLException {
+    getRef(getPrepare(sharedConn));
+    getRef(getPrepare(sharedConnBinary));
+  }
+
+  public void getRef(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getRef(1), "Method ResultSet.getRef not supported");
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getRef("t2alias"), "Method ResultSet.getRef not supported");
+  }
+
+  @Test
+  public void getBlob() throws SQLException {
+    getBlob(get());
+  }
+
+  @Test
+  public void getBlobPrepare() throws SQLException {
+    getBlob(getPrepare(sharedConn));
+    getBlob(getPrepare(sharedConnBinary));
+  }
+
+  public void getBlob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type DECIMAL cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream("t1alias"),
+        "Data type DECIMAL cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getClob() throws SQLException {
+    getClob(get());
+  }
+
+  @Test
+  public void getClobPrepare() throws SQLException {
+    getClob(getPrepare(sharedConn));
+    getClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getClob(1), "Data type DECIMAL cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getClob("t1alias"),
+        "Data type DECIMAL cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getNClob() throws SQLException {
+    getNClob(get());
+  }
+
+  @Test
+  public void getNClobPrepare() throws SQLException {
+    getNClob(getPrepare(sharedConn));
+    getNClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getNClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNClob(1),
+        "Data type DECIMAL cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNClob("t1alias"),
+        "Data type DECIMAL cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getURL() throws SQLException {
+    getURL(get());
+  }
+
+  @Test
+  public void getURLPrepare() throws SQLException {
+    getURL(getPrepare(sharedConn));
+    getURL(getPrepare(sharedConnBinary));
+  }
+
+  public void getURL(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLSyntaxErrorException.class, () -> rs.getURL(1), "Could not parse '0' as URL");
+    Common.assertThrowsContains(
+        SQLSyntaxErrorException.class, () -> rs.getURL("t1alias"), "Could not parse '0' as URL");
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = get();
+    ResultSetMetaData meta = rs.getMetaData();
+    assertEquals("DECIMAL", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.math.BigDecimal", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.DECIMAL, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(10, meta.getPrecision(1));
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    assertEquals(11, meta.getColumnDisplaySize(1));
+
+    assertEquals(30, meta.getPrecision(2));
+    assertEquals(6, meta.getScale(2));
+    assertEquals("", meta.getSchemaName(2));
+    assertEquals(32, meta.getColumnDisplaySize(2));
+  }
+
+  @Test
+  public void setParameter() throws SQLException {
+    try (PreparedStatement prep =
+        sharedConn.prepareStatement("INSERT INTO DecimalCodec2 VALUE (?, ?, ?, ?)")) {
+      prep.setBigDecimal(1, new BigDecimal("789.123"));
+      prep.setBigDecimal(2, new BigDecimal("1789.123456"));
+      prep.setNull(3, Types.DECIMAL);
+      prep.setObject(4, new BigDecimal("-211789.987987"));
+      prep.execute();
+      Statement stmt = sharedConn.createStatement();
+      ResultSet rs = stmt.executeQuery("SELECT * FROM DecimalCodec2");
+      rs.next();
+      assertEquals(new BigDecimal("789"), rs.getBigDecimal(1));
+      assertEquals(new BigDecimal("1789.123456"), rs.getBigDecimal(2));
+      assertNull(rs.getBigDecimal(3));
+      assertEquals(new BigDecimal("-211790"), rs.getBigDecimal(4));
+      Common.assertThrowsContains(
+          SQLException.class,
+          () -> prep.setObject(4, this),
+          "Type org.mariadb.jdbc.integration.codec.DecimalCodecTest not supported type");
+    }
+  }
+
+  @Test
+  public void sendParam() throws SQLException {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE DecimalCodec3");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO DecimalCodec3(t1) VALUES (?)")) {
+      prep.setBigDecimal(1, BigDecimal.valueOf(1));
+      prep.execute();
+      prep.setBigDecimal(1, null);
+      prep.execute();
+      prep.setObject(1, BigDecimal.valueOf(2));
+      prep.execute();
+      prep.setObject(1, null);
+      prep.execute();
+      prep.setObject(1, BigDecimal.valueOf(3), Types.DECIMAL);
+      prep.execute();
+      prep.setObject(1, "4", Types.DECIMAL);
+      prep.execute();
+      prep.setObject(1, "5", Types.NUMERIC);
+      prep.execute();
+      prep.setObject(1, 7D, JDBCType.NUMERIC);
+      prep.execute();
+      prep.setObject(1, "6", Types.BIGINT);
+      prep.execute();
+      prep.setObject(1, null, Types.DECIMAL);
+      prep.execute();
+    }
+
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM DecimalCodec3");
+
+    assertTrue(rs.next());
+    assertEquals("1", rs.getBigDecimal(2).toString());
+    rs.updateBigDecimal(2, null);
+    rs.updateRow();
+    assertNull(rs.getBigDecimal(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getBigDecimal(2));
+    rs.updateBigDecimal("t1", BigDecimal.ONE);
+    rs.updateRow();
+    assertEquals("1", rs.getBigDecimal(2).toString());
+
+    assertTrue(rs.next());
+    assertEquals("2", rs.getBigDecimal(2).toString());
+    rs.updateObject(2, null);
+    rs.updateRow();
+    assertNull(rs.getBigDecimal(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    rs.updateObject(2, BigDecimal.valueOf(20));
+    rs.updateRow();
+    assertEquals(BigDecimal.valueOf(20), rs.getBigDecimal(2));
+
+    assertTrue(rs.next());
+    assertEquals("3", rs.getBigDecimal(2).toString());
+    rs.updateObject("t1", null, Types.DECIMAL);
+    rs.updateRow();
+    assertNull(rs.getBigDecimal(2));
+
+    assertTrue(rs.next());
+    assertEquals("4", rs.getBigDecimal(2).toString());
+
+    assertTrue(rs.next());
+    assertEquals("5", rs.getBigDecimal(2).toString());
+
+    assertTrue(rs.next());
+    assertEquals("7", rs.getBigDecimal(2).toString());
+
+    assertTrue(rs.next());
+    assertEquals("6", rs.getBigDecimal(2).toString());
+
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    rs.updateObject(2, BigDecimal.valueOf(30), Types.DECIMAL);
+    rs.updateRow();
+    assertEquals(BigDecimal.valueOf(30), rs.getBigDecimal(2));
+
+    rs = stmt.executeQuery("SELECT * FROM DecimalCodec3");
+
+    assertTrue(rs.next());
+    assertNull(rs.getBigDecimal(2));
+
+    assertTrue(rs.next());
+    assertEquals("1", rs.getBigDecimal(2).toString());
+
+    assertTrue(rs.next());
+    assertNull(rs.getBigDecimal(2));
+
+    assertTrue(rs.next());
+    assertEquals(BigDecimal.valueOf(20), rs.getBigDecimal(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getBigDecimal(2));
+
+    assertTrue(rs.next());
+    assertEquals("4", rs.getBigDecimal(2).toString());
+    assertTrue(rs.next());
+    assertEquals("5", rs.getBigDecimal(2).toString());
+    assertTrue(rs.next());
+    assertEquals("7", rs.getBigDecimal(2).toString());
+    assertTrue(rs.next());
+    assertEquals("6", rs.getBigDecimal(2).toString());
+
+    assertTrue(rs.next());
+    assertEquals(BigDecimal.valueOf(30), rs.getBigDecimal(2));
+    con.commit();
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/DoubleCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/DoubleCodecTest.java
new file mode 100644
index 0000000..b21bf15
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/DoubleCodecTest.java
@@ -0,0 +1,777 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import java.time.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.integration.Common;
+
+public class DoubleCodecTest extends CommonCodecTest {
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS DoubleCodec");
+    stmt.execute("DROP TABLE IF EXISTS DoubleCodec2");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE DoubleCodec (t1 DOUBLE, t2 DOUBLE, t3 DOUBLE, t4 DOUBLE)");
+    stmt.execute("INSERT INTO DoubleCodec VALUES (0, 105.21, -1.6, null)");
+    stmt.execute(
+        "CREATE TABLE DoubleCodec2 (id int not null primary key auto_increment, t1 DOUBLE)");
+    stmt.execute("FLUSH TABLES");
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from DoubleCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getPrepare(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from DoubleCodec"
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    ResultSet rs = preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws SQLException {
+    getObject(get());
+  }
+
+  @Test
+  public void getObjectPrepare() throws SQLException {
+    getObject(getPrepare(sharedConn));
+    getObject(getPrepare(sharedConnBinary));
+  }
+
+  public void getObject(ResultSet rs) throws SQLException {
+    assertEquals(0d, (double) rs.getObject(1), 0.00001d);
+    assertFalse(rs.wasNull());
+    assertEquals(105.21d, (double) rs.getObject(2), 0.00001d);
+    assertEquals(105.21d, (double) rs.getObject("t2alias"), 0.00001d);
+    assertFalse(rs.wasNull());
+    assertEquals(-1.6d, (double) rs.getObject(3), 0.00001d);
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testObject(rs, Integer.class, 0);
+    //    testObject(rs, String.class, "0");
+    testObject(rs, Long.class, 0L);
+    testObject(rs, Short.class, (short) 0);
+    //    testObject(rs, BigDecimal.class, BigDecimal.valueOf(0));
+    testObject(rs, BigInteger.class, BigInteger.valueOf(0));
+    testObject(rs, Double.class, (double) 0);
+    testObject(rs, Float.class, (float) 0);
+    testObject(rs, Byte.class, (byte) 0);
+    testErrObject(rs, byte[].class);
+    testErrObject(rs, Date.class);
+    testErrObject(rs, Time.class);
+    testErrObject(rs, Timestamp.class);
+    testErrObject(rs, java.util.Date.class);
+    testErrObject(rs, LocalDate.class);
+    testErrObject(rs, ZonedDateTime.class);
+    testErrObject(rs, OffsetDateTime.class);
+    testErrObject(rs, LocalDateTime.class);
+    testErrObject(rs, OffsetTime.class);
+    testObject(rs, Boolean.class, Boolean.FALSE);
+    testErrObject(rs, Clob.class);
+    testErrObject(rs, NClob.class);
+    testErrObject(rs, InputStream.class);
+    testErrObject(rs, Reader.class);
+  }
+
+  @Test
+  public void getString() throws Exception {
+    getString(get());
+  }
+
+  @Test
+  public void getStringPrepare() throws Exception {
+    getString(getPrepare(sharedConn));
+    getString(getPrepare(sharedConnBinary));
+  }
+
+  public void getString(ResultSet rs) throws Exception {
+    assertTrue("0".equals(rs.getString(1)) || "0.0".equals(rs.getString(1)));
+    assertFalse(rs.wasNull());
+    assertEquals("105.21", rs.getString(2));
+    assertEquals("105.21", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("-1.6", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNString() throws Exception {
+    getNString(get());
+  }
+
+  @Test
+  public void getNStringPrepare() throws Exception {
+    getNString(getPrepare(sharedConn));
+    getNString(getPrepare(sharedConnBinary));
+  }
+
+  public void getNString(ResultSet rs) throws Exception {
+    assertTrue("0".equals(rs.getString(1)) || "0.0".equals(rs.getString(1)));
+    assertFalse(rs.wasNull());
+    assertEquals("105.21", rs.getNString(2));
+    assertEquals("105.21", rs.getNString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("-1.6", rs.getNString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBoolean() throws Exception {
+    getBoolean(get());
+  }
+
+  @Test
+  public void getBooleanPrepare() throws Exception {
+    getBoolean(getPrepare(sharedConn));
+    getBoolean(getPrepare(sharedConnBinary));
+  }
+
+  public void getBoolean(ResultSet rs) throws Exception {
+    assertFalse(rs.getBoolean(1));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(2));
+    assertTrue(rs.getBoolean("t2alias"));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(3));
+    assertFalse(rs.wasNull());
+    assertFalse(rs.getBoolean(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getByte() throws Exception {
+    getByte(get());
+  }
+
+  @Test
+  public void getBytePrepare() throws Exception {
+    getByte(getPrepare(sharedConn));
+    getByte(getPrepare(sharedConnBinary));
+  }
+
+  public void getByte(ResultSet rs) throws Exception {
+    assertEquals((byte) 0, rs.getByte(1));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 105, rs.getByte(2));
+    assertEquals((byte) 105, rs.getByte("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) -1, rs.getByte(3));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 0, rs.getByte(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getShort() throws Exception {
+    getShort(get());
+  }
+
+  @Test
+  public void getShortPrepare() throws Exception {
+    getShort(getPrepare(sharedConn));
+    getShort(getPrepare(sharedConnBinary));
+  }
+
+  public void getShort(ResultSet rs) throws Exception {
+    assertEquals(0, rs.getShort(1));
+    assertFalse(rs.wasNull());
+    assertEquals(105, rs.getShort(2));
+    assertEquals(105, rs.getShort("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1, rs.getShort(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getShort(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getInt() throws Exception {
+    getInt(get());
+  }
+
+  @Test
+  public void getIntPrepare() throws Exception {
+    getInt(getPrepare(sharedConn));
+    getInt(getPrepare(sharedConnBinary));
+  }
+
+  public void getInt(ResultSet rs) throws Exception {
+    assertEquals(0, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(105, rs.getInt(2));
+    assertEquals(105, rs.getInt("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1, rs.getInt(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLong() throws Exception {
+    getLong(get());
+  }
+
+  @Test
+  public void getLongPrepare() throws Exception {
+    getLong(getPrepare(sharedConn));
+    getLong(getPrepare(sharedConnBinary));
+  }
+
+  public void getLong(ResultSet rs) throws Exception {
+    assertEquals(0L, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(105L, rs.getLong(2));
+    assertEquals(105L, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1L, rs.getLong(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0L, rs.getLong(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloat() throws Exception {
+    getFloat(get());
+  }
+
+  @Test
+  public void getFloatPrepare() throws Exception {
+    getFloat(getPrepare(sharedConn));
+    getFloat(getPrepare(sharedConnBinary));
+  }
+
+  public void getFloat(ResultSet rs) throws Exception {
+    assertEquals(0F, rs.getFloat(1));
+    assertFalse(rs.wasNull());
+    assertEquals(105.21F, rs.getFloat(2));
+    assertEquals(105.21F, rs.getFloat("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1.6F, rs.getFloat(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0F, rs.getFloat(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDouble() throws Exception {
+    getDouble(get());
+  }
+
+  @Test
+  public void getDoublePrepare() throws Exception {
+    getDouble(getPrepare(sharedConn));
+    getDouble(getPrepare(sharedConnBinary));
+  }
+
+  public void getDouble(ResultSet rs) throws Exception {
+    assertEquals(0D, rs.getDouble(1));
+    assertFalse(rs.wasNull());
+    assertEquals(105.21D, rs.getDouble(2));
+    assertEquals(105.21D, rs.getDouble("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1.6D, rs.getDouble(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0D, rs.getDouble(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimal() throws Exception {
+    getBigDecimal(get());
+  }
+
+  @Test
+  public void getBigDecimalPrepare() throws Exception {
+    getBigDecimal(getPrepare(sharedConn));
+    getBigDecimal(getPrepare(sharedConnBinary));
+  }
+
+  public void getBigDecimal(ResultSet rs) throws Exception {
+    assertTrue(
+        BigDecimal.ZERO.equals(rs.getBigDecimal(1))
+            || new BigDecimal("0.0").equals(rs.getBigDecimal(1)));
+    assertFalse(rs.wasNull());
+    assertEquals(new BigDecimal("105.21"), rs.getBigDecimal(2));
+    assertEquals(new BigDecimal("105.21"), rs.getBigDecimal("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(new BigDecimal("-1.6"), rs.getBigDecimal(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDate() throws Exception {
+    getDate(get());
+  }
+
+  @Test
+  public void getDatePrepare() throws Exception {
+    getDate(getPrepare(sharedConn));
+    getDate(getPrepare(sharedConnBinary));
+  }
+
+  public void getDate(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getDate(1), "Data type DOUBLE cannot be decoded as Date");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDate("t1alias"),
+        "Data type DOUBLE cannot be decoded as Date");
+  }
+
+  @Test
+  public void getTime() throws Exception {
+    getTime(get());
+  }
+
+  @Test
+  public void getTimePrepare() throws Exception {
+    getTime(getPrepare(sharedConn));
+    getTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getTime(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getTime(1), "Data type DOUBLE cannot be decoded as Time");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime("t1alias"),
+        "Data type DOUBLE cannot be decoded as Time");
+  }
+
+  @Test
+  public void getTimestamp() throws Exception {
+    getTimestamp(get());
+  }
+
+  @Test
+  public void getTimestampPrepare() throws Exception {
+    getTimestamp(getPrepare(sharedConn));
+    getTimestamp(getPrepare(sharedConnBinary));
+  }
+
+  public void getTimestamp(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTimestamp(1),
+        "Data type DOUBLE cannot be decoded as Timestamp");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTimestamp("t1alias"),
+        "Data type DOUBLE cannot be decoded as Timestamp");
+  }
+
+  @Test
+  public void getOffsetDateTime() throws SQLException {
+    getOffsetDateTime(get());
+  }
+
+  @Test
+  public void getOffsetDateTimePrepare() throws SQLException {
+    getOffsetDateTime(getPrepare(sharedConn));
+    getOffsetDateTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getOffsetDateTime(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(1, OffsetDateTime.class),
+        "Type class java.time.OffsetDateTime not supported type for DOUBLE type");
+  }
+
+  @Test
+  public void getAsciiStream() throws Exception {
+    getAsciiStream(get());
+  }
+
+  @Test
+  public void getAsciiStreamPrepare() throws Exception {
+    getAsciiStream(getPrepare(sharedConn));
+    getAsciiStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getAsciiStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getAsciiStream(1),
+        "Data type DOUBLE cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getAsciiStream("t1alias"),
+        "Data type DOUBLE cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getUnicodeStream() throws Exception {
+    getUnicodeStream(get());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepare() throws Exception {
+    getUnicodeStream(getPrepare(sharedConn));
+    getUnicodeStream(getPrepare(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  public void getUnicodeStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getUnicodeStream(1),
+        "Data type DOUBLE cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getUnicodeStream("t1alias"),
+        "Data type DOUBLE cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBinaryStream() throws Exception {
+    getBinaryStream(get());
+  }
+
+  @Test
+  public void getBinaryStreamPrepare() throws Exception {
+    getBinaryStream(getPrepare(sharedConn));
+    getBinaryStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getBinaryStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBinaryStream(1),
+        "Data type DOUBLE cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBinaryStream("t1alias"),
+        "Data type DOUBLE cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBytes() throws Exception {
+    getBytes(get());
+  }
+
+  @Test
+  public void getBytesPrepare() throws Exception {
+    getBytes(getPrepare(sharedConn));
+    getBytes(getPrepare(sharedConnBinary));
+  }
+
+  public void getBytes(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBytes(1),
+        "Data type DOUBLE cannot be decoded as byte[]");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBytes("t1alias"),
+        "Data type DOUBLE cannot be decoded as byte[]");
+  }
+
+  @Test
+  public void getCharacterStream() throws Exception {
+    getCharacterStream(get());
+  }
+
+  @Test
+  public void getCharacterStreamPrepare() throws Exception {
+    getCharacterStream(getPrepare(sharedConn));
+    getCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type DOUBLE cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream("t1alias"),
+        "Data type DOUBLE cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getNCharacterStream() throws Exception {
+    getNCharacterStream(get());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepare() throws Exception {
+    getNCharacterStream(getPrepare(sharedConn));
+    getNCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getNCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream(1),
+        "Data type DOUBLE cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream("t2alias"),
+        "Data type DOUBLE cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getRef() throws Exception {
+    getRef(get());
+  }
+
+  @Test
+  public void getRefPrepare() throws Exception {
+    getRef(getPrepare(sharedConn));
+    getRef(getPrepare(sharedConnBinary));
+  }
+
+  public void getRef(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getRef(1), "Method ResultSet.getRef not supported");
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getRef("t2alias"), "Method ResultSet.getRef not supported");
+  }
+
+  @Test
+  public void getBlob() throws Exception {
+    getBlob(get());
+  }
+
+  @Test
+  public void getBlobPrepare() throws Exception {
+    getBlob(getPrepare(sharedConn));
+    getBlob(getPrepare(sharedConnBinary));
+  }
+
+  public void getBlob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type DOUBLE cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream("t1alias"),
+        "Data type DOUBLE cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getClob() throws Exception {
+    getClob(get());
+  }
+
+  @Test
+  public void getClobPrepare() throws Exception {
+    getClob(getPrepare(sharedConn));
+    getClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getClob(1), "Data type DOUBLE cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getClob("t1alias"),
+        "Data type DOUBLE cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getNClob() throws Exception {
+    getNClob(get());
+  }
+
+  @Test
+  public void getNClobPrepare() throws Exception {
+    getNClob(getPrepare(sharedConn));
+    getNClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getNClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getNClob(1), "Data type DOUBLE cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNClob("t1alias"),
+        "Data type DOUBLE cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getURL() throws Exception {
+    getURL(get());
+  }
+
+  @Test
+  public void getURLPrepare() throws Exception {
+    getURL(getPrepare(sharedConn));
+    getURL(getPrepare(sharedConnBinary));
+  }
+
+  public void getURL(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLSyntaxErrorException.class, () -> rs.getURL(1), "Could not parse");
+    Common.assertThrowsContains(
+        SQLSyntaxErrorException.class, () -> rs.getURL("t1alias"), "Could not parse");
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = get();
+    ResultSetMetaData meta = rs.getMetaData();
+    assertEquals("DOUBLE", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.lang.Double", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.DOUBLE, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals("", meta.getSchemaName(1));
+    // https://jira.mariadb.org/browse/XPT-276
+    if (!isXpand()) {
+      assertEquals(22, meta.getColumnDisplaySize(1));
+      assertEquals(22, meta.getPrecision(1));
+      assertEquals(31, meta.getScale(1));
+    }
+  }
+
+  @Test
+  public void sendParam() throws SQLException {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE DoubleCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep = con.prepareStatement("INSERT INTO DoubleCodec2(t1) VALUES (?)")) {
+      prep.setDouble(1, 1D);
+      prep.execute();
+      prep.setObject(1, 2D);
+      prep.execute();
+      prep.setObject(1, null);
+      prep.execute();
+      prep.setObject(1, 3D, Types.DECIMAL);
+      prep.execute();
+      prep.setObject(1, "4", Types.FLOAT);
+      prep.execute();
+      prep.setObject(1, 5D, Types.DOUBLE);
+      prep.execute();
+      prep.setObject(1, null, Types.DECIMAL);
+      prep.execute();
+    }
+
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM DoubleCodec2");
+    assertTrue(rs.next());
+    assertEquals(1D, rs.getDouble(2));
+    rs.updateDouble("t1", 10D);
+    rs.updateRow();
+    assertEquals(10D, rs.getDouble(2));
+
+    assertTrue(rs.next());
+    assertEquals(2D, rs.getDouble(2));
+    rs.updateObject(2, null);
+    rs.updateRow();
+    assertNull(rs.getString(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    rs.updateObject("t1", 20D);
+    rs.updateRow();
+    assertEquals(20D, rs.getDouble(2));
+
+    assertTrue(rs.next());
+    assertEquals(3D, rs.getDouble(2));
+    rs.updateObject(2, null, Types.DECIMAL);
+    rs.updateRow();
+    assertNull(rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals(4D, rs.getDouble(2));
+    assertTrue(rs.next());
+    assertEquals(5D, rs.getDouble(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    rs.updateObject("t1", 30D, Types.DECIMAL);
+    rs.updateRow();
+    assertEquals(30D, rs.getDouble(2));
+
+    rs = stmt.executeQuery("SELECT * FROM DoubleCodec2");
+    assertTrue(rs.next());
+    assertEquals(10D, rs.getDouble(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals(20D, rs.getDouble(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals(4D, rs.getDouble(2));
+    assertTrue(rs.next());
+    assertEquals(5D, rs.getDouble(2));
+
+    assertTrue(rs.next());
+    assertEquals(30D, rs.getDouble(2));
+    con.commit();
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/EnumCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/EnumCodecTest.java
new file mode 100644
index 0000000..75bbf70
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/EnumCodecTest.java
@@ -0,0 +1,770 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.sql.*;
+import java.time.*;
+import java.util.Calendar;
+import java.util.TimeZone;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.MariaDbClob;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.integration.Common;
+
+public class EnumCodecTest extends CommonCodecTest {
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS EnumCodec");
+    stmt.execute("DROP TABLE IF EXISTS EnumCodec2");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE EnumCodec (t1 ENUM('0', '1', 'some🌟', '2011-01-01', '2010-12-31"
+            + " 23:59:59.152', '23:54:51.840010'), t2 ENUM('0', '1', 'some🌟', '2011-01-01',"
+            + " '2010-12-31 23:59:59.152', '23:54:51.840010'),  t3 ENUM('0', '1', 'some🌟',"
+            + " '2011-01-01', '2010-12-31 23:59:59.152', '23:54:51.840010'), t4 ENUM('0', '1',"
+            + " 'some🌟', '2011-01-01', '2010-12-31 23:59:59.152', '23:54:51.840010')) CHARACTER"
+            + " SET utf8mb4 COLLATE utf8mb4_unicode_ci");
+    stmt.execute(
+        "INSERT INTO EnumCodec VALUES ('0', '1', 'some🌟', null), ('2011-01-01', '2010-12-31"
+            + " 23:59:59.152', '23:54:51.840010', null)");
+    stmt.execute("CREATE TABLE EnumCodec2 (t1 ENUM('0', '1', '2')) CHARACTER SET binary");
+    stmt.execute("INSERT INTO EnumCodec2 VALUES ('0'), ('1'), (null)");
+    stmt.execute("FLUSH TABLES");
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from EnumCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getPrepare(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from EnumCodec"
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    ResultSet rs = preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws SQLException {
+    getObject(get());
+  }
+
+  @Test
+  public void getObjectPrepare() throws SQLException {
+    getObject(getPrepare(sharedConn));
+    getObject(getPrepare(sharedConnBinary));
+  }
+
+  public void getObject(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getObject(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getObject(2));
+    assertEquals("1", rs.getObject("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("some🌟", rs.getObject(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4));
+    assertTrue(rs.wasNull());
+  }
+
+  private ResultSet getBinary() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs = stmt.executeQuery("select t1 as t1alias from EnumCodec2");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getBinaryPrepare(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement("select t1 as t1alias from EnumCodec2" + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    ResultSet rs = preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getBinaryObject() throws SQLException {
+    getBinaryObject(getBinary());
+  }
+
+  @Test
+  public void getBinaryObjectPrepare() throws SQLException {
+    getBinaryObject(getBinaryPrepare(sharedConn));
+    getBinaryObject(getBinaryPrepare(sharedConnBinary));
+  }
+
+  public void getBinaryObject(ResultSet rs) throws SQLException {
+    assertArrayEquals("0".getBytes(StandardCharsets.UTF_8), (byte[]) rs.getObject(1));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.next());
+    assertArrayEquals("1".getBytes(StandardCharsets.UTF_8), (byte[]) rs.getObject(1));
+    assertFalse(rs.wasNull());
+    assertFalse(rs.wasNull());
+    assertTrue(rs.next());
+    assertNull(rs.getObject(1));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testObject(rs, Integer.class, 0);
+    testObject(rs, String.class, "0");
+    testObject(rs, Long.class, 0L);
+    testObject(rs, Short.class, (short) 0);
+    testObject(rs, BigDecimal.class, BigDecimal.valueOf(0));
+    testObject(rs, BigInteger.class, BigInteger.valueOf(0));
+    testObject(rs, Double.class, (double) 0);
+    testObject(rs, Float.class, (float) 0);
+    testObject(rs, Byte.class, (byte) 0);
+    testArrObject(rs, new byte[] {(byte) '0'});
+    testObject(rs, Boolean.class, Boolean.FALSE);
+    testObject(rs, Clob.class, new MariaDbClob("0".getBytes()));
+    testObject(rs, NClob.class, new MariaDbClob("0".getBytes()));
+    testObject(rs, InputStream.class, new MariaDbClob("0".getBytes()).getBinaryStream());
+    testObject(rs, Reader.class, new StringReader("0"));
+    rs.next();
+    testObject(rs, LocalDate.class, LocalDate.parse("2011-01-01"));
+    testObject(rs, LocalDateTime.class, LocalDateTime.parse("2011-01-01T00:00"));
+    testObject(rs, LocalTime.class, LocalTime.parse("23:54:51.840010"), 3);
+    Time t = Time.valueOf("23:54:51");
+    testObject(rs, Time.class, new Time(t.getTime() + 840), 3);
+    testObject(rs, Date.class, Date.valueOf("2011-01-01"));
+    Timestamp tt = Timestamp.valueOf("2010-12-31 23:59:59");
+    testObject(rs, Timestamp.class, new Timestamp(tt.getTime() + 152), 2);
+    testObject(
+        rs,
+        ZonedDateTime.class,
+        LocalDateTime.parse("2011-01-01T00:00").atZone(ZoneId.systemDefault()));
+    testErrObject(rs, OffsetDateTime.class);
+    testErrObject(rs, OffsetTime.class);
+    testObject(rs, java.util.Date.class, Date.valueOf("2010-12-31"), 2);
+  }
+
+  @Test
+  public void getString() throws SQLException {
+    getString(get());
+  }
+
+  @Test
+  public void getStringPrepare() throws SQLException {
+    getString(getPrepare(sharedConn));
+    getString(getPrepare(sharedConnBinary));
+  }
+
+  public void getString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getString(2));
+    assertEquals("1", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("some🌟", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNString() throws SQLException {
+    getNString(get());
+  }
+
+  @Test
+  public void getNStringPrepare() throws SQLException {
+    getNString(getPrepare(sharedConn));
+    getNString(getPrepare(sharedConnBinary));
+  }
+
+  public void getNString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getNString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getNString(2));
+    assertEquals("1", rs.getNString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("some🌟", rs.getNString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBoolean() throws SQLException {
+    getBoolean(get());
+  }
+
+  @Test
+  public void getBooleanPrepare() throws SQLException {
+    getBoolean(getPrepare(sharedConn));
+    getBoolean(getPrepare(sharedConnBinary));
+  }
+
+  public void getBoolean(ResultSet rs) throws SQLException {
+    assertFalse(rs.getBoolean(1));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(2));
+    assertTrue(rs.getBoolean("t2alias"));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(3));
+    assertFalse(rs.wasNull());
+    assertFalse(rs.getBoolean(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getByte() throws SQLException {
+    getByte(get());
+  }
+
+  @Test
+  public void getBytePrepare() throws SQLException {
+    getByte(getPrepare(sharedConn));
+    getByte(getPrepare(sharedConnBinary));
+  }
+
+  public void getByte(ResultSet rs) throws SQLException {
+    assertEquals((byte) 0, rs.getByte(1));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 1, rs.getByte(2));
+    assertEquals((byte) 1, rs.getByte("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getByte(3), " cannot be decoded as Byte");
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 0, rs.getByte(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getShort() throws SQLException {
+    getShort(get());
+  }
+
+  @Test
+  public void getShortPrepare() throws SQLException {
+    getShort(getPrepare(sharedConn));
+    getShort(getPrepare(sharedConnBinary));
+  }
+
+  public void getShort(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getShort(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getShort(2));
+    assertEquals(1, rs.getShort("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getShort(3), "value 'some🌟' cannot be decoded as Short");
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getShort(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(get());
+  }
+
+  @Test
+  public void getIntPrepare() throws SQLException {
+    getInt(getPrepare(sharedConn));
+    getInt(getPrepare(sharedConnBinary));
+  }
+
+  public void getInt(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getInt(2));
+    assertEquals(1, rs.getInt("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getInt(3), "value 'some🌟' cannot be decoded as Integer");
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(get());
+  }
+
+  @Test
+  public void getLongPrepare() throws SQLException {
+    getLong(getPrepare(sharedConn));
+    getLong(getPrepare(sharedConnBinary));
+  }
+
+  public void getLong(ResultSet rs) throws SQLException {
+    assertEquals(0L, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1L, rs.getLong(2));
+    assertEquals(1L, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getLong(3),
+        "value 'some\uD83C\uDF1F' cannot be decoded as Long");
+    assertFalse(rs.wasNull());
+    assertEquals(0L, rs.getLong(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(get());
+  }
+
+  @Test
+  public void getFloatPrepare() throws SQLException {
+    getFloat(getPrepare(sharedConn));
+    getFloat(getPrepare(sharedConnBinary));
+  }
+
+  public void getFloat(ResultSet rs) throws SQLException {
+    assertEquals(0F, rs.getFloat(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1F, rs.getFloat(2));
+    assertEquals(1F, rs.getFloat("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getFloat(3), "value 'some🌟' cannot be decoded as Float");
+    assertFalse(rs.wasNull());
+    assertEquals(0F, rs.getFloat(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(get());
+  }
+
+  @Test
+  public void getDoublePrepare() throws SQLException {
+    getDouble(getPrepare(sharedConn));
+    getDouble(getPrepare(sharedConnBinary));
+  }
+
+  public void getDouble(ResultSet rs) throws SQLException {
+    assertEquals(0D, rs.getDouble(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1D, rs.getDouble(2));
+    assertEquals(1D, rs.getDouble("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDouble(3),
+        "value 'some🌟' cannot be decoded as Double");
+    assertFalse(rs.wasNull());
+    assertEquals(0D, rs.getDouble(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(get());
+  }
+
+  @Test
+  public void getBigDecimalPrepare() throws SQLException {
+    getBigDecimal(getPrepare(sharedConn));
+    getBigDecimal(getPrepare(sharedConnBinary));
+  }
+
+  public void getBigDecimal(ResultSet rs) throws SQLException {
+    assertEquals(BigDecimal.ZERO, rs.getBigDecimal(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal(2));
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBigDecimal(3),
+        "value 'some🌟' cannot be decoded as BigDecimal");
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(get());
+  }
+
+  @Test
+  public void getDatePrepare() throws SQLException {
+    getDate(getPrepare(sharedConn));
+    getDate(getPrepare(sharedConnBinary));
+  }
+
+  public void getDate(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getDate(1), " cannot be decoded as Date");
+    rs.next();
+    assertEquals("2011-01-01", rs.getDate(1).toString());
+    assertFalse(rs.wasNull());
+    assertEquals("2010-12-31", rs.getDate(2).toString());
+    assertFalse(rs.wasNull());
+    assertEquals("2010-12-31", rs.getDate("t2alias").toString());
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getDate(3), " cannot be decoded as Date");
+    assertFalse(rs.wasNull());
+    assertNull(rs.getDate(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(get());
+  }
+
+  @Test
+  public void getTimePrepare() throws SQLException {
+    getTime(getPrepare(sharedConn));
+    getTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getTime(ResultSet rs) throws SQLException {
+    rs.next();
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime(1),
+        "STRING value '2011-01-01' cannot be decoded as Time");
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime(2),
+        "STRING value '2010-12-31 23:59:59.152' cannot be decoded as Time");
+    assertEquals(Time.valueOf("23:54:51").getTime() + 840, rs.getTime(3).getTime());
+    assertEquals(Time.valueOf("23:54:51").getTime() + 840, rs.getTime("t3alias").getTime());
+    Calendar utc = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+    assertEquals(86091840, rs.getTime(3, utc).getTime());
+    assertEquals(86091840, rs.getTime("t3alias", utc).getTime());
+
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getTimestamp() throws SQLException {
+    getTimestamp(get());
+  }
+
+  @Test
+  public void getTimestampPrepare() throws SQLException {
+    getTimestamp(getPrepare(sharedConn));
+    getTimestamp(getPrepare(sharedConnBinary));
+  }
+
+  public void getTimestamp(ResultSet rs) throws SQLException {
+    rs.next();
+
+    assertEquals(Timestamp.valueOf("2011-01-01 00:00:00").getTime(), rs.getTimestamp(1).getTime());
+    assertEquals(
+        Timestamp.valueOf("2011-01-01 00:00:00").getTime() + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp(1, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertEquals(
+        Timestamp.valueOf("2011-01-01 00:00:00").getTime(), rs.getTimestamp("t1alias").getTime());
+    assertEquals(
+        Timestamp.valueOf("2011-01-01 00:00:00").getTime() + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp("t1alias", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime() + 152, rs.getTimestamp(2).getTime());
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime()
+            + 152
+            + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp(2, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime() + 152,
+        rs.getTimestamp("t2alias").getTime());
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime()
+            + 152
+            + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp("t2alias", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+
+    //
+    //    ('2011-01-01', '2010-12-31 23:59:59.152',"
+    //            + " '23:54:51.840010', null)
+  }
+
+  @Test
+  public void getAsciiStream() throws Exception {
+    getAsciiStream(get());
+  }
+
+  @Test
+  public void getAsciiStreamPrepare() throws Exception {
+    getAsciiStream(getPrepare(sharedConn));
+    getAsciiStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getAsciiStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getAsciiStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getAsciiStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getAsciiStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getAsciiStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getAsciiStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getUnicodeStream() throws Exception {
+    getUnicodeStream(get());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepare() throws Exception {
+    getUnicodeStream(getPrepare(sharedConn));
+    getUnicodeStream(getPrepare(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  public void getUnicodeStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getUnicodeStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getUnicodeStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getUnicodeStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)),
+        rs.getUnicodeStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getUnicodeStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBinaryStream() throws Exception {
+    getBinaryStream(get());
+  }
+
+  @Test
+  public void getBinaryStreamPrepare() throws Exception {
+    getBinaryStream(getPrepare(sharedConn));
+    getBinaryStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getBinaryStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getBinaryStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getBinaryStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getBinaryStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getBinaryStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBinaryStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBytes() throws SQLException {
+    getBytes(get());
+  }
+
+  @Test
+  public void getBytesPrepare() throws SQLException {
+    getBytes(getPrepare(sharedConn));
+    getBytes(getPrepare(sharedConnBinary));
+  }
+
+  public void getBytes(ResultSet rs) throws SQLException {
+    assertArrayEquals("0".getBytes(), rs.getBytes(1));
+    assertFalse(rs.wasNull());
+    assertArrayEquals("1".getBytes(), rs.getBytes(2));
+    assertArrayEquals("1".getBytes(), rs.getBytes("t2alias"));
+    assertFalse(rs.wasNull());
+    assertArrayEquals("some🌟".getBytes(StandardCharsets.UTF_8), rs.getBytes(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBytes(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getCharacterStream() throws Exception {
+    getCharacterStream(get());
+  }
+
+  @Test
+  public void getCharacterStreamPrepare() throws Exception {
+    getCharacterStream(getPrepare(sharedConn));
+    getCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getCharacterStream(ResultSet rs) throws Exception {
+    assertReaderEquals(new StringReader("0"), rs.getCharacterStream(1));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("1"), rs.getCharacterStream(2));
+    assertReaderEquals(new StringReader("1"), rs.getCharacterStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("some🌟"), rs.getCharacterStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getCharacterStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNCharacterStream() throws Exception {
+    getNCharacterStream(get());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepare() throws Exception {
+    getNCharacterStream(getPrepare(sharedConn));
+    getNCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getNCharacterStream(ResultSet rs) throws Exception {
+    assertReaderEquals(new StringReader("0"), rs.getNCharacterStream(1));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("1"), rs.getNCharacterStream(2));
+    assertReaderEquals(new StringReader("1"), rs.getNCharacterStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("some🌟"), rs.getNCharacterStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNCharacterStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBlob() throws SQLException {
+    getBlob(get());
+  }
+
+  @Test
+  public void getBlobPrepare() throws SQLException {
+    getBlob(getPrepare(sharedConn));
+    getBlob(getPrepare(sharedConnBinary));
+  }
+
+  public void getBlob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getBlob(1), " (not binary) cannot be decoded as Blob");
+  }
+
+  @Test
+  public void getClob() throws Exception {
+    getClob(get());
+  }
+
+  @Test
+  public void getClobPrepare() throws Exception {
+    getClob(getPrepare(sharedConn));
+    getClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getClob(ResultSet rs) throws Exception {
+    assertStreamEquals(new MariaDbClob("0".getBytes()), rs.getClob(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getClob(2));
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getClob("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getClob(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getClob(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNClob() throws Exception {
+    getNClob(get());
+  }
+
+  @Test
+  public void getNClobPrepare() throws Exception {
+    getNClob(getPrepare(sharedConn));
+    getNClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getNClob(ResultSet rs) throws Exception {
+    assertStreamEquals(new MariaDbClob("0".getBytes()), rs.getNClob(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getNClob(2));
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getNClob("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getNClob(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNClob(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = get();
+    ResultSetMetaData meta = rs.getMetaData();
+    if (isXpand()) {
+      assertEquals("TEXT", meta.getColumnTypeName(1));
+      assertEquals(Types.VARCHAR, meta.getColumnType(1));
+      assertEquals(65535, meta.getPrecision(1));
+      assertEquals(65535, meta.getColumnDisplaySize(1));
+    } else {
+      assertEquals("CHAR", meta.getColumnTypeName(1));
+      assertEquals(Types.CHAR, meta.getColumnType(1));
+      assertEquals(23, meta.getPrecision(1));
+      assertEquals(23, meta.getColumnDisplaySize(1));
+    }
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.lang.String", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/FloatCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/FloatCodecTest.java
new file mode 100644
index 0000000..11e9816
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/FloatCodecTest.java
@@ -0,0 +1,795 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.RoundingMode;
+import java.sql.*;
+import java.time.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.integration.Common;
+
+public class FloatCodecTest extends CommonCodecTest {
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS FloatCodec");
+    stmt.execute("DROP TABLE IF EXISTS FloatCodec2");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE FloatCodec (t1 FLOAT, t2 FLOAT, t3 FLOAT, t4 FLOAT)");
+    stmt.execute("INSERT INTO FloatCodec VALUES (0, 105.21, -1.6, null)");
+    stmt.execute("CREATE TABLE FloatCodec2 (id int not null primary key auto_increment, t1 FLOAT)");
+    stmt.execute("FLUSH TABLES");
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from FloatCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getPrepare(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from FloatCodec"
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    ResultSet rs = preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws SQLException {
+    getObject(get());
+  }
+
+  @Test
+  public void getObjectPrepare() throws SQLException {
+    getObject(getPrepare(sharedConn));
+    getObject(getPrepare(sharedConnBinary));
+  }
+
+  public void getObject(ResultSet rs) throws SQLException {
+    assertEquals(0f, (float) rs.getObject(1), 0.00001f);
+    assertFalse(rs.wasNull());
+    assertEquals(105.21f, (float) rs.getObject(2), 0.00001f);
+    assertEquals(105.21f, (float) rs.getObject("t2alias"), 0.00001f);
+    assertFalse(rs.wasNull());
+    assertEquals(-1.6f, (float) rs.getObject(3), 0.00001f);
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testObject(rs, Integer.class, 0);
+    //    testObject(rs, String.class, "0");
+    testObject(rs, Long.class, 0L);
+    testObject(rs, Short.class, (short) 0);
+    //    testObject(rs, BigDecimal.class, BigDecimal.valueOf(0));
+    testObject(rs, BigInteger.class, BigInteger.valueOf(0));
+    testObject(rs, Double.class, (double) 0);
+    testObject(rs, Float.class, (float) 0);
+    testObject(rs, Byte.class, (byte) 0);
+    testErrObject(rs, byte[].class);
+    testErrObject(rs, Date.class);
+    testErrObject(rs, Time.class);
+    testErrObject(rs, Timestamp.class);
+    testErrObject(rs, java.util.Date.class);
+    testErrObject(rs, LocalDate.class);
+    testErrObject(rs, ZonedDateTime.class);
+    testErrObject(rs, OffsetDateTime.class);
+    testErrObject(rs, LocalDateTime.class);
+    testErrObject(rs, OffsetTime.class);
+    testObject(rs, Boolean.class, Boolean.FALSE);
+    testErrObject(rs, Clob.class);
+    testErrObject(rs, NClob.class);
+    testErrObject(rs, InputStream.class);
+    testErrObject(rs, Reader.class);
+  }
+
+  @Test
+  public void getString() throws SQLException {
+    getString(get());
+  }
+
+  @Test
+  public void getStringPrepare() throws SQLException {
+    getString(getPrepare(sharedConn));
+    getString(getPrepare(sharedConnBinary));
+  }
+
+  public void getString(ResultSet rs) throws SQLException {
+    String s = rs.getString(1);
+    assertTrue("0".equals(s) || "0.0".equals(s));
+    assertFalse(rs.wasNull());
+    assertEquals("105.21", rs.getString(2));
+    assertEquals("105.21", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("-1.6", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNString() throws SQLException {
+    getNString(get());
+  }
+
+  @Test
+  public void getNStringPrepare() throws SQLException {
+    getNString(getPrepare(sharedConn));
+    getNString(getPrepare(sharedConnBinary));
+  }
+
+  public void getNString(ResultSet rs) throws SQLException {
+    String s = rs.getNString(1);
+    assertTrue("0".equals(s) || "0.0".equals(s));
+    assertFalse(rs.wasNull());
+    assertEquals("105.21", rs.getNString(2));
+    assertEquals("105.21", rs.getNString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("-1.6", rs.getNString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBoolean() throws SQLException {
+    getBoolean(get());
+  }
+
+  @Test
+  public void getBooleanPrepare() throws SQLException {
+    getBoolean(getPrepare(sharedConn));
+    getBoolean(getPrepare(sharedConnBinary));
+  }
+
+  public void getBoolean(ResultSet rs) throws SQLException {
+    assertFalse(rs.getBoolean(1));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(2));
+    assertTrue(rs.getBoolean("t2alias"));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(3));
+    assertFalse(rs.wasNull());
+    assertFalse(rs.getBoolean(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getByte() throws SQLException {
+    getByte(get());
+  }
+
+  @Test
+  public void getBytePrepare() throws SQLException {
+    getByte(getPrepare(sharedConn));
+    getByte(getPrepare(sharedConnBinary));
+  }
+
+  public void getByte(ResultSet rs) throws SQLException {
+    assertEquals((byte) 0, rs.getByte(1));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 105, rs.getByte(2));
+    assertEquals((byte) 105, rs.getByte("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) -1, rs.getByte(3));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 0, rs.getByte(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getShort() throws SQLException {
+    getShort(get());
+  }
+
+  @Test
+  public void getShortPrepare() throws SQLException {
+    getShort(getPrepare(sharedConn));
+    getShort(getPrepare(sharedConnBinary));
+  }
+
+  public void getShort(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getShort(1));
+    assertFalse(rs.wasNull());
+    assertEquals(105, rs.getShort(2));
+    assertEquals(105, rs.getShort("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1, rs.getShort(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getShort(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(get());
+  }
+
+  @Test
+  public void getIntPrepare() throws SQLException {
+    getInt(getPrepare(sharedConn));
+    getInt(getPrepare(sharedConnBinary));
+  }
+
+  public void getInt(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(105, rs.getInt(2));
+    assertEquals(105, rs.getInt("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1, rs.getInt(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(get());
+  }
+
+  @Test
+  public void getLongPrepare() throws SQLException {
+    getLong(getPrepare(sharedConn));
+    getLong(getPrepare(sharedConnBinary));
+  }
+
+  public void getLong(ResultSet rs) throws SQLException {
+    assertEquals(0L, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(105L, rs.getLong(2));
+    assertEquals(105L, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1L, rs.getLong(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0L, rs.getLong(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(get());
+  }
+
+  @Test
+  public void getFloatPrepare() throws SQLException {
+    getFloat(getPrepare(sharedConn));
+    getFloat(getPrepare(sharedConnBinary));
+  }
+
+  public void getFloat(ResultSet rs) throws SQLException {
+    assertEquals(0F, rs.getFloat(1));
+    assertFalse(rs.wasNull());
+    assertEquals(105.21F, rs.getFloat(2));
+    assertEquals(105.21F, rs.getFloat("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1.6F, rs.getFloat(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0F, rs.getFloat(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(get());
+  }
+
+  @Test
+  public void getDoublePrepare() throws SQLException {
+    getDouble(getPrepare(sharedConn));
+    getDouble(getPrepare(sharedConnBinary));
+  }
+
+  public void getDouble(ResultSet rs) throws SQLException {
+    assertEquals(0D, rs.getDouble(1));
+    assertFalse(rs.wasNull());
+    assertEquals(105.21D, rs.getDouble(2), 0.01D);
+    assertEquals(105.21D, rs.getDouble("t2alias"), 0.01D);
+    assertFalse(rs.wasNull());
+    assertEquals(-1.6D, rs.getDouble(3), 0.01D);
+    assertFalse(rs.wasNull());
+    assertEquals(0D, rs.getDouble(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(get());
+  }
+
+  @Test
+  public void getBigDecimalPrepare() throws SQLException {
+    getBigDecimal(getPrepare(sharedConn));
+    getBigDecimal(getPrepare(sharedConnBinary));
+  }
+
+  public void getBigDecimal(ResultSet rs) throws SQLException {
+    BigDecimal b = rs.getBigDecimal(1);
+    assertTrue(BigDecimal.ZERO.equals(b) || new BigDecimal("0.0").equals(b));
+    assertFalse(rs.wasNull());
+    assertEquals(new BigDecimal("105.21"), rs.getBigDecimal(2).setScale(2, RoundingMode.HALF_DOWN));
+    assertEquals(
+        new BigDecimal("105.21"), rs.getBigDecimal("t2alias").setScale(2, RoundingMode.HALF_DOWN));
+    assertFalse(rs.wasNull());
+    assertEquals(new BigDecimal("-1.6"), rs.getBigDecimal(3).setScale(1, RoundingMode.HALF_DOWN));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(get());
+  }
+
+  @Test
+  public void getDatePrepare() throws SQLException {
+    getDate(getPrepare(sharedConn));
+    getDate(getPrepare(sharedConnBinary));
+  }
+
+  public void getDate(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getDate(1), "Data type FLOAT cannot be decoded as Date");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDate("t1alias"),
+        "Data type FLOAT cannot be decoded as Date");
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(get());
+  }
+
+  @Test
+  public void getTimePrepare() throws SQLException {
+    getTime(getPrepare(sharedConn));
+    getTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getTime(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getTime(1), "Data type FLOAT cannot be decoded as Time");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime("t1alias"),
+        "Data type FLOAT cannot be decoded as Time");
+  }
+
+  @Test
+  public void getTimestamp() throws SQLException {
+    getTimestamp(get());
+  }
+
+  @Test
+  public void getTimestampPrepare() throws SQLException {
+    getTimestamp(getPrepare(sharedConn));
+    getTimestamp(getPrepare(sharedConnBinary));
+  }
+
+  public void getTimestamp(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTimestamp(1),
+        "Data type FLOAT cannot be decoded as Timestamp");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTimestamp("t1alias"),
+        "Data type FLOAT cannot be decoded as Timestamp");
+  }
+
+  @Test
+  public void getOffsetDateTime() throws SQLException {
+    getOffsetDateTime(get());
+  }
+
+  @Test
+  public void getOffsetDateTimePrepare() throws SQLException {
+    getOffsetDateTime(getPrepare(sharedConn));
+    getOffsetDateTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getOffsetDateTime(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(1, OffsetDateTime.class),
+        "Type class java.time.OffsetDateTime not supported type for FLOAT type");
+  }
+
+  @Test
+  public void getAsciiStream() throws SQLException {
+    getAsciiStream(get());
+  }
+
+  @Test
+  public void getAsciiStreamPrepare() throws SQLException {
+    getAsciiStream(getPrepare(sharedConn));
+    getAsciiStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getAsciiStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getAsciiStream(1),
+        "Data type FLOAT cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getAsciiStream("t1alias"),
+        "Data type FLOAT cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getUnicodeStream() throws SQLException {
+    getUnicodeStream(get());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepare() throws SQLException {
+    getUnicodeStream(getPrepare(sharedConn));
+    getUnicodeStream(getPrepare(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  public void getUnicodeStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getUnicodeStream(1),
+        "Data type FLOAT cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getUnicodeStream("t1alias"),
+        "Data type FLOAT cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBinaryStream() throws SQLException {
+    getBinaryStream(get());
+  }
+
+  @Test
+  public void getBinaryStreamPrepare() throws SQLException {
+    getBinaryStream(getPrepare(sharedConn));
+    getBinaryStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getBinaryStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBinaryStream(1),
+        "Data type FLOAT cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBinaryStream("t1alias"),
+        "Data type FLOAT cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBytes() throws SQLException {
+    getBytes(get());
+  }
+
+  @Test
+  public void getBytesPrepare() throws SQLException {
+    getBytes(getPrepare(sharedConn));
+    getBytes(getPrepare(sharedConnBinary));
+  }
+
+  public void getBytes(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBytes(1),
+        "Data type FLOAT cannot be decoded as byte[]");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBytes("t1alias"),
+        "Data type FLOAT cannot be decoded as byte[]");
+  }
+
+  @Test
+  public void getCharacterStream() throws SQLException {
+    getCharacterStream(get());
+  }
+
+  @Test
+  public void getCharacterStreamPrepare() throws SQLException {
+    getCharacterStream(getPrepare(sharedConn));
+    getCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type FLOAT cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream("t1alias"),
+        "Data type FLOAT cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getNCharacterStream() throws SQLException {
+    getNCharacterStream(get());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepare() throws SQLException {
+    getNCharacterStream(getPrepare(sharedConn));
+    getNCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getNCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream(1),
+        "Data type FLOAT cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream("t2alias"),
+        "Data type FLOAT cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getRef() throws SQLException {
+    ResultSet rs = get();
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getRef(1), "Method ResultSet.getRef not supported");
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getRef("t2alias"), "Method ResultSet.getRef not supported");
+  }
+
+  @Test
+  public void getBlob() throws SQLException {
+    getBlob(get());
+  }
+
+  @Test
+  public void getBlobPrepare() throws SQLException {
+    getBlob(getPrepare(sharedConn));
+    getBlob(getPrepare(sharedConnBinary));
+  }
+
+  public void getBlob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type FLOAT cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream("t1alias"),
+        "Data type FLOAT cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getClob() throws SQLException {
+    getClob(get());
+  }
+
+  @Test
+  public void getClobPrepare() throws SQLException {
+    getClob(getPrepare(sharedConn));
+    getClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getClob(1), "Data type FLOAT cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getClob("t1alias"),
+        "Data type FLOAT cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getNClob() throws SQLException {
+    getNClob(get());
+  }
+
+  @Test
+  public void getNClobPrepare() throws SQLException {
+    getNClob(getPrepare(sharedConn));
+    getNClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getNClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getNClob(1), "Data type FLOAT cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNClob("t1alias"),
+        "Data type FLOAT cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getArray() throws SQLException {
+    ResultSet rs = get();
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getArray(1), "Method ResultSet.getArray not supported");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getArray("t1alias"),
+        "Method ResultSet.getArray not supported");
+  }
+
+  @Test
+  public void getURL() throws SQLException {
+    getURL(get());
+  }
+
+  @Test
+  public void getURLPrepare() throws SQLException {
+    getURL(getPrepare(sharedConn));
+    getURL(getPrepare(sharedConnBinary));
+  }
+
+  public void getURL(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLSyntaxErrorException.class, () -> rs.getURL(1), "Could not parse");
+    Common.assertThrowsContains(
+        SQLSyntaxErrorException.class, () -> rs.getURL("t1alias"), "Could not parse");
+  }
+
+  @Test
+  public void getSQLXML() throws SQLException {
+    ResultSet rs = get();
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getSQLXML(1), "Method ResultSet.getSQLXML not supported");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getSQLXML("t1alias"),
+        "Method ResultSet.getSQLXML not supported");
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = get();
+    ResultSetMetaData meta = rs.getMetaData();
+    assertEquals("FLOAT", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.lang.Float", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.REAL, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    // https://jira.mariadb.org/browse/XPT-276
+    if (!isXpand()) {
+      assertEquals(12, meta.getPrecision(1));
+      assertEquals(12, meta.getColumnDisplaySize(1));
+      assertEquals(31, meta.getScale(1));
+    }
+    assertEquals("", meta.getSchemaName(1));
+  }
+
+  @Test
+  public void sendParam() throws SQLException {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE FloatCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep = con.prepareStatement("INSERT INTO FloatCodec2(t1) VALUES (?)")) {
+      prep.setFloat(1, 1.1F);
+      prep.execute();
+      prep.setObject(1, 2.1F);
+      prep.execute();
+      prep.setObject(1, null);
+      prep.execute();
+      prep.setObject(1, 3.1F, Types.DECIMAL);
+      prep.execute();
+      prep.setObject(1, "3.2", Types.REAL);
+      prep.execute();
+      prep.setObject(1, 3.3F, Types.REAL);
+      prep.execute();
+      prep.setObject(1, null, Types.DECIMAL);
+      prep.execute();
+    }
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM FloatCodec2");
+    assertTrue(rs.next());
+    assertEquals(1.1F, rs.getFloat(2));
+    rs.updateFloat(2, 4.4f);
+    rs.updateRow();
+    assertEquals(4.4F, rs.getFloat(2));
+
+    assertTrue(rs.next());
+    assertEquals(2.1F, rs.getFloat(2));
+    rs.updateFloat("t1", 5.4f);
+    rs.updateRow();
+    assertEquals(5.4F, rs.getFloat(2));
+
+    assertTrue(rs.next());
+    assertEquals(0F, rs.getFloat(2));
+    assertTrue(rs.wasNull());
+    rs.updateObject(2, 5.1F, Types.DECIMAL);
+    rs.updateRow();
+    assertEquals(5.1F, rs.getFloat(2));
+    assertFalse(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(3.1F, rs.getFloat(2));
+    rs.updateObject("t1", null, Types.DECIMAL);
+    rs.updateRow();
+    assertEquals(0F, rs.getFloat(2));
+    assertTrue(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(3.2F, rs.getFloat(2));
+    assertTrue(rs.next());
+    assertEquals(3.3F, rs.getFloat(2));
+
+    assertTrue(rs.next());
+    assertEquals(0F, rs.getFloat(2));
+    assertTrue(rs.wasNull());
+
+    rs = stmt.executeQuery("SELECT * FROM FloatCodec2");
+    assertTrue(rs.next());
+    assertEquals(4.4F, rs.getFloat(2));
+
+    assertTrue(rs.next());
+    assertEquals(5.4F, rs.getFloat(2));
+
+    assertTrue(rs.next());
+    assertEquals(5.1F, rs.getFloat(2));
+    assertFalse(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(0F, rs.getFloat(2));
+    assertTrue(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(3.2F, rs.getFloat(2));
+    assertTrue(rs.next());
+    assertEquals(3.3F, rs.getFloat(2));
+
+    assertTrue(rs.next());
+    assertEquals(0F, rs.getFloat(2));
+    assertTrue(rs.wasNull());
+    con.commit();
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/GeometryCollectionCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/GeometryCollectionCodecTest.java
new file mode 100644
index 0000000..548a8ef
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/GeometryCollectionCodecTest.java
@@ -0,0 +1,361 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.type.*;
+import org.mariadb.jdbc.util.constants.Capabilities;
+
+public class GeometryCollectionCodecTest extends CommonCodecTest {
+  public static org.mariadb.jdbc.Connection geoConn;
+
+  final GeometryCollection geo1 =
+      new GeometryCollection(
+          new Geometry[] {
+            new Point(0, 0),
+            new LineString(
+                new Point[] {
+                  new Point(10, 10),
+                  new Point(20, 10),
+                  new Point(20, 20),
+                  new Point(10, 20),
+                  new Point(10, 10)
+                },
+                true)
+          });
+  final GeometryCollection geo2 =
+      new GeometryCollection(
+          new Geometry[] {
+            new Polygon(
+                new LineString[] {
+                  new LineString(
+                      new Point[] {
+                        new Point(0, 0),
+                        new Point(50, 0),
+                        new Point(50, 50),
+                        new Point(0, 50),
+                        new Point(0, 0)
+                      },
+                      false),
+                  new LineString(
+                      new Point[] {
+                        new Point(10, 10),
+                        new Point(20, 10),
+                        new Point(20, 20),
+                        new Point(10, 20),
+                        new Point(10, 10)
+                      },
+                      false)
+                }),
+            new MultiPoint(new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0)})
+          });
+  final GeometryCollection geo3 =
+      new GeometryCollection(
+          new Geometry[] {
+            new MultiLineString(
+                new LineString[] {
+                  new LineString(
+                      new Point[] {
+                        new Point(0, 0), new Point(50, 0), new Point(50, 50), new Point(0, 50)
+                      },
+                      true),
+                  new LineString(
+                      new Point[] {
+                        new Point(10, 10), new Point(20, 10), new Point(20, 20), new Point(10, 20)
+                      },
+                      true)
+                }),
+            new MultiPolygon(
+                new Polygon[] {
+                  new Polygon(
+                      new LineString[] {
+                        new LineString(
+                            new Point[] {
+                              new Point(1, 1),
+                              new Point(1, 8),
+                              new Point(4, 9),
+                              new Point(6, 9),
+                              new Point(9, 3),
+                              new Point(7, 2),
+                              new Point(1, 1)
+                            },
+                            false),
+                      })
+                })
+          });
+
+  @BeforeAll
+  public static void beforeAll2() throws Exception {
+    drop();
+    // xpand doesn't recognized GeometryCollection
+    Assumptions.assumeFalse(isXpand());
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE GeometryCollectionCodec (t1 GeometryCollection, t2 GeometryCollection, t3"
+            + " GeometryCollection, t4 GeometryCollection)");
+    stmt.execute(
+        "INSERT INTO GeometryCollectionCodec VALUES (ST_GeomFromText('GeometryCollection(POINT (0"
+            + " 0), LINESTRING(10 10,20 10,20 20,10 20,10 10))'),"
+            + " ST_GeomFromText('GeometryCollection(POLYGON((0 0,50 0,50 50,0 50,0 0), (10 10,20"
+            + " 10,20 20,10 20,10 10)), MULTIPOINT(0 0,0 10,10 0))'),"
+            + " ST_GeomFromText('GeometryCollection(MULTILINESTRING((0 0,50 0,50 50,0 50), (10"
+            + " 10,20 10,20 20,10 20)), MULTIPOLYGON(((1 1, 1 8,4 9,6 9,9 3,7 2, 1 1))))'), null)");
+    stmt.execute(
+        "CREATE TABLE GeometryCollectionCodec2 (id int not null primary key auto_increment, t1"
+            + " GeometryCollection)");
+    stmt.execute("FLUSH TABLES");
+    String binUrl =
+        mDefUrl + (mDefUrl.indexOf("?") > 0 ? "&" : "?") + "geometryDefaultType=default";
+    geoConn = (org.mariadb.jdbc.Connection) DriverManager.getConnection(binUrl);
+  }
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS GeometryCollectionCodec");
+    stmt.execute("DROP TABLE IF EXISTS GeometryCollectionCodec2");
+    if (geoConn != null) geoConn.close();
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from"
+                + " GeometryCollectionCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private CompleteResult getPrepare(org.mariadb.jdbc.Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from"
+                + " GeometryCollectionCodec WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    CompleteResult rs = (CompleteResult) preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws Exception {
+    getObject(get(), false);
+  }
+
+  @Test
+  public void getObjectPrepare() throws Exception {
+    getObject(getPrepare(sharedConn), false);
+    getObject(getPrepare(sharedConnBinary), false);
+    getObject(getPrepare(geoConn), true);
+  }
+
+  public void getObject(ResultSet rs, boolean defaultGeo) throws SQLException {
+    if (defaultGeo && hasCapability(Capabilities.EXTENDED_TYPE_INFO)) {
+      assertEquals(geo1, rs.getObject(1));
+      assertFalse(rs.wasNull());
+      assertEquals(geo2, rs.getObject(2));
+      assertFalse(rs.wasNull());
+      assertEquals(geo3, rs.getObject(3));
+      assertFalse(rs.wasNull());
+      assertNull(rs.getObject(4));
+      assertTrue(rs.wasNull());
+    } else {
+      assertEquals(geo1, rs.getObject(1, GeometryCollection.class));
+      assertFalse(rs.wasNull());
+      assertEquals(geo2, rs.getObject(2, GeometryCollection.class));
+      assertFalse(rs.wasNull());
+      assertEquals(geo3, rs.getObject(3, GeometryCollection.class));
+      assertFalse(rs.wasNull());
+      assertNull(rs.getObject(4));
+      assertTrue(rs.wasNull());
+    }
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testErrObject(rs, Integer.class, 1);
+    testErrObject(rs, String.class, 1);
+    testErrObject(rs, Long.class, 1);
+    testErrObject(rs, Short.class, 1);
+    testErrObject(rs, BigDecimal.class, 1);
+    testErrObject(rs, BigInteger.class, 1);
+    testErrObject(rs, LineString.class, 1);
+    testErrObject(rs, Point.class, 1);
+    testErrObject(rs, Polygon.class, 1);
+    testErrObject(rs, MultiLineString.class, 1);
+    testErrObject(rs, MultiPoint.class, 1);
+    testErrObject(rs, MultiPolygon.class, 1);
+    testErrObject(rs, Double.class, 1);
+    testErrObject(rs, Float.class, 1);
+    testErrObject(rs, Byte.class, 1);
+
+    String hexa =
+        "000000000107000000020000000101000000000000000000000000000000000000000102000000050000000000000000002440000000000000244000000000000034400000000000002440000000000000344000000000000034400000000000002440000000000000344000000000000024400000000000002440";
+    testArrObject(rs, MultiPolygonCodecTest.decodeHexString(hexa), 1);
+
+    testErrObject(rs, Boolean.class, 1);
+    testErrObject(rs, Clob.class, 1);
+    testErrObject(rs, NClob.class, 1);
+    testErrObject(rs, InputStream.class, 1);
+    testErrObject(rs, Reader.class, 1);
+    testErrObject(rs, java.util.Date.class, 1);
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    getMetaData(sharedConn, false);
+    try (org.mariadb.jdbc.Connection con = createCon("geometryDefaultType=default")) {
+      getMetaData(con, true);
+    }
+  }
+
+  private void getMetaData(org.mariadb.jdbc.Connection con, boolean geoDefault)
+      throws SQLException {
+    ResultSet rs = getPrepare(con);
+    ResultSetMetaData meta = rs.getMetaData();
+    if (hasCapability(Capabilities.EXTENDED_TYPE_INFO)) {
+      assertEquals("GEOMETRYCOLLECTION", meta.getColumnTypeName(1));
+    } else {
+      assertEquals("GEOMETRY", meta.getColumnTypeName(1));
+    }
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals(
+        geoDefault ? GeometryCollection.class.getName() : "byte[]", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.VARBINARY, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+  }
+
+  @Test
+  public void sendParam() throws Exception {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws Exception {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE GeometryCollectionCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO GeometryCollectionCodec2(t1) VALUES (?)")) {
+      prep.setObject(1, geo1);
+      prep.execute();
+      prep.setObject(1, (GeometryCollection) null);
+      prep.execute();
+
+      prep.setObject(1, geo2);
+      prep.addBatch();
+      prep.setObject(1, geo3);
+      prep.addBatch();
+      prep.executeBatch();
+    }
+
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM GeometryCollectionCodec2");
+    assertTrue(rs.next());
+    assertEquals(geo1, rs.getObject(2, GeometryCollection.class));
+    rs.updateNull(2);
+    rs.updateRow();
+    assertNull(rs.getObject(2, GeometryCollection.class));
+
+    assertTrue(rs.next());
+    assertNull(rs.getObject(2, GeometryCollection.class));
+    rs.updateObject(2, geo2);
+    rs.updateRow();
+    assertEquals(geo2, rs.getObject(2, GeometryCollection.class));
+    assertTrue(rs.next());
+
+    assertEquals(geo2, rs.getObject(2, GeometryCollection.class));
+    assertTrue(rs.next());
+    assertEquals(geo3, rs.getObject(2, GeometryCollection.class));
+    con.commit();
+  }
+
+  @Test
+  public void equal() {
+    assertEquals(geo1, geo1);
+    assertEquals(
+        new GeometryCollection(
+            new Geometry[] {
+              new Point(0, 0),
+              new LineString(
+                  new Point[] {
+                    new Point(10, 10),
+                    new Point(20, 10),
+                    new Point(20, 20),
+                    new Point(10, 20),
+                    new Point(10, 10)
+                  },
+                  true)
+            }),
+        geo1);
+    assertEquals(
+        new GeometryCollection(
+                new Geometry[] {
+                  new Point(0, 0),
+                  new LineString(
+                      new Point[] {
+                        new Point(10, 10),
+                        new Point(20, 10),
+                        new Point(20, 20),
+                        new Point(10, 20),
+                        new Point(10, 10)
+                      },
+                      true)
+                })
+            .hashCode(),
+        geo1.hashCode());
+    assertFalse(geo1.equals(null));
+    assertFalse(geo1.equals(""));
+    assertNotEquals(
+        new GeometryCollection(
+            new Geometry[] {
+              new Point(0, 10),
+              new LineString(
+                  new Point[] {
+                    new Point(10, 10),
+                    new Point(20, 10),
+                    new Point(20, 20),
+                    new Point(10, 20),
+                    new Point(10, 10)
+                  },
+                  true)
+            }),
+        geo1);
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/IntCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/IntCodecTest.java
new file mode 100644
index 0000000..ea91e5b
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/IntCodecTest.java
@@ -0,0 +1,1083 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import java.time.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.integration.Common;
+
+public class IntCodecTest extends CommonCodecTest {
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS IntCodec");
+    stmt.execute("DROP TABLE IF EXISTS IntCodecUnsigned");
+    stmt.execute("DROP TABLE IF EXISTS IntCodec2");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE IntCodec (t1 int, t2 int, t3 int, t4 int)");
+    stmt.execute(
+        "CREATE TABLE IntCodecUnsigned (t1 INT UNSIGNED, t2 INT UNSIGNED, t3 INT UNSIGNED, t4 INT "
+            + "UNSIGNED)");
+    stmt.execute("INSERT INTO IntCodec VALUES (0, 1, -1, null)");
+    stmt.execute("INSERT INTO IntCodecUnsigned VALUES (0, 1, 4294967295, null)");
+    stmt.execute("CREATE TABLE IntCodec2 (id int not null primary key auto_increment, t1 int)");
+    stmt.execute("FLUSH TABLES");
+  }
+
+  private ResultSet getSigned() throws SQLException {
+    return get("IntCodec");
+  }
+
+  private ResultSet getUnsigned() throws SQLException {
+    return get("IntCodecUnsigned");
+  }
+
+  private ResultSet get(String table) throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from " + table);
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getPreparedSigned(Connection con) throws SQLException {
+    return getPrepare(con, "IntCodec");
+  }
+
+  private ResultSet getPreparedUnsigned(Connection con) throws SQLException {
+    return getPrepare(con, "IntCodecUnsigned");
+  }
+
+  private ResultSet getPrepare(Connection con, String table) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4"
+                + " as t4alias from "
+                + table
+                + " WHERE 1 > ?")) {
+      preparedStatement.setInt(1, 0);
+      ResultSet rs = preparedStatement.executeQuery();
+      assertTrue(rs.next());
+      return rs;
+    } finally {
+      con.commit();
+    }
+  }
+
+  @Test
+  public void getObject() throws SQLException {
+    getObject(getSigned());
+  }
+
+  @Test
+  public void getObjectPrepared() throws SQLException {
+    getObject(getPreparedSigned(sharedConn));
+    getObject(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getObject(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getObject(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getObject(2));
+    assertEquals(1, rs.getObject("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1, rs.getObject(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4));
+    assertTrue(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(4, int.class),
+        "Cannot return null for primitive int");
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(getSigned());
+  }
+
+  @Test
+  public void getObjectTypePrepared() throws Exception {
+    getObjectType(getPreparedSigned(sharedConn));
+    getObjectType(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getObjectType(ResultSet rs) throws Exception {
+    testObject(rs, Integer.class, 0);
+    testObject(rs, String.class, "0");
+    testObject(rs, Long.class, 0L);
+    testObject(rs, Short.class, (short) 0);
+    testObject(rs, BigDecimal.class, BigDecimal.valueOf(0));
+    testObject(rs, BigInteger.class, BigInteger.valueOf(0));
+    testObject(rs, Double.class, (double) 0);
+    testObject(rs, Float.class, (float) 0);
+    testObject(rs, Byte.class, (byte) 0);
+    testErrObject(rs, byte[].class);
+    testErrObject(rs, Date.class);
+    testErrObject(rs, Time.class);
+    testErrObject(rs, Timestamp.class);
+    testErrObject(rs, java.util.Date.class);
+    testErrObject(rs, LocalDate.class);
+    testErrObject(rs, ZonedDateTime.class);
+    testErrObject(rs, OffsetDateTime.class);
+    testErrObject(rs, LocalDateTime.class);
+    testErrObject(rs, OffsetTime.class);
+    testObject(rs, Boolean.class, Boolean.FALSE);
+    testErrObject(rs, Clob.class);
+    testErrObject(rs, NClob.class);
+    testErrObject(rs, InputStream.class);
+    testErrObject(rs, Reader.class);
+    rs.next();
+  }
+
+  @Test
+  public void getString() throws SQLException {
+    getString(getSigned());
+  }
+
+  @Test
+  public void getStringPrepared() throws SQLException {
+    getString(getPreparedSigned(sharedConn));
+    getString(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getString(2));
+    assertEquals("1", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("-1", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getStringUnsigned() throws SQLException {
+    getStringUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getStringUnsignedPrepared() throws SQLException {
+    getStringUnsigned(getPreparedUnsigned(sharedConn));
+    getStringUnsigned(getPreparedUnsigned(sharedConnBinary));
+  }
+
+  private void getStringUnsigned(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getString(2));
+    assertEquals("1", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("4294967295", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNString() throws SQLException {
+    getNString(getSigned());
+  }
+
+  @Test
+  public void getNStringPrepared() throws SQLException {
+    getNString(getPreparedSigned(sharedConn));
+    getNString(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getNString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getNString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getNString(2));
+    assertEquals("1", rs.getNString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("-1", rs.getNString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBoolean() throws SQLException {
+    getBoolean(getSigned());
+  }
+
+  @Test
+  public void getBooleanPrepared() throws SQLException {
+    getBoolean(getPreparedSigned(sharedConn));
+    getBoolean(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getBoolean(ResultSet rs) throws SQLException {
+    assertFalse(rs.getBoolean(1));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(2));
+    assertTrue(rs.getBoolean("t2alias"));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(3));
+    assertFalse(rs.wasNull());
+    assertFalse(rs.getBoolean(4));
+    assertFalse(rs.getBoolean("t4alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getByte() throws SQLException {
+    getByte(getSigned());
+  }
+
+  @Test
+  public void getBytePrepared() throws SQLException {
+    getByte(getPreparedSigned(sharedConn));
+    getByte(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getByte(ResultSet rs) throws SQLException {
+    assertEquals((byte) 0, rs.getByte(1));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 1, rs.getByte(2));
+    assertEquals((byte) 1, rs.getByte("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) -1, rs.getByte(3));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 0, rs.getByte(4));
+    assertEquals((byte) 0, rs.getByte("t4alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getUnsignedByte() throws SQLException {
+    getUnsignedByte(getUnsigned());
+  }
+
+  @Test
+  public void getUnsignedBytePrepared() throws SQLException {
+    getUnsignedByte(getPreparedUnsigned(sharedConn));
+    getUnsignedByte(getPreparedUnsigned(sharedConnBinary));
+  }
+
+  private void getUnsignedByte(ResultSet rs) throws SQLException {
+    assertEquals((byte) 0, rs.getByte(1));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 1, rs.getByte(2));
+    assertEquals((byte) 1, rs.getByte("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(SQLDataException.class, () -> rs.getByte(3), "byte overflow");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getShort() throws SQLException {
+    getShort(getSigned());
+  }
+
+  @Test
+  public void getShortPrepared() throws SQLException {
+    getShort(getPreparedSigned(sharedConn));
+    getShort(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getShort(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getShort(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getShort(2));
+    assertEquals(1, rs.getShort("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1, rs.getShort(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getShort(4));
+    assertEquals(0, rs.getShort("t4alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(getSigned());
+  }
+
+  @Test
+  public void getIntPrepared() throws SQLException {
+    getInt(getPreparedSigned(sharedConn));
+    getInt(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getInt(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getInt(2));
+    assertEquals(1, rs.getInt("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1, rs.getInt(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getIntUnsigned() throws SQLException {
+    getIntUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getIntUnsignedPrepared() throws SQLException {
+    getIntUnsigned(getPreparedUnsigned(sharedConn));
+    getIntUnsigned(getPreparedUnsigned(sharedConnBinary));
+  }
+
+  private void getIntUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getInt(2));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(SQLDataException.class, () -> rs.getInt(3), "integer overflow");
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(getSigned());
+  }
+
+  @Test
+  public void getLongPrepared() throws SQLException {
+    getLong(getPreparedSigned(sharedConn));
+    getLong(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getLong(ResultSet rs) throws SQLException {
+    assertEquals(0L, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1L, rs.getLong(2));
+    assertEquals(1L, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1L, rs.getLong(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0L, rs.getLong(4));
+    assertEquals(0L, rs.getLong("t4alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLongUnsigned() throws SQLException {
+    getLongUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getLongUnsignedPrepared() throws SQLException {
+    getLongUnsigned(getPreparedUnsigned(sharedConn));
+    getLongUnsigned(getPreparedUnsigned(sharedConnBinary));
+  }
+
+  private void getLongUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0L, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1L, rs.getLong(2));
+    assertEquals(1L, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(4294967295L, rs.getLong(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0L, rs.getLong(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(getSigned());
+  }
+
+  @Test
+  public void getFloatPrepared() throws SQLException {
+    getFloat(getPreparedSigned(sharedConn));
+    getFloat(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getFloat(ResultSet rs) throws SQLException {
+    assertEquals(0F, rs.getFloat(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1F, rs.getFloat(2));
+    assertEquals(1F, rs.getFloat("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1F, rs.getFloat(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0F, rs.getFloat(4));
+    assertEquals(0F, rs.getFloat("t4alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloatUnsigned() throws SQLException {
+    getFloatUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getFloatUnsignedPrepared() throws SQLException {
+    getFloatUnsigned(getPreparedUnsigned(sharedConn));
+    getFloatUnsigned(getPreparedUnsigned(sharedConnBinary));
+  }
+
+  private void getFloatUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0F, rs.getFloat(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1F, rs.getFloat(2));
+    assertEquals(1F, rs.getFloat("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(4294967295F, rs.getFloat(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0F, rs.getFloat(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(getSigned());
+  }
+
+  @Test
+  public void getDoublePrepared() throws SQLException {
+    getDouble(getPreparedSigned(sharedConn));
+    getDouble(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getDouble(ResultSet rs) throws SQLException {
+    assertEquals(0D, rs.getDouble(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1D, rs.getDouble(2));
+    assertEquals(1D, rs.getDouble("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1D, rs.getDouble(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0D, rs.getDouble(4));
+    assertEquals(0D, rs.getDouble("t4alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDoubleUnsigned() throws SQLException {
+    getDoubleUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getDoubleUnsignedPrepared() throws SQLException {
+    getDoubleUnsigned(getPreparedUnsigned(sharedConn));
+    getDoubleUnsigned(getPreparedUnsigned(sharedConnBinary));
+  }
+
+  private void getDoubleUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0D, rs.getDouble(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1D, rs.getDouble(2));
+    assertEquals(1D, rs.getDouble("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(4294967295D, rs.getDouble(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0D, rs.getDouble(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(getSigned());
+  }
+
+  @Test
+  public void getBigDecimalPrepared() throws SQLException {
+    getBigDecimal(getPreparedSigned(sharedConn));
+    getBigDecimal(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getBigDecimal(ResultSet rs) throws SQLException {
+    assertEquals(BigDecimal.ZERO, rs.getBigDecimal(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal(2));
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.valueOf(-1), rs.getBigDecimal(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertNull(rs.getBigDecimal("t4alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimalUnsigned() throws SQLException {
+    getBigDecimalUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getBigDecimalUnsignedPrepared() throws SQLException {
+    getBigDecimalUnsigned(getPreparedUnsigned(sharedConn));
+    getBigDecimalUnsigned(getPreparedUnsigned(sharedConnBinary));
+  }
+
+  private void getBigDecimalUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(BigDecimal.ZERO, rs.getBigDecimal(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal(2));
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.valueOf(4294967295L), rs.getBigDecimal(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigInteger() throws SQLException {
+    getBigInteger(getSigned());
+  }
+
+  @Test
+  public void getBigIntegerPrepared() throws SQLException {
+    getBigInteger(getPreparedSigned(sharedConn));
+    getBigInteger(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getBigInteger(ResultSet res) throws SQLException {
+    CompleteResult rs = (CompleteResult) res;
+    assertEquals(BigInteger.ZERO, rs.getBigInteger(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigInteger.ONE, rs.getBigInteger(2));
+    assertEquals(BigInteger.ONE, rs.getBigInteger("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(BigInteger.valueOf(-1), rs.getBigInteger(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigInteger(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigIntegerUnsigned() throws SQLException {
+    getBigIntegerUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getBigIntegerUnsignedPrepared() throws SQLException {
+    getBigIntegerUnsigned(getPreparedUnsigned(sharedConn));
+    getBigIntegerUnsigned(getPreparedUnsigned(sharedConnBinary));
+  }
+
+  private void getBigIntegerUnsigned(ResultSet res) throws SQLException {
+    CompleteResult rs = (CompleteResult) res;
+    assertEquals(BigInteger.ZERO, rs.getBigInteger(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigInteger.ONE, rs.getBigInteger(2));
+    assertEquals(BigInteger.ONE, rs.getBigInteger("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(BigInteger.valueOf(4294967295L), rs.getBigInteger(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigInteger(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDuration() throws SQLException {
+    getDuration(getSigned());
+  }
+
+  @Test
+  public void getDurationPrepare() throws SQLException {
+    getDuration(getPreparedSigned(sharedConn));
+    getDuration(getPreparedSigned(sharedConnBinary));
+  }
+
+  public void getDuration(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(1, Duration.class),
+        "Type class java.time.Duration not supported type for INTEGER type");
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(getSigned());
+  }
+
+  @Test
+  public void getDatePrepared() throws SQLException {
+    getDate(getPreparedSigned(sharedConn));
+    getDate(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getDate(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getDate(1), "Data type INTEGER cannot be decoded as Date");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDate("t1alias"),
+        "Data type INTEGER cannot be decoded as Date");
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(getSigned());
+  }
+
+  @Test
+  public void getTimePrepared() throws SQLException {
+    getTime(getPreparedSigned(sharedConn));
+    getTime(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getTime(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getTime(1), "Data type INTEGER cannot be decoded as Time");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime("t1alias"),
+        "Data type INTEGER cannot be decoded as Time");
+  }
+
+  @Test
+  public void getTimestamp() throws SQLException {
+    getTimestamp(getSigned());
+  }
+
+  @Test
+  public void getTimestampPrepared() throws SQLException {
+    getTimestamp(getPreparedSigned(sharedConn));
+    getTimestamp(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getTimestamp(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTimestamp(1),
+        "Data type INTEGER cannot be decoded as Timestamp");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTimestamp("t1alias"),
+        "Data type INTEGER cannot be decoded as Timestamp");
+  }
+
+  @Test
+  public void getAsciiStream() throws SQLException {
+    getAsciiStream(getSigned());
+  }
+
+  @Test
+  public void getAsciiStreamPrepared() throws SQLException {
+    getAsciiStream(getPreparedSigned(sharedConn));
+    getAsciiStream(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getAsciiStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getAsciiStream(1),
+        "Data type INTEGER cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getAsciiStream("t1alias"),
+        "Data type INTEGER cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getUnicodeStream() throws SQLException {
+    getUnicodeStream(getSigned());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepared() throws SQLException {
+    getUnicodeStream(getPreparedSigned(sharedConn));
+    getUnicodeStream(getPreparedSigned(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  private void getUnicodeStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getUnicodeStream(1),
+        "Data type INTEGER cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getUnicodeStream("t1alias"),
+        "Data type INTEGER cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBinaryStream() throws SQLException {
+    getBinaryStream(getSigned());
+  }
+
+  @Test
+  public void getBinaryStreamPrepared() throws SQLException {
+    getBinaryStream(getPreparedSigned(sharedConn));
+    getBinaryStream(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getBinaryStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBinaryStream(1),
+        "Data type INTEGER cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBinaryStream("t1alias"),
+        "Data type INTEGER cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBytes() throws SQLException {
+    getBytes(getSigned());
+  }
+
+  @Test
+  public void getBytesPrepared() throws SQLException {
+    getBytes(getPreparedSigned(sharedConn));
+    getBytes(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getBytes(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBytes(1),
+        "Data type INTEGER cannot be decoded as byte[]");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBytes("t1alias"),
+        "Data type INTEGER cannot be decoded as byte[]");
+  }
+
+  @Test
+  public void getCharacterStream() throws SQLException {
+    getCharacterStream(getSigned());
+  }
+
+  @Test
+  public void getCharacterStreamPrepared() throws SQLException {
+    getCharacterStream(getPreparedSigned(sharedConn));
+    getCharacterStream(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type INTEGER cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream("t1alias"),
+        "Data type INTEGER cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getNCharacterStream() throws SQLException {
+    getNCharacterStream(getSigned());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepared() throws SQLException {
+    getNCharacterStream(getPreparedSigned(sharedConn));
+    getNCharacterStream(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getNCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream(1),
+        "Data type INTEGER cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream("t2alias"),
+        "Data type INTEGER cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getRef() throws SQLException {
+    getRef(getSigned());
+  }
+
+  @Test
+  public void getRefPrepared() throws SQLException {
+    getRef(getPreparedSigned(sharedConn));
+    getRef(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getRef(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getRef(1), "Method ResultSet.getRef not supported");
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getRef("t2alias"), "Method ResultSet.getRef not supported");
+  }
+
+  @Test
+  public void getBlob() throws SQLException {
+    getBlob(getSigned());
+  }
+
+  @Test
+  public void getBlobPrepared() throws SQLException {
+    getBlob(getPreparedSigned(sharedConn));
+    getBlob(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getBlob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type INTEGER cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream("t1alias"),
+        "Data type INTEGER cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getClob() throws SQLException {
+    getClob(getSigned());
+  }
+
+  @Test
+  public void getClobPrepared() throws SQLException {
+    getClob(getPreparedSigned(sharedConn));
+    getClob(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getClob(1), "Data type INTEGER cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getClob("t1alias"),
+        "Data type INTEGER cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getNClob() throws SQLException {
+    getNClob(getSigned());
+  }
+
+  @Test
+  public void getNClobPrepared() throws SQLException {
+    getNClob(getPreparedSigned(sharedConn));
+    getNClob(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getNClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNClob(1),
+        "Data type INTEGER cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNClob("t1alias"),
+        "Data type INTEGER cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getArray() throws SQLException {
+    getArray(getSigned());
+  }
+
+  @Test
+  public void getArrayPrepared() throws SQLException {
+    getArray(getPreparedSigned(sharedConn));
+    getArray(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getArray(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getArray(1), "Method ResultSet.getArray not supported");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getArray("t1alias"),
+        "Method ResultSet.getArray not supported");
+  }
+
+  @Test
+  public void getURL() throws SQLException {
+    getURL(getSigned());
+  }
+
+  @Test
+  public void getURLPrepared() throws SQLException {
+    getURL(getPreparedSigned(sharedConn));
+    getURL(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getURL(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLSyntaxErrorException.class, () -> rs.getURL(1), "Could not parse '0' as URL");
+    Common.assertThrowsContains(
+        SQLSyntaxErrorException.class, () -> rs.getURL("t1alias"), "Could not parse '0' as URL");
+  }
+
+  @Test
+  public void getSQLXML() throws SQLException {
+    getSQLXML(getSigned());
+  }
+
+  @Test
+  public void getSQLXMLPrepared() throws SQLException {
+    getSQLXML(getPreparedSigned(sharedConn));
+    getSQLXML(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getSQLXML(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getSQLXML(1), "Method ResultSet.getSQLXML not supported");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getSQLXML("t1alias"),
+        "Method ResultSet.getSQLXML not supported");
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = getSigned();
+    ResultSetMetaData meta = rs.getMetaData();
+    assertEquals("INTEGER", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.lang.Integer", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.INTEGER, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(11, meta.getPrecision(1));
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    assertEquals(11, meta.getColumnDisplaySize(1));
+
+    rs = getUnsigned();
+    meta = rs.getMetaData();
+    assertEquals("INTEGER UNSIGNED", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.lang.Long", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.BIGINT, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+
+    // https://jira.mariadb.org/browse/XPT-276
+    if (!isXpand()) {
+      assertEquals(10, meta.getColumnDisplaySize(1));
+      assertEquals(10, meta.getPrecision(1));
+    }
+  }
+
+  @Test
+  public void sendParam() throws SQLException {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE IntCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep = con.prepareStatement("INSERT INTO IntCodec2(t1) VALUES (?)")) {
+      prep.setInt(1, 1);
+      prep.execute();
+      prep.setObject(1, 2);
+      prep.execute();
+      prep.setObject(1, null);
+      prep.execute();
+      prep.setObject(1, 3, Types.INTEGER);
+      prep.execute();
+      prep.setObject(1, "4", Types.INTEGER);
+      prep.execute();
+      prep.setObject(1, null, Types.INTEGER);
+      prep.execute();
+      prep.setObject(1, 5, Types.VARCHAR);
+      prep.execute();
+    }
+
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM IntCodec2");
+    assertTrue(rs.next());
+    assertEquals(1, rs.getInt(2));
+    rs.updateInt("t1", 10);
+    rs.updateRow();
+    assertEquals(10, rs.getInt(2));
+
+    assertTrue(rs.next());
+    assertEquals(2, rs.getInt(2));
+    rs.updateObject(2, null);
+    rs.updateRow();
+    assertEquals(0, rs.getInt(2));
+    assertTrue(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(0, rs.getInt(2));
+    assertTrue(rs.wasNull());
+    rs.updateObject(2, 20);
+    rs.updateRow();
+    assertEquals(20, rs.getInt(2));
+    assertFalse(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(3, rs.getInt(2));
+    rs.updateObject("t1", null, Types.INTEGER);
+    rs.updateRow();
+    assertEquals(0, rs.getInt(2));
+    assertTrue(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(4, rs.getInt(2));
+
+    assertTrue(rs.next());
+    assertEquals(0, rs.getInt(2));
+    assertTrue(rs.wasNull());
+    rs.updateObject(2, 25, Types.INTEGER);
+    rs.updateRow();
+    assertEquals(25, rs.getInt(2));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.next());
+    assertEquals(5, rs.getInt(2));
+
+    rs = stmt.executeQuery("SELECT * FROM IntCodec2");
+    assertTrue(rs.next());
+    assertEquals(10, rs.getInt(2));
+
+    assertTrue(rs.next());
+    assertEquals(0, rs.getInt(2));
+    assertTrue(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(20, rs.getInt(2));
+    assertFalse(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(0, rs.getInt(2));
+    assertTrue(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(4, rs.getInt(2));
+    assertFalse(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(25, rs.getInt(2));
+    assertFalse(rs.wasNull());
+    con.commit();
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/LineStringCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/LineStringCodecTest.java
new file mode 100644
index 0000000..951bf36
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/LineStringCodecTest.java
@@ -0,0 +1,362 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.type.GeometryCollection;
+import org.mariadb.jdbc.type.LineString;
+import org.mariadb.jdbc.type.Point;
+import org.mariadb.jdbc.util.constants.Capabilities;
+
+public class LineStringCodecTest extends CommonCodecTest {
+  public static org.mariadb.jdbc.Connection geoConn;
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS LineStringCodec");
+    stmt.execute("DROP TABLE IF EXISTS LineStringCodec2");
+    if (geoConn != null) geoConn.close();
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws Exception {
+    drop();
+    // xpand doesn't recognized LineString
+    Assumptions.assumeFalse(isXpand());
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE LineStringCodec (t1 LineString, t2 LineString, t3 LineString, t4"
+            + " LineString)");
+    stmt.execute(
+        "INSERT INTO LineStringCodec VALUES ( ST_LineStringFromText('LINESTRING(0 0,0 10,10 0)'),"
+            + " ST_LineStringFromText('LINESTRING(10 10,20 10,20 20,10 20,10 10)'),"
+            + " ST_LineStringFromText('LINESTRING(-1 0.55, 3 5, 1 1)'), null)");
+    stmt.execute(
+        "CREATE TABLE LineStringCodec2 (id int not null primary key auto_increment, t1"
+            + " LineString)");
+    stmt.execute("FLUSH TABLES");
+    String binUrl =
+        mDefUrl + (mDefUrl.indexOf("?") > 0 ? "&" : "?") + "geometryDefaultType=default";
+    geoConn = (org.mariadb.jdbc.Connection) DriverManager.getConnection(binUrl);
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from"
+                + " LineStringCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private CompleteResult getPrepare(org.mariadb.jdbc.Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from LineStringCodec"
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    CompleteResult rs = (CompleteResult) preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws Exception {
+    getObject(get(), false);
+  }
+
+  @Test
+  public void getObjectPrepare() throws Exception {
+    getObject(getPrepare(sharedConn), false);
+    getObject(getPrepare(sharedConnBinary), false);
+    getObject(getPrepare(geoConn), true);
+  }
+
+  public void getObject(ResultSet rs, boolean defaultGeo) throws SQLException {
+    if (defaultGeo && hasCapability(Capabilities.EXTENDED_TYPE_INFO)) {
+      assertEquals(
+          new LineString(new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0)}, true),
+          rs.getObject(1));
+      assertFalse(rs.wasNull());
+      assertEquals(
+          new LineString(
+              new Point[] {
+                new Point(10, 10),
+                new Point(20, 10),
+                new Point(20, 20),
+                new Point(10, 20),
+                new Point(10, 10)
+              },
+              true),
+          rs.getObject(2));
+      assertFalse(rs.wasNull());
+      assertEquals(
+          new LineString(new Point[] {new Point(-1, 0.55), new Point(3, 5), new Point(1, 1)}, true),
+          rs.getObject(3));
+      assertFalse(rs.wasNull());
+      assertNull(rs.getObject(4));
+      assertTrue(rs.wasNull());
+    } else {
+      assertEquals(
+          new LineString(new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0)}, true),
+          rs.getObject(1, LineString.class));
+      assertFalse(rs.wasNull());
+      assertEquals(
+          new LineString(
+              new Point[] {
+                new Point(10, 10),
+                new Point(20, 10),
+                new Point(20, 20),
+                new Point(10, 20),
+                new Point(10, 10)
+              },
+              true),
+          rs.getObject(2, LineString.class));
+      assertFalse(rs.wasNull());
+      assertEquals(
+          new LineString(new Point[] {new Point(-1, 0.55), new Point(3, 5), new Point(1, 1)}, true),
+          rs.getObject(3, LineString.class));
+      assertFalse(rs.wasNull());
+      assertNull(rs.getObject(4));
+      assertTrue(rs.wasNull());
+    }
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testErrObject(rs, Integer.class);
+    testErrObject(rs, String.class);
+    testErrObject(rs, Long.class);
+    testErrObject(rs, Short.class);
+    testErrObject(rs, BigDecimal.class);
+    testErrObject(rs, BigInteger.class);
+    testErrObject(rs, Double.class);
+    testErrObject(rs, Float.class);
+    testErrObject(rs, GeometryCollection.class);
+    testErrObject(rs, Byte.class);
+    testArrObject(
+        rs,
+        new byte[] {
+          (byte) 0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x01,
+          0x02,
+          0x00,
+          0x00,
+          0x00,
+          0x03,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x24,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x24,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00
+        });
+
+    testErrObject(rs, Boolean.class);
+    testErrObject(rs, Clob.class);
+    testErrObject(rs, NClob.class);
+    testErrObject(rs, InputStream.class);
+    testErrObject(rs, Reader.class);
+    testErrObject(rs, java.util.Date.class);
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    getMetaData(sharedConn, false);
+    try (org.mariadb.jdbc.Connection con = createCon("geometryDefaultType=default")) {
+      getMetaData(con, true);
+    }
+  }
+
+  private void getMetaData(org.mariadb.jdbc.Connection con, boolean geoDefault)
+      throws SQLException {
+    ResultSet rs = getPrepare(con);
+    ResultSetMetaData meta = rs.getMetaData();
+    if (hasCapability(Capabilities.EXTENDED_TYPE_INFO)) {
+      assertEquals("LINESTRING", meta.getColumnTypeName(1));
+    } else {
+      assertEquals("GEOMETRY", meta.getColumnTypeName(1));
+    }
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+
+    assertEquals(
+        geoDefault
+            ? (hasCapability(Capabilities.EXTENDED_TYPE_INFO)
+                ? LineString.class.getName()
+                : GeometryCollection.class.getName())
+            : "byte[]",
+        meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.VARBINARY, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+  }
+
+  @Test
+  public void sendParam() throws Exception {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws Exception {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE LineStringCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    LineString ls1 =
+        new LineString(new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0)}, true);
+    LineString ls2 =
+        new LineString(
+            new Point[] {
+              new Point(10, 10),
+              new Point(20, 10),
+              new Point(20, 20),
+              new Point(10, 20),
+              new Point(10, 10)
+            },
+            true);
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO LineStringCodec2(t1) VALUES (?)")) {
+      prep.setObject(1, ls1);
+      prep.execute();
+      prep.setObject(1, (LineString) null);
+      prep.execute();
+
+      prep.setObject(1, ls2);
+      prep.addBatch();
+      prep.setObject(1, ls1);
+      prep.addBatch();
+      prep.executeBatch();
+    }
+
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM LineStringCodec2");
+    assertTrue(rs.next());
+    assertEquals(ls1, rs.getObject(2, LineString.class));
+    rs.updateNull(2);
+    rs.updateRow();
+    assertNull(rs.getObject(2, LineString.class));
+
+    assertTrue(rs.next());
+    assertNull(rs.getObject(2, LineString.class));
+    rs.updateObject(2, ls2);
+    rs.updateRow();
+    assertEquals(ls2, rs.getObject(2, LineString.class));
+    assertTrue(rs.next());
+
+    assertEquals(ls2, rs.getObject(2, LineString.class));
+    assertTrue(rs.next());
+    assertEquals(ls1, rs.getObject(2, LineString.class));
+    con.commit();
+  }
+
+  @Test
+  public void equal() {
+    LineString ls =
+        new LineString(new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0)}, true);
+    assertTrue(ls.isOpen());
+    assertEquals(ls, ls);
+    assertEquals(
+        new LineString(new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0)}, true),
+        ls);
+    assertEquals(
+        new LineString(new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0)}, true)
+            .hashCode(),
+        ls.hashCode());
+    assertFalse(ls.equals(null));
+    assertFalse(ls.equals(""));
+    assertNotEquals(
+        new LineString(new Point[] {new Point(0, 0), new Point(0, 20), new Point(20, 0)}, true),
+        ls);
+    assertNotEquals(
+        new LineString(new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0)}, false),
+        ls);
+    assertNotEquals(
+        new LineString(
+            new Point[] {new Point(0, 0), new Point(0, 20), new Point(20, 10), new Point(10, 0)},
+            true),
+        ls);
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/LongCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/LongCodecTest.java
new file mode 100644
index 0000000..6869b5f
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/LongCodecTest.java
@@ -0,0 +1,1070 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import java.time.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.integration.Common;
+
+public class LongCodecTest extends CommonCodecTest {
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS LongCodec");
+    stmt.execute("DROP TABLE IF EXISTS LongCodecUnsigned");
+    stmt.execute("DROP TABLE IF EXISTS LongCodec2");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE LongCodec (t1 BIGINT, t2 BIGINT, t3 BIGINT, t4 BIGINT)");
+    stmt.execute(
+        "CREATE TABLE LongCodecUnsigned (t1 BIGINT UNSIGNED, t2 BIGINT UNSIGNED, t3 BIGINT"
+            + " UNSIGNED, t4 BIGINT UNSIGNED)");
+    stmt.execute("INSERT INTO LongCodec VALUES (0, 1, -1, null)");
+    stmt.execute("INSERT INTO LongCodecUnsigned VALUES (0, 1, 18446744073709551615, null)");
+    stmt.execute("CREATE TABLE LongCodec2 (id int not null primary key auto_increment, t1 BIGINT)");
+    stmt.execute("FLUSH TABLES");
+  }
+
+  private ResultSet getSigned() throws SQLException {
+    return get("LongCodec");
+  }
+
+  private ResultSet getUnsigned() throws SQLException {
+    return get("LongCodecUnsigned");
+  }
+
+  private ResultSet getSignedPrepared(Connection con) throws SQLException {
+    return getPrepare(con, "LongCodec");
+  }
+
+  private ResultSet getUnsignedPrepared(Connection con) throws SQLException {
+    return getPrepare(con, "LongCodecUnsigned");
+  }
+
+  private ResultSet get(String table) throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from " + table);
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getPrepare(Connection con, String table) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from "
+                + table
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    ResultSet rs = preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws SQLException {
+    getObject(getSigned());
+  }
+
+  @Test
+  public void getObjectPrepared() throws SQLException {
+    getObject(getSignedPrepared(sharedConn));
+    getObject(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getObject(ResultSet rs) throws SQLException {
+    assertEquals(0L, rs.getObject(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1L, rs.getObject(2));
+    assertEquals(1L, rs.getObject("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1L, rs.getObject(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(getSigned());
+  }
+
+  @Test
+  public void getObjectTypePrepared() throws Exception {
+    getObjectType(getSignedPrepared(sharedConn));
+    getObjectType(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getObjectType(ResultSet rs) throws Exception {
+    testObject(rs, Integer.class, 0);
+    testObject(rs, String.class, "0");
+    testObject(rs, Long.class, 0L);
+    testObject(rs, Short.class, (short) 0);
+    testObject(rs, BigDecimal.class, BigDecimal.valueOf(0));
+    testObject(rs, BigInteger.class, BigInteger.valueOf(0));
+    testObject(rs, Double.class, (double) 0);
+    testObject(rs, Float.class, (float) 0);
+    testObject(rs, Byte.class, (byte) 0);
+    testErrObject(rs, byte[].class);
+    testErrObject(rs, Date.class);
+    testErrObject(rs, Time.class);
+    testErrObject(rs, Timestamp.class);
+    testErrObject(rs, java.util.Date.class);
+    testErrObject(rs, LocalDate.class);
+    testErrObject(rs, ZonedDateTime.class);
+    testErrObject(rs, OffsetDateTime.class);
+    testErrObject(rs, LocalDateTime.class);
+    testErrObject(rs, OffsetTime.class);
+    testObject(rs, Boolean.class, Boolean.FALSE);
+    testErrObject(rs, Clob.class);
+    testErrObject(rs, NClob.class);
+    testErrObject(rs, InputStream.class);
+    testErrObject(rs, Reader.class);
+  }
+
+  @Test
+  public void getString() throws SQLException {
+    getString(getSigned());
+  }
+
+  @Test
+  public void getStringPrepared() throws SQLException {
+    getString(getSignedPrepared(sharedConn));
+    getString(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getString(2));
+    assertEquals("1", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("-1", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getStringUnsigned() throws SQLException {
+    getStringUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getStringUnsignedPrepared() throws SQLException {
+    getStringUnsigned(getUnsignedPrepared(sharedConn));
+    getStringUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getStringUnsigned(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getString(2));
+    assertEquals("1", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("18446744073709551615", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNString() throws SQLException {
+    getNString(getSigned());
+  }
+
+  @Test
+  public void getNStringPrepared() throws SQLException {
+    getNString(getSignedPrepared(sharedConn));
+    getNString(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getNString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getNString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getNString(2));
+    assertEquals("1", rs.getNString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("-1", rs.getNString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBoolean() throws SQLException {
+    getBoolean(getSigned());
+  }
+
+  @Test
+  public void getBooleanPrepared() throws SQLException {
+    getBoolean(getSignedPrepared(sharedConn));
+    getBoolean(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getBoolean(ResultSet rs) throws SQLException {
+    assertFalse(rs.getBoolean(1));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(2));
+    assertTrue(rs.getBoolean("t2alias"));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(3));
+    assertFalse(rs.wasNull());
+    assertFalse(rs.getBoolean(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getByte() throws SQLException {
+    getByte(getSigned());
+  }
+
+  @Test
+  public void getBytePrepared() throws SQLException {
+    getByte(getSignedPrepared(sharedConn));
+    getByte(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getByte(ResultSet rs) throws SQLException {
+    assertEquals((byte) 0, rs.getByte(1));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 1, rs.getByte(2));
+    assertEquals((byte) 1, rs.getByte("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) -1, rs.getByte(3));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 0, rs.getByte(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getByteUnsigned() throws SQLException {
+    getByteUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getByteUnsignedPrepared() throws SQLException {
+    getByteUnsigned(getUnsignedPrepared(sharedConn));
+    getByteUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getByteUnsigned(ResultSet rs) throws SQLException {
+    assertEquals((byte) 0, rs.getByte(1));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 1, rs.getByte(2));
+    assertEquals((byte) 1, rs.getByte("t2alias"));
+    assertFalse(rs.wasNull());
+    assertThrows(SQLDataException.class, () -> rs.getByte(3));
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getShort() throws SQLException {
+    getShort(getSigned());
+  }
+
+  @Test
+  public void getShortPrepared() throws SQLException {
+    getShort(getSignedPrepared(sharedConn));
+    getShort(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getShort(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getShort(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getShort(2));
+    assertEquals(1, rs.getShort("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1, rs.getShort(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getShort(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(getSigned());
+  }
+
+  @Test
+  public void getIntPrepared() throws SQLException {
+    getInt(getSignedPrepared(sharedConn));
+    getInt(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getInt(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getInt(2));
+    assertEquals(1, rs.getInt("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1, rs.getInt(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getIntUnsigned() throws SQLException {
+    getIntUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getIntUnsignedPrepared() throws SQLException {
+    getIntUnsigned(getUnsignedPrepared(sharedConn));
+    getIntUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getIntUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getInt(2));
+    assertFalse(rs.wasNull());
+    try {
+      rs.getInt(3);
+      fail();
+    } catch (SQLDataException e) {
+      assertTrue(
+          e.getMessage().contains("integer overflow")
+              || e.getMessage()
+                  .contains("value '18446744073709551615' cannot be decoded as Integer"));
+    }
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(getSigned());
+  }
+
+  @Test
+  public void getLongPrepared() throws SQLException {
+    getLong(getSignedPrepared(sharedConn));
+    getLong(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getLong(ResultSet rs) throws SQLException {
+    assertEquals(0L, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1L, rs.getLong(2));
+    assertEquals(1L, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1L, rs.getLong(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0L, rs.getLong(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLongUnsigned() throws SQLException {
+    getLongUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getLongUnsignedPrepared() throws SQLException {
+    getLongUnsigned(getUnsignedPrepared(sharedConn));
+    getLongUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getLongUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0L, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1L, rs.getLong(2));
+    assertEquals(1L, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    try {
+      rs.getLong(3);
+      fail();
+    } catch (SQLDataException e) {
+      assertTrue(
+          e.getMessage().contains("long overflow")
+              || e.getMessage().contains("value '18446744073709551615' cannot be decoded as Long"));
+    }
+    assertFalse(rs.wasNull());
+    assertEquals(0L, rs.getLong(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(getSigned());
+  }
+
+  @Test
+  public void getFloatPrepared() throws SQLException {
+    getFloat(getSignedPrepared(sharedConn));
+    getFloat(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getFloat(ResultSet rs) throws SQLException {
+    assertEquals(0F, rs.getFloat(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1F, rs.getFloat(2));
+    assertEquals(1F, rs.getFloat("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1F, rs.getFloat(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0F, rs.getFloat(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloatUnsigned() throws SQLException {
+    getFloatUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getFloatUnsignedPrepared() throws SQLException {
+    getFloatUnsigned(getUnsignedPrepared(sharedConn));
+    getFloatUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getFloatUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0F, rs.getFloat(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1F, rs.getFloat(2));
+    assertEquals(1F, rs.getFloat("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(18446744073709551615F, rs.getFloat(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0F, rs.getFloat(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(getSigned());
+  }
+
+  @Test
+  public void getDoublePrepared() throws SQLException {
+    getDouble(getSignedPrepared(sharedConn));
+    getDouble(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getDouble(ResultSet rs) throws SQLException {
+    assertEquals(0D, rs.getDouble(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1D, rs.getDouble(2));
+    assertEquals(1D, rs.getDouble("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1D, rs.getDouble(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0D, rs.getDouble(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDoubleUnsigned() throws SQLException {
+    getDoubleUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getDoubleUnsignedPrepared() throws SQLException {
+    getDoubleUnsigned(getUnsignedPrepared(sharedConn));
+    getDoubleUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getDoubleUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0D, rs.getDouble(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1D, rs.getDouble(2));
+    assertEquals(1D, rs.getDouble("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(18446744073709551615D, rs.getDouble(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0D, rs.getDouble(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(getSigned());
+  }
+
+  @Test
+  public void getBigDecimalPrepared() throws SQLException {
+    getBigDecimal(getSignedPrepared(sharedConn));
+    getBigDecimal(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getBigDecimal(ResultSet rs) throws SQLException {
+    assertEquals(BigDecimal.ZERO, rs.getBigDecimal(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal(2));
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.valueOf(-1), rs.getBigDecimal(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimalUnsigned() throws SQLException {
+    getBigDecimalUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getBigDecimalUnsignedPrepared() throws SQLException {
+    getBigDecimalUnsigned(getUnsignedPrepared(sharedConn));
+    getBigDecimalUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getBigDecimalUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(BigDecimal.ZERO, rs.getBigDecimal(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal(2));
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(new BigDecimal("18446744073709551615"), rs.getBigDecimal(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigInteger() throws SQLException {
+    getBigInteger(getSigned());
+  }
+
+  @Test
+  public void getBigIntegerPrepared() throws SQLException {
+    getBigInteger(getSignedPrepared(sharedConn));
+    getBigInteger(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getBigInteger(ResultSet res) throws SQLException {
+    CompleteResult rs = (CompleteResult) res;
+    assertEquals(BigInteger.ZERO, rs.getBigInteger(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigInteger.ONE, rs.getBigInteger(2));
+    assertEquals(BigInteger.ONE, rs.getBigInteger("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(BigInteger.valueOf(-1), rs.getBigInteger(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigInteger(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigIntegerUnsigned() throws SQLException {
+    getBigIntegerUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getBigIntegerUnsignedPrepared() throws SQLException {
+    getBigIntegerUnsigned(getUnsignedPrepared(sharedConn));
+    getBigIntegerUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getBigIntegerUnsigned(ResultSet res) throws SQLException {
+    CompleteResult rs = (CompleteResult) res;
+    assertEquals(BigInteger.ZERO, rs.getBigInteger(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigInteger.ONE, rs.getBigInteger(2));
+    assertEquals(BigInteger.ONE, rs.getBigInteger("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(new BigInteger("18446744073709551615"), rs.getBigInteger(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigInteger(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(getSigned());
+  }
+
+  @Test
+  public void getDatePrepared() throws SQLException {
+    getDate(getSignedPrepared(sharedConn));
+    getDate(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getDate(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getDate(1), "Data type BIGINT cannot be decoded as Date");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDate("t1alias"),
+        "Data type BIGINT cannot be decoded as Date");
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(getSigned());
+  }
+
+  @Test
+  public void getTimePrepared() throws SQLException {
+    getTime(getSignedPrepared(sharedConn));
+    getTime(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getTime(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getTime(1), "Data type BIGINT cannot be decoded as Time");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime("t1alias"),
+        "Data type BIGINT cannot be decoded as Time");
+  }
+
+  @Test
+  public void getTimestamp() throws SQLException {
+    getTimestamp(getSigned());
+  }
+
+  @Test
+  public void getTimestampPrepared() throws SQLException {
+    getTimestamp(getSignedPrepared(sharedConn));
+    getTimestamp(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getTimestamp(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTimestamp(1),
+        "Data type BIGINT cannot be decoded as Timestamp");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTimestamp("t1alias"),
+        "Data type BIGINT cannot be decoded as Timestamp");
+  }
+
+  @Test
+  public void getAsciiStream() throws SQLException {
+    getAsciiStream(getSigned());
+  }
+
+  @Test
+  public void getAsciiStreamPrepared() throws SQLException {
+    getAsciiStream(getSignedPrepared(sharedConn));
+    getAsciiStream(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getAsciiStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getAsciiStream(1),
+        "Data type BIGINT cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getAsciiStream("t1alias"),
+        "Data type BIGINT cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getUnicodeStream() throws SQLException {
+    getUnicodeStream(getSigned());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepared() throws SQLException {
+    getUnicodeStream(getSignedPrepared(sharedConn));
+    getUnicodeStream(getSignedPrepared(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  private void getUnicodeStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getUnicodeStream(1),
+        "Data type BIGINT cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getUnicodeStream("t1alias"),
+        "Data type BIGINT cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBinaryStream() throws SQLException {
+    getBinaryStream(getSigned());
+  }
+
+  @Test
+  public void getBinaryStreamPrepared() throws SQLException {
+    getBinaryStream(getSignedPrepared(sharedConn));
+    getBinaryStream(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getBinaryStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBinaryStream(1),
+        "Data type BIGINT cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBinaryStream("t1alias"),
+        "Data type BIGINT cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBytes() throws SQLException {
+    getBytes(getSigned());
+  }
+
+  @Test
+  public void getBytesPrepared() throws SQLException {
+    getBytes(getSignedPrepared(sharedConn));
+    getBytes(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getBytes(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBytes(1),
+        "Data type BIGINT cannot be decoded as byte[]");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBytes("t1alias"),
+        "Data type BIGINT cannot be decoded as byte[]");
+  }
+
+  @Test
+  public void getCharacterStream() throws SQLException {
+    getCharacterStream(getSigned());
+  }
+
+  @Test
+  public void getCharacterStreamPrepared() throws SQLException {
+    getCharacterStream(getSignedPrepared(sharedConn));
+    getCharacterStream(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type BIGINT cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream("t1alias"),
+        "Data type BIGINT cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getNCharacterStream() throws SQLException {
+    getNCharacterStream(getSigned());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepared() throws SQLException {
+    getNCharacterStream(getSignedPrepared(sharedConn));
+    getNCharacterStream(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getNCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream(1),
+        "Data type BIGINT cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream("t2alias"),
+        "Data type BIGINT cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getRef() throws SQLException {
+    getRef(getSigned());
+  }
+
+  @Test
+  public void getRefPrepared() throws SQLException {
+    getRef(getSignedPrepared(sharedConn));
+    getRef(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getRef(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getRef(1), "Method ResultSet.getRef not supported");
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getRef("t2alias"), "Method ResultSet.getRef not supported");
+  }
+
+  @Test
+  public void getBlob() throws SQLException {
+    getBlob(getSigned());
+  }
+
+  @Test
+  public void getBlobPrepared() throws SQLException {
+    getBlob(getSignedPrepared(sharedConn));
+    getBlob(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getBlob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type BIGINT cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream("t1alias"),
+        "Data type BIGINT cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getClob() throws SQLException {
+    getClob(getSigned());
+  }
+
+  @Test
+  public void getClobPrepared() throws SQLException {
+    getClob(getSignedPrepared(sharedConn));
+    getClob(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getClob(1), "Data type BIGINT cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getClob("t1alias"),
+        "Data type BIGINT cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getNClob() throws SQLException {
+    getNClob(getSigned());
+  }
+
+  @Test
+  public void getNClobPrepared() throws SQLException {
+    getNClob(getSignedPrepared(sharedConn));
+    getNClob(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getNClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getNClob(1), "Data type BIGINT cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNClob("t1alias"),
+        "Data type BIGINT cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getArray() throws SQLException {
+    getArray(getSigned());
+  }
+
+  @Test
+  public void getArrayPrepared() throws SQLException {
+    getArray(getSignedPrepared(sharedConn));
+    getArray(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getArray(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getArray(1), "Method ResultSet.getArray not supported");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getArray("t1alias"),
+        "Method ResultSet.getArray not supported");
+  }
+
+  @Test
+  public void getURL() throws SQLException {
+    getURL(getSigned());
+  }
+
+  @Test
+  public void getURLPrepared() throws SQLException {
+    getURL(getSignedPrepared(sharedConn));
+    getURL(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getURL(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLSyntaxErrorException.class, () -> rs.getURL(1), "Could not parse '0' as URL");
+    Common.assertThrowsContains(
+        SQLSyntaxErrorException.class, () -> rs.getURL("t1alias"), "Could not parse '0' as URL");
+  }
+
+  @Test
+  public void getSQLXML() throws SQLException {
+    getSQLXML(getSigned());
+  }
+
+  @Test
+  public void getSQLXMLPrepared() throws SQLException {
+    getSQLXML(getSignedPrepared(sharedConn));
+    getSQLXML(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getSQLXML(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getSQLXML(1), "Method ResultSet.getSQLXML not supported");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getSQLXML("t1alias"),
+        "Method ResultSet.getSQLXML not supported");
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = getSigned();
+    ResultSetMetaData meta = rs.getMetaData();
+    assertEquals("BIGINT", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.lang.Long", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.BIGINT, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(20, meta.getPrecision(1));
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    assertEquals(20, meta.getColumnDisplaySize(1));
+
+    rs = getUnsigned();
+    meta = rs.getMetaData();
+    assertEquals("BIGINT UNSIGNED", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.math.BigInteger", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.BIGINT, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(20, meta.getPrecision(1));
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    assertEquals(20, meta.getColumnDisplaySize(1));
+  }
+
+  @Test
+  public void sendParam() throws SQLException {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE LongCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep = con.prepareStatement("INSERT INTO LongCodec2(t1) VALUES (?)")) {
+      prep.setLong(1, 1L);
+      prep.execute();
+      prep.setObject(1, 2L);
+      prep.execute();
+      prep.setObject(1, BigInteger.TEN);
+      prep.execute();
+      prep.setObject(1, null);
+      prep.execute();
+      prep.setObject(1, 3L, Types.BIGINT);
+      prep.execute();
+      prep.setObject(1, BigInteger.valueOf(4), Types.BIGINT);
+      prep.execute();
+      prep.setObject(1, null, Types.BIGINT);
+      prep.execute();
+    }
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM LongCodec2");
+    assertTrue(rs.next());
+    assertEquals(1, rs.getLong(2));
+    rs.updateLong(2, 50L);
+    rs.updateRow();
+    assertEquals(50L, rs.getLong(2));
+
+    assertTrue(rs.next());
+    assertEquals(2, rs.getLong(2));
+    rs.updateLong("t1", 60L);
+    rs.updateRow();
+    assertEquals(60L, rs.getLong(2));
+
+    assertTrue(rs.next());
+    assertEquals(10, rs.getLong(2));
+    rs.updateObject("t1", null);
+    rs.updateRow();
+    assertEquals(0L, rs.getLong(2));
+    assertTrue(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(0, rs.getLong(2));
+    assertTrue(rs.wasNull());
+    rs.updateObject("t1", 70L);
+    rs.updateRow();
+    assertEquals(70L, rs.getLong(2));
+    assertFalse(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(3, rs.getLong(2));
+    rs.updateObject("t1", null, Types.BIGINT);
+    rs.updateRow();
+    assertEquals(0L, rs.getLong(2));
+    assertTrue(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(4, rs.getLong(2));
+    rs.updateObject(2, BigInteger.valueOf(80));
+    rs.updateRow();
+    assertEquals(80L, rs.getLong(2));
+    assertFalse(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(0, rs.getLong(2));
+    assertTrue(rs.wasNull());
+    rs.updateObject("t1", BigInteger.valueOf(180), Types.BIGINT);
+    rs.updateRow();
+    assertEquals(180L, rs.getLong(2));
+    assertFalse(rs.wasNull());
+
+    rs = stmt.executeQuery("SELECT * FROM LongCodec2");
+    assertTrue(rs.next());
+    assertEquals(50L, rs.getLong(2));
+
+    assertTrue(rs.next());
+    assertEquals(60L, rs.getLong(2));
+
+    assertTrue(rs.next());
+    assertEquals(0L, rs.getLong(2));
+    assertTrue(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(70L, rs.getLong(2));
+    assertFalse(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(0L, rs.getLong(2));
+    assertTrue(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(80L, rs.getLong(2));
+    assertFalse(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(180L, rs.getLong(2));
+    assertFalse(rs.wasNull());
+    con.commit();
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/MediumIntCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/MediumIntCodecTest.java
new file mode 100644
index 0000000..9f618eb
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/MediumIntCodecTest.java
@@ -0,0 +1,1017 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import java.time.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.integration.Common;
+
+public class MediumIntCodecTest extends CommonCodecTest {
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS MediumIntCodec");
+    stmt.execute("DROP TABLE IF EXISTS MediumIntCodec2");
+    stmt.execute("DROP TABLE IF EXISTS MediumIntCodec3");
+    stmt.execute("DROP TABLE IF EXISTS MediumIntCodecUnsigned");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE MediumIntCodec (t1 MEDIUMINT, t2 MEDIUMINT, t3 MEDIUMINT, t4 MEDIUMINT)");
+    stmt.execute(
+        "CREATE TABLE MediumIntCodecUnsigned (t1 MEDIUMINT UNSIGNED, t2 MEDIUMINT UNSIGNED, t3"
+            + " MEDIUMINT UNSIGNED, t4 MEDIUMINT UNSIGNED)");
+    stmt.execute("INSERT INTO MediumIntCodec VALUES (0, 1, -1, null)");
+    stmt.execute("INSERT INTO MediumIntCodecUnsigned VALUES (0, 1, 16777215, null)");
+    stmt.execute(
+        "CREATE TABLE MediumIntCodec2 (id int not null primary key auto_increment, t1 MEDIUMINT)");
+    stmt.execute(
+        "CREATE TABLE MediumIntCodec3 (id int not null, id2 int not null, t1 MEDIUMINT, PRIMARY KEY"
+            + " (id, id2))");
+    stmt.execute("FLUSH TABLES");
+  }
+
+  private ResultSet getSigned() throws SQLException {
+    return get("MediumIntCodec");
+  }
+
+  private ResultSet getUnsigned() throws SQLException {
+    return get("MediumIntCodecUnsigned");
+  }
+
+  private ResultSet getSignedPrepared(Connection con) throws SQLException {
+    return getPrepare(con, "MediumIntCodec");
+  }
+
+  private ResultSet getUnsignedPrepared(Connection con) throws SQLException {
+    return getPrepare(con, "MediumIntCodecUnsigned");
+  }
+
+  private ResultSet get(String table) throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from " + table);
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getPrepare(Connection con, String table) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from "
+                + table
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    ResultSet rs = preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws SQLException {
+    getObject(getSigned());
+  }
+
+  @Test
+  public void getObjectPrepared() throws SQLException {
+    getObject(getSignedPrepared(sharedConn));
+    getObject(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getObject(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getObject(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getObject(2));
+    assertEquals(1, rs.getObject("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1, rs.getObject(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(getSigned());
+  }
+
+  @Test
+  public void getObjectTypePrepared() throws Exception {
+    getObjectType(getSignedPrepared(sharedConn));
+    getObjectType(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getObjectType(ResultSet rs) throws Exception {
+    testObject(rs, Integer.class, 0);
+    testObject(rs, String.class, "0");
+    testObject(rs, Long.class, 0L);
+    testObject(rs, Short.class, (short) 0);
+    testObject(rs, BigDecimal.class, BigDecimal.valueOf(0));
+    testObject(rs, BigInteger.class, BigInteger.valueOf(0));
+    testObject(rs, Double.class, (double) 0);
+    testObject(rs, Float.class, (float) 0);
+    testObject(rs, Byte.class, (byte) 0);
+    testErrObject(rs, byte[].class);
+    testErrObject(rs, Date.class);
+    testErrObject(rs, Time.class);
+    testErrObject(rs, Timestamp.class);
+    testErrObject(rs, java.util.Date.class);
+    testErrObject(rs, LocalDate.class);
+    testErrObject(rs, ZonedDateTime.class);
+    testErrObject(rs, OffsetDateTime.class);
+    testErrObject(rs, LocalDateTime.class);
+    testErrObject(rs, OffsetTime.class);
+    testObject(rs, Boolean.class, Boolean.FALSE);
+    testErrObject(rs, Clob.class);
+    testErrObject(rs, NClob.class);
+    testErrObject(rs, InputStream.class);
+    testErrObject(rs, Reader.class);
+  }
+
+  @Test
+  public void getString() throws SQLException {
+    getString(getSigned());
+  }
+
+  @Test
+  public void getStringPrepared() throws SQLException {
+    getString(getSignedPrepared(sharedConn));
+    getString(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getString(2));
+    assertEquals("1", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("-1", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getStringUnsigned() throws SQLException {
+    getStringUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getStringUnsignedPrepared() throws SQLException {
+    getStringUnsigned(getUnsignedPrepared(sharedConn));
+    getStringUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getStringUnsigned(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getString(2));
+    assertEquals("1", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("16777215", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNString() throws SQLException {
+    getNString(getSigned());
+  }
+
+  @Test
+  public void getNStringPrepared() throws SQLException {
+    getNString(getSignedPrepared(sharedConn));
+    getNString(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getNString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getNString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getNString(2));
+    assertEquals("1", rs.getNString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("-1", rs.getNString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBoolean() throws SQLException {
+    getBoolean(getSigned());
+  }
+
+  @Test
+  public void getBooleanPrepared() throws SQLException {
+    getBoolean(getSignedPrepared(sharedConn));
+    getBoolean(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getBoolean(ResultSet rs) throws SQLException {
+    assertFalse(rs.getBoolean(1));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(2));
+    assertTrue(rs.getBoolean("t2alias"));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(3));
+    assertFalse(rs.wasNull());
+    assertFalse(rs.getBoolean(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getByte() throws SQLException {
+    getByte(getSigned());
+  }
+
+  @Test
+  public void getBytePrepared() throws SQLException {
+    getByte(getSignedPrepared(sharedConn));
+    getByte(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getByte(ResultSet rs) throws SQLException {
+    assertEquals((byte) 0, rs.getByte(1));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 1, rs.getByte(2));
+    assertEquals((byte) 1, rs.getByte("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) -1, rs.getByte(3));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 0, rs.getByte(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getShort() throws SQLException {
+    getShort(getSigned());
+  }
+
+  @Test
+  public void getShortPrepared() throws SQLException {
+    getShort(getSignedPrepared(sharedConn));
+    getShort(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getShort(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getShort(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getShort(2));
+    assertEquals(1, rs.getShort("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1, rs.getShort(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getShort(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(getSigned());
+  }
+
+  @Test
+  public void getIntPrepared() throws SQLException {
+    getInt(getSignedPrepared(sharedConn));
+    getInt(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getInt(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getInt(2));
+    assertEquals(1, rs.getInt("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1, rs.getInt(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getIntUnsigned() throws SQLException {
+    getIntUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getIntUnsignedPrepared() throws SQLException {
+    getIntUnsigned(getUnsignedPrepared(sharedConn));
+    getIntUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getIntUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getInt(2));
+    assertFalse(rs.wasNull());
+    assertEquals(16777215, rs.getInt(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(getSigned());
+  }
+
+  @Test
+  public void getLongPrepared() throws SQLException {
+    getLong(getSignedPrepared(sharedConn));
+    getLong(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getLong(ResultSet rs) throws SQLException {
+    assertEquals(0L, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1L, rs.getLong(2));
+    assertEquals(1L, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1L, rs.getLong(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0L, rs.getLong(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLongUnsigned() throws SQLException {
+    getLongUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getLongUnsignedPrepared() throws SQLException {
+    getLongUnsigned(getUnsignedPrepared(sharedConn));
+    getLongUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getLongUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0L, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1L, rs.getLong(2));
+    assertEquals(1L, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(16777215L, rs.getLong(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0L, rs.getLong(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(getSigned());
+  }
+
+  @Test
+  public void getFloatPrepared() throws SQLException {
+    getFloat(getSignedPrepared(sharedConn));
+    getFloat(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getFloat(ResultSet rs) throws SQLException {
+    assertEquals(0F, rs.getFloat(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1F, rs.getFloat(2));
+    assertEquals(1F, rs.getFloat("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1F, rs.getFloat(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0F, rs.getFloat(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloatUnsigned() throws SQLException {
+    getFloatUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getFloatUnsignedPrepared() throws SQLException {
+    getFloatUnsigned(getUnsignedPrepared(sharedConn));
+    getFloatUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getFloatUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0F, rs.getFloat(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1F, rs.getFloat(2));
+    assertEquals(1F, rs.getFloat("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(16777215F, rs.getFloat(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0F, rs.getFloat(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(getSigned());
+  }
+
+  @Test
+  public void getDoublePrepared() throws SQLException {
+    getDouble(getSignedPrepared(sharedConn));
+    getDouble(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getDouble(ResultSet rs) throws SQLException {
+    assertEquals(0D, rs.getDouble(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1D, rs.getDouble(2));
+    assertEquals(1D, rs.getDouble("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1D, rs.getDouble(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0D, rs.getDouble(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDoubleUnsigned() throws SQLException {
+    getDoubleUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getDoubleUnsignedPrepared() throws SQLException {
+    getDoubleUnsigned(getUnsignedPrepared(sharedConn));
+    getDoubleUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getDoubleUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0D, rs.getDouble(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1D, rs.getDouble(2));
+    assertEquals(1D, rs.getDouble("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(16777215D, rs.getDouble(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0D, rs.getDouble(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(getSigned());
+  }
+
+  @Test
+  public void getBigDecimalPrepared() throws SQLException {
+    getBigDecimal(getSignedPrepared(sharedConn));
+    getBigDecimal(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getBigDecimal(ResultSet rs) throws SQLException {
+    assertEquals(BigDecimal.ZERO, rs.getBigDecimal(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal(2));
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.valueOf(-1), rs.getBigDecimal(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimalUnsigned() throws SQLException {
+    getBigDecimalUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getBigDecimalUnsignedPrepared() throws SQLException {
+    getBigDecimalUnsigned(getUnsignedPrepared(sharedConn));
+    getBigDecimalUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getBigDecimalUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(BigDecimal.ZERO, rs.getBigDecimal(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal(2));
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.valueOf(16777215L), rs.getBigDecimal(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigInteger() throws SQLException {
+    getBigInteger(getSigned());
+  }
+
+  @Test
+  public void getBigIntegerPrepared() throws SQLException {
+    getBigInteger(getSignedPrepared(sharedConn));
+    getBigInteger(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getBigInteger(ResultSet res) throws SQLException {
+    CompleteResult rs = (CompleteResult) res;
+    assertEquals(BigInteger.ZERO, rs.getBigInteger(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigInteger.ONE, rs.getBigInteger(2));
+    assertEquals(BigInteger.ONE, rs.getBigInteger("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(BigInteger.valueOf(-1), rs.getBigInteger(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigInteger(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigIntegerUnsigned() throws SQLException {
+    getBigIntegerUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getBigIntegerUnsignedPrepared() throws SQLException {
+    getBigIntegerUnsigned(getUnsignedPrepared(sharedConn));
+    getBigIntegerUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getBigIntegerUnsigned(ResultSet res) throws SQLException {
+    CompleteResult rs = (CompleteResult) res;
+    assertEquals(BigInteger.ZERO, rs.getBigInteger(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigInteger.ONE, rs.getBigInteger(2));
+    assertEquals(BigInteger.ONE, rs.getBigInteger("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(BigInteger.valueOf(16777215L), rs.getBigInteger(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigInteger(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(getSigned());
+  }
+
+  @Test
+  public void getDatePrepared() throws SQLException {
+    getDate(getSignedPrepared(sharedConn));
+    getDate(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getDate(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDate(1),
+        "Data type MEDIUMINT cannot be decoded as Date");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDate("t1alias"),
+        "Data type MEDIUMINT cannot be decoded as Date");
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(getSigned());
+  }
+
+  @Test
+  public void getTimePrepared() throws SQLException {
+    getTime(getSignedPrepared(sharedConn));
+    getTime(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getTime(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime(1),
+        "Data type MEDIUMINT cannot be decoded as Time");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime("t1alias"),
+        "Data type MEDIUMINT cannot be decoded as Time");
+  }
+
+  @Test
+  public void getTimestamp() throws SQLException {
+    getTimestamp(getSigned());
+  }
+
+  @Test
+  public void getTimestampPrepared() throws SQLException {
+    getTimestamp(getSignedPrepared(sharedConn));
+    getTimestamp(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getTimestamp(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTimestamp(1),
+        "Data type MEDIUMINT cannot be decoded as Timestamp");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTimestamp("t1alias"),
+        "Data type MEDIUMINT cannot be decoded as Timestamp");
+  }
+
+  @Test
+  public void getAsciiStream() throws SQLException {
+    getAsciiStream(getSigned());
+  }
+
+  @Test
+  public void getAsciiStreamPrepared() throws SQLException {
+    getAsciiStream(getSignedPrepared(sharedConn));
+    getAsciiStream(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getAsciiStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getAsciiStream(1),
+        "Data type MEDIUMINT cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getAsciiStream("t1alias"),
+        "Data type MEDIUMINT cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getUnicodeStream() throws SQLException {
+    getUnicodeStream(getSigned());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepared() throws SQLException {
+    getUnicodeStream(getSignedPrepared(sharedConn));
+    getUnicodeStream(getSignedPrepared(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  private void getUnicodeStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getUnicodeStream(1),
+        "Data type MEDIUMINT cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getUnicodeStream("t1alias"),
+        "Data type MEDIUMINT cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBinaryStream() throws SQLException {
+    getBinaryStream(getSigned());
+  }
+
+  @Test
+  public void getBinaryStreamPrepared() throws SQLException {
+    getBinaryStream(getSignedPrepared(sharedConn));
+    getBinaryStream(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getBinaryStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBinaryStream(1),
+        "Data type MEDIUMINT cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBinaryStream("t1alias"),
+        "Data type MEDIUMINT cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBytes() throws SQLException {
+    getBytes(getSigned());
+  }
+
+  @Test
+  public void getBytesPrepared() throws SQLException {
+    getBytes(getSignedPrepared(sharedConn));
+    getBytes(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getBytes(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBytes(1),
+        "Data type MEDIUMINT cannot be decoded as byte[]");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBytes("t1alias"),
+        "Data type MEDIUMINT cannot be decoded as byte[]");
+  }
+
+  @Test
+  public void getCharacterStream() throws SQLException {
+    getCharacterStream(getSigned());
+  }
+
+  @Test
+  public void getCharacterStreamPrepared() throws SQLException {
+    getCharacterStream(getSignedPrepared(sharedConn));
+    getCharacterStream(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type MEDIUMINT cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream("t1alias"),
+        "Data type MEDIUMINT cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getNCharacterStream() throws SQLException {
+    getNCharacterStream(getSigned());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepared() throws SQLException {
+    getNCharacterStream(getSignedPrepared(sharedConn));
+    getNCharacterStream(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getNCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream(1),
+        "Data type MEDIUMINT cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream("t2alias"),
+        "Data type MEDIUMINT cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getRef() throws SQLException {
+    getRef(getSigned());
+  }
+
+  @Test
+  public void getRefPrepared() throws SQLException {
+    getRef(getSignedPrepared(sharedConn));
+    getRef(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getRef(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getRef(1), "Method ResultSet.getRef not supported");
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getRef("t2alias"), "Method ResultSet.getRef not supported");
+  }
+
+  @Test
+  public void getBlob() throws SQLException {
+    getBlob(getSigned());
+  }
+
+  @Test
+  public void getBlobPrepared() throws SQLException {
+    getBlob(getSignedPrepared(sharedConn));
+    getBlob(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getBlob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type MEDIUMINT cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream("t1alias"),
+        "Data type MEDIUMINT cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getClob() throws SQLException {
+    getClob(getSigned());
+  }
+
+  @Test
+  public void getClobPrepared() throws SQLException {
+    getClob(getSignedPrepared(sharedConn));
+    getClob(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getClob(1),
+        "Data type MEDIUMINT cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getClob("t1alias"),
+        "Data type MEDIUMINT cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getNClob() throws SQLException {
+    getNClob(getSigned());
+  }
+
+  @Test
+  public void getNClobPrepared() throws SQLException {
+    getNClob(getSignedPrepared(sharedConn));
+    getNClob(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getNClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNClob(1),
+        "Data type MEDIUMINT cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNClob("t1alias"),
+        "Data type MEDIUMINT cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getArray() throws SQLException {
+    getArray(getSigned());
+  }
+
+  @Test
+  public void getArrayPrepared() throws SQLException {
+    getArray(getSignedPrepared(sharedConn));
+    getArray(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getArray(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getArray(1), "Method ResultSet.getArray not supported");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getArray("t1alias"),
+        "Method ResultSet.getArray not supported");
+  }
+
+  @Test
+  public void getURL() throws SQLException {
+    getURL(getSigned());
+  }
+
+  @Test
+  public void getURLPrepared() throws SQLException {
+    getURL(getSignedPrepared(sharedConn));
+    getURL(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getURL(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLSyntaxErrorException.class, () -> rs.getURL(1), "Could not parse '0' as URL");
+    Common.assertThrowsContains(
+        SQLSyntaxErrorException.class, () -> rs.getURL("t1alias"), "Could not parse '0' as URL");
+  }
+
+  @Test
+  public void getSQLXML() throws SQLException {
+    getSQLXML(getSigned());
+  }
+
+  @Test
+  public void getSQLXMLPrepared() throws SQLException {
+    getSQLXML(getSignedPrepared(sharedConn));
+    getSQLXML(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getSQLXML(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getSQLXML(1), "Method ResultSet.getSQLXML not supported");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getSQLXML("t1alias"),
+        "Method ResultSet.getSQLXML not supported");
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = getSigned();
+    ResultSetMetaData meta = rs.getMetaData();
+    assertEquals("MEDIUMINT", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.lang.Integer", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.INTEGER, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(9, meta.getPrecision(1));
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    assertEquals(9, meta.getColumnDisplaySize(1));
+  }
+
+  @Test
+  public void sendParam() throws SQLException {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE MediumIntCodec3");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO MediumIntCodec3(id, id2, t1) VALUES (?,?,?)")) {
+      prep.setInt(1, 1);
+      prep.setInt(2, 1);
+      prep.setInt(3, 3);
+      prep.execute();
+      prep.setInt(1, 2);
+      prep.setInt(2, 1);
+      prep.setObject(3, 2);
+      prep.execute();
+      prep.setInt(1, 3);
+      prep.setInt(2, 1);
+      prep.setObject(3, null);
+      prep.execute();
+      prep.setInt(1, 4);
+      prep.setInt(2, 1);
+      prep.setObject(3, 3, Types.INTEGER);
+      prep.execute();
+      prep.setInt(1, 5);
+      prep.setInt(2, 1);
+      prep.setObject(3, null, Types.INTEGER);
+      prep.execute();
+    }
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM MediumIntCodec3");
+    assertTrue(rs.next());
+    assertEquals(3, rs.getInt(3));
+    rs.updateInt("t1", 10);
+    rs.updateRow();
+    assertEquals(10, rs.getInt(3));
+
+    assertTrue(rs.next());
+    assertEquals(2, rs.getInt(3));
+    rs.updateObject("t1", null);
+    rs.updateRow();
+    assertEquals(0, rs.getInt(3));
+
+    assertTrue(rs.next());
+    assertEquals(0, rs.getInt(3));
+    assertTrue(rs.wasNull());
+    rs.updateInt(3, 50);
+    rs.updateRow();
+    assertEquals(50, rs.getInt(3));
+
+    assertTrue(rs.next());
+    assertEquals(3, rs.getInt(3));
+    rs.updateObject(3, null, Types.INTEGER);
+    rs.updateRow();
+    assertEquals(0, rs.getInt(3));
+    assertTrue(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(0, rs.getInt(3));
+    assertTrue(rs.wasNull());
+    rs.updateObject(3, 85, Types.INTEGER);
+    rs.updateRow();
+    assertEquals(85, rs.getInt(3));
+    assertFalse(rs.wasNull());
+
+    rs = stmt.executeQuery("SELECT * FROM MediumIntCodec3");
+    assertTrue(rs.next());
+    assertEquals(10, rs.getInt(3));
+
+    assertTrue(rs.next());
+    assertEquals(0, rs.getInt(3));
+
+    assertTrue(rs.next());
+    assertEquals(50, rs.getInt(3));
+
+    assertTrue(rs.next());
+    assertEquals(0, rs.getInt(3));
+    assertTrue(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(85, rs.getInt(3));
+    assertFalse(rs.wasNull());
+    con.commit();
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/MultiLineStringCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/MultiLineStringCodecTest.java
new file mode 100644
index 0000000..4c5633e
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/MultiLineStringCodecTest.java
@@ -0,0 +1,445 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.type.GeometryCollection;
+import org.mariadb.jdbc.type.LineString;
+import org.mariadb.jdbc.type.MultiLineString;
+import org.mariadb.jdbc.type.Point;
+import org.mariadb.jdbc.util.constants.Capabilities;
+
+public class MultiLineStringCodecTest extends CommonCodecTest {
+  public static org.mariadb.jdbc.Connection geoConn;
+  private final MultiLineString ls1 =
+      new MultiLineString(
+          new LineString[] {
+            new LineString(
+                new Point[] {
+                  new Point(1, 1),
+                  new Point(1, 5),
+                  new Point(4, 9),
+                  new Point(6, 9),
+                  new Point(9, 3),
+                  new Point(7, 2)
+                },
+                true)
+          });
+  private final MultiLineString ls2 =
+      new MultiLineString(
+          new LineString[] {
+            new LineString(
+                new Point[] {
+                  new Point(0, 0), new Point(50, 0), new Point(50, 50), new Point(0, 50)
+                },
+                true),
+            new LineString(
+                new Point[] {
+                  new Point(10, 10), new Point(20, 10), new Point(20, 20), new Point(10, 20)
+                },
+                true)
+          });
+
+  private final MultiLineString ls3 =
+      new MultiLineString(
+          new LineString[] {
+            new LineString(
+                new Point[] {
+                  new Point(0, 0), new Point(50, 0), new Point(50, 50), new Point(0, 50)
+                },
+                true)
+          });
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS MultiLineStringCodec");
+    stmt.execute("DROP TABLE IF EXISTS MultiLineStringCodec2");
+    if (geoConn != null) geoConn.close();
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws Exception {
+    drop();
+    // xpand doesn't recognized MultiLineString
+    Assumptions.assumeFalse(isXpand());
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE MultiLineStringCodec (t1 MultiLineString, t2 MultiLineString, t3"
+            + " MultiLineString, t4 MultiLineString)");
+    stmt.execute(
+        "INSERT INTO MultiLineStringCodec VALUES (ST_MLineFromText('MULTILINESTRING((1 1,1 5,4 9,6"
+            + " 9,9 3,7 2))'), ST_MLineFromText('MULTILINESTRING((0 0,50 0,50 50,0 50), (10 10,20"
+            + " 10,20 20,10 20))'), ST_MLineFromText('MULTILINESTRING((0 0,50 0,50 50,0 50))'),"
+            + " null)");
+    stmt.execute(
+        "CREATE TABLE MultiLineStringCodec2 (id int not null primary key auto_increment, t1"
+            + " MultiLineString)");
+    stmt.execute("FLUSH TABLES");
+
+    String binUrl =
+        mDefUrl + (mDefUrl.indexOf("?") > 0 ? "&" : "?") + "geometryDefaultType=default";
+    geoConn = (org.mariadb.jdbc.Connection) DriverManager.getConnection(binUrl);
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from"
+                + " MultiLineStringCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private CompleteResult getPrepare(org.mariadb.jdbc.Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from"
+                + " MultiLineStringCodec WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    CompleteResult rs = (CompleteResult) preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws Exception {
+    getObject(get(), false);
+  }
+
+  @Test
+  public void getObjectPrepare() throws Exception {
+    getObject(getPrepare(sharedConn), false);
+    getObject(getPrepare(sharedConnBinary), false);
+    getObject(getPrepare(geoConn), true);
+  }
+
+  public void getObject(ResultSet rs, boolean defaultGeo) throws SQLException {
+    if (defaultGeo && hasCapability(Capabilities.EXTENDED_TYPE_INFO)) {
+      assertEquals(ls1, rs.getObject(1));
+      assertFalse(rs.wasNull());
+      assertEquals(ls2, rs.getObject(2));
+      assertFalse(rs.wasNull());
+      assertEquals(ls3, rs.getObject(3));
+      assertFalse(rs.wasNull());
+      assertNull(rs.getObject(4));
+      assertTrue(rs.wasNull());
+    } else {
+      assertEquals(ls1, rs.getObject(1, MultiLineString.class));
+      assertFalse(rs.wasNull());
+      // MultiLineString((0 0,50 0,50 50,0 50,0 0), (10 10,20 10,20 20,10 20,10 10))
+      assertEquals(ls2, rs.getObject(2, MultiLineString.class));
+      assertFalse(rs.wasNull());
+      assertEquals(ls3, rs.getObject(3, MultiLineString.class));
+      assertFalse(rs.wasNull());
+      assertNull(rs.getObject(4));
+      assertTrue(rs.wasNull());
+    }
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testErrObject(rs, Integer.class);
+    testErrObject(rs, String.class);
+    testErrObject(rs, Long.class);
+    testErrObject(rs, Short.class);
+    testErrObject(rs, BigDecimal.class);
+    testErrObject(rs, BigInteger.class);
+    testErrObject(rs, Double.class);
+    testErrObject(rs, Float.class);
+    testErrObject(rs, Byte.class);
+    testArrObject(
+        rs,
+        new byte[] {
+          (byte) 0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x01,
+          0x05,
+          0x00,
+          0x00,
+          0x00,
+          0x01,
+          0x00,
+          0x00,
+          0x00,
+          0x01,
+          0x02,
+          0x00,
+          0x00,
+          0x00,
+          0x06,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          (byte) 0xF0,
+          0x3F,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          (byte) 0xF0,
+          0x3F,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          (byte) 0xF0,
+          0x3F,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x14,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x10,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x22,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x18,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x22,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x22,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x08,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x1C,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x40
+        });
+
+    testErrObject(rs, Boolean.class);
+    testErrObject(rs, Clob.class);
+    testErrObject(rs, NClob.class);
+    testErrObject(rs, InputStream.class);
+    testErrObject(rs, Reader.class);
+    testErrObject(rs, java.util.Date.class);
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    getMetaData(sharedConn, false);
+    try (org.mariadb.jdbc.Connection con = createCon("geometryDefaultType=default")) {
+      getMetaData(con, true);
+    }
+  }
+
+  private void getMetaData(org.mariadb.jdbc.Connection con, boolean geoDefault)
+      throws SQLException {
+    ResultSet rs = getPrepare(con);
+    ResultSetMetaData meta = rs.getMetaData();
+    if (hasCapability(Capabilities.EXTENDED_TYPE_INFO)) {
+      assertEquals("MULTILINESTRING", meta.getColumnTypeName(1));
+    } else {
+      assertEquals("GEOMETRY", meta.getColumnTypeName(1));
+    }
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals(
+        geoDefault
+            ? (hasCapability(Capabilities.EXTENDED_TYPE_INFO)
+                ? MultiLineString.class.getName()
+                : GeometryCollection.class.getName())
+            : "byte[]",
+        meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.VARBINARY, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+  }
+
+  @Test
+  public void sendParam() throws Exception {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws Exception {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE MultiLineStringCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO MultiLineStringCodec2(t1) VALUES (?)")) {
+      prep.setObject(1, ls1);
+      prep.execute();
+      prep.setObject(1, (MultiLineString) null);
+      prep.execute();
+
+      prep.setObject(1, ls2);
+      prep.addBatch();
+      prep.setObject(1, ls1);
+      prep.addBatch();
+      prep.executeBatch();
+    }
+
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM MultiLineStringCodec2");
+    assertTrue(rs.next());
+    assertEquals(ls1, rs.getObject(2, MultiLineString.class));
+    rs.updateNull(2);
+    rs.updateRow();
+    assertNull(rs.getObject(2, MultiLineString.class));
+
+    assertTrue(rs.next());
+    assertNull(rs.getObject(2, MultiLineString.class));
+    rs.updateObject(2, ls2);
+    rs.updateRow();
+    assertEquals(ls2, rs.getObject(2, MultiLineString.class));
+    assertTrue(rs.next());
+
+    assertEquals(ls2, rs.getObject(2, MultiLineString.class));
+    assertTrue(rs.next());
+    assertEquals(ls1, rs.getObject(2, MultiLineString.class));
+    con.commit();
+  }
+
+  @Test
+  public void equal() {
+    assertEquals(ls2, ls2);
+    assertEquals(
+        new MultiLineString(
+            new LineString[] {
+              new LineString(
+                  new Point[] {
+                    new Point(0, 0), new Point(50, 0), new Point(50, 50), new Point(0, 50)
+                  },
+                  true),
+              new LineString(
+                  new Point[] {
+                    new Point(10, 10), new Point(20, 10), new Point(20, 20), new Point(10, 20)
+                  },
+                  true)
+            }),
+        ls2);
+    assertEquals(
+        new MultiLineString(
+                new LineString[] {
+                  new LineString(
+                      new Point[] {
+                        new Point(0, 0), new Point(50, 0), new Point(50, 50), new Point(0, 50)
+                      },
+                      true),
+                  new LineString(
+                      new Point[] {
+                        new Point(10, 10), new Point(20, 10), new Point(20, 20), new Point(10, 20)
+                      },
+                      true)
+                })
+            .hashCode(),
+        ls2.hashCode());
+    assertFalse(ls2.equals(null));
+    assertFalse(ls2.equals(""));
+    assertNotEquals(ls1, ls2);
+    assertNotEquals(
+        new MultiLineString(
+            new LineString[] {
+              new LineString(
+                  new Point[] {
+                    new Point(0, 0), new Point(50, 0), new Point(50, 50), new Point(0, 50)
+                  },
+                  true),
+              new LineString(
+                  new Point[] {
+                    new Point(10, 10), new Point(20, 10), new Point(20, 20), new Point(10, 20)
+                  },
+                  false)
+            }),
+        ls1);
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/MultiPointCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/MultiPointCodecTest.java
new file mode 100644
index 0000000..c51e682
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/MultiPointCodecTest.java
@@ -0,0 +1,366 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.type.GeometryCollection;
+import org.mariadb.jdbc.type.MultiPoint;
+import org.mariadb.jdbc.type.Point;
+import org.mariadb.jdbc.util.constants.Capabilities;
+
+public class MultiPointCodecTest extends CommonCodecTest {
+  public static org.mariadb.jdbc.Connection geoConn;
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS MultiPointCodec");
+    stmt.execute("DROP TABLE IF EXISTS MultiPointCodec2");
+    if (geoConn != null) geoConn.close();
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws Exception {
+    drop();
+    // xpand doesn't recognized MultiPoint
+    Assumptions.assumeFalse(isXpand());
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE MultiPointCodec (t1 MultiPoint, t2 MultiPoint, t3 MultiPoint, t4"
+            + " MultiPoint)");
+    stmt.execute(
+        "INSERT INTO MultiPointCodec VALUES (ST_MPointFromText('MULTIPOINT(0 0,0 10,10 0)'),"
+            + " ST_MPointFromText('MULTIPOINT(10 10,20 10,20 20,10 20,10 10)'),"
+            + " ST_MPointFromText('MULTIPOINT(-1 0.55, 3 5, 1 1)'), null)");
+    stmt.execute(
+        "CREATE TABLE MultiPointCodec2 (id int not null primary key auto_increment, t1"
+            + " MultiPoint)");
+    stmt.execute("FLUSH TABLES");
+
+    String binUrl =
+        mDefUrl + (mDefUrl.indexOf("?") > 0 ? "&" : "?") + "geometryDefaultType=default";
+    geoConn = (org.mariadb.jdbc.Connection) DriverManager.getConnection(binUrl);
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from"
+                + " MultiPointCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private CompleteResult getPrepare(org.mariadb.jdbc.Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from MultiPointCodec"
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    CompleteResult rs = (CompleteResult) preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws Exception {
+    getObject(get(), false);
+  }
+
+  @Test
+  public void getObjectPrepare() throws Exception {
+    getObject(getPrepare(sharedConn), false);
+    getObject(getPrepare(sharedConnBinary), false);
+    getObject(getPrepare(geoConn), true);
+  }
+
+  public void getObject(ResultSet rs, boolean defaultGeo) throws SQLException {
+    if (defaultGeo && hasCapability(Capabilities.EXTENDED_TYPE_INFO)) {
+      assertEquals(
+          new MultiPoint(new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0)}),
+          rs.getObject(1));
+      assertFalse(rs.wasNull());
+      assertEquals(
+          new MultiPoint(
+              new Point[] {
+                new Point(10, 10),
+                new Point(20, 10),
+                new Point(20, 20),
+                new Point(10, 20),
+                new Point(10, 10)
+              }),
+          rs.getObject(2));
+      assertFalse(rs.wasNull());
+      assertEquals(
+          new MultiPoint(new Point[] {new Point(-1, 0.55), new Point(3, 5), new Point(1, 1)}),
+          rs.getObject(3));
+      assertFalse(rs.wasNull());
+      assertNull(rs.getObject(4));
+      assertTrue(rs.wasNull());
+    } else {
+      assertEquals(
+          new MultiPoint(new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0)}),
+          rs.getObject(1, MultiPoint.class));
+      assertFalse(rs.wasNull());
+      assertEquals(
+          new MultiPoint(
+              new Point[] {
+                new Point(10, 10),
+                new Point(20, 10),
+                new Point(20, 20),
+                new Point(10, 20),
+                new Point(10, 10)
+              }),
+          rs.getObject(2, MultiPoint.class));
+      assertFalse(rs.wasNull());
+      assertEquals(
+          new MultiPoint(new Point[] {new Point(-1, 0.55), new Point(3, 5), new Point(1, 1)}),
+          rs.getObject(3, MultiPoint.class));
+      assertFalse(rs.wasNull());
+      assertNull(rs.getObject(4));
+      assertTrue(rs.wasNull());
+    }
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testErrObject(rs, Integer.class);
+    testErrObject(rs, String.class);
+    testErrObject(rs, Long.class);
+    testErrObject(rs, Short.class);
+    testErrObject(rs, BigDecimal.class);
+    testErrObject(rs, BigInteger.class);
+    testErrObject(rs, Double.class);
+    testErrObject(rs, Float.class);
+    testErrObject(rs, Byte.class);
+    testArrObject(
+        rs,
+        new byte[] {
+          (byte) 0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x01,
+          0x04,
+          0x00,
+          0x00,
+          0x00,
+          0x03,
+          0x00,
+          0x00,
+          0x00,
+          0x01,
+          0x01,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x01,
+          0x01,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x24,
+          0x40,
+          0x01,
+          0x01,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x24,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00
+        });
+
+    testErrObject(rs, Boolean.class);
+    testErrObject(rs, Clob.class);
+    testErrObject(rs, NClob.class);
+    testErrObject(rs, InputStream.class);
+    testErrObject(rs, Reader.class);
+    testErrObject(rs, java.util.Date.class);
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    getMetaData(sharedConn, false);
+    try (org.mariadb.jdbc.Connection con = createCon("geometryDefaultType=default")) {
+      getMetaData(con, true);
+    }
+  }
+
+  private void getMetaData(org.mariadb.jdbc.Connection con, boolean geoDefault)
+      throws SQLException {
+    ResultSet rs = getPrepare(con);
+    ResultSetMetaData meta = rs.getMetaData();
+    if (hasCapability(Capabilities.EXTENDED_TYPE_INFO)) {
+      assertEquals("MULTIPOINT", meta.getColumnTypeName(1));
+    } else {
+      assertEquals("GEOMETRY", meta.getColumnTypeName(1));
+    }
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals(
+        geoDefault
+            ? (hasCapability(Capabilities.EXTENDED_TYPE_INFO)
+                ? MultiPoint.class.getName()
+                : GeometryCollection.class.getName())
+            : "byte[]",
+        meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.VARBINARY, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+  }
+
+  @Test
+  public void sendParam() throws Exception {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws Exception {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE MultiPointCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    MultiPoint ls1 =
+        new MultiPoint(new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0)});
+    MultiPoint ls2 =
+        new MultiPoint(
+            new Point[] {
+              new Point(10, 10),
+              new Point(20, 10),
+              new Point(20, 20),
+              new Point(10, 20),
+              new Point(10, 10)
+            });
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO MultiPointCodec2(t1) VALUES (?)")) {
+      prep.setObject(1, ls1);
+      prep.execute();
+      prep.setObject(1, (MultiPoint) null);
+      prep.execute();
+
+      prep.setObject(1, ls2);
+      prep.addBatch();
+      prep.setObject(1, ls1);
+      prep.addBatch();
+      prep.executeBatch();
+    }
+
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM MultiPointCodec2");
+    assertTrue(rs.next());
+    assertEquals(ls1, rs.getObject(2, MultiPoint.class));
+    rs.updateNull(2);
+    rs.updateRow();
+    assertNull(rs.getObject(2, MultiPoint.class));
+
+    assertTrue(rs.next());
+    assertNull(rs.getObject(2, MultiPoint.class));
+    rs.updateObject(2, ls2);
+    rs.updateRow();
+    assertEquals(ls2, rs.getObject(2, MultiPoint.class));
+    assertTrue(rs.next());
+
+    assertEquals(ls2, rs.getObject(2, MultiPoint.class));
+    assertTrue(rs.next());
+    assertEquals(ls1, rs.getObject(2, MultiPoint.class));
+    con.commit();
+  }
+
+  @Test
+  public void equal() {
+    MultiPoint mp =
+        new MultiPoint(new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0)});
+    assertEquals(mp, mp);
+    assertEquals(
+        new MultiPoint(new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0)}), mp);
+    assertEquals(
+        new MultiPoint(new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0)})
+            .hashCode(),
+        mp.hashCode());
+    assertFalse(mp.equals(null));
+    assertFalse(mp.equals(""));
+    assertNotEquals(
+        new MultiPoint(new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 20)}), mp);
+    assertNotEquals(
+        new MultiPoint(
+            new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0), new Point(10, 20)}),
+        mp);
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/MultiPolygonCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/MultiPolygonCodecTest.java
new file mode 100644
index 0000000..469dad2
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/MultiPolygonCodecTest.java
@@ -0,0 +1,429 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.type.*;
+import org.mariadb.jdbc.util.constants.Capabilities;
+
+public class MultiPolygonCodecTest extends CommonCodecTest {
+  public static org.mariadb.jdbc.Connection geoConn;
+  private final MultiPolygon ls1 =
+      new MultiPolygon(
+          new Polygon[] {
+            new Polygon(
+                new LineString[] {
+                  new LineString(
+                      new Point[] {
+                        new Point(1, 1),
+                        new Point(1, 5),
+                        new Point(4, 9),
+                        new Point(6, 9),
+                        new Point(9, 3),
+                        new Point(7, 2),
+                        new Point(1, 1)
+                      },
+                      false),
+                }),
+            new Polygon(
+                new LineString[] {
+                  new LineString(
+                      new Point[] {
+                        new Point(0, 0),
+                        new Point(50, 0),
+                        new Point(50, 50),
+                        new Point(0, 50),
+                        new Point(0, 0)
+                      },
+                      false),
+                  new LineString(
+                      new Point[] {
+                        new Point(10, 10),
+                        new Point(20, 10),
+                        new Point(20, 20),
+                        new Point(10, 20),
+                        new Point(10, 10)
+                      },
+                      false)
+                })
+          });
+  private final MultiPolygon ls2 =
+      new MultiPolygon(
+          new Polygon[] {
+            new Polygon(
+                new LineString[] {
+                  new LineString(
+                      new Point[] {
+                        new Point(1, 1),
+                        new Point(1, 8),
+                        new Point(4, 9),
+                        new Point(6, 9),
+                        new Point(9, 3),
+                        new Point(7, 2),
+                        new Point(1, 1)
+                      },
+                      false),
+                })
+          });
+
+  private final MultiPolygon ls3 =
+      new MultiPolygon(
+          new Polygon[] {
+            new Polygon(
+                new LineString[] {
+                  new LineString(
+                      new Point[] {
+                        new Point(0, 0),
+                        new Point(50, 0),
+                        new Point(50, 50),
+                        new Point(0, 50),
+                        new Point(0, 0)
+                      },
+                      false),
+                  new LineString(
+                      new Point[] {
+                        new Point(10, 10),
+                        new Point(20, 10),
+                        new Point(20, 20),
+                        new Point(10, 20),
+                        new Point(10, 10)
+                      },
+                      false)
+                })
+          });
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS MultiPolygonCodec");
+    stmt.execute("DROP TABLE IF EXISTS MultiPolygonCodec2");
+    if (geoConn != null) geoConn.close();
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws Exception {
+    drop();
+    // xpand doesn't recognized MultiPolygon
+    Assumptions.assumeFalse(isXpand());
+
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE MultiPolygonCodec (t1 MultiPolygon, t2 MultiPolygon, t3 MultiPolygon, t4"
+            + " MultiPolygon)");
+    stmt.execute(
+        "INSERT INTO MultiPolygonCodec VALUES (ST_MPolyFromText('MULTIPOLYGON(((1 1, 1 5,4 9,6 9,9"
+            + " 3,7 2, 1 1)), ((0 0, 50 0,50 50,0 50,0 0), (10 10,20 10,20 20,10 20,10 10)))'),"
+            + " ST_MPolyFromText('MULTIPOLYGON(((1 1, 1 8,4 9,6 9,9 3,7 2, 1 1)))'),"
+            + " ST_MPolyFromText('MULTIPOLYGON(((0 0, 50 0,50 50,0 50,0 0), (10 10,20 10,20 20,10"
+            + " 20,10 10)))'), null)");
+    stmt.execute(
+        "CREATE TABLE MultiPolygonCodec2 (id int not null primary key auto_increment, t1"
+            + " MultiPolygon)");
+    stmt.execute("FLUSH TABLES");
+
+    String binUrl =
+        mDefUrl + (mDefUrl.indexOf("?") > 0 ? "&" : "?") + "geometryDefaultType=default";
+    geoConn = (org.mariadb.jdbc.Connection) DriverManager.getConnection(binUrl);
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from"
+                + " MultiPolygonCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private CompleteResult getPrepare(org.mariadb.jdbc.Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from"
+                + " MultiPolygonCodec WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    CompleteResult rs = (CompleteResult) preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws Exception {
+    getObject(get(), false);
+  }
+
+  @Test
+  public void getObjectPrepare() throws Exception {
+    getObject(getPrepare(sharedConn), false);
+    getObject(getPrepare(sharedConnBinary), false);
+    getObject(getPrepare(geoConn), true);
+  }
+
+  public void getObject(ResultSet rs, boolean defaultGeo) throws SQLException {
+    if (defaultGeo && hasCapability(Capabilities.EXTENDED_TYPE_INFO)) {
+      assertEquals(ls1, rs.getObject(1));
+      assertFalse(rs.wasNull());
+      assertEquals(ls2, rs.getObject(2));
+      assertFalse(rs.wasNull());
+      assertEquals(ls3, rs.getObject(3));
+      assertFalse(rs.wasNull());
+      assertNull(rs.getObject(4));
+      assertTrue(rs.wasNull());
+    } else {
+      assertEquals(ls1, rs.getObject(1, MultiPolygon.class));
+      assertFalse(rs.wasNull());
+      assertEquals(ls2, rs.getObject(2, MultiPolygon.class));
+      assertFalse(rs.wasNull());
+      assertEquals(ls3, rs.getObject(3, MultiPolygon.class));
+      assertFalse(rs.wasNull());
+      assertNull(rs.getObject(4));
+      assertTrue(rs.wasNull());
+    }
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  private static int toDigit(char hexChar) {
+    int digit = Character.digit(hexChar, 16);
+    if (digit == -1) {
+      throw new IllegalArgumentException("Invalid Hexadecimal Character: " + hexChar);
+    }
+    return digit;
+  }
+
+  public static byte hexToByte(String hexString) {
+    int firstDigit = toDigit(hexString.charAt(0));
+    int secondDigit = toDigit(hexString.charAt(1));
+    return (byte) ((firstDigit << 4) + secondDigit);
+  }
+
+  public static byte[] decodeHexString(String hexString) {
+    if (hexString.length() % 2 == 1) {
+      throw new IllegalArgumentException("Invalid hexadecimal String supplied.");
+    }
+
+    byte[] bytes = new byte[hexString.length() / 2];
+    for (int i = 0; i < hexString.length(); i += 2) {
+      bytes[i / 2] = hexToByte(hexString.substring(i, i + 2));
+    }
+    return bytes;
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testErrObject(rs, Integer.class, 1);
+    testErrObject(rs, String.class, 1);
+    testErrObject(rs, Long.class, 1);
+    testErrObject(rs, Short.class, 1);
+    testErrObject(rs, BigDecimal.class, 1);
+    testErrObject(rs, BigInteger.class, 1);
+    testErrObject(rs, Double.class, 1);
+    testErrObject(rs, Float.class, 1);
+    testErrObject(rs, Byte.class, 1);
+    String hexa =
+        "0000000001060000000200000001030000000100000007000000000000000000F03F000000000000F03F000000000000F03F00000000000014400000000000001040000000000000224000000000000018400000000000002240000000000000224000000000000008400000000000001C400000000000000040000000000000F03F000000000000F03F010300000002000000050000000000000000000000000000000000000000000000000049400000000000000000000000000000494000000000000049400000000000000000000000000000494000000000000000000000000000000000050000000000000000002440000000000000244000000000000034400000000000002440000000000000344000000000000034400000000000002440000000000000344000000000000024400000000000002440";
+    testArrObject(rs, decodeHexString(hexa), 1);
+
+    testErrObject(rs, Boolean.class, 1);
+    testErrObject(rs, Clob.class, 1);
+    testErrObject(rs, NClob.class, 1);
+    testErrObject(rs, InputStream.class, 1);
+    testErrObject(rs, Reader.class, 1);
+    testErrObject(rs, java.util.Date.class, 1);
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    getMetaData(sharedConn, false);
+    try (org.mariadb.jdbc.Connection con = createCon("geometryDefaultType=default")) {
+      getMetaData(con, true);
+    }
+  }
+
+  private void getMetaData(org.mariadb.jdbc.Connection con, boolean geoDefault)
+      throws SQLException {
+    ResultSet rs = getPrepare(con);
+    ResultSetMetaData meta = rs.getMetaData();
+    if (hasCapability(Capabilities.EXTENDED_TYPE_INFO)) {
+      assertEquals("MULTIPOLYGON", meta.getColumnTypeName(1));
+    } else {
+      assertEquals("GEOMETRY", meta.getColumnTypeName(1));
+    }
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals(
+        geoDefault
+            ? (hasCapability(Capabilities.EXTENDED_TYPE_INFO)
+                ? MultiPolygon.class.getName()
+                : GeometryCollection.class.getName())
+            : "byte[]",
+        meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.VARBINARY, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+  }
+
+  @Test
+  public void sendParam() throws Exception {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws Exception {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE MultiPolygonCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO MultiPolygonCodec2(t1) VALUES (?)")) {
+      prep.setObject(1, ls1);
+      prep.execute();
+      prep.setObject(1, (MultiPolygon) null);
+      prep.execute();
+
+      prep.setObject(1, ls2);
+      prep.addBatch();
+      prep.setObject(1, ls1);
+      prep.addBatch();
+      prep.executeBatch();
+    }
+
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM MultiPolygonCodec2");
+    assertTrue(rs.next());
+    assertEquals(ls1, rs.getObject(2, MultiPolygon.class));
+    rs.updateNull(2);
+    rs.updateRow();
+    assertNull(rs.getObject(2, MultiPolygon.class));
+
+    assertTrue(rs.next());
+    assertNull(rs.getObject(2, MultiPolygon.class));
+    rs.updateObject(2, ls2);
+    rs.updateRow();
+    assertEquals(ls2, rs.getObject(2, MultiPolygon.class));
+    assertTrue(rs.next());
+
+    assertEquals(ls2, rs.getObject(2, MultiPolygon.class));
+    assertTrue(rs.next());
+    assertEquals(ls1, rs.getObject(2, MultiPolygon.class));
+    con.commit();
+  }
+
+  @Test
+  public void equal() {
+    assertEquals(ls1, ls1);
+    MultiPolygon testPoly =
+        new MultiPolygon(
+            new Polygon[] {
+              new Polygon(
+                  new LineString[] {
+                    new LineString(
+                        new Point[] {
+                          new Point(1, 1),
+                          new Point(1, 5),
+                          new Point(4, 9),
+                          new Point(6, 9),
+                          new Point(9, 3),
+                          new Point(7, 2),
+                          new Point(1, 1)
+                        },
+                        false),
+                  }),
+              new Polygon(
+                  new LineString[] {
+                    new LineString(
+                        new Point[] {
+                          new Point(0, 0),
+                          new Point(50, 0),
+                          new Point(50, 50),
+                          new Point(0, 50),
+                          new Point(0, 0)
+                        },
+                        false),
+                    new LineString(
+                        new Point[] {
+                          new Point(10, 10),
+                          new Point(20, 10),
+                          new Point(20, 20),
+                          new Point(10, 20),
+                          new Point(10, 10)
+                        },
+                        false)
+                  })
+            });
+    assertEquals(testPoly, ls1);
+    assertEquals(testPoly.hashCode(), ls1.hashCode());
+    assertFalse(ls1.equals(null));
+    assertFalse(ls1.equals(""));
+    assertNotEquals(
+        new MultiPolygon(
+            new Polygon[] {
+              new Polygon(
+                  new LineString[] {
+                    new LineString(
+                        new Point[] {
+                          new Point(1, 1),
+                          new Point(1, 5),
+                          new Point(4, 9),
+                          new Point(6, 9),
+                          new Point(9, 3),
+                          new Point(7, 2),
+                          new Point(1, 1)
+                        },
+                        false),
+                  }),
+              new Polygon(
+                  new LineString[] {
+                    new LineString(
+                        new Point[] {
+                          new Point(0, 0),
+                          new Point(50, 0),
+                          new Point(50, 50),
+                          new Point(0, 50),
+                          new Point(0, 0)
+                        },
+                        false),
+                    new LineString(
+                        new Point[] {
+                          new Point(10, 10),
+                          new Point(20, 10),
+                          new Point(20, 20),
+                          new Point(10, 20),
+                          new Point(10, 15)
+                        },
+                        false)
+                  })
+            }),
+        ls1);
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/NullCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/NullCodecTest.java
new file mode 100644
index 0000000..2bea3d8
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/NullCodecTest.java
@@ -0,0 +1,743 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import java.time.*;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.integration.Common;
+
+public class NullCodecTest extends CommonCodecTest {
+
+  private ResultSet getSigned() throws SQLException {
+    return get("IntCodec");
+  }
+
+  private ResultSet getUnsigned() throws SQLException {
+    return get("IntCodecUnsigned");
+  }
+
+  private ResultSet get(String table) throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs = stmt.executeQuery("select NULL as t1alias");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getPreparedSigned(Connection con) throws SQLException {
+    return getPrepare(con, "IntCodec");
+  }
+
+  private ResultSet getPreparedUnsigned(Connection con) throws SQLException {
+    return getPrepare(con, "IntCodecUnsigned");
+  }
+
+  private ResultSet getPrepare(Connection con, String table) throws SQLException {
+    Assumptions.assumeTrue(
+        (isMariaDBServer() && minVersion(10, 4, 0)) || (!isMariaDBServer() && minVersion(8, 0, 0)));
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement preparedStatement =
+        con.prepareStatement("select NULL as t1alias WHERE 1 > ?")) {
+      preparedStatement.setInt(1, 0);
+      ResultSet rs = preparedStatement.executeQuery();
+      assertTrue(rs.next());
+      return rs;
+    } finally {
+      con.commit();
+    }
+  }
+
+  @Test
+  public void getObject() throws SQLException {
+    getObject(getSigned());
+  }
+
+  @Test
+  public void getObjectPrepared() throws SQLException {
+    getObject(getPreparedSigned(sharedConn));
+    getObject(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getObject(ResultSet rs) throws SQLException {
+    assertNull(rs.getObject(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getObject("t1alias"));
+    assertTrue(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(1, int.class),
+        "Cannot return null for primitive int");
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(getSigned());
+  }
+
+  @Test
+  public void getObjectTypePrepared() throws Exception {
+    getObjectType(getPreparedSigned(sharedConn));
+    getObjectType(getPreparedSigned(sharedConnBinary));
+  }
+
+  void testNullObject(ResultSet rs, Class<?> objClass) throws Exception {
+    assertNull(rs.getObject(1, objClass));
+    assertNull(rs.getObject("t1alias", objClass));
+  }
+
+  private void getObjectType(ResultSet rs) throws Exception {
+    testNullObject(rs, Integer.class);
+    testNullObject(rs, String.class);
+    testNullObject(rs, Long.class);
+    testNullObject(rs, Short.class);
+    testNullObject(rs, BigDecimal.class);
+    testNullObject(rs, BigInteger.class);
+    testNullObject(rs, Double.class);
+    testNullObject(rs, Float.class);
+    testNullObject(rs, Byte.class);
+    testNullObject(rs, byte[].class);
+    testNullObject(rs, Date.class);
+    testNullObject(rs, Time.class);
+    testNullObject(rs, Timestamp.class);
+    testNullObject(rs, java.util.Date.class);
+    testNullObject(rs, LocalDate.class);
+    testNullObject(rs, ZonedDateTime.class);
+    testNullObject(rs, OffsetDateTime.class);
+    testNullObject(rs, LocalDateTime.class);
+    testNullObject(rs, OffsetTime.class);
+    testNullObject(rs, Boolean.class);
+    testNullObject(rs, Clob.class);
+    testNullObject(rs, NClob.class);
+    testNullObject(rs, InputStream.class);
+    testNullObject(rs, Reader.class);
+    rs.next();
+  }
+
+  @Test
+  public void getString() throws SQLException {
+    getString(getSigned());
+  }
+
+  @Test
+  public void getStringPrepared() throws SQLException {
+    getString(getPreparedSigned(sharedConn));
+    getString(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getString(ResultSet rs) throws SQLException {
+    assertNull(rs.getString(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getString("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getStringUnsigned() throws SQLException {
+    getStringUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getStringUnsignedPrepared() throws SQLException {
+    getStringUnsigned(getPreparedUnsigned(sharedConn));
+    getStringUnsigned(getPreparedUnsigned(sharedConnBinary));
+  }
+
+  private void getStringUnsigned(ResultSet rs) throws SQLException {
+    assertNull(rs.getString(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getString("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNString() throws SQLException {
+    getNString(getSigned());
+  }
+
+  @Test
+  public void getNStringPrepared() throws SQLException {
+    getNString(getPreparedSigned(sharedConn));
+    getNString(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getNString(ResultSet rs) throws SQLException {
+    assertNull(rs.getNString(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getNString("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBoolean() throws SQLException {
+    getBoolean(getSigned());
+  }
+
+  @Test
+  public void getBooleanPrepared() throws SQLException {
+    getBoolean(getPreparedSigned(sharedConn));
+    getBoolean(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getBoolean(ResultSet rs) throws SQLException {
+    assertEquals(false, rs.getBoolean(1));
+    assertTrue(rs.wasNull());
+    assertEquals(false, rs.getBoolean("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getByte() throws SQLException {
+    getByte(getSigned());
+  }
+
+  @Test
+  public void getBytePrepared() throws SQLException {
+    getByte(getPreparedSigned(sharedConn));
+    getByte(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getByte(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getByte(1));
+    assertTrue(rs.wasNull());
+    assertEquals(0, rs.getByte("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getUnsignedByte() throws SQLException {
+    getUnsignedByte(getUnsigned());
+  }
+
+  @Test
+  public void getUnsignedBytePrepared() throws SQLException {
+    getUnsignedByte(getPreparedUnsigned(sharedConn));
+    getUnsignedByte(getPreparedUnsigned(sharedConnBinary));
+  }
+
+  private void getUnsignedByte(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getByte(1));
+    assertTrue(rs.wasNull());
+    assertEquals(0, rs.getByte("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getShort() throws SQLException {
+    getShort(getSigned());
+  }
+
+  @Test
+  public void getShortPrepared() throws SQLException {
+    getShort(getPreparedSigned(sharedConn));
+    getShort(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getShort(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getShort(1));
+    assertTrue(rs.wasNull());
+    assertEquals(0, rs.getShort("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(getSigned());
+  }
+
+  @Test
+  public void getIntPrepared() throws SQLException {
+    getInt(getPreparedSigned(sharedConn));
+    getInt(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getInt(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertTrue(rs.wasNull());
+    assertEquals(0, rs.getInt("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getIntUnsigned() throws SQLException {
+    getIntUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getIntUnsignedPrepared() throws SQLException {
+    getIntUnsigned(getPreparedUnsigned(sharedConn));
+    getIntUnsigned(getPreparedUnsigned(sharedConnBinary));
+  }
+
+  private void getIntUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertTrue(rs.wasNull());
+    assertEquals(0, rs.getInt("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(getSigned());
+  }
+
+  @Test
+  public void getLongPrepared() throws SQLException {
+    getLong(getPreparedSigned(sharedConn));
+    getLong(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getLong(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getLong(1));
+    assertTrue(rs.wasNull());
+    assertEquals(0, rs.getLong("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLongUnsigned() throws SQLException {
+    getLongUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getLongUnsignedPrepared() throws SQLException {
+    getLongUnsigned(getPreparedUnsigned(sharedConn));
+    getLongUnsigned(getPreparedUnsigned(sharedConnBinary));
+  }
+
+  private void getLongUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getLong(1));
+    assertTrue(rs.wasNull());
+    assertEquals(0, rs.getLong("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(getSigned());
+  }
+
+  @Test
+  public void getFloatPrepared() throws SQLException {
+    getFloat(getPreparedSigned(sharedConn));
+    getFloat(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getFloat(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getFloat(1));
+    assertTrue(rs.wasNull());
+    assertEquals(0, rs.getFloat("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloatUnsigned() throws SQLException {
+    getFloatUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getFloatUnsignedPrepared() throws SQLException {
+    getFloatUnsigned(getPreparedUnsigned(sharedConn));
+    getFloatUnsigned(getPreparedUnsigned(sharedConnBinary));
+  }
+
+  private void getFloatUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getFloat(1));
+    assertTrue(rs.wasNull());
+    assertEquals(0, rs.getFloat("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(getSigned());
+  }
+
+  @Test
+  public void getDoublePrepared() throws SQLException {
+    getDouble(getPreparedSigned(sharedConn));
+    getDouble(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getDouble(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getDouble(1));
+    assertTrue(rs.wasNull());
+    assertEquals(0, rs.getDouble("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDoubleUnsigned() throws SQLException {
+    getDoubleUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getDoubleUnsignedPrepared() throws SQLException {
+    getDoubleUnsigned(getPreparedUnsigned(sharedConn));
+    getDoubleUnsigned(getPreparedUnsigned(sharedConnBinary));
+  }
+
+  private void getDoubleUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getDouble(1));
+    assertTrue(rs.wasNull());
+    assertEquals(0, rs.getDouble("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(getSigned());
+  }
+
+  @Test
+  public void getBigDecimalPrepared() throws SQLException {
+    getBigDecimal(getPreparedSigned(sharedConn));
+    getBigDecimal(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getBigDecimal(ResultSet rs) throws SQLException {
+    assertNull(rs.getBigDecimal(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getBigDecimal("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimalUnsigned() throws SQLException {
+    getBigDecimalUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getBigDecimalUnsignedPrepared() throws SQLException {
+    getBigDecimalUnsigned(getPreparedUnsigned(sharedConn));
+    getBigDecimalUnsigned(getPreparedUnsigned(sharedConnBinary));
+  }
+
+  private void getBigDecimalUnsigned(ResultSet rs) throws SQLException {
+    assertNull(rs.getBigDecimal(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getBigDecimal("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigInteger() throws SQLException {
+    getBigInteger(getSigned());
+  }
+
+  @Test
+  public void getBigIntegerPrepared() throws SQLException {
+    getBigInteger(getPreparedSigned(sharedConn));
+    getBigInteger(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getBigInteger(ResultSet res) throws SQLException {
+    CompleteResult rs = (CompleteResult) res;
+    assertNull(rs.getBigInteger(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getBigInteger("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigIntegerUnsigned() throws SQLException {
+    getBigIntegerUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getBigIntegerUnsignedPrepared() throws SQLException {
+    getBigIntegerUnsigned(getPreparedUnsigned(sharedConn));
+    getBigIntegerUnsigned(getPreparedUnsigned(sharedConnBinary));
+  }
+
+  private void getBigIntegerUnsigned(ResultSet res) throws SQLException {
+    CompleteResult rs = (CompleteResult) res;
+    assertNull(rs.getBigInteger(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getBigInteger("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDuration() throws SQLException {
+    getDuration(getSigned());
+  }
+
+  @Test
+  public void getDurationPrepare() throws SQLException {
+    getDuration(getPreparedSigned(sharedConn));
+    getDuration(getPreparedSigned(sharedConnBinary));
+  }
+
+  public void getDuration(ResultSet rs) throws SQLException {
+    assertNull(rs.getObject(1, Duration.class));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getObject("t1alias", Duration.class));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(getSigned());
+  }
+
+  @Test
+  public void getDatePrepared() throws SQLException {
+    getDate(getPreparedSigned(sharedConn));
+    getDate(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getDate(ResultSet rs) throws SQLException {
+    assertNull(rs.getDate(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getDate("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(getSigned());
+  }
+
+  @Test
+  public void getTimePrepared() throws SQLException {
+    getTime(getPreparedSigned(sharedConn));
+    getTime(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getTime(ResultSet rs) throws SQLException {
+    assertNull(rs.getTime(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getTime("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getTimestamp() throws SQLException {
+    getTimestamp(getSigned());
+  }
+
+  @Test
+  public void getTimestampPrepared() throws SQLException {
+    getTimestamp(getPreparedSigned(sharedConn));
+    getTimestamp(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getTimestamp(ResultSet rs) throws SQLException {
+    assertNull(rs.getTimestamp(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getTimestamp("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getAsciiStream() throws SQLException {
+    getAsciiStream(getSigned());
+  }
+
+  @Test
+  public void getAsciiStreamPrepared() throws SQLException {
+    getAsciiStream(getPreparedSigned(sharedConn));
+    getAsciiStream(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getAsciiStream(ResultSet rs) throws SQLException {
+    assertNull(rs.getAsciiStream(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getAsciiStream("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getUnicodeStream() throws SQLException {
+    getUnicodeStream(getSigned());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepared() throws SQLException {
+    getUnicodeStream(getPreparedSigned(sharedConn));
+    getUnicodeStream(getPreparedSigned(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  private void getUnicodeStream(ResultSet rs) throws SQLException {
+    assertNull(rs.getUnicodeStream(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getUnicodeStream("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBinaryStream() throws SQLException {
+    getBinaryStream(getSigned());
+  }
+
+  @Test
+  public void getBinaryStreamPrepared() throws SQLException {
+    getBinaryStream(getPreparedSigned(sharedConn));
+    getBinaryStream(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getBinaryStream(ResultSet rs) throws SQLException {
+    assertNull(rs.getBinaryStream(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getBinaryStream("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBytes() throws SQLException {
+    getBytes(getSigned());
+  }
+
+  @Test
+  public void getBytesPrepared() throws SQLException {
+    getBytes(getPreparedSigned(sharedConn));
+    getBytes(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getBytes(ResultSet rs) throws SQLException {
+    assertNull(rs.getBytes(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getBytes("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getCharacterStream() throws SQLException {
+    getCharacterStream(getSigned());
+  }
+
+  @Test
+  public void getCharacterStreamPrepared() throws SQLException {
+    getCharacterStream(getPreparedSigned(sharedConn));
+    getCharacterStream(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getCharacterStream(ResultSet rs) throws SQLException {
+    assertNull(rs.getCharacterStream(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getCharacterStream("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNCharacterStream() throws SQLException {
+    getNCharacterStream(getSigned());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepared() throws SQLException {
+    getNCharacterStream(getPreparedSigned(sharedConn));
+    getNCharacterStream(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getNCharacterStream(ResultSet rs) throws SQLException {
+    assertNull(rs.getNCharacterStream(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getNCharacterStream("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBlob() throws SQLException {
+    getBlob(getSigned());
+  }
+
+  @Test
+  public void getBlobPrepared() throws SQLException {
+    getBlob(getPreparedSigned(sharedConn));
+    getBlob(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getBlob(ResultSet rs) throws SQLException {
+    assertNull(rs.getCharacterStream(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getCharacterStream("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getClob() throws SQLException {
+    getClob(getSigned());
+  }
+
+  @Test
+  public void getClobPrepared() throws SQLException {
+    getClob(getPreparedSigned(sharedConn));
+    getClob(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getClob(ResultSet rs) throws SQLException {
+    assertNull(rs.getClob(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getClob("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNClob() throws SQLException {
+    getNClob(getSigned());
+  }
+
+  @Test
+  public void getNClobPrepared() throws SQLException {
+    getNClob(getPreparedSigned(sharedConn));
+    getNClob(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getNClob(ResultSet rs) throws SQLException {
+    assertNull(rs.getNClob(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getNClob("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getURL() throws SQLException {
+    getURL(getSigned());
+  }
+
+  @Test
+  public void getURLPrepared() throws SQLException {
+    getURL(getPreparedSigned(sharedConn));
+    getURL(getPreparedSigned(sharedConnBinary));
+  }
+
+  private void getURL(ResultSet rs) throws SQLException {
+    assertNull(rs.getURL(1));
+    assertTrue(rs.wasNull());
+    assertNull(rs.getURL("t1alias"));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = getSigned();
+    ResultSetMetaData meta = rs.getMetaData();
+    assertEquals("NULL", meta.getColumnTypeName(1));
+    assertEquals("", meta.getCatalogName(1));
+    assertEquals("byte[]", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1alias", meta.getColumnName(1));
+    assertEquals(Types.NULL, meta.getColumnType(1));
+    assertEquals(1, meta.getColumnCount());
+    assertEquals(0, meta.getPrecision(1));
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    assertEquals(0, meta.getColumnDisplaySize(1));
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/PointCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/PointCodecTest.java
new file mode 100644
index 0000000..f2f010e
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/PointCodecTest.java
@@ -0,0 +1,259 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.type.GeometryCollection;
+import org.mariadb.jdbc.type.Point;
+import org.mariadb.jdbc.util.constants.Capabilities;
+
+public class PointCodecTest extends CommonCodecTest {
+  public static org.mariadb.jdbc.Connection geoConn;
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS PointCodec");
+    stmt.execute("DROP TABLE IF EXISTS PointCodec2");
+    if (geoConn != null) geoConn.close();
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws Exception {
+    drop();
+    // xpand doesn't recognized POINT
+    Assumptions.assumeFalse(isXpand());
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE PointCodec (t1 POINT, t2 POINT, t3 POINT, t4 POINT)");
+    stmt.execute(
+        "INSERT INTO PointCodec VALUES (ST_PointFromText('POINT(10 1)'),"
+            + " ST_PointFromText('POINT(1.5 18)'), ST_PointFromText('POINT(-1 0.55)'), null)");
+    stmt.execute("CREATE TABLE PointCodec2 (id int not null primary key auto_increment, t1 POINT)");
+    stmt.execute("FLUSH TABLES");
+
+    String binUrl =
+        mDefUrl + (mDefUrl.indexOf("?") > 0 ? "&" : "?") + "geometryDefaultType=default";
+    geoConn = (org.mariadb.jdbc.Connection) DriverManager.getConnection(binUrl);
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from PointCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private CompleteResult getPrepare(org.mariadb.jdbc.Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from PointCodec"
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    CompleteResult rs = (CompleteResult) preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws Exception {
+    getObject(get(), false);
+  }
+
+  @Test
+  public void getObjectPrepare() throws Exception {
+    getObject(getPrepare(sharedConn), false);
+    getObject(getPrepare(sharedConnBinary), false);
+    getObject(getPrepare(geoConn), true);
+  }
+
+  public void getObject(ResultSet rs, boolean defaultGeo) throws SQLException {
+    if (defaultGeo && hasCapability(Capabilities.EXTENDED_TYPE_INFO)) {
+      assertEquals(new Point(10, 1), rs.getObject(1));
+      assertFalse(rs.wasNull());
+      assertEquals(new Point(1.5, 18), rs.getObject(2));
+      assertFalse(rs.wasNull());
+      assertEquals(new Point(-1, 0.55), rs.getObject(3));
+      assertFalse(rs.wasNull());
+      assertNull(rs.getObject(4));
+      assertTrue(rs.wasNull());
+    } else {
+      assertEquals(new Point(10, 1), rs.getObject(1, Point.class));
+      assertFalse(rs.wasNull());
+      assertEquals(new Point(1.5, 18), rs.getObject(2, Point.class));
+      assertFalse(rs.wasNull());
+      assertEquals(new Point(-1, 0.55), rs.getObject(3, Point.class));
+      assertFalse(rs.wasNull());
+      assertNull(rs.getObject(4, Point.class));
+      assertTrue(rs.wasNull());
+    }
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testErrObject(rs, Integer.class);
+    testErrObject(rs, String.class);
+    testErrObject(rs, Long.class);
+    testErrObject(rs, Short.class);
+    testErrObject(rs, BigDecimal.class);
+    testErrObject(rs, BigInteger.class);
+    testErrObject(rs, Double.class);
+    testErrObject(rs, Float.class);
+    testErrObject(rs, Byte.class);
+    testArrObject(
+        rs,
+        new byte[] {
+          (byte) 0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x01,
+          0x01,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x24,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          (byte) 0xF0,
+          0x3F
+        });
+    testErrObject(rs, Boolean.class);
+    testErrObject(rs, Clob.class);
+    testErrObject(rs, NClob.class);
+    testErrObject(rs, InputStream.class);
+    testErrObject(rs, Reader.class);
+    testErrObject(rs, java.util.Date.class);
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    getMetaData(sharedConn, false);
+    try (org.mariadb.jdbc.Connection con = createCon("geometryDefaultType=default")) {
+      getMetaData(con, true);
+    }
+  }
+
+  private void getMetaData(org.mariadb.jdbc.Connection con, boolean geoDefault)
+      throws SQLException {
+    ResultSet rs = getPrepare(con);
+    ResultSetMetaData meta = rs.getMetaData();
+    if (hasCapability(Capabilities.EXTENDED_TYPE_INFO)) {
+      assertEquals("POINT", meta.getColumnTypeName(1));
+    } else {
+      assertEquals("GEOMETRY", meta.getColumnTypeName(1));
+    }
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals(
+        geoDefault
+            ? (hasCapability(Capabilities.EXTENDED_TYPE_INFO)
+                ? Point.class.getName()
+                : GeometryCollection.class.getName())
+            : "byte[]",
+        meta.getColumnClassName(1));
+
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.VARBINARY, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+  }
+
+  @Test
+  public void sendParam() throws Exception {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws Exception {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE PointCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep = con.prepareStatement("INSERT INTO PointCodec2(t1) VALUES (?)")) {
+      prep.setObject(1, new Point(52.1, 12.8));
+      prep.execute();
+      prep.setObject(1, (Point) null);
+      prep.execute();
+
+      prep.setObject(1, new Point(2.2, 3.3));
+      prep.addBatch();
+      prep.setObject(1, new Point(2, 3));
+      prep.addBatch();
+      prep.executeBatch();
+    }
+
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM PointCodec2");
+    assertTrue(rs.next());
+    assertEquals(new Point(52.1, 12.8), rs.getObject(2, Point.class));
+    rs.updateNull(2);
+    rs.updateRow();
+    assertNull(rs.getObject(2, Point.class));
+    assertTrue(rs.next());
+    assertNull(rs.getObject(2, Point.class));
+    rs.updateObject(2, new Point(1, 8));
+    rs.updateRow();
+    assertEquals(new Point(1, 8), rs.getObject(2, Point.class));
+
+    assertTrue(rs.next());
+    assertEquals(new Point(2.2, 3.3), rs.getObject(2, Point.class));
+    assertTrue(rs.next());
+    assertEquals(new Point(2, 3), rs.getObject(2, Point.class));
+    con.commit();
+  }
+
+  @Test
+  public void equal() {
+    Point pt = new Point(0, 10);
+    assertEquals(pt, pt);
+    assertEquals(new Point(0, 10), pt);
+    assertEquals(new Point(0, 10).hashCode(), pt.hashCode());
+    assertFalse(pt.equals(null));
+    assertFalse(pt.equals(""));
+    assertNotEquals(new Point(0, 20), pt);
+    assertNotEquals(new Point(10, 10), pt);
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/PolygonCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/PolygonCodecTest.java
new file mode 100644
index 0000000..0fd1960
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/PolygonCodecTest.java
@@ -0,0 +1,464 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.type.GeometryCollection;
+import org.mariadb.jdbc.type.LineString;
+import org.mariadb.jdbc.type.Point;
+import org.mariadb.jdbc.type.Polygon;
+import org.mariadb.jdbc.util.constants.Capabilities;
+
+public class PolygonCodecTest extends CommonCodecTest {
+  public static org.mariadb.jdbc.Connection geoConn;
+  private final Polygon ls1 =
+      new Polygon(
+          new LineString[] {
+            new LineString(
+                new Point[] {
+                  new Point(1, 1),
+                  new Point(1, 5),
+                  new Point(4, 9),
+                  new Point(6, 9),
+                  new Point(9, 3),
+                  new Point(7, 2),
+                  new Point(1, 1)
+                },
+                false)
+          });
+  private final Polygon ls2 =
+      new Polygon(
+          new LineString[] {
+            new LineString(
+                new Point[] {
+                  new Point(0, 0),
+                  new Point(50, 0),
+                  new Point(50, 50),
+                  new Point(0, 50),
+                  new Point(0, 0)
+                },
+                false),
+            new LineString(
+                new Point[] {
+                  new Point(10, 10),
+                  new Point(20, 10),
+                  new Point(20, 20),
+                  new Point(10, 20),
+                  new Point(10, 10)
+                },
+                false)
+          });
+
+  private final Polygon ls3 =
+      new Polygon(
+          new LineString[] {
+            new LineString(
+                new Point[] {
+                  new Point(0, 0),
+                  new Point(50, 0),
+                  new Point(50, 50),
+                  new Point(0, 50),
+                  new Point(0, 0)
+                },
+                false)
+          });
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS PolygonCodec");
+    stmt.execute("DROP TABLE IF EXISTS PolygonCodec2");
+    if (geoConn != null) geoConn.close();
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws Exception {
+    drop();
+    // xpand doesn't recognized Polygon
+    Assumptions.assumeFalse(isXpand());
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE PolygonCodec (t1 Polygon, t2 Polygon, t3 Polygon, t4 Polygon)");
+    stmt.execute(
+        "INSERT INTO PolygonCodec VALUES (ST_PolygonFromText('POLYGON((1 1,1 5,4 9,6 9,9 3,7 2,1"
+            + " 1))'), ST_PolygonFromText('POLYGON((0 0,50 0,50 50,0 50,0 0), (10 10,20 10,20 20,10"
+            + " 20,10 10))'), ST_PolygonFromText('POLYGON((0 0,50 0,50 50,0 50,0 0))'), null)");
+    stmt.execute(
+        "CREATE TABLE PolygonCodec2 (id int not null primary key auto_increment, t1 Polygon)");
+    stmt.execute("FLUSH TABLES");
+
+    String binUrl =
+        mDefUrl + (mDefUrl.indexOf("?") > 0 ? "&" : "?") + "geometryDefaultType=default";
+    geoConn = (org.mariadb.jdbc.Connection) DriverManager.getConnection(binUrl);
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from PolygonCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private CompleteResult getPrepare(org.mariadb.jdbc.Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from PolygonCodec"
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    CompleteResult rs = (CompleteResult) preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws Exception {
+    getObject(get(), false);
+  }
+
+  @Test
+  public void getObjectPrepare() throws Exception {
+    getObject(getPrepare(sharedConn), false);
+    getObject(getPrepare(sharedConnBinary), false);
+    getObject(getPrepare(geoConn), true);
+  }
+
+  public void getObject(ResultSet rs, boolean defaultGeo) throws SQLException {
+    if (defaultGeo && hasCapability(Capabilities.EXTENDED_TYPE_INFO)) {
+      assertEquals(ls1, rs.getObject(1));
+      assertFalse(rs.wasNull());
+      assertEquals(ls2, rs.getObject(2));
+      assertFalse(rs.wasNull());
+      assertEquals(ls3, rs.getObject(3));
+      assertFalse(rs.wasNull());
+      assertNull(rs.getObject(4));
+      assertTrue(rs.wasNull());
+    } else {
+      assertEquals(ls1, rs.getObject(1, Polygon.class));
+      assertFalse(rs.wasNull());
+      // Polygon((0 0,50 0,50 50,0 50,0 0), (10 10,20 10,20 20,10 20,10 10))
+      assertEquals(ls2, rs.getObject(2, Polygon.class));
+      assertFalse(rs.wasNull());
+      assertEquals(ls3, rs.getObject(3, Polygon.class));
+      assertFalse(rs.wasNull());
+      assertNull(rs.getObject(4));
+      assertTrue(rs.wasNull());
+    }
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testErrObject(rs, Integer.class);
+    testErrObject(rs, String.class);
+    testErrObject(rs, Long.class);
+    testErrObject(rs, Short.class);
+    testErrObject(rs, BigDecimal.class);
+    testErrObject(rs, BigInteger.class);
+    testErrObject(rs, Double.class);
+    testErrObject(rs, Float.class);
+    testErrObject(rs, Byte.class);
+    testArrObject(
+        rs,
+        new byte[] {
+          (byte) 0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x01,
+          0x03,
+          0x00,
+          0x00,
+          0x00,
+          0x01,
+          0x00,
+          0x00,
+          0x00,
+          0x07,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          (byte) 0xF0,
+          0x3F,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          (byte) 0xF0,
+          0x3F,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          (byte) 0xF0,
+          0x3F,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x14,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x10,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x22,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x18,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x22,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x22,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x08,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x1C,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x40,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          (byte) 0xF0,
+          0x3F,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          0x00,
+          (byte) 0xF0,
+          0x3F
+        });
+
+    testErrObject(rs, Boolean.class);
+    testErrObject(rs, Clob.class);
+    testErrObject(rs, NClob.class);
+    testErrObject(rs, InputStream.class);
+    testErrObject(rs, Reader.class);
+    testErrObject(rs, java.util.Date.class);
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    getMetaData(sharedConn, false);
+    try (org.mariadb.jdbc.Connection con = createCon("geometryDefaultType=default")) {
+      getMetaData(con, true);
+    }
+  }
+
+  private void getMetaData(org.mariadb.jdbc.Connection con, boolean geoDefault)
+      throws SQLException {
+    ResultSet rs = getPrepare(con);
+    ResultSetMetaData meta = rs.getMetaData();
+    if (hasCapability(Capabilities.EXTENDED_TYPE_INFO)) {
+      assertEquals("POLYGON", meta.getColumnTypeName(1));
+    } else {
+      assertEquals("GEOMETRY", meta.getColumnTypeName(1));
+    }
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals(
+        (geoDefault
+            ? (hasCapability(Capabilities.EXTENDED_TYPE_INFO)
+                ? Polygon.class.getName()
+                : GeometryCollection.class.getName())
+            : "byte[]"),
+        meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.VARBINARY, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+  }
+
+  @Test
+  public void sendParam() throws Exception {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws Exception {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE PolygonCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO PolygonCodec2(t1) VALUES (?)")) {
+      prep.setObject(1, ls1);
+      prep.execute();
+      prep.setObject(1, (Polygon) null);
+      prep.execute();
+
+      prep.setObject(1, ls2);
+      prep.addBatch();
+      prep.setObject(1, ls1);
+      prep.addBatch();
+      prep.executeBatch();
+    }
+
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM PolygonCodec2");
+    assertTrue(rs.next());
+    assertEquals(ls1, rs.getObject(2, Polygon.class));
+    rs.updateNull(2);
+    rs.updateRow();
+    assertNull(rs.getObject(2, Polygon.class));
+
+    assertTrue(rs.next());
+    assertNull(rs.getObject(2, Polygon.class));
+    rs.updateObject(2, ls2);
+    rs.updateRow();
+    assertEquals(ls2, rs.getObject(2, Polygon.class));
+    assertTrue(rs.next());
+
+    assertEquals(ls2, rs.getObject(2, Polygon.class));
+    assertTrue(rs.next());
+    assertEquals(ls1, rs.getObject(2, Polygon.class));
+    con.commit();
+  }
+
+  @Test
+  public void equal() {
+    assertEquals(ls2, ls2);
+    Polygon testPoly =
+        new Polygon(
+            new LineString[] {
+              new LineString(
+                  new Point[] {
+                    new Point(0, 0),
+                    new Point(50, 0),
+                    new Point(50, 50),
+                    new Point(0, 50),
+                    new Point(0, 0)
+                  },
+                  false),
+              new LineString(
+                  new Point[] {
+                    new Point(10, 10),
+                    new Point(20, 10),
+                    new Point(20, 20),
+                    new Point(10, 20),
+                    new Point(10, 10)
+                  },
+                  false)
+            });
+    assertEquals(testPoly, ls2);
+    assertEquals(testPoly.hashCode(), ls2.hashCode());
+    assertFalse(ls2.equals(null));
+    assertFalse(ls2.equals(""));
+    assertNotEquals(
+        new Polygon(
+            new LineString[] {
+              new LineString(
+                  new Point[] {
+                    new Point(0, 0),
+                    new Point(50, 0),
+                    new Point(50, 60),
+                    new Point(0, 50),
+                    new Point(0, 0)
+                  },
+                  false),
+              new LineString(
+                  new Point[] {
+                    new Point(10, 10),
+                    new Point(20, 10),
+                    new Point(20, 20),
+                    new Point(10, 20),
+                    new Point(10, 10)
+                  },
+                  false)
+            }),
+        ls2);
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/SmallIntCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/SmallIntCodecTest.java
new file mode 100644
index 0000000..c5ca35a
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/SmallIntCodecTest.java
@@ -0,0 +1,1028 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import java.time.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.integration.Common;
+
+public class SmallIntCodecTest extends CommonCodecTest {
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS SmallIntCodec");
+    stmt.execute("DROP TABLE IF EXISTS SmallIntCodec2");
+    stmt.execute("DROP TABLE IF EXISTS SmallIntCodecUnsigned");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE SmallIntCodec (t1 SMALLINT, t2 SMALLINT, t3 SMALLINT, t4 SMALLINT)");
+    stmt.execute(
+        "CREATE TABLE SmallIntCodec2 (id int not null primary key auto_increment, t1 SMALLINT)");
+    stmt.execute(
+        "CREATE TABLE SmallIntCodecUnsigned (t1 SMALLINT UNSIGNED, t2 SMALLINT UNSIGNED, t3"
+            + " SMALLINT UNSIGNED, t4 SMALLINT UNSIGNED)");
+    stmt.execute("INSERT INTO SmallIntCodec VALUES (0, 1, -1, null)");
+    stmt.execute("INSERT INTO SmallIntCodecUnsigned VALUES (0, 1, 65535, null)");
+    stmt.execute("FLUSH TABLES");
+  }
+
+  private ResultSet getSigned() throws SQLException {
+    return get("SmallIntCodec");
+  }
+
+  private ResultSet getUnsigned() throws SQLException {
+    return get("SmallIntCodecUnsigned");
+  }
+
+  private ResultSet getSignedPrepared(Connection con) throws SQLException {
+    return getPrepare(con, "SmallIntCodec");
+  }
+
+  private ResultSet getUnsignedPrepared(Connection con) throws SQLException {
+    return getPrepare(con, "SmallIntCodecUnsigned");
+  }
+
+  private ResultSet get(String table) throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from " + table);
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getPrepare(Connection con, String table) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from "
+                + table
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    ResultSet rs = preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws SQLException {
+    getObject(getSigned());
+  }
+
+  @Test
+  public void getObjectPrepared() throws SQLException {
+    getObject(getSignedPrepared(sharedConn));
+    getObject(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getObject(ResultSet rs) throws SQLException {
+    Object o = rs.getObject(1);
+    assertEquals(Short.valueOf("0"), rs.getObject(1));
+    assertFalse(rs.wasNull());
+    assertEquals(Short.valueOf("1"), rs.getObject(2));
+    assertEquals(Short.valueOf("1"), rs.getObject("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(Short.valueOf("-1"), rs.getObject(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(getSigned());
+  }
+
+  @Test
+  public void getObjectTypePrepared() throws Exception {
+    getObjectType(getSignedPrepared(sharedConn));
+    getObjectType(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getObjectType(ResultSet rs) throws Exception {
+    testObject(rs, Integer.class, 0);
+    testObject(rs, String.class, "0");
+    testObject(rs, Long.class, 0L);
+    testObject(rs, Short.class, (short) 0);
+    testObject(rs, BigDecimal.class, BigDecimal.valueOf(0));
+    testObject(rs, BigInteger.class, BigInteger.valueOf(0));
+    testObject(rs, Double.class, (double) 0);
+    testObject(rs, Float.class, (float) 0);
+    testObject(rs, Byte.class, (byte) 0);
+    testErrObject(rs, byte[].class);
+    testErrObject(rs, Date.class);
+    testErrObject(rs, Time.class);
+    testErrObject(rs, Timestamp.class);
+    testErrObject(rs, java.util.Date.class);
+    testErrObject(rs, LocalDate.class);
+    testErrObject(rs, ZonedDateTime.class);
+    testErrObject(rs, OffsetDateTime.class);
+    testErrObject(rs, LocalDateTime.class);
+    testErrObject(rs, OffsetTime.class);
+    testObject(rs, Boolean.class, Boolean.FALSE);
+    testErrObject(rs, Clob.class);
+    testErrObject(rs, NClob.class);
+    testErrObject(rs, InputStream.class);
+    testErrObject(rs, Reader.class);
+  }
+
+  @Test
+  public void getString() throws SQLException {
+    getString(getSigned());
+  }
+
+  @Test
+  public void getStringPrepared() throws SQLException {
+    getString(getSignedPrepared(sharedConn));
+    getString(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getString(2));
+    assertEquals("1", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("-1", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getStringUnsigned() throws SQLException {
+    getStringUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getStringUnsignedPrepared() throws SQLException {
+    getStringUnsigned(getUnsignedPrepared(sharedConn));
+    getStringUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getStringUnsigned(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getString(2));
+    assertEquals("1", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("65535", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNString() throws SQLException {
+    getNString(getSigned());
+  }
+
+  @Test
+  public void getNStringPrepared() throws SQLException {
+    getNString(getSignedPrepared(sharedConn));
+    getNString(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getNString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getNString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getNString(2));
+    assertEquals("1", rs.getNString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("-1", rs.getNString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBoolean() throws SQLException {
+    getBoolean(getSigned());
+  }
+
+  @Test
+  public void getBooleanPrepared() throws SQLException {
+    getBoolean(getSignedPrepared(sharedConn));
+    getBoolean(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getBoolean(ResultSet rs) throws SQLException {
+    assertFalse(rs.getBoolean(1));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(2));
+    assertTrue(rs.getBoolean("t2alias"));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(3));
+    assertFalse(rs.wasNull());
+    assertFalse(rs.getBoolean(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getByte() throws SQLException {
+    getByte(getSigned());
+  }
+
+  @Test
+  public void getBytePrepared() throws SQLException {
+    getByte(getSignedPrepared(sharedConn));
+    getByte(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getByte(ResultSet rs) throws SQLException {
+    assertEquals((byte) 0, rs.getByte(1));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 1, rs.getByte(2));
+    assertEquals((byte) 1, rs.getByte("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) -1, rs.getByte(3));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 0, rs.getByte(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getShort() throws SQLException {
+    getShort(getSigned());
+  }
+
+  @Test
+  public void getShortPrepared() throws SQLException {
+    getShort(getSignedPrepared(sharedConn));
+    getShort(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getShort(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getShort(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getShort(2));
+    assertEquals(1, rs.getShort("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1, rs.getShort(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getShort(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(getSigned());
+  }
+
+  @Test
+  public void getIntPrepared() throws SQLException {
+    getInt(getSignedPrepared(sharedConn));
+    getInt(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getInt(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getInt(2));
+    assertEquals(1, rs.getInt("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1, rs.getInt(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getIntUnsigned() throws SQLException {
+    getIntUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getIntUnsignedPrepared() throws SQLException {
+    getIntUnsigned(getUnsignedPrepared(sharedConn));
+    getIntUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getIntUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getInt(2));
+    assertFalse(rs.wasNull());
+    assertEquals(65535, rs.getInt(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(getSigned());
+  }
+
+  @Test
+  public void getLongPrepared() throws SQLException {
+    getLong(getSignedPrepared(sharedConn));
+    getLong(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getLong(ResultSet rs) throws SQLException {
+    assertEquals(0L, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1L, rs.getLong(2));
+    assertEquals(1L, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1L, rs.getLong(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0L, rs.getLong(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLongUnsigned() throws SQLException {
+    getLongUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getLongUnsignedPrepared() throws SQLException {
+    getLongUnsigned(getUnsignedPrepared(sharedConn));
+    getLongUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getLongUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0L, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1L, rs.getLong(2));
+    assertEquals(1L, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(65535L, rs.getLong(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0L, rs.getLong(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(getSigned());
+  }
+
+  @Test
+  public void getFloatPrepared() throws SQLException {
+    getFloat(getSignedPrepared(sharedConn));
+    getFloat(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getFloat(ResultSet rs) throws SQLException {
+    assertEquals(0F, rs.getFloat(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1F, rs.getFloat(2));
+    assertEquals(1F, rs.getFloat("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1F, rs.getFloat(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0F, rs.getFloat(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloatUnsigned() throws SQLException {
+    getFloatUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getFloatUnsignedPrepared() throws SQLException {
+    getFloatUnsigned(getUnsignedPrepared(sharedConn));
+    getFloatUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getFloatUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0F, rs.getFloat(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1F, rs.getFloat(2));
+    assertEquals(1F, rs.getFloat("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(65535F, rs.getFloat(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0F, rs.getFloat(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(getSigned());
+  }
+
+  @Test
+  public void getDoublePrepared() throws SQLException {
+    getDouble(getSignedPrepared(sharedConn));
+    getDouble(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getDouble(ResultSet rs) throws SQLException {
+    assertEquals(0D, rs.getDouble(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1D, rs.getDouble(2));
+    assertEquals(1D, rs.getDouble("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1D, rs.getDouble(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0D, rs.getDouble(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDoubleUnsigned() throws SQLException {
+    getDoubleUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getDoubleUnsignedPrepared() throws SQLException {
+    getDoubleUnsigned(getUnsignedPrepared(sharedConn));
+    getDoubleUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getDoubleUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0D, rs.getDouble(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1D, rs.getDouble(2));
+    assertEquals(1D, rs.getDouble("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(65535D, rs.getDouble(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0D, rs.getDouble(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigInteger() throws SQLException {
+    getBigInteger(getSigned());
+  }
+
+  @Test
+  public void getBigIntegerPrepared() throws SQLException {
+    getBigInteger(getSignedPrepared(sharedConn));
+    getBigInteger(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getBigInteger(ResultSet rs) throws SQLException {
+    assertEquals(BigInteger.ZERO, rs.getObject(1, BigInteger.class));
+    assertFalse(rs.wasNull());
+    assertEquals(BigInteger.ONE, rs.getObject(2, BigInteger.class));
+    assertEquals(BigInteger.ONE, rs.getObject("t2alias", BigInteger.class));
+    assertFalse(rs.wasNull());
+    assertEquals(BigInteger.valueOf(-1), rs.getObject(3, BigInteger.class));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4, BigInteger.class));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigIntegerUnsigned() throws SQLException {
+    getBigIntegerUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getBigIntegerUnsignedPrepared() throws SQLException {
+    getBigIntegerUnsigned(getUnsignedPrepared(sharedConn));
+    getBigIntegerUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getBigIntegerUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(BigInteger.ZERO, rs.getObject(1, BigInteger.class));
+    assertFalse(rs.wasNull());
+    assertEquals(BigInteger.ONE, rs.getObject(2, BigInteger.class));
+    assertEquals(BigInteger.ONE, rs.getObject("t2alias", BigInteger.class));
+    assertFalse(rs.wasNull());
+    assertEquals(BigInteger.valueOf(65535L), rs.getObject(3, BigInteger.class));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4, BigInteger.class));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(getSigned());
+  }
+
+  @Test
+  public void getBigDecimalPrepared() throws SQLException {
+    getBigDecimal(getSignedPrepared(sharedConn));
+    getBigDecimal(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getBigDecimal(ResultSet rs) throws SQLException {
+    assertEquals(BigDecimal.ZERO, rs.getBigDecimal(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal(2));
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.valueOf(-1), rs.getBigDecimal(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimalUnsigned() throws SQLException {
+    getBigDecimalUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getBigDecimalUnsignedPrepared() throws SQLException {
+    getBigDecimalUnsigned(getUnsignedPrepared(sharedConn));
+    getBigDecimalUnsigned(getUnsignedPrepared(sharedConnBinary));
+  }
+
+  private void getBigDecimalUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(BigDecimal.ZERO, rs.getBigDecimal(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal(2));
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.valueOf(65535L), rs.getBigDecimal(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(getSigned());
+  }
+
+  @Test
+  public void getDatePrepared() throws SQLException {
+    getDate(getSignedPrepared(sharedConn));
+    getDate(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getDate(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDate(1),
+        "Data type SMALLINT cannot be decoded as Date");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDate("t1alias"),
+        "Data type SMALLINT cannot be decoded as Date");
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(getSigned());
+  }
+
+  @Test
+  public void getTimePrepared() throws SQLException {
+    getTime(getSignedPrepared(sharedConn));
+    getTime(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getTime(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime(1),
+        "Data type SMALLINT cannot be decoded as Time");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime("t1alias"),
+        "Data type SMALLINT cannot be decoded as Time");
+  }
+
+  @Test
+  public void getTimestamp() throws SQLException {
+    getTimestamp(getSigned());
+  }
+
+  @Test
+  public void getTimestampPrepared() throws SQLException {
+    getTimestamp(getSignedPrepared(sharedConn));
+    getTimestamp(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getTimestamp(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTimestamp(1),
+        "Data type SMALLINT cannot be decoded as Timestamp");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTimestamp("t1alias"),
+        "Data type SMALLINT cannot be decoded as Timestamp");
+  }
+
+  @Test
+  public void getAsciiStream() throws SQLException {
+    getAsciiStream(getSigned());
+  }
+
+  @Test
+  public void getAsciiStreamPrepared() throws SQLException {
+    getAsciiStream(getSignedPrepared(sharedConn));
+    getAsciiStream(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getAsciiStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getAsciiStream(1),
+        "Data type SMALLINT cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getAsciiStream("t1alias"),
+        "Data type SMALLINT cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getUnicodeStream() throws SQLException {
+    getUnicodeStream(getSigned());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepared() throws SQLException {
+    getUnicodeStream(getSignedPrepared(sharedConn));
+    getUnicodeStream(getSignedPrepared(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  private void getUnicodeStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getUnicodeStream(1),
+        "Data type SMALLINT cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getUnicodeStream("t1alias"),
+        "Data type SMALLINT cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBinaryStream() throws SQLException {
+    getBinaryStream(getSigned());
+  }
+
+  @Test
+  public void getBinaryStreamPrepared() throws SQLException {
+    getBinaryStream(getSignedPrepared(sharedConn));
+    getBinaryStream(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getBinaryStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBinaryStream(1),
+        "Data type SMALLINT cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBinaryStream("t1alias"),
+        "Data type SMALLINT cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBytes() throws SQLException {
+    getBytes(getSigned());
+  }
+
+  @Test
+  public void getBytesPrepared() throws SQLException {
+    getBytes(getSignedPrepared(sharedConn));
+    getBytes(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getBytes(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBytes(1),
+        "Data type SMALLINT cannot be decoded as byte[]");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBytes("t1alias"),
+        "Data type SMALLINT cannot be decoded as byte[]");
+  }
+
+  @Test
+  public void getCharacterStream() throws SQLException {
+    getCharacterStream(getSigned());
+  }
+
+  @Test
+  public void getCharacterStreamPrepared() throws SQLException {
+    getCharacterStream(getSignedPrepared(sharedConn));
+    getCharacterStream(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type SMALLINT cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream("t1alias"),
+        "Data type SMALLINT cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getNCharacterStream() throws SQLException {
+    getNCharacterStream(getSigned());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepared() throws SQLException {
+    getNCharacterStream(getSignedPrepared(sharedConn));
+    getNCharacterStream(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getNCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream(1),
+        "Data type SMALLINT cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream("t2alias"),
+        "Data type SMALLINT cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getRef() throws SQLException {
+    getRef(getSigned());
+  }
+
+  @Test
+  public void getRefPrepared() throws SQLException {
+    getRef(getSignedPrepared(sharedConn));
+    getRef(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getRef(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getRef(1), "Method ResultSet.getRef not supported");
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getRef("t2alias"), "Method ResultSet.getRef not supported");
+  }
+
+  @Test
+  public void getBlob() throws SQLException {
+    getBlob(getSigned());
+  }
+
+  @Test
+  public void getBlobPrepared() throws SQLException {
+    getBlob(getSignedPrepared(sharedConn));
+    getBlob(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getBlob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type SMALLINT cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream("t1alias"),
+        "Data type SMALLINT cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getClob() throws SQLException {
+    getClob(getSigned());
+  }
+
+  @Test
+  public void getClobPrepared() throws SQLException {
+    getClob(getSignedPrepared(sharedConn));
+    getClob(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getClob(1),
+        "Data type SMALLINT cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getClob("t1alias"),
+        "Data type SMALLINT cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getNClob() throws SQLException {
+    getNClob(getSigned());
+  }
+
+  @Test
+  public void getNClobPrepared() throws SQLException {
+    getNClob(getSignedPrepared(sharedConn));
+    getNClob(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getNClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNClob(1),
+        "Data type SMALLINT cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNClob("t1alias"),
+        "Data type SMALLINT cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getArray() throws SQLException {
+    getArray(getSigned());
+  }
+
+  @Test
+  public void getArrayPrepared() throws SQLException {
+    getArray(getSignedPrepared(sharedConn));
+    getArray(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getArray(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getArray(1), "Method ResultSet.getArray not supported");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getArray("t1alias"),
+        "Method ResultSet.getArray not supported");
+  }
+
+  @Test
+  public void getURL() throws SQLException {
+    getURL(getSigned());
+  }
+
+  @Test
+  public void getURLPrepared() throws SQLException {
+    getURL(getSignedPrepared(sharedConn));
+    getURL(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getURL(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLSyntaxErrorException.class, () -> rs.getURL(1), "Could not parse '0' as URL");
+    Common.assertThrowsContains(
+        SQLSyntaxErrorException.class, () -> rs.getURL("t1alias"), "Could not parse '0' as URL");
+  }
+
+  @Test
+  public void getSQLXML() throws SQLException {
+    getSQLXML(getSigned());
+  }
+
+  @Test
+  public void getSQLXMLPrepared() throws SQLException {
+    getSQLXML(getSignedPrepared(sharedConn));
+    getSQLXML(getSignedPrepared(sharedConnBinary));
+  }
+
+  private void getSQLXML(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getSQLXML(1), "Method ResultSet.getSQLXML not supported");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getSQLXML("t1alias"),
+        "Method ResultSet.getSQLXML not supported");
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = getSigned();
+    ResultSetMetaData meta = rs.getMetaData();
+    assertEquals("SMALLINT", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.lang.Short", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.SMALLINT, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(6, meta.getPrecision(1));
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    assertEquals(6, meta.getColumnDisplaySize(1));
+
+    rs = getUnsigned();
+    meta = rs.getMetaData();
+    assertEquals("SMALLINT UNSIGNED", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.lang.Integer", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.INTEGER, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+
+    // https://jira.mariadb.org/browse/XPT-276
+    if (!isXpand()) {
+      assertEquals(5, meta.getPrecision(1));
+      assertEquals(5, meta.getColumnDisplaySize(1));
+    }
+  }
+
+  @Test
+  public void sendParam() throws SQLException {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE SmallIntCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO SmallIntCodec2(t1) VALUES (?)")) {
+      prep.setShort(1, (short) 1);
+      prep.execute();
+      prep.setObject(1, 2);
+      prep.execute();
+      prep.setObject(1, null);
+      prep.execute();
+      prep.setObject(1, 3, Types.SMALLINT);
+      prep.execute();
+      prep.setObject(1, "4", Types.SMALLINT);
+      prep.execute();
+      prep.setObject(1, null, Types.SMALLINT);
+      prep.execute();
+    }
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM SmallIntCodec2");
+    assertTrue(rs.next());
+    assertEquals(1, rs.getShort(2));
+    rs.updateShort("t1", (short) 45);
+    rs.updateRow();
+    assertEquals((short) 45, rs.getShort(2));
+
+    assertTrue(rs.next());
+    assertEquals(2, rs.getShort(2));
+    rs.updateObject("t1", (Short) null);
+    rs.updateRow();
+    assertEquals(0, rs.getShort(2));
+    assertTrue(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(0, rs.getShort(2));
+    assertTrue(rs.wasNull());
+    rs.updateShort(2, (short) 55);
+    rs.updateRow();
+    assertEquals((short) 55, rs.getShort(2));
+
+    assertTrue(rs.next());
+    assertEquals(3, rs.getShort(2));
+    rs.updateObject(2, Short.valueOf("5"), Types.SMALLINT);
+    rs.updateRow();
+    assertEquals(5, rs.getShort(2));
+    assertFalse(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(4, rs.getShort(2));
+
+    assertTrue(rs.next());
+    assertEquals(0, rs.getShort(2));
+    assertTrue(rs.wasNull());
+    rs.updateObject(2, null, Types.SMALLINT);
+    rs.updateRow();
+    assertEquals(0, rs.getShort(2));
+    assertTrue(rs.wasNull());
+
+    rs = stmt.executeQuery("SELECT * FROM SmallIntCodec2");
+    assertTrue(rs.next());
+    assertEquals((short) 45, rs.getShort(2));
+
+    assertTrue(rs.next());
+    assertEquals(0, rs.getShort(2));
+    assertTrue(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals((short) 55, rs.getShort(2));
+
+    assertTrue(rs.next());
+    assertEquals(5, rs.getShort(2));
+    assertFalse(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(4, rs.getShort(2));
+
+    assertTrue(rs.next());
+    assertEquals(0, rs.getShort(2));
+    assertTrue(rs.wasNull());
+    con.commit();
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/TimeCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/TimeCodecTest.java
new file mode 100644
index 0000000..85bd146
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/TimeCodecTest.java
@@ -0,0 +1,855 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import java.time.*;
+import java.util.Calendar;
+import java.util.TimeZone;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.integration.Common;
+
+public class TimeCodecTest extends CommonCodecTest {
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS TimeCodec");
+    stmt.execute("DROP TABLE IF EXISTS TimeCodec2");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE TimeCodec (t1 TIME(3), t2 TIME(6), t3 TIME(6), t4 TIME)");
+    stmt.execute(
+        "CREATE TABLE TimeCodec2 (id int not null primary key auto_increment, t1 TIME(3))");
+    stmt.execute(
+        "INSERT INTO TimeCodec VALUES ('01:55:12', '01:55:13.234567', '-18:30:12.55', null), "
+            + "('-838:59:58.999', '838:59:58.999999', '00:00:00', '00:00:00')");
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from TimeCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getPrepare(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from TimeCodec"
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    ResultSet rs = preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws SQLException {
+    getObject(get());
+  }
+
+  @Test
+  public void getObjectPrepare() throws SQLException {
+    getObject(getPrepare(sharedConn));
+    getObject(getPrepare(sharedConnBinary));
+  }
+
+  public void getObject(ResultSet rs) throws SQLException {
+    assertEquals(
+        6912000, rs.getTime(1, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(Time.valueOf("01:55:12").getTime(), rs.getTime(1).getTime());
+    assertFalse(rs.wasNull());
+
+    assertEquals(
+        6913234, rs.getTime(2, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(Time.valueOf("01:55:13").getTime() + 234, rs.getTime(2).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(Time.valueOf("-18:-30:-12").getTime() - 550, rs.getTime(3).getTime());
+    assertFalse(rs.wasNull());
+    assertNull(rs.getTime(4));
+    assertTrue(rs.wasNull());
+    rs.next();
+    assertEquals(Time.valueOf("00:00:00").getTime(), rs.getTime(3).getTime());
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get(), true);
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn), true);
+    getObjectType(getPrepare(sharedConnBinary), false);
+  }
+
+  public void getObjectType(ResultSet rs, boolean text) throws Exception {
+    testErrObject(rs, Integer.class);
+    if (isXpand() && !text) {
+      testObject(rs, String.class, "01:55:12");
+    } else {
+      testObject(rs, String.class, "01:55:12.000");
+    }
+    testErrObject(rs, Long.class);
+    testErrObject(rs, Short.class);
+    testErrObject(rs, BigDecimal.class);
+    testErrObject(rs, BigInteger.class);
+    testErrObject(rs, Double.class);
+    testErrObject(rs, Float.class);
+    testErrObject(rs, Byte.class);
+    testErrObject(rs, byte[].class);
+    testErrObject(rs, Boolean.class);
+    testErrObject(rs, Clob.class);
+    testErrObject(rs, NClob.class);
+    testErrObject(rs, InputStream.class);
+    testErrObject(rs, Reader.class);
+    testErrObject(rs, LocalDate.class);
+    testObject(rs, LocalDateTime.class, LocalDateTime.parse("1970-01-01T01:55:12"));
+    testObject(rs, LocalTime.class, LocalTime.parse("01:55:12"));
+    testObject(rs, Time.class, Time.valueOf("01:55:12"));
+    testObject(rs, Timestamp.class, Timestamp.valueOf("1970-01-01 01:55:12"));
+    testObject(
+        rs,
+        ZonedDateTime.class,
+        LocalDateTime.parse("1970-01-01T01:55:12").atZone(ZoneId.systemDefault()));
+    testObject(rs, java.util.Date.class, Timestamp.valueOf("1970-01-01 01:55:12.0"));
+  }
+
+  @Test
+  public void getString() throws SQLException {
+    getString(get(), true);
+  }
+
+  @Test
+  public void getStringPrepare() throws SQLException {
+    getString(getPrepare(sharedConn), true);
+    getString(getPrepare(sharedConnBinary), false);
+  }
+
+  public void getString(ResultSet rs, boolean text) throws SQLException {
+    if (isXpand() && !text) {
+      // https://jira.mariadb.org/browse/XPT-273
+      assertEquals("01:55:12", rs.getString(1));
+      assertFalse(rs.wasNull());
+      assertEquals("01:55:13.234567", rs.getString(2));
+      assertEquals("01:55:13.234567", rs.getString("t2alias"));
+      assertFalse(rs.wasNull());
+      assertEquals("-18:30:12.550000", rs.getString(3));
+      assertFalse(rs.wasNull());
+      assertNull(rs.getNString(4));
+      assertTrue(rs.wasNull());
+      rs.next();
+      assertTrue(
+          "-838:59:58.999".equals(rs.getString(1)) || "-838:59:58.999000".equals(rs.getString(1)));
+      assertFalse(rs.wasNull());
+      assertEquals("838:59:58.999999", rs.getString(2));
+      assertEquals("838:59:58.999999", rs.getString("t2alias"));
+      assertFalse(rs.wasNull());
+      assertEquals("00:00:00", rs.getString(3));
+      assertEquals("00:00:00", rs.getString(4));
+      assertFalse(rs.wasNull());
+
+    } else {
+      assertEquals("01:55:12.000", rs.getString(1));
+      assertFalse(rs.wasNull());
+      assertEquals("01:55:13.234567", rs.getString(2));
+      assertEquals("01:55:13.234567", rs.getString("t2alias"));
+      assertFalse(rs.wasNull());
+      assertEquals("-18:30:12.550000", rs.getString(3));
+      assertFalse(rs.wasNull());
+      assertNull(rs.getNString(4));
+      assertTrue(rs.wasNull());
+      rs.next();
+      assertTrue(
+          "-838:59:58.999".equals(rs.getString(1)) || "-838:59:58.999000".equals(rs.getString(1)));
+      assertFalse(rs.wasNull());
+      assertEquals("838:59:58.999999", rs.getString(2));
+      assertEquals("838:59:58.999999", rs.getString("t2alias"));
+      assertFalse(rs.wasNull());
+      assertEquals("00:00:00.000000", rs.getString(3));
+      assertEquals("00:00:00", rs.getString(4));
+      assertFalse(rs.wasNull());
+    }
+  }
+
+  @Test
+  public void getNString() throws SQLException {
+    getNString(get(), true);
+  }
+
+  @Test
+  public void getNStringPrepare() throws SQLException {
+    getNString(getPrepare(sharedConn), true);
+    getNString(getPrepare(sharedConnBinary), false);
+  }
+
+  public void getNString(ResultSet rs, boolean text) throws SQLException {
+    if (isXpand() && !text) {
+      assertEquals("01:55:12", rs.getNString(1));
+    } else {
+      assertEquals("01:55:12.000", rs.getNString(1));
+    }
+    assertFalse(rs.wasNull());
+    assertEquals("01:55:13.234567", rs.getNString(2));
+    assertEquals("01:55:13.234567", rs.getNString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("-18:30:12.550000", rs.getNString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBoolean() throws SQLException {
+    getBoolean(get());
+  }
+
+  @Test
+  public void getBooleanPrepare() throws SQLException {
+    getBoolean(getPrepare(sharedConn));
+    getBoolean(getPrepare(sharedConnBinary));
+  }
+
+  public void getBoolean(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getBoolean(1), "Data type TIME cannot be decoded as Boolean");
+  }
+
+  @Test
+  public void getByte() throws SQLException {
+    getByte(get());
+  }
+
+  @Test
+  public void getBytePrepare() throws SQLException {
+    getByte(getPrepare(sharedConn));
+    getByte(getPrepare(sharedConnBinary));
+  }
+
+  public void getByte(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getByte(1), "Data type TIME cannot be decoded as Byte");
+  }
+
+  @Test
+  public void getShort() throws SQLException {
+    getShort(get());
+  }
+
+  @Test
+  public void getShortPrepare() throws SQLException {
+    getShort(getPrepare(sharedConn));
+    getShort(getPrepare(sharedConnBinary));
+  }
+
+  public void getShort(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getShort(1), "Data type TIME cannot be decoded as Short");
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(get());
+  }
+
+  @Test
+  public void getIntPrepare() throws SQLException {
+    getInt(getPrepare(sharedConn));
+    getInt(getPrepare(sharedConnBinary));
+  }
+
+  public void getInt(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getInt(1), "Data type TIME cannot be decoded as Integer");
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(get());
+  }
+
+  @Test
+  public void getLongPrepare() throws SQLException {
+    getLong(getPrepare(sharedConn));
+    getLong(getPrepare(sharedConnBinary));
+  }
+
+  public void getLong(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getLong(1), "Data type TIME cannot be decoded as Long");
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(get());
+  }
+
+  @Test
+  public void getFloatPrepare() throws SQLException {
+    getFloat(getPrepare(sharedConn));
+    getFloat(getPrepare(sharedConnBinary));
+  }
+
+  public void getFloat(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getFloat(1), "Data type TIME cannot be decoded as Float");
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(get());
+  }
+
+  @Test
+  public void getDoublePrepare() throws SQLException {
+    getDouble(getPrepare(sharedConn));
+    getDouble(getPrepare(sharedConnBinary));
+  }
+
+  public void getDouble(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getDouble(1), "Data type TIME cannot be decoded as Double");
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(get());
+  }
+
+  @Test
+  public void getBigDecimalPrepare() throws SQLException {
+    getBigDecimal(getPrepare(sharedConn));
+    getBigDecimal(getPrepare(sharedConnBinary));
+  }
+
+  public void getBigDecimal(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getBigDecimal(1),
+        "Data type TIME cannot be decoded as BigDecimal");
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(get());
+  }
+
+  @Test
+  public void getDatePrepare() throws SQLException {
+    getDate(getPrepare(sharedConn));
+    getDate(getPrepare(sharedConnBinary));
+  }
+
+  public void getDate(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getDate(1), "Data type TIME cannot be decoded as Date");
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(get());
+  }
+
+  @Test
+  public void getTimePrepare() throws SQLException {
+    getTime(getPrepare(sharedConn));
+    getTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getTime(ResultSet rs) throws SQLException {
+    assertEquals(
+        6912000, rs.getTime(1, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(Time.valueOf("01:55:12").getTime(), rs.getTime(1).getTime());
+    assertFalse(rs.wasNull());
+
+    assertEquals(
+        6913234, rs.getTime(2, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(Time.valueOf("01:55:13").getTime() + 234, rs.getTime(2).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(Time.valueOf("-18:-30:-12").getTime() - 550, rs.getTime(3).getTime());
+    assertFalse(rs.wasNull());
+    assertNull(rs.getTime(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDuration() throws SQLException {
+    getDuration(get());
+  }
+
+  @Test
+  public void getDurationPrepare() throws SQLException {
+    getDuration(getPrepare(sharedConn));
+    getDuration(getPrepare(sharedConnBinary));
+  }
+
+  public void getDuration(ResultSet rs) throws SQLException {
+    assertEquals(Duration.parse("PT1H55M12S"), rs.getObject(1, Duration.class));
+    assertFalse(rs.wasNull());
+    assertEquals(Duration.parse("PT1H55M13.234567S"), rs.getObject(2, Duration.class));
+    assertEquals(Duration.parse("PT1H55M13.234567S"), rs.getObject("t2alias", Duration.class));
+    assertFalse(rs.wasNull());
+    assertEquals(Duration.parse("PT-18H-30M-12.55S"), rs.getObject(3, Duration.class));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4, Duration.class));
+    assertTrue(rs.wasNull());
+
+    rs.next();
+    assertEquals(Duration.parse("PT-838H-59M-58.999S"), rs.getObject(1, Duration.class));
+    assertFalse(rs.wasNull());
+    assertEquals(Duration.parse("PT838H59M58.999999S"), rs.getObject(2, Duration.class));
+    assertEquals(Duration.parse("PT838H59M58.999999S"), rs.getObject("t2alias", Duration.class));
+    assertFalse(rs.wasNull());
+    assertEquals(Duration.parse("PT0S"), rs.getObject(3, Duration.class));
+    assertEquals(Duration.parse("PT0S"), rs.getObject(4, Duration.class));
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getLocalTime() throws SQLException {
+    getLocalTime(get());
+  }
+
+  @Test
+  public void getLocalTimePrepare() throws SQLException {
+    getLocalTime(getPrepare(sharedConn));
+    getLocalTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getLocalTime(ResultSet rs) throws SQLException {
+    assertEquals(LocalTime.parse("01:55:12"), rs.getObject(1, LocalTime.class));
+    assertFalse(rs.wasNull());
+    assertEquals(LocalTime.parse("01:55:13.234567"), rs.getObject(2, LocalTime.class));
+    assertEquals(LocalTime.parse("01:55:13.234567"), rs.getObject("t2alias", LocalTime.class));
+    assertFalse(rs.wasNull());
+    // Duration.parse("PT-18H-30M-12.55S")
+    assertEquals(LocalTime.parse("05:29:47.450"), rs.getObject(3, LocalTime.class));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4, LocalTime.class));
+    assertTrue(rs.wasNull());
+
+    rs.next();
+    assertEquals(Duration.parse("PT-838H-59M-58.999S"), rs.getObject(1, Duration.class));
+    assertFalse(rs.wasNull());
+    assertEquals(Duration.parse("PT838H59M58.999999S"), rs.getObject(2, Duration.class));
+    assertEquals(Duration.parse("PT838H59M58.999999S"), rs.getObject("t2alias", Duration.class));
+    assertFalse(rs.wasNull());
+    assertEquals(Duration.parse("PT0S"), rs.getObject(3, Duration.class));
+    assertEquals(Duration.parse("PT0S"), rs.getObject(4, Duration.class));
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getTimestamp() throws SQLException {
+    getTimestamp(get());
+  }
+
+  @Test
+  public void getTimestampPrepare() throws SQLException {
+    getTimestamp(getPrepare(sharedConn));
+    getTimestamp(getPrepare(sharedConnBinary));
+  }
+
+  public void getTimestamp(ResultSet rs) throws SQLException {
+    assertEquals(Timestamp.valueOf("1970-01-01 01:55:12").getTime(), rs.getTimestamp(1).getTime());
+    assertEquals(
+        6912000, rs.getTimestamp(1, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertEquals(
+        Timestamp.valueOf("1970-01-01 01:55:12").getTime(), rs.getTimestamp("t1alias").getTime());
+    assertEquals(
+        6912000,
+        rs.getTimestamp("t1alias", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+
+    assertEquals(
+        Timestamp.valueOf("1970-01-01 01:55:13.234567").getTime(), rs.getTimestamp(2).getTime());
+    assertEquals(
+        6913234, rs.getTimestamp(2, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(Timestamp.valueOf("1969-12-31 05:29:47.45"), rs.getTimestamp(3));
+    assertNull(rs.getTimestamp(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLocalDateTime() throws SQLException {
+    getLocalDateTime(get());
+  }
+
+  @Test
+  public void getLocalDateTimePrepare() throws SQLException {
+    getLocalDateTime(getPrepare(sharedConn));
+    getLocalDateTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getLocalDateTime(ResultSet rs) throws SQLException {
+    assertEquals(LocalDateTime.parse("1970-01-01T01:55:12"), rs.getObject(1, LocalDateTime.class));
+    assertFalse(rs.wasNull());
+    assertEquals(
+        LocalDateTime.parse("1970-01-01T01:55:13.234567"), rs.getObject(2, LocalDateTime.class));
+    assertFalse(rs.wasNull());
+    assertEquals(
+        LocalDateTime.parse("1969-12-31T05:29:47.45"), rs.getObject(3, LocalDateTime.class));
+    assertNull(rs.getObject(4, LocalDateTime.class));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getInstant() throws SQLException {
+    getInstant(get());
+  }
+
+  @Test
+  public void getInstantPrepare() throws SQLException {
+    getInstant(getPrepare(sharedConn));
+    getInstant(getPrepare(sharedConnBinary));
+  }
+
+  public void getInstant(ResultSet rs) throws SQLException {
+    assertFalse(rs.wasNull());
+    assertEquals(
+        ZonedDateTime.of(LocalDateTime.parse("1970-01-01T01:55:12"), ZoneId.systemDefault())
+            .toInstant(),
+        rs.getObject(1, Instant.class));
+    assertFalse(rs.wasNull());
+    assertEquals(
+        ZonedDateTime.of(LocalDateTime.parse("1970-01-01T01:55:13.234567"), ZoneId.systemDefault())
+            .toInstant(),
+        rs.getObject(2, Instant.class));
+    assertFalse(rs.wasNull());
+    assertEquals(
+        ZonedDateTime.of(LocalDateTime.parse("1969-12-31T05:29:47.45"), ZoneId.systemDefault())
+            .toInstant(),
+        rs.getObject(3, Instant.class));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4, Instant.class));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getOffsetDateTime() throws SQLException {
+    getOffsetDateTime(get());
+  }
+
+  @Test
+  public void getOffsetDateTimePrepare() throws SQLException {
+    getOffsetDateTime(getPrepare(sharedConn));
+    getOffsetDateTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getOffsetDateTime(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(1, OffsetDateTime.class),
+        "cannot be decoded as OffsetDateTime");
+  }
+
+  @Test
+  public void getAsciiStream() throws SQLException {
+    getAsciiStream(get());
+  }
+
+  @Test
+  public void getAsciiStreamPrepare() throws SQLException {
+    getAsciiStream(getPrepare(sharedConn));
+    getAsciiStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getAsciiStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getAsciiStream(1),
+        "Data type TIME cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getUnicodeStream() throws Exception {
+    getUnicodeStream(get());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepare() throws Exception {
+    getUnicodeStream(getPrepare(sharedConn));
+    getUnicodeStream(getPrepare(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  public void getUnicodeStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getUnicodeStream(1),
+        "Data type TIME cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBinaryStream() throws Exception {
+    getBinaryStream(get());
+  }
+
+  @Test
+  public void getBinaryStreamPrepare() throws Exception {
+    getBinaryStream(getPrepare(sharedConn));
+    getBinaryStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getBinaryStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getBinaryStream(1),
+        "Data type TIME cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBytes() throws SQLException {
+    getBytes(get());
+  }
+
+  @Test
+  public void getBytesPrepare() throws SQLException {
+    getBytes(getPrepare(sharedConn));
+    getBytes(getPrepare(sharedConnBinary));
+  }
+
+  public void getBytes(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getBytes(1), "Data type TIME cannot be decoded as byte[]");
+  }
+
+  @Test
+  public void getCharacterStream() throws Exception {
+    getCharacterStream(get());
+  }
+
+  @Test
+  public void getCharacterStreamPrepare() throws Exception {
+    getCharacterStream(getPrepare(sharedConn));
+    getCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type TIME cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getNCharacterStream() throws Exception {
+    getNCharacterStream(get());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepare() throws Exception {
+    getNCharacterStream(getPrepare(sharedConn));
+    getNCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getNCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream(1),
+        "Data type TIME cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getBlob() throws Exception {
+    getBlob(get());
+  }
+
+  @Test
+  public void getBlobPrepare() throws Exception {
+    getBlob(getPrepare(sharedConn));
+    getBlob(getPrepare(sharedConnBinary));
+  }
+
+  public void getBlob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getBlob(1), "Data type TIME cannot be decoded as Blob");
+  }
+
+  @Test
+  public void getClob() throws Exception {
+    getClob(get());
+  }
+
+  @Test
+  public void getClobPrepare() throws Exception {
+    getClob(getPrepare(sharedConn));
+    getClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getClob(1), "Data type TIME cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getNClob() throws Exception {
+    getNClob(get());
+  }
+
+  @Test
+  public void getNClobPrepare() throws Exception {
+    getNClob(getPrepare(sharedConn));
+    getNClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getNClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getNClob(1), "Data type TIME cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = get();
+    ResultSetMetaData meta = rs.getMetaData();
+    assertEquals("TIME", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.sql.Time", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.TIME, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(14, meta.getPrecision(1));
+    assertEquals("", meta.getSchemaName(1));
+
+    // https://jira.mariadb.org/browse/XPT-273
+    if (!isXpand()) {
+      assertEquals(3, meta.getScale(1));
+      assertEquals(14, meta.getColumnDisplaySize(1));
+    }
+  }
+
+  @Test
+  public void sendParam() throws SQLException {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE TimeCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    Time tt = Time.valueOf("01:55:12");
+    tt.setTime(tt.getTime() + 120);
+    try (PreparedStatement prep = con.prepareStatement("INSERT INTO TimeCodec2(t1) VALUES (?)")) {
+      prep.setTime(1, tt);
+      prep.execute();
+      prep.setTime(1, null);
+      prep.execute();
+      prep.setObject(1, new Time(Time.valueOf("01:55:13").getTime() + 234));
+      prep.execute();
+      prep.setObject(1, null);
+      prep.execute();
+      prep.setObject(1, Time.valueOf("01:55:14"), Types.TIME);
+      prep.execute();
+      prep.setObject(1, null, Types.TIME);
+      prep.execute();
+      prep.setObject(1, Duration.parse("PT23H54M51.84001S"), Types.TIME);
+      prep.execute();
+      prep.setObject(1, Duration.parse("PT23H54M52S"), Types.TIME);
+      prep.execute();
+      prep.setObject(1, LocalTime.parse("05:29:47.450"), Types.TIME);
+      prep.execute();
+      prep.setObject(1, LocalTime.parse("05:29:57"), Types.TIME);
+      prep.execute();
+      prep.setObject(1, "05:29:58", Types.TIME);
+      prep.execute();
+    }
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM TimeCodec2");
+    assertTrue(rs.next());
+    assertEquals(tt.getTime(), rs.getTime(2).getTime());
+    rs.updateTime("t1", null);
+    rs.updateRow();
+    assertNull(rs.getTime(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getTime(2));
+    rs.updateTime("t1", tt);
+    rs.updateRow();
+    assertEquals(tt.getTime(), rs.getTime(2).getTime());
+
+    assertTrue(rs.next());
+    assertEquals(Time.valueOf("01:55:13").getTime() + 234, rs.getTime(2).getTime());
+    rs.updateObject(2, Time.valueOf("01:55:14"), JDBCType.TIME);
+    rs.updateRow();
+    assertEquals(Time.valueOf("01:55:14").getTime(), rs.getTime(2).getTime());
+
+    assertTrue(rs.next());
+    assertNull(rs.getTime(2));
+    rs.updateTime(2, tt);
+    rs.updateRow();
+    assertEquals(tt.getTime(), rs.getTime(2).getTime());
+
+    assertTrue(rs.next());
+    assertEquals(Time.valueOf("01:55:14").getTime(), rs.getTime(2).getTime());
+    assertTrue(rs.next());
+    assertNull(rs.getTime(2));
+    assertTrue(rs.next());
+    assertEquals(Time.valueOf("23:54:51").getTime() + 840, rs.getTime(2).getTime());
+    assertTrue(rs.next());
+    assertEquals(Time.valueOf("23:54:52").getTime(), rs.getTime(2).getTime());
+    assertTrue(rs.next());
+    assertEquals(Time.valueOf("05:29:47").getTime() + 450, rs.getTime(2).getTime());
+    assertTrue(rs.next());
+    assertEquals(Time.valueOf("05:29:57").getTime(), rs.getTime(2).getTime());
+    assertTrue(rs.next());
+    assertEquals(Time.valueOf("05:29:58").getTime(), rs.getTime(2).getTime());
+
+    rs = stmt.executeQuery("SELECT * FROM TimeCodec2");
+    assertTrue(rs.next());
+    assertNull(rs.getTime(2));
+
+    assertTrue(rs.next());
+    assertEquals(tt.getTime(), rs.getTime(2).getTime());
+
+    assertTrue(rs.next());
+    assertEquals(Time.valueOf("01:55:14").getTime(), rs.getTime(2).getTime());
+
+    assertTrue(rs.next());
+    assertEquals(tt.getTime(), rs.getTime(2).getTime());
+
+    assertTrue(rs.next());
+    assertEquals(Time.valueOf("01:55:14").getTime(), rs.getTime(2).getTime());
+    assertTrue(rs.next());
+    assertNull(rs.getTime(2));
+    assertTrue(rs.next());
+    assertEquals(Time.valueOf("23:54:51").getTime() + 840, rs.getTime(2).getTime());
+    assertTrue(rs.next());
+    assertEquals(Time.valueOf("23:54:52").getTime(), rs.getTime(2).getTime());
+    assertTrue(rs.next());
+    assertEquals(Time.valueOf("05:29:47").getTime() + 450, rs.getTime(2).getTime());
+    assertTrue(rs.next());
+    assertEquals(Time.valueOf("05:29:57").getTime(), rs.getTime(2).getTime());
+    con.commit();
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/TinyIntCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/TinyIntCodecTest.java
new file mode 100644
index 0000000..5a32c20
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/TinyIntCodecTest.java
@@ -0,0 +1,1090 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import java.time.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.integration.Common;
+
+public class TinyIntCodecTest extends CommonCodecTest {
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS TinyIntCodec");
+    stmt.execute("DROP TABLE IF EXISTS TinyIntCodec2");
+    stmt.execute("DROP TABLE IF EXISTS TinyIntCodecUnsigned");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE TinyIntCodec (t1 TINYINT, t2 TINYINT, t3 TINYINT, t4 TINYINT)");
+    stmt.execute(
+        "CREATE TABLE TinyIntCodec2 (id int not null primary key auto_increment, t1 TINYINT)");
+    stmt.execute(
+        "CREATE TABLE TinyIntCodecUnsigned (t1 TINYINT UNSIGNED, t2 TINYINT UNSIGNED, t3 TINYINT"
+            + " UNSIGNED, t4 TINYINT UNSIGNED)");
+    stmt.execute("INSERT INTO TinyIntCodec VALUES (0, 1, -1, null)");
+    stmt.execute("INSERT INTO TinyIntCodecUnsigned VALUES (0, 1, 255, null)");
+    stmt.execute("FLUSH TABLES");
+  }
+
+  private ResultSet getSigned() throws SQLException {
+    return get("TinyIntCodec");
+  }
+
+  private ResultSet getUnsigned() throws SQLException {
+    return get("TinyIntCodecUnsigned");
+  }
+
+  private ResultSet getPrepareSigned(Connection con) throws SQLException {
+    return getPrepare(con, "TinyIntCodec");
+  }
+
+  private ResultSet getPrepareUnsigned(Connection con) throws SQLException {
+    return getPrepare(con, "TinyIntCodecUnsigned");
+  }
+
+  private ResultSet get(String table) throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from " + table);
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getPrepare(Connection con, String table) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from "
+                + table
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    ResultSet rs = preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws Exception {
+    getObject(getSigned());
+  }
+
+  @Test
+  public void getObjectPrepare() throws Exception {
+    getObject(getPrepareSigned(sharedConn));
+    getObject(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getObject(ResultSet rs) throws Exception {
+    assertEquals(0, rs.getObject(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getObject(2));
+    assertEquals(1, rs.getObject("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1, rs.getObject(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getObjectUnsigned() throws Exception {
+    getObjectUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getObjectUnsignedPrepare() throws Exception {
+    getObjectUnsigned(getPrepareUnsigned(sharedConn));
+    getObjectUnsigned(getPrepareUnsigned(sharedConnBinary));
+  }
+
+  public void getObjectUnsigned(ResultSet rs) throws Exception {
+    assertEquals(0, rs.getObject(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getObject(2));
+    assertEquals(1, rs.getObject("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(255, rs.getObject(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(getSigned());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepareSigned(sharedConn));
+    getObjectType(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testObject(rs, Integer.class, 0);
+    testObject(rs, String.class, "0");
+    testObject(rs, Long.class, 0L);
+    testObject(rs, Short.class, (short) 0);
+    testObject(rs, BigDecimal.class, BigDecimal.valueOf(0));
+    testObject(rs, BigInteger.class, BigInteger.valueOf(0));
+    testObject(rs, Double.class, (double) 0);
+    testObject(rs, Float.class, (float) 0);
+    testObject(rs, Byte.class, (byte) 0);
+    testErrObject(rs, byte[].class);
+    testErrObject(rs, Date.class);
+    testErrObject(rs, Time.class);
+    testErrObject(rs, Timestamp.class);
+    testErrObject(rs, java.util.Date.class);
+    testErrObject(rs, LocalDate.class);
+    testErrObject(rs, ZonedDateTime.class);
+    testErrObject(rs, OffsetDateTime.class);
+    testErrObject(rs, LocalDateTime.class);
+    testErrObject(rs, OffsetTime.class);
+    testObject(rs, Boolean.class, Boolean.FALSE);
+    testErrObject(rs, Clob.class);
+    testErrObject(rs, NClob.class);
+    testErrObject(rs, InputStream.class);
+    testErrObject(rs, Reader.class);
+  }
+
+  @Test
+  public void getObjectTypeBoolean() throws Exception {
+    getObjectTypeBoolean(getSigned());
+  }
+
+  @Test
+  public void getObjectTypeBooleanPrepare() throws Exception {
+    getObjectTypeBoolean(getPrepareSigned(sharedConn));
+    getObjectTypeBoolean(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getObjectTypeBoolean(ResultSet rs) throws Exception {
+    assertEquals(Boolean.FALSE, rs.getObject(1, Boolean.class));
+    assertFalse(rs.wasNull());
+    assertEquals(Boolean.TRUE, rs.getObject(2, Boolean.class));
+    assertEquals(Boolean.TRUE, rs.getObject("t2alias", Boolean.class));
+    assertFalse(rs.wasNull());
+    assertEquals(Boolean.TRUE, rs.getObject(3, Boolean.class));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4, Boolean.class));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getString() throws Exception {
+    getString(getSigned());
+  }
+
+  @Test
+  public void getStringPrepare() throws Exception {
+    getString(getPrepareSigned(sharedConn));
+    getString(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getString(ResultSet rs) throws Exception {
+    assertEquals("0", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getString(2));
+    assertEquals("1", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("-1", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getStringUnsigned() throws SQLException {
+    getStringUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getStringUnsignedPrepared() throws SQLException {
+    getStringUnsigned(getPrepareUnsigned(sharedConn));
+    getStringUnsigned(getPrepareUnsigned(sharedConnBinary));
+  }
+
+  private void getStringUnsigned(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getString(2));
+    assertEquals("1", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("255", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNString() throws Exception {
+    getNString(getSigned());
+  }
+
+  @Test
+  public void getNStringPrepare() throws Exception {
+    getNString(getPrepareSigned(sharedConn));
+    getNString(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getNString(ResultSet rs) throws Exception {
+    assertEquals("0", rs.getNString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getNString(2));
+    assertEquals("1", rs.getNString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("-1", rs.getNString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBoolean() throws Exception {
+    getBoolean(getSigned());
+  }
+
+  @Test
+  public void getBooleanPrepare() throws Exception {
+    getBoolean(getPrepareSigned(sharedConn));
+    getBoolean(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getBoolean(ResultSet rs) throws Exception {
+    assertFalse(rs.getBoolean(1));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(2));
+    assertTrue(rs.getBoolean("t2alias"));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(3));
+    assertFalse(rs.wasNull());
+    assertFalse(rs.getBoolean(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getByte() throws SQLException {
+    getByte(getSigned());
+  }
+
+  @Test
+  public void getBytePrepare() throws SQLException {
+    getByte(getPrepareSigned(sharedConn));
+    getByte(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getByte(ResultSet rs) throws SQLException {
+    assertEquals((byte) 0, rs.getByte(1));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 1, rs.getByte(2));
+    assertEquals((byte) 1, rs.getByte("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) -1, rs.getByte(3));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 0, rs.getByte(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getShort() throws SQLException {
+    getShort(getSigned());
+  }
+
+  @Test
+  public void getShortPrepare() throws SQLException {
+    getShort(getPrepareSigned(sharedConn));
+    getShort(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getShort(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getShort(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getShort(2));
+    assertEquals(1, rs.getShort("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1, rs.getShort(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getShort(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(getSigned());
+  }
+
+  @Test
+  public void getIntPrepare() throws SQLException {
+    getInt(getPrepareSigned(sharedConn));
+    getInt(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getInt(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getInt(2));
+    assertEquals(1, rs.getInt("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1, rs.getInt(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getIntUnsigned() throws SQLException {
+    getIntUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getIntUnsignedPrepare() throws SQLException {
+    getIntUnsigned(getPrepareUnsigned(sharedConn));
+    getIntUnsigned(getPrepareUnsigned(sharedConnBinary));
+  }
+
+  public void getIntUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getInt(2));
+    assertFalse(rs.wasNull());
+    assertEquals(255, rs.getInt(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(getSigned());
+  }
+
+  @Test
+  public void getLongPrepare() throws SQLException {
+    getLong(getPrepareSigned(sharedConn));
+    getLong(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getLong(ResultSet rs) throws SQLException {
+    assertEquals(0L, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1L, rs.getLong(2));
+    assertEquals(1L, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1L, rs.getLong(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0L, rs.getLong(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLongUnsigned() throws SQLException {
+    getLongUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getLongUnsignedPrepare() throws SQLException {
+    getLongUnsigned(getPrepareUnsigned(sharedConn));
+    getLongUnsigned(getPrepareUnsigned(sharedConnBinary));
+  }
+
+  public void getLongUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0L, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1L, rs.getLong(2));
+    assertEquals(1L, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(255L, rs.getLong(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0L, rs.getLong(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(getSigned());
+  }
+
+  @Test
+  public void getFloatPrepare() throws SQLException {
+    getFloat(getPrepareSigned(sharedConn));
+    getFloat(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getFloat(ResultSet rs) throws SQLException {
+    assertEquals(0F, rs.getFloat(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1F, rs.getFloat(2));
+    assertEquals(1F, rs.getFloat("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1F, rs.getFloat(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0F, rs.getFloat(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloatUnsigned() throws SQLException {
+    getFloatUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getFloatUnsignedPrepare() throws SQLException {
+    getFloatUnsigned(getPrepareUnsigned(sharedConn));
+    getFloatUnsigned(getPrepareUnsigned(sharedConnBinary));
+  }
+
+  public void getFloatUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0F, rs.getFloat(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1F, rs.getFloat(2));
+    assertEquals(1F, rs.getFloat("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(255F, rs.getFloat(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0F, rs.getFloat(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(getSigned());
+  }
+
+  @Test
+  public void getDoublePrepare() throws SQLException {
+    getDouble(getPrepareSigned(sharedConn));
+    getDouble(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getDouble(ResultSet rs) throws SQLException {
+    assertEquals(0D, rs.getDouble(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1D, rs.getDouble(2));
+    assertEquals(1D, rs.getDouble("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(-1D, rs.getDouble(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0D, rs.getDouble(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDoubleUnsigned() throws SQLException {
+    getDoubleUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getDoubleUnsignedPrepare() throws SQLException {
+    getDoubleUnsigned(getPrepareUnsigned(sharedConn));
+    getDoubleUnsigned(getPrepareUnsigned(sharedConnBinary));
+  }
+
+  public void getDoubleUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(0D, rs.getDouble(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1D, rs.getDouble(2));
+    assertEquals(1D, rs.getDouble("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(255D, rs.getDouble(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0D, rs.getDouble(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(getSigned());
+  }
+
+  @Test
+  public void getBigDecimalPrepare() throws SQLException {
+    getBigDecimal(getPrepareSigned(sharedConn));
+    getBigDecimal(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getBigDecimal(ResultSet rs) throws SQLException {
+    assertEquals(BigDecimal.ZERO, rs.getBigDecimal(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal(2));
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.valueOf(-1), rs.getBigDecimal(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimalUnsigned() throws SQLException {
+    getBigDecimalUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getBigDecimalUnsignedPrepare() throws SQLException {
+    getBigDecimalUnsigned(getPrepareUnsigned(sharedConn));
+    getBigDecimalUnsigned(getPrepareUnsigned(sharedConnBinary));
+  }
+
+  public void getBigDecimalUnsigned(ResultSet rs) throws SQLException {
+    assertEquals(BigDecimal.ZERO, rs.getBigDecimal(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal(2));
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.valueOf(255L), rs.getBigDecimal(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigIntegerUnsigned() throws SQLException {
+    getBigIntegerUnsigned(getUnsigned());
+  }
+
+  @Test
+  public void getBigIntegerUnsignedPrepare() throws SQLException {
+    getBigIntegerUnsigned(getPrepareUnsigned(sharedConn));
+    getBigIntegerUnsigned(getPrepareUnsigned(sharedConnBinary));
+  }
+
+  public void getBigIntegerUnsigned(ResultSet res) throws SQLException {
+    CompleteResult rs = (CompleteResult) res;
+    assertEquals(BigInteger.ZERO, rs.getBigInteger(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigInteger.ONE, rs.getBigInteger(2));
+    assertEquals(BigInteger.ONE, rs.getBigInteger("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(BigInteger.valueOf(255L), rs.getBigInteger(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigInteger(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(getSigned());
+  }
+
+  @Test
+  public void getDatePrepare() throws SQLException {
+    getDate(getPrepareSigned(sharedConn));
+    getDate(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getDate(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getDate(1), "Data type TINYINT cannot be decoded as Date");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDate("t1alias"),
+        "Data type TINYINT cannot be decoded as Date");
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(getSigned());
+  }
+
+  @Test
+  public void getTimePrepare() throws SQLException {
+    getTime(getPrepareSigned(sharedConn));
+    getTime(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getTime(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getTime(1), "Data type TINYINT cannot be decoded as Time");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime("t1alias"),
+        "Data type TINYINT cannot be decoded as Time");
+  }
+
+  @Test
+  public void getTimestamp() throws SQLException {
+    getTimestamp(getSigned());
+  }
+
+  @Test
+  public void getTimestampPrepare() throws SQLException {
+    getTimestamp(getPrepareSigned(sharedConn));
+    getTimestamp(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getTimestamp(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTimestamp(1),
+        "Data type TINYINT cannot be decoded as Timestamp");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTimestamp("t1alias"),
+        "Data type TINYINT cannot be decoded as Timestamp");
+  }
+
+  @Test
+  public void getAsciiStream() throws SQLException {
+    getAsciiStream(getSigned());
+  }
+
+  @Test
+  public void getAsciiStreamPrepare() throws SQLException {
+    getAsciiStream(getPrepareSigned(sharedConn));
+    getAsciiStream(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getAsciiStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getAsciiStream(1),
+        "Data type TINYINT cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getAsciiStream("t1alias"),
+        "Data type TINYINT cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getUnicodeStream() throws SQLException {
+    getUnicodeStream(getSigned());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepare() throws SQLException {
+    getUnicodeStream(getPrepareSigned(sharedConn));
+    getUnicodeStream(getPrepareSigned(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  public void getUnicodeStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getUnicodeStream(1),
+        "Data type TINYINT cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getUnicodeStream("t1alias"),
+        "Data type TINYINT cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBinaryStream() throws SQLException {
+    getBinaryStream(getSigned());
+  }
+
+  @Test
+  public void getBinaryStreamPrepare() throws SQLException {
+    getBinaryStream(getPrepareSigned(sharedConn));
+    getBinaryStream(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getBinaryStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBinaryStream(1),
+        "Data type TINYINT cannot be decoded as Stream");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBinaryStream("t1alias"),
+        "Data type TINYINT cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBytes() throws SQLException {
+    getBytes(getSigned());
+  }
+
+  @Test
+  public void getBytesPrepare() throws SQLException {
+    getBytes(getPrepareSigned(sharedConn));
+    getBytes(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getBytes(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBytes(1),
+        "Data type TINYINT cannot be decoded as byte[]");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBytes("t1alias"),
+        "Data type TINYINT cannot be decoded as byte[]");
+  }
+
+  @Test
+  public void getCharacterStream() throws SQLException {
+    getCharacterStream(getSigned());
+  }
+
+  @Test
+  public void getCharacterStreamPrepare() throws SQLException {
+    getCharacterStream(getPrepareSigned(sharedConn));
+    getCharacterStream(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type TINYINT cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream("t1alias"),
+        "Data type TINYINT cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getNCharacterStream() throws SQLException {
+    getNCharacterStream(getSigned());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepare() throws SQLException {
+    getNCharacterStream(getPrepareSigned(sharedConn));
+    getNCharacterStream(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getNCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream(1),
+        "Data type TINYINT cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream("t2alias"),
+        "Data type TINYINT cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getRef() throws SQLException {
+    getRef(getSigned());
+  }
+
+  @Test
+  public void getRefPrepare() throws SQLException {
+    getRef(getPrepareSigned(sharedConn));
+    getRef(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getRef(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getRef(1), "Method ResultSet.getRef not supported");
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getRef("t2alias"), "Method ResultSet.getRef not supported");
+  }
+
+  @Test
+  public void getBlob() throws SQLException {
+    getBlob(getSigned());
+  }
+
+  @Test
+  public void getBlobPrepare() throws SQLException {
+    getBlob(getPrepareSigned(sharedConn));
+    getBlob(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getBlob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type TINYINT cannot be decoded as Reader");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getCharacterStream("t1alias"),
+        "Data type TINYINT cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getClob() throws SQLException {
+    getClob(getSigned());
+  }
+
+  @Test
+  public void getClobPrepare() throws SQLException {
+    getClob(getPrepareSigned(sharedConn));
+    getClob(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getClob(1), "Data type TINYINT cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getClob("t1alias"),
+        "Data type TINYINT cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getNClob() throws SQLException {
+    getNClob(getSigned());
+  }
+
+  @Test
+  public void getNClobPrepare() throws SQLException {
+    getNClob(getPrepareSigned(sharedConn));
+    getNClob(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getNClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNClob(1),
+        "Data type TINYINT cannot be decoded as Clob");
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNClob("t1alias"),
+        "Data type TINYINT cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getArray() throws SQLException {
+    getArray(getSigned());
+  }
+
+  @Test
+  public void getArrayPrepare() throws SQLException {
+    getArray(getPrepareSigned(sharedConn));
+    getArray(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getArray(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getArray(1), "Method ResultSet.getArray not supported");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getArray("t1alias"),
+        "Method ResultSet.getArray not supported");
+  }
+
+  @Test
+  public void getURL() throws SQLException {
+    getURL(getSigned());
+  }
+
+  @Test
+  public void getURLPrepare() throws SQLException {
+    getURL(getPrepareSigned(sharedConn));
+    getURL(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getURL(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLSyntaxErrorException.class, () -> rs.getURL(1), "Could not parse '0' as URL");
+    Common.assertThrowsContains(
+        SQLSyntaxErrorException.class, () -> rs.getURL("t1alias"), "Could not parse '0' as URL");
+  }
+
+  @Test
+  public void getSQLXML() throws SQLException {
+    getSQLXML(getSigned());
+  }
+
+  @Test
+  public void getSQLXMLPrepare() throws SQLException {
+    getSQLXML(getPrepareSigned(sharedConn));
+    getSQLXML(getPrepareSigned(sharedConnBinary));
+  }
+
+  public void getSQLXML(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getSQLXML(1), "Method ResultSet.getSQLXML not supported");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getSQLXML("t1alias"),
+        "Method ResultSet.getSQLXML not supported");
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = getSigned();
+    ResultSetMetaData meta = rs.getMetaData();
+    assertEquals("TINYINT", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.lang.Integer", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.TINYINT, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(4, meta.getPrecision(1));
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    assertEquals(4, meta.getColumnDisplaySize(1));
+
+    rs = getUnsigned();
+    meta = rs.getMetaData();
+    assertEquals("TINYINT UNSIGNED", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.lang.Integer", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.SMALLINT, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    // https://jira.mariadb.org/browse/XPT-276
+    if (!isXpand()) {
+      assertEquals(3, meta.getPrecision(1));
+      assertEquals(3, meta.getColumnDisplaySize(1));
+    }
+  }
+
+  @Test
+  public void sendParam() throws SQLException {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE TinyIntCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO TinyIntCodec2(t1) VALUES (?)")) {
+      prep.setByte(1, (byte) 1);
+      prep.execute();
+      prep.setBoolean(1, true);
+      prep.execute();
+      prep.setBoolean(1, false);
+      prep.execute();
+      prep.setBoolean(1, true);
+      prep.execute();
+      prep.setBoolean(1, false);
+      prep.execute();
+      prep.setObject(1, Byte.valueOf("2"));
+      prep.execute();
+      prep.setObject(1, null);
+      prep.execute();
+      prep.setObject(1, (byte) 3, Types.TINYINT);
+      prep.execute();
+      prep.setObject(1, "4", Types.TINYINT);
+      prep.execute();
+      prep.setObject(1, Byte.valueOf("4"), Types.TINYINT);
+      prep.execute();
+      prep.setObject(1, null, Types.TINYINT);
+      prep.execute();
+      prep.setObject(1, "true", Types.BOOLEAN);
+      prep.execute();
+      prep.setObject(1, "false", Types.BOOLEAN);
+      prep.execute();
+    }
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM TinyIntCodec2");
+    assertTrue(rs.next());
+    assertEquals(1, rs.getByte(2));
+    rs.updateByte("t1", (byte) 10);
+    rs.updateRow();
+    assertEquals((byte) 10, rs.getByte(2));
+
+    assertTrue(rs.next());
+    assertTrue(rs.getBoolean(2));
+    rs.updateBoolean("t1", false);
+    rs.updateRow();
+    assertFalse(rs.getBoolean(2));
+
+    assertTrue(rs.next());
+    assertFalse(rs.getBoolean(2));
+    rs.updateBoolean("t1", true);
+    rs.updateRow();
+    assertTrue(rs.getBoolean(2));
+
+    assertTrue(rs.next());
+    assertTrue(rs.getBoolean(2));
+    rs.updateBoolean(2, false);
+    rs.updateRow();
+    assertFalse(rs.getBoolean(2));
+
+    assertTrue(rs.next());
+    assertFalse(rs.getBoolean(2));
+    rs.updateBoolean(2, true);
+    rs.updateRow();
+    assertTrue(rs.getBoolean(2));
+
+    assertTrue(rs.next());
+    assertEquals(2, rs.getByte(2));
+    rs.updateObject(2, (Byte) null);
+    rs.updateRow();
+    assertEquals(0, rs.getByte(2));
+    assertTrue(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals(0, rs.getByte(2));
+    assertTrue(rs.wasNull());
+    rs.updateByte(2, (byte) 15);
+    rs.updateRow();
+    assertEquals((byte) 15, rs.getByte(2));
+
+    assertTrue(rs.next());
+    assertEquals(3, rs.getByte(2));
+    assertTrue(rs.next());
+    assertEquals(4, rs.getByte(2));
+    assertTrue(rs.next());
+    assertEquals(4, rs.getShort(2));
+    assertTrue(rs.next());
+    assertEquals(0, rs.getByte(2));
+    assertTrue(rs.wasNull());
+    assertTrue(rs.next());
+    assertTrue(rs.getBoolean(2));
+    assertTrue(rs.next());
+    assertFalse(rs.getBoolean(2));
+
+    rs = stmt.executeQuery("SELECT * FROM TinyIntCodec2");
+    assertTrue(rs.next());
+    assertEquals((byte) 10, rs.getByte(2));
+
+    assertTrue(rs.next());
+    assertFalse(rs.getBoolean(2));
+
+    assertTrue(rs.next());
+    assertTrue(rs.getBoolean(2));
+
+    assertTrue(rs.next());
+    assertFalse(rs.getBoolean(2));
+
+    assertTrue(rs.next());
+    assertTrue(rs.getBoolean(2));
+
+    assertTrue(rs.next());
+    assertEquals(0, rs.getByte(2));
+    assertTrue(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertEquals((byte) 15, rs.getByte(2));
+
+    assertTrue(rs.next());
+    assertEquals(3, rs.getByte(2));
+    assertTrue(rs.next());
+    assertEquals(4, rs.getShort(2));
+    assertTrue(rs.next());
+    assertEquals(4, rs.getShort(2));
+    assertTrue(rs.next());
+    assertEquals(0, rs.getByte(2));
+    assertTrue(rs.wasNull());
+    con.commit();
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/UuidCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/UuidCodecTest.java
new file mode 100644
index 0000000..6fac7d8
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/UuidCodecTest.java
@@ -0,0 +1,404 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.Reader;
+import java.io.StringReader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import java.time.*;
+import java.util.UUID;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.integration.Common;
+import org.mariadb.jdbc.util.constants.Capabilities;
+
+public class UuidCodecTest extends CommonCodecTest {
+
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS UuidCodec");
+    stmt.execute("DROP TABLE IF EXISTS UuidCodec2");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Assumptions.assumeTrue(
+        isMariaDBServer()
+            && minVersion(10, 7, 0)
+            && hasCapability(Capabilities.EXTENDED_TYPE_INFO));
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE UuidCodec (t1 UUID, t2 UUID, t3 UUID, t4 UUID)");
+    stmt.execute("CREATE TABLE UuidCodec2 (t1 UUID)");
+    stmt.execute(
+        "INSERT INTO UuidCodec VALUES ('123e4567-e89b-12d3-a456-426655440000',"
+            + " '93aac041-1a14-11ec-ab4e-f859713e4be4', 'ffffffff-ffff-ffff-ffff-fffffffffffe',"
+            + " null)");
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    stmt.closeOnCompletion();
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from UuidCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getAdd(String additional) throws SQLException {
+    try (Connection con = createCon(additional)) {
+      java.sql.Statement stmt = con.createStatement();
+      stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+      stmt.closeOnCompletion();
+      ResultSet rs =
+          stmt.executeQuery(
+              "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from UuidCodec");
+      assertTrue(rs.next());
+      con.commit();
+      return rs;
+    }
+  }
+
+  private ResultSet getPrepare(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from UuidCodec"
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    ResultSet rs = preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws Exception {
+    getObject(get());
+  }
+
+  @Test
+  public void getObjectPrepare() throws Exception {
+    getObject(getPrepare(sharedConn));
+    getObject(getPrepare(sharedConnBinary));
+    try (Connection con = createCon("&uuidAsString=1")) {
+      getObjectString(getPrepare(con));
+    }
+  }
+
+  public void getObject(ResultSet rs) throws Exception {
+    assertEquals(UUID.fromString("123e4567-e89b-12d3-a456-426655440000"), rs.getObject(1));
+    assertFalse(rs.wasNull());
+
+    assertEquals(UUID.fromString("93aac041-1a14-11ec-ab4e-f859713e4be4"), rs.getObject(2));
+    assertEquals(UUID.fromString("93aac041-1a14-11ec-ab4e-f859713e4be4"), rs.getObject("t2alias"));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4));
+    assertEquals(
+        UUID.fromString("ffffffff-ffff-ffff-ffff-fffffffffffe"), rs.getObject(3, UUID.class));
+    assertEquals(UUID.class, rs.getObject(3, UUID.class).getClass());
+  }
+
+  public void getObjectString(ResultSet rs) throws Exception {
+    assertEquals("123e4567-e89b-12d3-a456-426655440000", rs.getObject(1));
+    assertFalse(rs.wasNull());
+
+    assertEquals("93aac041-1a14-11ec-ab4e-f859713e4be4", rs.getObject(2));
+    assertEquals("93aac041-1a14-11ec-ab4e-f859713e4be4", rs.getObject("t2alias"));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4));
+    assertEquals("ffffffff-ffff-ffff-ffff-fffffffffffe", rs.getObject(3));
+    assertEquals(UUID.class, rs.getObject(3, UUID.class).getClass());
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testErrObject(rs, Integer.class);
+    testObject(rs, String.class, "123e4567-e89b-12d3-a456-426655440000");
+    testErrObject(rs, Long.class);
+    testErrObject(rs, Short.class);
+    testErrObject(rs, BigDecimal.class);
+    testErrObject(rs, BigInteger.class);
+    testErrObject(rs, Double.class);
+    testErrObject(rs, Float.class);
+    testErrObject(rs, Byte.class);
+    testObject(rs, Reader.class, new StringReader("123e4567-e89b-12d3-a456-426655440000"));
+    testErrObject(rs, LocalDate.class);
+    testErrObject(rs, LocalDateTime.class);
+    testErrObject(rs, LocalTime.class);
+    testErrObject(rs, Time.class);
+    testErrObject(rs, Date.class);
+    testErrObject(rs, ZonedDateTime.class);
+    testErrObject(rs, OffsetDateTime.class);
+    testErrObject(rs, OffsetTime.class);
+    testErrObject(rs, java.util.Date.class);
+  }
+
+  @Test
+  public void getString() throws SQLException {
+    getString(get());
+  }
+
+  @Test
+  public void getStringPrepare() throws SQLException {
+    getString(getPrepare(sharedConn));
+    getString(getPrepare(sharedConnBinary));
+    try (Connection con = createCon("&uuidAsString=1")) {
+      getString(getPrepare(con));
+    }
+  }
+
+  public void getString(ResultSet rs) throws SQLException {
+    assertEquals("123e4567-e89b-12d3-a456-426655440000", rs.getObject(1, String.class));
+    assertFalse(rs.wasNull());
+
+    assertEquals("93aac041-1a14-11ec-ab4e-f859713e4be4", rs.getString(2));
+    assertEquals("93aac041-1a14-11ec-ab4e-f859713e4be4", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4));
+    assertEquals("ffffffff-ffff-ffff-ffff-fffffffffffe", rs.getString(3));
+    assertEquals(String.class, rs.getString(3).getClass());
+  }
+
+  @Test
+  public void getUuid() throws SQLException {
+    getUuid(get());
+  }
+
+  @Test
+  public void getUuidPrepare() throws SQLException {
+    getUuid(getPrepare(sharedConn));
+    getUuid(getPrepare(sharedConnBinary));
+  }
+
+  public void getUuid(ResultSet rs) throws SQLException {
+    assertEquals(
+        UUID.fromString("123e4567-e89b-12d3-a456-426655440000"), rs.getObject(1, UUID.class));
+    assertFalse(rs.wasNull());
+
+    assertEquals(
+        UUID.fromString("93aac041-1a14-11ec-ab4e-f859713e4be4"), rs.getObject(2, UUID.class));
+    assertEquals(
+        UUID.fromString("93aac041-1a14-11ec-ab4e-f859713e4be4"),
+        rs.getObject("t2alias", UUID.class));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4, UUID.class));
+    assertEquals(
+        UUID.fromString("ffffffff-ffff-ffff-ffff-fffffffffffe"), rs.getObject(3, UUID.class));
+    assertEquals(UUID.class, rs.getObject(3, UUID.class).getClass());
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(get());
+  }
+
+  @Test
+  public void getIntPrepare() throws SQLException {
+    getInt(getPrepare(sharedConn));
+    getInt(getPrepare(sharedConnBinary));
+  }
+
+  public void getInt(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getInt(1), "Data type UUID cannot be decoded as Integer");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(get());
+  }
+
+  @Test
+  public void getLongPrepare() throws SQLException {
+    getLong(getPrepare(sharedConn));
+    getLong(getPrepare(sharedConnBinary));
+  }
+
+  public void getLong(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getLong(1), "Data type UUID cannot be decoded as Long");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(get());
+  }
+
+  @Test
+  public void getFloatPrepare() throws SQLException {
+    getFloat(getPrepare(sharedConn));
+    getFloat(getPrepare(sharedConnBinary));
+  }
+
+  public void getFloat(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getFloat(1), "Data type UUID cannot be decoded as Float");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(get());
+  }
+
+  @Test
+  public void getDoublePrepare() throws SQLException {
+    getDouble(getPrepare(sharedConn));
+    getDouble(getPrepare(sharedConnBinary));
+  }
+
+  public void getDouble(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDouble(1),
+        "Data type UUID cannot be decoded as Double");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(get());
+  }
+
+  @Test
+  public void getBigDecimalPrepare() throws SQLException {
+    getBigDecimal(getPrepare(sharedConn));
+    getBigDecimal(getPrepare(sharedConnBinary));
+  }
+
+  public void getBigDecimal(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBigDecimal(1),
+        "value '123e4567-e89b-12d3-a456-426655440000' cannot be decoded as BigDecimal");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(get());
+  }
+
+  @Test
+  public void getDatePrepare() throws SQLException {
+    getDate(getPrepare(sharedConn));
+    getDate(getPrepare(sharedConnBinary));
+  }
+
+  public void getDate(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getDate(1), "Data type UUID cannot be decoded as Date");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(get());
+  }
+
+  @Test
+  public void getTimePrepare() throws SQLException {
+    getTime(getPrepare(sharedConn));
+    getTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getTime(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getTime(1), "Data type UUID cannot be decoded as Time");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = get();
+    ResultSetMetaData meta = rs.getMetaData();
+    assertEquals("uuid", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.util.UUID", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.OTHER, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(36, meta.getPrecision(1));
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    assertEquals(36, meta.getColumnDisplaySize(1));
+
+    rs = getAdd("&uuidAsString=True");
+    meta = rs.getMetaData();
+    assertEquals("uuid", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.lang.String", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.CHAR, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(36, meta.getPrecision(1));
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    assertEquals(36, meta.getColumnDisplaySize(1));
+  }
+
+  @Test
+  public void sendParam() throws Exception {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws Exception {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE UuidCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep = con.prepareStatement("INSERT INTO UuidCodec2(t1) VALUES (?)")) {
+      prep.setObject(1, UUID.fromString("123e4567-e89b-12d3-a456-426655440000"));
+      prep.execute();
+      prep.setString(1, "123e4568-e89b-12d3-a456-426655440000");
+      prep.execute();
+      prep.setObject(1, null);
+      prep.execute();
+    }
+
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM UuidCodec2");
+
+    assertTrue(rs.next());
+    assertEquals("123e4567-e89b-12d3-a456-426655440000", rs.getString(1));
+
+    assertTrue(rs.next());
+    assertEquals("123e4568-e89b-12d3-a456-426655440000", rs.getString(1));
+
+    assertTrue(rs.next());
+    assertNull(rs.getString(1));
+
+    con.commit();
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/VarbinaryCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/VarbinaryCodecTest.java
new file mode 100644
index 0000000..e7ec858
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/VarbinaryCodecTest.java
@@ -0,0 +1,771 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.sql.*;
+import java.time.*;
+import java.util.Calendar;
+import java.util.TimeZone;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.MariaDbBlob;
+import org.mariadb.jdbc.MariaDbClob;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.integration.Common;
+
+public class VarbinaryCodecTest extends CommonCodecTest {
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS VarbinaryCodec");
+    stmt.execute("DROP TABLE IF EXISTS VarbinaryCodec2");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE VarbinaryCodec (t1 VARBINARY(20), t2 VARBINARY(30), t3 VARBINARY(20), t4"
+            + " VARBINARY(20))");
+    stmt.execute("CREATE TABLE VarbinaryCodec2 (t1 VARBINARY(20))");
+    stmt.execute(
+        "INSERT INTO VarbinaryCodec VALUES ('0', '1', 'some🌟', null), ('2011-01-01', '2010-12-31"
+            + " 23:59:59.152', '23:54:51.840010', null)");
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    stmt.closeOnCompletion();
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from"
+                + " VarbinaryCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getPrepare(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from VarbinaryCodec"
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    ResultSet rs = preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws Exception {
+    getObject(get());
+  }
+
+  @Test
+  public void getObjectPrepare() throws Exception {
+    getObject(getPrepare(sharedConn));
+    getObject(getPrepare(sharedConnBinary));
+  }
+
+  public void getObject(ResultSet rs) throws Exception {
+    assertArrayEquals("0".getBytes(StandardCharsets.UTF_8), (byte[]) rs.getObject(1));
+    assertFalse(rs.wasNull());
+    assertArrayEquals("1".getBytes(StandardCharsets.UTF_8), (byte[]) rs.getObject(2));
+    assertArrayEquals("1".getBytes(StandardCharsets.UTF_8), (byte[]) rs.getObject("t2alias"));
+    assertFalse(rs.wasNull());
+    assertArrayEquals("some🌟".getBytes(StandardCharsets.UTF_8), (byte[]) rs.getObject(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBlob(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testObject(rs, Integer.class, 0);
+    testObject(rs, String.class, "0");
+    testObject(rs, Long.class, 0L);
+    testObject(rs, Short.class, (short) 0);
+    testObject(rs, BigDecimal.class, BigDecimal.valueOf(0));
+    testObject(rs, BigInteger.class, BigInteger.valueOf(0));
+    testObject(rs, Double.class, (double) 0);
+    testObject(rs, Float.class, (float) 0);
+    testObject(rs, Byte.class, (byte) 0);
+    testArrObject(rs, new byte[] {(byte) '0'});
+    testObject(rs, Boolean.class, Boolean.FALSE);
+    testObject(rs, Clob.class, new MariaDbClob("0".getBytes()));
+    testObject(rs, NClob.class, new MariaDbClob("0".getBytes()));
+    testObject(rs, InputStream.class, new MariaDbClob("0".getBytes()).getBinaryStream());
+    testObject(rs, Reader.class, new StringReader("0"));
+    rs.next();
+    testObject(rs, LocalDate.class, LocalDate.parse("2011-01-01"));
+    testObject(rs, LocalDateTime.class, LocalDateTime.parse("2011-01-01T00:00"));
+    testObject(rs, LocalTime.class, LocalTime.parse("23:54:51.840010"), 3);
+    Time t = Time.valueOf("23:54:51");
+    testObject(rs, Time.class, new Time(t.getTime() + 840), 3);
+    testObject(rs, Date.class, Date.valueOf("2011-01-01"));
+    Timestamp tt = Timestamp.valueOf("2010-12-31 23:59:59");
+    testObject(rs, Timestamp.class, new Timestamp(tt.getTime() + 152), 2);
+    testObject(
+        rs,
+        ZonedDateTime.class,
+        LocalDateTime.parse("2011-01-01T00:00").atZone(ZoneId.systemDefault()));
+    testErrObject(rs, OffsetDateTime.class);
+    testErrObject(rs, OffsetTime.class);
+    testObject(rs, java.util.Date.class, Date.valueOf("2010-12-31"), 2);
+  }
+
+  @Test
+  public void getString() throws SQLException {
+    getString(get());
+  }
+
+  @Test
+  public void getStringPrepare() throws SQLException {
+    getString(getPrepare(sharedConn));
+    getString(getPrepare(sharedConnBinary));
+  }
+
+  public void getString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getString(2));
+    assertEquals("1", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("some🌟", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNString() throws SQLException {
+    getNString(get());
+  }
+
+  @Test
+  public void getNStringPrepare() throws SQLException {
+    getNString(getPrepare(sharedConn));
+    getNString(getPrepare(sharedConnBinary));
+  }
+
+  public void getNString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getNString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getNString(2));
+    assertEquals("1", rs.getNString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("some🌟", rs.getNString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBoolean() throws SQLException {
+    getBoolean(get());
+  }
+
+  @Test
+  public void getBooleanPrepare() throws SQLException {
+    getBoolean(getPrepare(sharedConn));
+    getBoolean(getPrepare(sharedConnBinary));
+  }
+
+  public void getBoolean(ResultSet rs) throws SQLException {
+    assertFalse(rs.getBoolean(1));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(2));
+    assertTrue(rs.getBoolean("t2alias"));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(3));
+    assertFalse(rs.wasNull());
+    assertFalse(rs.getBoolean(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getByte() throws SQLException {
+    getByte(get());
+  }
+
+  @Test
+  public void getBytePrepare() throws SQLException {
+    getByte(getPrepare(sharedConn));
+    getByte(getPrepare(sharedConnBinary));
+  }
+
+  public void getByte(ResultSet rs) throws SQLException {
+    assertEquals((byte) 0, rs.getByte(1));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 1, rs.getByte(2));
+    assertEquals((byte) 1, rs.getByte("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getByte(3),
+        "value 'some\uD83C\uDF1F' (VARSTRING) cannot be decoded as Byte");
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 0, rs.getByte(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getShort() throws SQLException {
+    getShort(get());
+  }
+
+  @Test
+  public void getShortPrepare() throws SQLException {
+    getShort(getPrepare(sharedConn));
+    getShort(getPrepare(sharedConnBinary));
+  }
+
+  public void getShort(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getShort(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getShort(2));
+    assertEquals(1, rs.getShort("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getShort(3), "value 'some🌟' cannot be decoded as Short");
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getShort(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(get());
+  }
+
+  @Test
+  public void getIntPrepare() throws SQLException {
+    getInt(getPrepare(sharedConn));
+    getInt(getPrepare(sharedConnBinary));
+  }
+
+  public void getInt(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getInt(2));
+    assertEquals(1, rs.getInt("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getInt(3), "value 'some🌟' cannot be decoded as Integer");
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(get());
+  }
+
+  @Test
+  public void getLongPrepare() throws SQLException {
+    getLong(getPrepare(sharedConn));
+    getLong(getPrepare(sharedConnBinary));
+  }
+
+  public void getLong(ResultSet rs) throws SQLException {
+    assertEquals(0L, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1L, rs.getLong(2));
+    assertEquals(1L, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getLong(3),
+        "value 'some\uD83C\uDF1F' cannot be decoded as Long");
+    assertFalse(rs.wasNull());
+    assertEquals(0L, rs.getLong(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(get());
+  }
+
+  @Test
+  public void getFloatPrepare() throws SQLException {
+    getFloat(getPrepare(sharedConn));
+    getFloat(getPrepare(sharedConnBinary));
+  }
+
+  public void getFloat(ResultSet rs) throws SQLException {
+    assertEquals(0F, rs.getFloat(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1F, rs.getFloat(2));
+    assertEquals(1F, rs.getFloat("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getFloat(3), "value 'some🌟' cannot be decoded as Float");
+    assertFalse(rs.wasNull());
+    assertEquals(0F, rs.getFloat(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(get());
+  }
+
+  @Test
+  public void getDoublePrepare() throws SQLException {
+    getDouble(getPrepare(sharedConn));
+    getDouble(getPrepare(sharedConnBinary));
+  }
+
+  public void getDouble(ResultSet rs) throws SQLException {
+    assertEquals(0D, rs.getDouble(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1D, rs.getDouble(2));
+    assertEquals(1D, rs.getDouble("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDouble(3),
+        "value 'some🌟' cannot be decoded as Double");
+    assertFalse(rs.wasNull());
+    assertEquals(0D, rs.getDouble(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(get());
+  }
+
+  @Test
+  public void getBigDecimalPrepare() throws SQLException {
+    getBigDecimal(getPrepare(sharedConn));
+    getBigDecimal(getPrepare(sharedConnBinary));
+  }
+
+  public void getBigDecimal(ResultSet rs) throws SQLException {
+    assertEquals(BigDecimal.ZERO, rs.getBigDecimal(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal(2));
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBigDecimal(3),
+        "value 'some🌟' cannot be decoded as BigDecimal");
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(get());
+  }
+
+  @Test
+  public void getDatePrepare() throws SQLException {
+    getDate(getPrepare(sharedConn));
+    getDate(getPrepare(sharedConnBinary));
+  }
+
+  public void getDate(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDate(1),
+        "value '0' (VARSTRING) cannot be decoded as Date");
+    rs.next();
+    assertEquals("2011-01-01", rs.getDate(1).toString());
+    assertFalse(rs.wasNull());
+    assertEquals("2010-12-31", rs.getDate(2).toString());
+    assertFalse(rs.wasNull());
+    assertEquals("2010-12-31", rs.getDate("t2alias").toString());
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDate(3),
+        "value '23:54:51.840010' (VARSTRING) cannot be decoded as Date");
+    assertFalse(rs.wasNull());
+    assertNull(rs.getDate(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(get());
+  }
+
+  @Test
+  public void getTimePrepare() throws SQLException {
+    getTime(getPrepare(sharedConn));
+    getTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getTime(ResultSet rs) throws SQLException {
+    rs.next();
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime(1),
+        "VARSTRING value '2011-01-01' cannot be decoded as Time");
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime(2),
+        "VARSTRING value '2010-12-31 23:59:59.152' cannot be decoded as Time");
+    assertEquals(Time.valueOf("23:54:51").getTime() + 840, rs.getTime(3).getTime());
+    assertEquals(Time.valueOf("23:54:51").getTime() + 840, rs.getTime("t3alias").getTime());
+    Calendar utc = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+    assertEquals(86091840, rs.getTime(3, utc).getTime());
+    assertEquals(86091840, rs.getTime("t3alias", utc).getTime());
+
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getTimestamp() throws SQLException {
+    getTimestamp(get());
+  }
+
+  @Test
+  public void getTimestampPrepare() throws SQLException {
+    getTimestamp(getPrepare(sharedConn));
+    getTimestamp(getPrepare(sharedConnBinary));
+  }
+
+  public void getTimestamp(ResultSet rs) throws SQLException {
+    rs.next();
+    assertEquals(Timestamp.valueOf("2011-01-01 00:00:00").getTime(), rs.getTimestamp(1).getTime());
+    assertEquals(
+        Timestamp.valueOf("2011-01-01 00:00:00").getTime() + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp(1, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertEquals(
+        Timestamp.valueOf("2011-01-01 00:00:00").getTime(), rs.getTimestamp("t1alias").getTime());
+    assertEquals(
+        Timestamp.valueOf("2011-01-01 00:00:00").getTime() + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp("t1alias", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime() + 152, rs.getTimestamp(2).getTime());
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime()
+            + 152
+            + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp(2, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime() + 152,
+        rs.getTimestamp("t2alias").getTime());
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime()
+            + 152
+            + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp("t2alias", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+
+    //
+    //    ('2011-01-01', '2010-12-31 23:59:59.152',"
+    //            + " '23:54:51.840010', null)
+  }
+
+  @Test
+  public void getAsciiStream() throws Exception {
+    getAsciiStream(get());
+  }
+
+  @Test
+  public void getAsciiStreamPrepare() throws Exception {
+    getAsciiStream(getPrepare(sharedConn));
+    getAsciiStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getAsciiStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getAsciiStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getAsciiStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getAsciiStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getAsciiStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getAsciiStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getUnicodeStream() throws Exception {
+    getUnicodeStream(get());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepare() throws Exception {
+    getUnicodeStream(getPrepare(sharedConn));
+    getUnicodeStream(getPrepare(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  public void getUnicodeStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getUnicodeStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getUnicodeStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getUnicodeStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)),
+        rs.getUnicodeStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getUnicodeStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBinaryStream() throws Exception {
+    getBinaryStream(get());
+  }
+
+  @Test
+  public void getBinaryStreamPrepare() throws Exception {
+    getBinaryStream(getPrepare(sharedConn));
+    getBinaryStream(getPrepare(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecated")
+  public void getBinaryStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getBinaryStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getBinaryStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getBinaryStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getBinaryStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBinaryStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBytes() throws Exception {
+    getBytes(get());
+  }
+
+  @Test
+  public void getBytesPrepare() throws Exception {
+    getBytes(getPrepare(sharedConn));
+    getBytes(getPrepare(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecated")
+  public void getBytes(ResultSet rs) throws Exception {
+    assertArrayEquals("0".getBytes(), rs.getBytes(1));
+    assertFalse(rs.wasNull());
+    assertArrayEquals("1".getBytes(), rs.getBytes(2));
+    assertArrayEquals("1".getBytes(), rs.getBytes("t2alias"));
+    assertFalse(rs.wasNull());
+    assertArrayEquals("some🌟".getBytes(StandardCharsets.UTF_8), rs.getBytes(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBytes(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getCharacterStream() throws Exception {
+    getCharacterStream(get());
+  }
+
+  @Test
+  public void getCharacterStreamPrepare() throws Exception {
+    getCharacterStream(getPrepare(sharedConn));
+    getCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getCharacterStream(ResultSet rs) throws Exception {
+    assertReaderEquals(new StringReader("0"), rs.getCharacterStream(1));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("1"), rs.getCharacterStream(2));
+    assertReaderEquals(new StringReader("1"), rs.getCharacterStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("some🌟"), rs.getCharacterStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getCharacterStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNCharacterStream() throws Exception {
+    getNCharacterStream(get());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepare() throws Exception {
+    getNCharacterStream(getPrepare(sharedConn));
+    getNCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getNCharacterStream(ResultSet rs) throws Exception {
+    assertReaderEquals(new StringReader("0"), rs.getNCharacterStream(1));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("1"), rs.getNCharacterStream(2));
+    assertReaderEquals(new StringReader("1"), rs.getNCharacterStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("some🌟"), rs.getNCharacterStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNCharacterStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBlob() throws Exception {
+    getBlob(get());
+  }
+
+  @Test
+  public void getBlobPrepare() throws Exception {
+    getBlob(getPrepare(sharedConn));
+    getBlob(getPrepare(sharedConnBinary));
+  }
+
+  public void getBlob(ResultSet rs) throws Exception {
+    assertStreamEquals(new MariaDbBlob("0".getBytes()), rs.getBlob(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbBlob("1".getBytes()), rs.getBlob(2));
+    assertStreamEquals(new MariaDbBlob("1".getBytes()), rs.getBlob("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbBlob("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getBlob(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBlob(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getClob() throws Exception {
+    getClob(get());
+  }
+
+  @Test
+  public void getClobPrepare() throws Exception {
+    getClob(getPrepare(sharedConn));
+    getClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getClob(ResultSet rs) throws Exception {
+    assertStreamEquals(new MariaDbClob("0".getBytes()), rs.getClob(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getClob(2));
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getClob("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getClob(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getClob(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNClob() throws Exception {
+    getNClob(get());
+  }
+
+  @Test
+  public void getNClobPrepare() throws Exception {
+    getNClob(getPrepare(sharedConn));
+    getNClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getNClob(ResultSet rs) throws Exception {
+    assertStreamEquals(new MariaDbClob("0".getBytes()), rs.getNClob(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getNClob(2));
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getNClob("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getNClob(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNClob(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = get();
+    ResultSetMetaData meta = rs.getMetaData();
+    assertEquals("VARBINARY", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("byte[]", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.VARBINARY, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(20, meta.getPrecision(1));
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    assertEquals(20, meta.getColumnDisplaySize(1));
+  }
+
+  @Test
+  public void sendParam() throws Exception {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+  }
+
+  private void sendParam(Connection con) throws Exception {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE VarbinaryCodec2");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO VarbinaryCodec2(t1) VALUES (?)")) {
+      prep.setBytes(1, null);
+      prep.execute();
+      prep.setBytes(1, "e🌟1".getBytes(StandardCharsets.UTF_8));
+      prep.execute();
+      prep.setNull(1, Types.BLOB);
+      prep.execute();
+
+      prep.setObject(1, "e🌟2".getBytes(StandardCharsets.UTF_8));
+      prep.execute();
+      prep.setObject(1, "e🌟3".getBytes(StandardCharsets.UTF_8), Types.VARBINARY);
+      prep.execute();
+    }
+
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM VarbinaryCodec2");
+    assertTrue(rs.next());
+    assertNull(rs.getBytes(1));
+
+    assertTrue(rs.next());
+    assertArrayEquals("e🌟1".getBytes(StandardCharsets.UTF_8), rs.getBytes(1));
+
+    assertTrue(rs.next());
+    assertNull(rs.getBytes(1));
+
+    assertTrue(rs.next());
+    assertArrayEquals("e🌟2".getBytes(StandardCharsets.UTF_8), rs.getBytes(1));
+
+    assertTrue(rs.next());
+    assertArrayEquals("e🌟3".getBytes(StandardCharsets.UTF_8), rs.getBytes(1));
+    con.commit();
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/VarcharCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/VarcharCodecTest.java
new file mode 100644
index 0000000..fc588b3
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/VarcharCodecTest.java
@@ -0,0 +1,1065 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.sql.*;
+import java.time.*;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TimeZone;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.MariaDbClob;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.integration.Common;
+
+public class VarcharCodecTest extends CommonCodecTest {
+  @AfterAll
+  public static void after2() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS StringCodec");
+    stmt.execute("DROP TABLE IF EXISTS StringParamCodec");
+    stmt.execute("DROP TABLE IF EXISTS StringCodecWrong");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    after2();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute(
+        "CREATE TABLE StringCodec (t1 VARCHAR(255), t2 VARCHAR(30), t3 VARCHAR(20), t4 VARCHAR(20))"
+            + " CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
+    stmt.execute(
+        "INSERT INTO StringCodec VALUES ('0', '1', 'some🌟', null), "
+            + "('2011-01-01', '2010-12-31 23:59:59.152', '23:54:51.840010', null), "
+            + "('aaaa-bb-cc', '0000-00-00 00:00:00', '23:54:51.840010', null),"
+            + "('', '', '', null)");
+    stmt.execute(
+        "CREATE TABLE StringParamCodec(id int not null primary key auto_increment, t1 VARCHAR(20)) "
+            + "CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
+    stmt.execute(
+        "CREATE TABLE StringCodecWrong (t1 VARCHAR(20)) CHARACTER SET utf8mb4 COLLATE"
+            + " utf8mb4_unicode_ci");
+
+    stmt.execute("FLUSH TABLES");
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from StringCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getPrepare(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from StringCodec"
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    ResultSet rs = preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws SQLException {
+    getObject(get());
+  }
+
+  @Test
+  public void getObjectPrepare() throws SQLException {
+    getObject(getPrepare(sharedConn));
+    getObject(getPrepare(sharedConnBinary));
+  }
+
+  public void getObject(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getObject(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getObject(2));
+    assertEquals("1", rs.getObject("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("some🌟", rs.getObject(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testObject(rs, Integer.class, 0);
+    testObject(rs, String.class, "0");
+    testObject(rs, Byte.class, Byte.valueOf("0"));
+    testObject(rs, Long.class, 0L);
+    testObject(rs, Short.class, (short) 0);
+    testObject(rs, BigDecimal.class, BigDecimal.valueOf(0));
+    testObject(rs, BigInteger.class, BigInteger.valueOf(0));
+    testObject(rs, Double.class, (double) 0);
+    testObject(rs, Float.class, (float) 0);
+    testObject(rs, Byte.class, (byte) 0);
+    testArrObject(rs, new byte[] {(byte) '0'});
+    testObject(rs, Boolean.class, Boolean.FALSE);
+    testObject(rs, Clob.class, new MariaDbClob("0".getBytes()));
+    testObject(rs, NClob.class, new MariaDbClob("0".getBytes()));
+    testObject(rs, InputStream.class, new MariaDbClob("0".getBytes()).getBinaryStream());
+    testObject(rs, Reader.class, new StringReader("0"));
+    rs.next();
+    testObject(rs, LocalDate.class, LocalDate.parse("2011-01-01"));
+    testObject(rs, LocalDateTime.class, LocalDateTime.parse("2011-01-01T00:00"));
+    testObject(rs, LocalTime.class, LocalTime.parse("23:54:51.840010"), 3);
+    Time t = Time.valueOf("23:54:51");
+    testObject(rs, Time.class, new Time(t.getTime() + 840), 3);
+    testObject(rs, Date.class, Date.valueOf("2011-01-01"));
+    Timestamp tt = Timestamp.valueOf("2010-12-31 23:59:59");
+    testObject(rs, Timestamp.class, new Timestamp(tt.getTime() + 152), 2);
+    testObject(
+        rs,
+        ZonedDateTime.class,
+        LocalDateTime.parse("2011-01-01T00:00").atZone(ZoneId.systemDefault()));
+    testErrObject(rs, BigInteger.class);
+    testErrObject(rs, OffsetDateTime.class);
+    testErrObject(rs, OffsetTime.class);
+    testObject(rs, java.util.Date.class, Date.valueOf("2010-12-31"), 2);
+  }
+
+  @Test
+  public void getString() throws SQLException {
+    getString(get());
+  }
+
+  @Test
+  public void getStringPrepare() throws SQLException {
+    getString(getPrepare(sharedConn));
+    getString(getPrepare(sharedConnBinary));
+  }
+
+  public void getString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getString(2));
+    assertEquals("1", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("some🌟", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNString() throws SQLException {
+    getNString(get());
+  }
+
+  @Test
+  public void getNStringPrepare() throws SQLException {
+    getNString(getPrepare(sharedConn));
+    getNString(getPrepare(sharedConnBinary));
+  }
+
+  public void getNString(ResultSet rs) throws SQLException {
+    assertEquals("0", rs.getNString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1", rs.getNString(2));
+    assertEquals("1", rs.getNString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("some🌟", rs.getNString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBoolean() throws SQLException {
+    getBoolean(get());
+  }
+
+  @Test
+  public void getBooleanPrepare() throws SQLException {
+    getBoolean(getPrepare(sharedConn));
+    getBoolean(getPrepare(sharedConnBinary));
+  }
+
+  public void getBoolean(ResultSet rs) throws SQLException {
+    assertFalse(rs.getBoolean(1));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(2));
+    assertTrue(rs.getBoolean("t2alias"));
+    assertFalse(rs.wasNull());
+    assertTrue(rs.getBoolean(3));
+    assertFalse(rs.wasNull());
+    assertFalse(rs.getBoolean(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getByte() throws SQLException {
+    getByte(get());
+  }
+
+  @Test
+  public void getBytePrepare() throws SQLException {
+    getByte(getPrepare(sharedConn));
+    getByte(getPrepare(sharedConnBinary));
+  }
+
+  public void getByte(ResultSet rs) throws SQLException {
+    assertEquals((byte) 0, rs.getByte(1));
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 1, rs.getByte(2));
+    assertEquals((byte) 1, rs.getByte("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getByte(3),
+        "value 'some\uD83C\uDF1F' (VARSTRING) cannot be decoded as Byte");
+    assertFalse(rs.wasNull());
+    assertEquals((byte) 0, rs.getByte(4));
+    assertTrue(rs.wasNull());
+    rs.next();
+    rs.next();
+    rs.next();
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getByte(1),
+        "value '' (VARSTRING) cannot be decoded as Byte");
+  }
+
+  @Test
+  public void getShort() throws SQLException {
+    getShort(get());
+  }
+
+  @Test
+  public void getShortPrepare() throws SQLException {
+    getShort(getPrepare(sharedConn));
+    getShort(getPrepare(sharedConnBinary));
+  }
+
+  public void getShort(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getShort(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getShort(2));
+    assertEquals(1, rs.getShort("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getShort(3), "value 'some🌟' cannot be decoded as Short");
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getShort(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(get());
+  }
+
+  @Test
+  public void getIntPrepare() throws SQLException {
+    getInt(getPrepare(sharedConn));
+    getInt(getPrepare(sharedConnBinary));
+  }
+
+  public void getInt(ResultSet rs) throws SQLException {
+    assertEquals(0, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1, rs.getInt(2));
+    assertEquals(1, rs.getInt("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getInt(3), "value 'some🌟' cannot be decoded as Integer");
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(get());
+  }
+
+  @Test
+  public void getLongPrepare() throws SQLException {
+    getLong(getPrepare(sharedConn));
+    getLong(getPrepare(sharedConnBinary));
+  }
+
+  public void getLong(ResultSet rs) throws SQLException {
+    assertEquals(0L, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1L, rs.getLong(2));
+    assertEquals(1L, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getLong(3),
+        "value 'some\uD83C\uDF1F' cannot be decoded as Long");
+    assertFalse(rs.wasNull());
+    assertEquals(0L, rs.getLong(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(get());
+  }
+
+  @Test
+  public void getFloatPrepare() throws SQLException {
+    getFloat(getPrepare(sharedConn));
+    getFloat(getPrepare(sharedConnBinary));
+  }
+
+  public void getFloat(ResultSet rs) throws SQLException {
+    assertEquals(0F, rs.getFloat(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1F, rs.getFloat(2));
+    assertEquals(1F, rs.getFloat("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getFloat(3), "value 'some🌟' cannot be decoded as Float");
+    assertFalse(rs.wasNull());
+    assertEquals(0F, rs.getFloat(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(get());
+  }
+
+  @Test
+  public void getDoublePrepare() throws SQLException {
+    getDouble(getPrepare(sharedConn));
+    getDouble(getPrepare(sharedConnBinary));
+  }
+
+  public void getDouble(ResultSet rs) throws SQLException {
+    assertEquals(0D, rs.getDouble(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1D, rs.getDouble(2));
+    assertEquals(1D, rs.getDouble("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDouble(3),
+        "value 'some🌟' cannot be decoded as Double");
+    assertFalse(rs.wasNull());
+    assertEquals(0D, rs.getDouble(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(get());
+  }
+
+  @Test
+  public void getBigDecimalPrepare() throws SQLException {
+    getBigDecimal(getPrepare(sharedConn));
+    getBigDecimal(getPrepare(sharedConnBinary));
+  }
+
+  public void getBigDecimal(ResultSet rs) throws SQLException {
+    assertEquals(BigDecimal.ZERO, rs.getBigDecimal(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal(2));
+    assertEquals(BigDecimal.ONE, rs.getBigDecimal("t2alias"));
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBigDecimal(3),
+        "value 'some🌟' cannot be decoded as BigDecimal");
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(get());
+  }
+
+  @Test
+  public void getDatePrepare() throws SQLException {
+    getDate(getPrepare(sharedConn));
+    getDate(getPrepare(sharedConnBinary));
+  }
+
+  public void getDate(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDate(1),
+        "value '0' (VARSTRING) cannot be decoded as Date");
+    rs.next();
+    assertEquals("2011-01-01", rs.getDate(1).toString());
+    assertFalse(rs.wasNull());
+    assertEquals("2010-12-31", rs.getDate(2).toString());
+    assertFalse(rs.wasNull());
+    assertEquals("2010-12-31", rs.getDate("t2alias").toString());
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDate(3),
+        "value '23:54:51.840010' (VARSTRING) cannot be decoded as Date");
+    assertFalse(rs.wasNull());
+    assertNull(rs.getDate(4));
+    assertTrue(rs.wasNull());
+    rs.next();
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getDate(1),
+        "value 'aaaa-bb-cc' (VARSTRING) cannot be decoded as Date");
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(get());
+  }
+
+  @Test
+  public void getTimePrepare() throws SQLException {
+    getTime(getPrepare(sharedConn));
+    getTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getTime(ResultSet rs) throws SQLException {
+    rs.next();
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime(1),
+        "VARSTRING value '2011-01-01' cannot be decoded as Time");
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getTime(2),
+        "VARSTRING value '2010-12-31 23:59:59.152' cannot be decoded as Time");
+    assertEquals(Time.valueOf("23:54:51").getTime() + 840, rs.getTime(3).getTime());
+    assertEquals(Time.valueOf("23:54:51").getTime() + 840, rs.getTime("t3alias").getTime());
+    Calendar utc = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+    assertEquals(86091840, rs.getTime(3, utc).getTime());
+    assertEquals(86091840, rs.getTime("t3alias", utc).getTime());
+
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getLocalTime() throws SQLException {
+    getLocalTime(get());
+  }
+
+  @Test
+  public void getLocalTimePrepare() throws SQLException {
+    getLocalTime(getPrepare(sharedConn));
+    getLocalTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getLocalTime(ResultSet rs) throws SQLException {
+    rs.next();
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getObject(1, LocalTime.class),
+        "value '2011-01-01' (VARSTRING) cannot be decoded as LocalTime");
+    assertFalse(rs.wasNull());
+
+    assertEquals(LocalTime.parse("23:59:59.152"), rs.getObject(2, LocalTime.class));
+    assertEquals(LocalTime.parse("23:54:51.840010"), rs.getObject(3, LocalTime.class));
+    assertEquals(LocalTime.parse("23:54:51.840010"), rs.getObject("t3alias", LocalTime.class));
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getDuration() throws SQLException {
+    getDuration(get());
+  }
+
+  @Test
+  public void getDurationPrepare() throws SQLException {
+    getDuration(getPrepare(sharedConn));
+    getDuration(getPrepare(sharedConnBinary));
+  }
+
+  public void getDuration(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getObject(1, Duration.class),
+        "VARSTRING value '0' cannot be decoded as Time");
+    rs.next();
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getObject(1, Duration.class),
+        "VARSTRING value '2011-01-01' cannot be decoded as Time");
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getObject(2, Duration.class),
+        "VARSTRING value '2010-12-31 23:59:59.152' cannot be decoded as Time");
+    assertEquals(Duration.parse("PT23H54M51.84001S"), rs.getObject(3, Duration.class));
+    assertEquals(Duration.parse("PT23H54M51.84001S"), rs.getObject("t3alias", Duration.class));
+  }
+
+  @Test
+  public void getTimestamp() throws SQLException {
+    getTimestamp(get());
+  }
+
+  @Test
+  public void getTimestampPrepare() throws SQLException {
+    getTimestamp(getPrepare(sharedConn));
+    getTimestamp(getPrepare(sharedConnBinary));
+  }
+
+  public void getTimestamp(ResultSet rs) throws SQLException {
+    rs.next();
+
+    assertEquals(Timestamp.valueOf("2011-01-01 00:00:00").getTime(), rs.getTimestamp(1).getTime());
+    assertEquals(
+        Timestamp.valueOf("2011-01-01 00:00:00").getTime() + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp(1, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertEquals(
+        Timestamp.valueOf("2011-01-01 00:00:00").getTime(), rs.getTimestamp("t1alias").getTime());
+    assertEquals(
+        Timestamp.valueOf("2011-01-01 00:00:00").getTime() + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp("t1alias", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime() + 152, rs.getTimestamp(2).getTime());
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime()
+            + 152
+            + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp(2, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime() + 152,
+        rs.getTimestamp("t2alias").getTime());
+    assertEquals(
+        Timestamp.valueOf("2010-12-31 23:59:59").getTime()
+            + 152
+            + TimeZone.getDefault().getDSTSavings(),
+        rs.getTimestamp("t2alias", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getLocalDateTime() throws SQLException {
+    getLocalDateTime(get());
+  }
+
+  @Test
+  public void getLocalDateTimePrepare() throws SQLException {
+    getLocalDateTime(getPrepare(sharedConn));
+    getLocalDateTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getLocalDateTime(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getObject(1, LocalDateTime.class),
+        "value '0' (VARSTRING) cannot be decoded as LocalDateTime");
+    rs.next();
+    assertEquals(LocalDateTime.parse("2011-01-01T00:00:00"), rs.getObject(1, LocalDateTime.class));
+    assertFalse(rs.wasNull());
+    assertEquals(
+        LocalDateTime.parse("2010-12-31T23:59:59.152"), rs.getObject(2, LocalDateTime.class));
+    rs.next();
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getObject(1, LocalDateTime.class),
+        "value 'aaaa-bb-cc' (VARSTRING) cannot be decoded as LocalDateTime");
+    assertNull(rs.getObject(2, LocalDateTime.class));
+  }
+
+  @Test
+  public void getLocalDate() throws SQLException {
+    getLocalDate(get());
+  }
+
+  @Test
+  public void getLocalDatePrepare() throws SQLException {
+    getLocalDate(getPrepare(sharedConn));
+    getLocalDate(getPrepare(sharedConnBinary));
+  }
+
+  public void getLocalDate(ResultSet rs) throws SQLException {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getObject(1, LocalDate.class),
+        "value '0' (VARSTRING) cannot be decoded as Date");
+    rs.next();
+    assertEquals(LocalDate.parse("2011-01-01"), rs.getObject(1, LocalDate.class));
+    assertFalse(rs.wasNull());
+    assertEquals(LocalDate.parse("2010-12-31"), rs.getObject(2, LocalDate.class));
+    rs.next();
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getObject(1, LocalDate.class),
+        "value 'aaaa-bb-cc' (VARSTRING) cannot be decoded as Date");
+    assertNull(rs.getObject(2, LocalDate.class));
+  }
+
+  @Test
+  public void getAsciiStream() throws Exception {
+    getAsciiStream(get());
+  }
+
+  @Test
+  public void getAsciiStreamPrepare() throws Exception {
+    getAsciiStream(getPrepare(sharedConn));
+    getAsciiStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getAsciiStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getAsciiStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getAsciiStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getAsciiStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getAsciiStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getAsciiStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getUnicodeStream() throws Exception {
+    getUnicodeStream(get());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepare() throws Exception {
+    getUnicodeStream(getPrepare(sharedConn));
+    getUnicodeStream(getPrepare(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  public void getUnicodeStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getUnicodeStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getUnicodeStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getUnicodeStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)),
+        rs.getUnicodeStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getUnicodeStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBinaryStream() throws Exception {
+    getBinaryStream(get());
+  }
+
+  @Test
+  public void getBinaryStreamPrepare() throws Exception {
+    getBinaryStream(getPrepare(sharedConn));
+    getBinaryStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getBinaryStream(ResultSet rs) throws Exception {
+    assertStreamEquals(new ByteArrayInputStream("0".getBytes()), rs.getBinaryStream(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getBinaryStream(2));
+    assertStreamEquals(new ByteArrayInputStream("1".getBytes()), rs.getBinaryStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(
+        new ByteArrayInputStream("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getBinaryStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBinaryStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBytes() throws SQLException {
+    getBytes(get());
+  }
+
+  @Test
+  public void getBytesPrepare() throws SQLException {
+    getBytes(getPrepare(sharedConn));
+    getBytes(getPrepare(sharedConnBinary));
+  }
+
+  public void getBytes(ResultSet rs) throws SQLException {
+    assertArrayEquals("0".getBytes(), rs.getBytes(1));
+    assertFalse(rs.wasNull());
+    assertArrayEquals("1".getBytes(), rs.getBytes(2));
+    assertArrayEquals("1".getBytes(), rs.getBytes("t2alias"));
+    assertFalse(rs.wasNull());
+    assertArrayEquals("some🌟".getBytes(StandardCharsets.UTF_8), rs.getBytes(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBytes(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getCharacterStream() throws Exception {
+    getCharacterStream(get());
+  }
+
+  @Test
+  public void getCharacterStreamPrepare() throws Exception {
+    getCharacterStream(getPrepare(sharedConn));
+    getCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getCharacterStream(ResultSet rs) throws Exception {
+    assertReaderEquals(new StringReader("0"), rs.getCharacterStream(1));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("1"), rs.getCharacterStream(2));
+    assertReaderEquals(new StringReader("1"), rs.getCharacterStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("some🌟"), rs.getCharacterStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getCharacterStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNCharacterStream() throws Exception {
+    getNCharacterStream(get());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepare() throws Exception {
+    getNCharacterStream(getPrepare(sharedConn));
+    getNCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getNCharacterStream(ResultSet rs) throws Exception {
+    assertReaderEquals(new StringReader("0"), rs.getNCharacterStream(1));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("1"), rs.getNCharacterStream(2));
+    assertReaderEquals(new StringReader("1"), rs.getNCharacterStream("t2alias"));
+    assertFalse(rs.wasNull());
+    assertReaderEquals(new StringReader("some🌟"), rs.getNCharacterStream(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNCharacterStream(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getBlob() throws Exception {
+    getBlob(get());
+  }
+
+  @Test
+  public void getBlobPrepare() throws Exception {
+    getBlob(getPrepare(sharedConn));
+    getBlob(getPrepare(sharedConnBinary));
+  }
+
+  public void getBlob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getBlob(1),
+        "Data type VARSTRING (not binary) cannot be decoded as Blob");
+  }
+
+  @Test
+  public void getClob() throws Exception {
+    getClob(get());
+  }
+
+  @Test
+  public void getClobPrepare() throws Exception {
+    getClob(getPrepare(sharedConn));
+    getClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getClob(ResultSet rs) throws Exception {
+    assertStreamEquals(new MariaDbClob("0".getBytes()), rs.getClob(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getClob(2));
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getClob("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getClob(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getClob(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getNClob() throws Exception {
+    getNClob(get());
+  }
+
+  @Test
+  public void getNClobPrepare() throws Exception {
+    getNClob(getPrepare(sharedConn));
+    getNClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getNClob(ResultSet rs) throws Exception {
+    assertStreamEquals(new MariaDbClob("0".getBytes()), rs.getNClob(1));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getNClob(2));
+    assertStreamEquals(new MariaDbClob("1".getBytes()), rs.getNClob("t2alias"));
+    assertFalse(rs.wasNull());
+    assertStreamEquals(new MariaDbClob("some🌟".getBytes(StandardCharsets.UTF_8)), rs.getNClob(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNClob(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = get();
+    ResultSetMetaData meta = rs.getMetaData();
+    assertEquals("VARCHAR", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.lang.String", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.VARCHAR, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    if (!isXpand()) {
+      assertEquals(255, meta.getPrecision(1));
+      assertEquals(255, meta.getColumnDisplaySize(1));
+    }
+
+    try (ResultSet rs2 = sharedConn.getMetaData().getColumns(database, null, "StringCodec", "t1")) {
+      rs2.next();
+      assertEquals(String.class, rs2.getObject("TYPE_NAME").getClass());
+    }
+  }
+
+  @Test
+  public void sendParam() throws SQLException {
+    sendParam(sharedConn);
+    sendParam(sharedConnBinary);
+    try (Connection con = createCon()) {
+      java.sql.Statement stmt = con.createStatement();
+      stmt.execute("SET sql_mode = concat(@@sql_mode,',NO_BACKSLASH_ESCAPES')");
+      sendParam(con);
+    }
+  }
+
+  private void sendParam(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("TRUNCATE TABLE StringParamCodec");
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    try (PreparedStatement prep =
+        con.prepareStatement("INSERT INTO StringParamCodec(t1) VALUES (?)")) {
+      prep.setString(1, "e'\\n🌟'\\'1Ã");
+      prep.execute();
+      prep.setString(1, null);
+      prep.execute();
+      prep.setObject(1, "e🌟2");
+      prep.execute();
+      prep.setObject(1, null);
+      prep.execute();
+      prep.setObject(1, "e🌟3", Types.VARCHAR);
+      prep.execute();
+      prep.setObject(1, null, Types.VARCHAR);
+      prep.execute();
+      prep.setAsciiStream(1, new ByteArrayInputStream("e🌟3654".getBytes(StandardCharsets.UTF_8)));
+      prep.execute();
+      prep.setAsciiStream(
+          1, new ByteArrayInputStream("e🌟3654".getBytes(StandardCharsets.UTF_8)), 7);
+      prep.execute();
+      prep.setAsciiStream(
+          1, new ByteArrayInputStream("e🌟4654".getBytes(StandardCharsets.UTF_8)), 7L);
+      prep.execute();
+      prep.setAsciiStream(1, new ByteArrayInputStream("e🌟3654".getBytes(StandardCharsets.UTF_8)));
+      prep.execute();
+      prep.setAsciiStream(
+          1, new ByteArrayInputStream("e🌟3654".getBytes(StandardCharsets.UTF_8)), 7);
+      prep.execute();
+      prep.setAsciiStream(
+          1, new ByteArrayInputStream("e🌟4654".getBytes(StandardCharsets.UTF_8)), 7L);
+      prep.execute();
+      prep.setNString(1, "e🌟13");
+      prep.execute();
+      prep.setNString(1, "e🌟12");
+      prep.execute();
+    }
+
+    ResultSet rs =
+        con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)
+            .executeQuery("SELECT * FROM StringParamCodec");
+    assertTrue(rs.next());
+    assertEquals("e'\\n🌟'\\'1Ã", rs.getString(2));
+    rs.updateString("t1", "f🌟12");
+    rs.updateRow();
+    assertEquals("f🌟12", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    assertNull(rs.getURL(2));
+    rs.updateString(2, "f🌟125");
+    rs.updateRow();
+    assertEquals("f🌟125", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟2", rs.getString(2));
+    rs.updateString(2, null);
+    rs.updateRow();
+    assertNull(rs.getString(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    rs.updateObject(2, "f🌟125", JDBCType.VARCHAR, 5);
+    rs.updateRow();
+    assertEquals("f🌟12", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟3", rs.getString(2));
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟3654", rs.getString(2));
+    rs.updateAsciiStream(
+        "t1", new ByteArrayInputStream("f🌟3654".getBytes(StandardCharsets.UTF_8)));
+    rs.updateRow();
+    assertEquals("f🌟3654", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟36", rs.getString(2));
+    rs.updateAsciiStream(
+        "t1", new ByteArrayInputStream("f🌟3654".getBytes(StandardCharsets.UTF_8)), 7);
+    rs.updateRow();
+    assertEquals("f🌟36", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟46", rs.getString(2));
+    rs.updateAsciiStream(
+        "t1", new ByteArrayInputStream("f🌟4654".getBytes(StandardCharsets.UTF_8)), 7L);
+    rs.updateRow();
+    assertEquals("f🌟46", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟3654", rs.getString(2));
+    rs.updateAsciiStream(2, new ByteArrayInputStream("f🌟3654".getBytes(StandardCharsets.UTF_8)));
+    rs.updateRow();
+    assertEquals("f🌟3654", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟36", rs.getString(2));
+    rs.updateAsciiStream(
+        2, new ByteArrayInputStream("f🌟3654".getBytes(StandardCharsets.UTF_8)), 7);
+    rs.updateRow();
+    assertEquals("f🌟36", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟46", rs.getString(2));
+    rs.updateAsciiStream(
+        2, new ByteArrayInputStream("f🌟4654".getBytes(StandardCharsets.UTF_8)), 7L);
+    rs.updateRow();
+    assertEquals("f🌟46", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟13", rs.getString(2));
+    rs.updateNString(2, "f🌟14");
+    rs.updateRow();
+    assertEquals("f🌟14", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟12", rs.getString(2));
+    rs.updateNString("t1", "http://f🌟15");
+    rs.updateRow();
+    assertEquals("http://f🌟15", rs.getString(2));
+
+    rs = stmt.executeQuery("SELECT * FROM StringParamCodec");
+    assertTrue(rs.next());
+    assertEquals("f🌟12", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("f🌟125", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("f🌟12", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("e🌟3", rs.getString(2));
+    assertTrue(rs.next());
+    assertNull(rs.getString(2));
+    assertTrue(rs.next());
+    assertEquals("f🌟3654", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("f🌟36", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("f🌟46", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("f🌟3654", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("f🌟36", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("f🌟46", rs.getString(2));
+
+    assertTrue(rs.next());
+    assertEquals("f🌟14", rs.getString(2));
+    assertEquals("f🌟14", rs.getObject(2, (Class<String>) null));
+
+    assertTrue(rs.next());
+    assertEquals("http://f🌟15", rs.getObject(2, (Map<String, Class<?>>) null));
+    assertEquals("http://f🌟15", rs.getObject("t1", (Map<String, Class<?>>) null));
+    Map<String, Class<?>> empty = new HashMap<>();
+    assertEquals("http://f🌟15", rs.getObject("t1", empty));
+    assertEquals("http://f🌟15", rs.getURL(2).toString());
+    assertEquals("http://f🌟15", rs.getURL("t1").toString());
+    con.commit();
+  }
+
+  @Test
+  public void wrongUtf8String() throws SQLException {
+    final byte[] utf8WrongBytes =
+        new byte[] {(byte) 0xc2, (byte) 0f, (byte) 0xDB, (byte) 0, (byte) 0xE1, (byte) 0};
+    String st = new String(utf8WrongBytes);
+
+    wrongUtf8(sharedConn, st);
+    wrongUtf8(sharedConnBinary, st);
+  }
+
+  private void wrongUtf8(Connection con, String wrong) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("INSERT INTO StringCodecWrong VALUES ('" + wrong + "')");
+    ResultSet rs = stmt.executeQuery("SELECT * FROM StringCodecWrong");
+    rs.next();
+    assertEquals(wrong, rs.getString(1));
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/YearCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/YearCodecTest.java
new file mode 100644
index 0000000..c303f38
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/codec/YearCodecTest.java
@@ -0,0 +1,770 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.codec;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.*;
+import java.time.*;
+import java.util.Calendar;
+import java.util.TimeZone;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.integration.Common;
+
+public class YearCodecTest extends CommonCodecTest {
+  @AfterAll
+  public static void drop() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS YearCodec");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    drop();
+    Statement stmt = sharedConn.createStatement();
+    if (isMariaDBServer() && !isXpand()) {
+      stmt.execute("CREATE TABLE YearCodec (t1 YEAR(2), t2 YEAR(4), t3 YEAR(4), t4 YEAR(4))");
+      stmt.execute(
+          "INSERT INTO YearCodec VALUES ('2010', '1901', '2155', null), (80, '1901', '2155',"
+              + " null)");
+    } else {
+      stmt.execute("CREATE TABLE YearCodec (t1 YEAR(4), t2 YEAR(4), t3 YEAR(4), t4 YEAR(4))");
+      stmt.execute(
+          "INSERT INTO YearCodec VALUES ('2010', '1901', '2155', null), (1980, '1901', '2155',"
+              + " null)");
+    }
+    stmt.execute("FLUSH TABLES");
+  }
+
+  private ResultSet get() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    ResultSet rs =
+        stmt.executeQuery(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from YearCodec");
+    assertTrue(rs.next());
+    sharedConn.commit();
+    return rs;
+  }
+
+  private ResultSet getPrepare(Connection con) throws SQLException {
+    java.sql.Statement stmt = con.createStatement();
+    stmt.execute("START TRANSACTION"); // if MAXSCALE ensure using WRITER
+    PreparedStatement preparedStatement =
+        con.prepareStatement(
+            "select t1 as t1alias, t2 as t2alias, t3 as t3alias, t4 as t4alias from YearCodec"
+                + " WHERE 1 > ?");
+    preparedStatement.closeOnCompletion();
+    preparedStatement.setInt(1, 0);
+    ResultSet rs = preparedStatement.executeQuery();
+    assertTrue(rs.next());
+    con.commit();
+    return rs;
+  }
+
+  @Test
+  public void getObject() throws SQLException {
+    getObject(get());
+  }
+
+  @Test
+  public void getObjectPrepare() throws SQLException {
+    getObject(getPrepare(sharedConn));
+    // https://jira.mariadb.org/browse/XPT-269
+    if (!isXpand()) getObject(getPrepare(sharedConnBinary));
+  }
+
+  public void getObject(ResultSet rs) throws SQLException {
+    assertFalse(rs.wasNull());
+    assertEquals(Date.valueOf("2010-01-01").getTime(), ((Date) rs.getObject(1)).getTime());
+    assertFalse(rs.wasNull());
+    assertFalse(rs.wasNull());
+    assertEquals(Date.valueOf("1901-01-01").getTime(), ((Date) rs.getObject(2)).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(Date.valueOf("2155-01-01").getTime(), ((Date) rs.getObject(3)).getTime());
+    assertFalse(rs.wasNull());
+    assertNull(rs.getDate(4));
+    assertTrue(rs.wasNull());
+    rs.next();
+    assertEquals(Date.valueOf("1980-01-01").getTime(), ((Date) rs.getObject(1)).getTime());
+  }
+
+  @Test
+  public void getObjectType() throws Exception {
+    getObjectType(get());
+  }
+
+  @Test
+  public void getObjectTypePrepare() throws Exception {
+    getObjectType(getPrepare(sharedConn));
+    // https://jira.mariadb.org/browse/XPT-269
+    if (!isXpand()) getObjectType(getPrepare(sharedConnBinary));
+  }
+
+  public void getObjectType(ResultSet rs) throws Exception {
+    testObject(rs, Integer.class, isMariaDBServer() && !isXpand() ? 10 : 2010);
+    testObject(rs, String.class, isMariaDBServer() && !isXpand() ? "10" : "2010");
+    testObject(rs, Long.class, isMariaDBServer() && !isXpand() ? 10L : 2010L);
+    testObject(rs, Short.class, (short) (isMariaDBServer() && !isXpand() ? 10 : 2010));
+    testObject(
+        rs,
+        BigDecimal.class,
+        isMariaDBServer() && !isXpand() ? BigDecimal.TEN : BigDecimal.valueOf(2010));
+    testObject(
+        rs,
+        BigInteger.class,
+        isMariaDBServer() && !isXpand() ? BigInteger.TEN : BigInteger.valueOf(2010));
+    testObject(rs, Double.class, isMariaDBServer() && !isXpand() ? 10d : 2010d);
+    testObject(rs, Float.class, isMariaDBServer() && !isXpand() ? 10f : 2010f);
+    if (isMariaDBServer() && !isXpand()) {
+      testObject(rs, Byte.class, (byte) 0x0a);
+    } else {
+      testErrObject(rs, Byte.class);
+    }
+    testErrObject(rs, byte[].class);
+    testObject(rs, Boolean.class, true);
+    testErrObject(rs, Clob.class);
+    testErrObject(rs, NClob.class);
+    testErrObject(rs, InputStream.class);
+    testErrObject(rs, Reader.class);
+    testObject(rs, LocalDate.class, LocalDate.parse("2010-01-01"));
+    testObject(rs, LocalDateTime.class, LocalDateTime.parse("2010-01-01T00:00:00"));
+    testErrObject(rs, LocalTime.class);
+    testErrObject(rs, Time.class);
+    testObject(
+        rs,
+        BigInteger.class,
+        isMariaDBServer() && !isXpand() ? BigInteger.TEN : BigInteger.valueOf(2010));
+    testObject(rs, Timestamp.class, Timestamp.valueOf("2010-01-01 00:00:00"));
+    testObject(
+        rs,
+        ZonedDateTime.class,
+        LocalDateTime.parse("2010-01-01T00:00:00").atZone(ZoneId.systemDefault()));
+    testObject(rs, java.util.Date.class, Date.valueOf("2010-01-01"));
+    rs.next();
+
+    testObject(rs, Integer.class, isMariaDBServer() && !isXpand() ? 80 : 1980);
+    testObject(rs, String.class, isMariaDBServer() && !isXpand() ? "80" : "1980");
+    testObject(rs, Long.class, isMariaDBServer() && !isXpand() ? 80L : 1980L);
+    testObject(rs, Short.class, (short) (isMariaDBServer() && !isXpand() ? 80 : 1980));
+    testObject(
+        rs, BigDecimal.class, BigDecimal.valueOf(isMariaDBServer() && !isXpand() ? 80 : 1980));
+    testObject(
+        rs, BigInteger.class, BigInteger.valueOf(isMariaDBServer() && !isXpand() ? 80 : 1980));
+    testObject(rs, Double.class, isMariaDBServer() && !isXpand() ? 80d : 1980d);
+    testObject(rs, Float.class, isMariaDBServer() && !isXpand() ? 80f : 1980f);
+
+    if (isMariaDBServer() && !isXpand()) {
+      testObject(rs, Byte.class, (byte) 80);
+    } else {
+      testErrObject(rs, Byte.class);
+    }
+    testErrObject(rs, byte[].class);
+    testObject(rs, Boolean.class, true);
+    testErrObject(rs, Clob.class);
+    testErrObject(rs, NClob.class);
+    testErrObject(rs, InputStream.class);
+    testErrObject(rs, Reader.class);
+    testObject(rs, LocalDate.class, LocalDate.parse("1980-01-01"));
+    testObject(rs, LocalDateTime.class, LocalDateTime.parse("1980-01-01T00:00:00"));
+    testErrObject(rs, LocalTime.class);
+    testErrObject(rs, Time.class);
+    testObject(
+        rs,
+        BigInteger.class,
+        isMariaDBServer() && !isXpand() ? BigInteger.valueOf(80) : BigInteger.valueOf(1980));
+    testObject(rs, Timestamp.class, Timestamp.valueOf("1980-01-01 00:00:00"));
+    testObject(
+        rs,
+        ZonedDateTime.class,
+        LocalDateTime.parse("1980-01-01T00:00:00").atZone(ZoneId.systemDefault()));
+    testObject(rs, java.util.Date.class, Date.valueOf("1980-01-01"));
+  }
+
+  @Test
+  public void getString() throws SQLException {
+    getString(get());
+  }
+
+  @Test
+  public void getStringPrepare() throws SQLException {
+    getString(getPrepare(sharedConn));
+    // https://jira.mariadb.org/browse/XPT-269
+    if (!isXpand()) getString(getPrepare(sharedConnBinary));
+  }
+
+  public void getString(ResultSet rs) throws SQLException {
+    assertEquals(isMariaDBServer() && !isXpand() ? "10" : "2010", rs.getString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1901", rs.getString(2));
+    assertEquals("1901", rs.getString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("2155", rs.getString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getString(4));
+    assertTrue(rs.wasNull());
+    rs.next();
+    assertEquals(isMariaDBServer() && !isXpand() ? "80" : "1980", rs.getString(1));
+  }
+
+  @Test
+  public void getNString() throws SQLException {
+    getNString(get());
+  }
+
+  @Test
+  public void getNStringPrepare() throws SQLException {
+    getNString(getPrepare(sharedConn));
+    // https://jira.mariadb.org/browse/XPT-269
+    if (!isXpand()) getNString(getPrepare(sharedConnBinary));
+  }
+
+  public void getNString(ResultSet rs) throws SQLException {
+    assertEquals(isMariaDBServer() && !isXpand() ? "10" : "2010", rs.getNString(1));
+    assertFalse(rs.wasNull());
+    assertEquals("1901", rs.getNString(2));
+    assertEquals("1901", rs.getNString("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals("2155", rs.getNString(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getNString(4));
+    assertTrue(rs.wasNull());
+    rs.next();
+    assertEquals(isMariaDBServer() && !isXpand() ? "80" : "1980", rs.getString(1));
+  }
+
+  @Test
+  public void getBoolean() throws SQLException {
+    getBoolean(get());
+  }
+
+  @Test
+  public void getBooleanPrepare() throws SQLException {
+    getBoolean(getPrepare(sharedConn));
+    // https://jira.mariadb.org/browse/XPT-269
+    if (!isXpand()) getBoolean(getPrepare(sharedConnBinary));
+  }
+
+  public void getBoolean(ResultSet rs) throws SQLException {
+    assertEquals(true, rs.getBoolean(1));
+    assertFalse(rs.wasNull());
+    assertEquals(true, rs.getBoolean(2));
+    assertEquals(true, rs.getBoolean("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(true, rs.getBoolean(3));
+    assertFalse(rs.wasNull());
+    assertEquals(false, rs.getBoolean(4));
+    assertTrue(rs.wasNull());
+  }
+
+  @Test
+  public void getByte() throws SQLException {
+    getByte(get());
+  }
+
+  @Test
+  public void getBytePrepare() throws SQLException {
+    getByte(getPrepare(sharedConn));
+    // https://jira.mariadb.org/browse/XPT-269
+    if (!isXpand()) getByte(getPrepare(sharedConnBinary));
+  }
+
+  public void getByte(ResultSet rs) throws SQLException {
+    if (isMariaDBServer() && !isXpand()) {
+      assertEquals(Byte.valueOf("10"), rs.getByte(1));
+    } else {
+      Common.assertThrowsContains(SQLDataException.class, () -> rs.getByte(1), "byte overflow");
+    }
+    assertFalse(rs.wasNull());
+    Common.assertThrowsContains(SQLDataException.class, () -> rs.getByte(2), "byte overflow");
+    Common.assertThrowsContains(
+        SQLDataException.class, () -> rs.getByte("t2alias"), "byte overflow");
+    assertFalse(rs.wasNull());
+  }
+
+  @Test
+  public void getShort() throws SQLException {
+    getShort(get());
+  }
+
+  @Test
+  public void getShortPrepare() throws SQLException {
+    getShort(getPrepare(sharedConn));
+
+    // https://jira.mariadb.org/browse/XPT-269
+    if (!isXpand()) getShort(getPrepare(sharedConnBinary));
+  }
+
+  public void getShort(ResultSet rs) throws SQLException {
+    assertEquals(isMariaDBServer() && !isXpand() ? 10 : 2010, rs.getShort(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1901, rs.getShort(2));
+    assertEquals(1901, rs.getShort("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(2155, rs.getShort(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getShort(4));
+    assertTrue(rs.wasNull());
+    rs.next();
+    assertEquals(isMariaDBServer() && !isXpand() ? 80 : 1980, rs.getShort(1));
+  }
+
+  @Test
+  public void getInt() throws SQLException {
+    getInt(get());
+  }
+
+  @Test
+  public void getIntPrepare() throws SQLException {
+    getInt(getPrepare(sharedConn));
+    // https://jira.mariadb.org/browse/XPT-269
+    if (!isXpand()) getInt(getPrepare(sharedConnBinary));
+  }
+
+  public void getInt(ResultSet rs) throws SQLException {
+    assertEquals(isMariaDBServer() && !isXpand() ? 10 : 2010, rs.getInt(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1901, rs.getInt(2));
+    assertEquals(1901, rs.getInt("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(2155, rs.getInt(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getInt(4));
+    assertTrue(rs.wasNull());
+    rs.next();
+    assertEquals(isMariaDBServer() && !isXpand() ? 80 : 1980, rs.getInt(1));
+  }
+
+  @Test
+  public void getLong() throws SQLException {
+    getLong(get());
+  }
+
+  @Test
+  public void getLongPrepare() throws SQLException {
+    getLong(getPrepare(sharedConn));
+    // https://jira.mariadb.org/browse/XPT-269
+    if (!isXpand()) getLong(getPrepare(sharedConnBinary));
+  }
+
+  public void getLong(ResultSet rs) throws SQLException {
+    assertEquals(isMariaDBServer() && !isXpand() ? 10 : 2010, rs.getLong(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1901, rs.getLong(2));
+    assertEquals(1901, rs.getLong("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(2155, rs.getLong(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0, rs.getLong(4));
+    assertTrue(rs.wasNull());
+    rs.next();
+    assertEquals(isMariaDBServer() && !isXpand() ? 80 : 1980, rs.getLong(1));
+  }
+
+  @Test
+  public void getFloat() throws SQLException {
+    getFloat(get());
+  }
+
+  @Test
+  public void getFloatPrepare() throws SQLException {
+    getFloat(getPrepare(sharedConn));
+    // https://jira.mariadb.org/browse/XPT-269
+    if (!isXpand()) getFloat(getPrepare(sharedConnBinary));
+  }
+
+  public void getFloat(ResultSet rs) throws SQLException {
+    assertEquals(isMariaDBServer() && !isXpand() ? 10F : 2010f, rs.getFloat(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1901F, rs.getFloat(2));
+    assertEquals(1901F, rs.getFloat("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(2155F, rs.getFloat(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0F, rs.getFloat(4));
+    assertTrue(rs.wasNull());
+    rs.next();
+    assertEquals(isMariaDBServer() && !isXpand() ? 80f : 1980f, rs.getFloat(1));
+  }
+
+  @Test
+  public void getDouble() throws SQLException {
+    getDouble(get());
+  }
+
+  @Test
+  public void getDoublePrepare() throws SQLException {
+    getDouble(getPrepare(sharedConn));
+    // https://jira.mariadb.org/browse/XPT-269
+    if (!isXpand()) getDouble(getPrepare(sharedConnBinary));
+  }
+
+  public void getDouble(ResultSet rs) throws SQLException {
+    assertEquals(isMariaDBServer() && !isXpand() ? 10d : 2010d, rs.getDouble(1));
+    assertFalse(rs.wasNull());
+    assertEquals(1901D, rs.getDouble(2));
+    assertEquals(1901D, rs.getDouble("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(2155D, rs.getDouble(3));
+    assertFalse(rs.wasNull());
+    assertEquals(0D, rs.getDouble(4));
+    assertTrue(rs.wasNull());
+    rs.next();
+    assertEquals(isMariaDBServer() && !isXpand() ? 80d : 1980d, rs.getDouble(1));
+  }
+
+  @Test
+  public void getBigDecimal() throws SQLException {
+    getBigDecimal(get());
+  }
+
+  @Test
+  public void getBigDecimalPrepare() throws SQLException {
+    getBigDecimal(getPrepare(sharedConn));
+    // https://jira.mariadb.org/browse/XPT-269
+    if (!isXpand()) getBigDecimal(getPrepare(sharedConnBinary));
+  }
+
+  public void getBigDecimal(ResultSet rs) throws SQLException {
+    assertEquals(
+        isMariaDBServer() && !isXpand() ? BigDecimal.TEN : BigDecimal.valueOf(2010),
+        rs.getBigDecimal(1));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.valueOf(1901), rs.getBigDecimal(2));
+    assertEquals(BigDecimal.valueOf(1901), rs.getBigDecimal("t2alias"));
+    assertFalse(rs.wasNull());
+    assertEquals(BigDecimal.valueOf(2155), rs.getBigDecimal(3));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getBigDecimal(4));
+    assertTrue(rs.wasNull());
+    rs.next();
+    assertEquals(
+        isMariaDBServer() && !isXpand() ? BigDecimal.valueOf(80) : BigDecimal.valueOf(1980),
+        rs.getBigDecimal(1));
+  }
+
+  @Test
+  public void getDate() throws SQLException {
+    getDate(get());
+  }
+
+  @Test
+  public void getDatePrepare() throws SQLException {
+    getDate(getPrepare(sharedConn));
+    // https://jira.mariadb.org/browse/XPT-269
+    if (!isXpand()) getDate(getPrepare(sharedConnBinary));
+  }
+
+  public void getDate(ResultSet rs) throws SQLException {
+    assertEquals(Date.valueOf("2010-01-01").getTime(), rs.getDate(1).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(Date.valueOf("1901-01-01").getTime(), rs.getDate(2).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(Date.valueOf("2155-01-01").getTime(), rs.getDate(3).getTime());
+    assertFalse(rs.wasNull());
+    assertNull(rs.getDate(4));
+    assertTrue(rs.wasNull());
+    rs.next();
+    assertEquals(Date.valueOf("1980-01-01").getTime(), rs.getDate(1).getTime());
+  }
+
+  @Test
+  public void getTime() throws SQLException {
+    getTime(get());
+  }
+
+  @Test
+  public void getTimePrepare() throws SQLException {
+    getTime(getPrepare(sharedConn));
+    getTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getTime(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getTime(1), "Data type YEAR cannot be decoded as Time");
+  }
+
+  @Test
+  public void getDuration() throws SQLException {
+    getDuration(get());
+  }
+
+  @Test
+  public void getDurationPrepare() throws SQLException {
+    getDuration(getPrepare(sharedConn));
+    getDuration(getPrepare(sharedConnBinary));
+  }
+
+  public void getDuration(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(1, Duration.class),
+        "Type class java.time.Duration not supported type for YEAR type");
+  }
+
+  @Test
+  public void getLocalTime() throws SQLException {
+    getLocalTime(get());
+  }
+
+  @Test
+  public void getLocalTimePrepare() throws SQLException {
+    getLocalTime(getPrepare(sharedConn));
+    getLocalTime(getPrepare(sharedConnBinary));
+  }
+
+  public void getLocalTime(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(1, LocalTime.class),
+        "Type class java.time.LocalTime not supported type for YEAR type");
+  }
+
+  @Test
+  public void getLocalDate() throws SQLException {
+    getLocalDate(get());
+  }
+
+  @Test
+  public void getLocalDatePrepare() throws SQLException {
+    getLocalDate(getPrepare(sharedConn));
+    // https://jira.mariadb.org/browse/XPT-269
+    if (!isXpand()) getLocalDate(getPrepare(sharedConnBinary));
+  }
+
+  public void getLocalDate(ResultSet rs) throws SQLException {
+    assertEquals(LocalDate.parse("2010-01-01"), rs.getObject(1, LocalDate.class));
+    assertFalse(rs.wasNull());
+    assertEquals(LocalDate.parse("1901-01-01"), rs.getObject(2, LocalDate.class));
+    assertEquals(LocalDate.parse("1901-01-01"), rs.getObject("t2alias", LocalDate.class));
+    assertFalse(rs.wasNull());
+    assertEquals(LocalDate.parse("2155-01-01"), rs.getObject(3, LocalDate.class));
+    assertFalse(rs.wasNull());
+    assertNull(rs.getObject(4, LocalDate.class));
+    assertTrue(rs.wasNull());
+    rs.next();
+    assertEquals(LocalDate.parse("1980-01-01"), rs.getObject(1, LocalDate.class));
+  }
+
+  @Test
+  public void getTimestamp() throws SQLException {
+    getTimestamp(get());
+  }
+
+  @Test
+  public void getTimestampPrepare() throws SQLException {
+    getTimestamp(getPrepare(sharedConn));
+    // https://jira.mariadb.org/browse/XPT-269
+    if (!isXpand()) getTimestamp(getPrepare(sharedConnBinary));
+  }
+
+  public void getTimestamp(ResultSet rs) throws SQLException {
+    assertEquals(Timestamp.valueOf("2010-01-01 00:00:00").getTime(), rs.getTimestamp(1).getTime());
+    assertEquals(
+        1262304000000L,
+        rs.getTimestamp(1, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertEquals(
+        Timestamp.valueOf("2010-01-01 00:00:00").getTime(), rs.getTimestamp("t1alias").getTime());
+    assertEquals(
+        1262304000000L,
+        rs.getTimestamp("t1alias", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(Timestamp.valueOf("1901-01-01 00:00:00").getTime(), rs.getTimestamp(2).getTime());
+    assertEquals(
+        -2177452800000L,
+        rs.getTimestamp(2, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+
+    assertEquals(Timestamp.valueOf("2155-01-01 00:00:00").getTime(), rs.getTimestamp(3).getTime());
+    assertEquals(
+        5838048000000L,
+        rs.getTimestamp(3, Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime());
+    assertFalse(rs.wasNull());
+    assertEquals(Timestamp.valueOf("2155-01-01 00:00:00"), rs.getTimestamp(3));
+    assertNull(rs.getTimestamp(4));
+    assertTrue(rs.wasNull());
+    rs.next();
+    assertEquals(Timestamp.valueOf("1980-01-01 00:00:00").getTime(), rs.getTimestamp(1).getTime());
+  }
+
+  @Test
+  public void getAsciiStream() throws SQLException {
+    getAsciiStream(get());
+  }
+
+  @Test
+  public void getAsciiStreamPrepare() throws SQLException {
+    getAsciiStream(getPrepare(sharedConn));
+    getAsciiStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getAsciiStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getAsciiStream(1),
+        "Data type YEAR cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getUnicodeStream() throws SQLException {
+    getUnicodeStream(get());
+  }
+
+  @Test
+  public void getUnicodeStreamPrepare() throws SQLException {
+    getUnicodeStream(getPrepare(sharedConn));
+    getUnicodeStream(getPrepare(sharedConnBinary));
+  }
+
+  @SuppressWarnings("deprecation")
+  public void getUnicodeStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getUnicodeStream(1),
+        "Data type YEAR cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBinaryStream() throws SQLException {
+    getBinaryStream(get());
+  }
+
+  @Test
+  public void getBinaryStreamPrepare() throws SQLException {
+    getBinaryStream(getPrepare(sharedConn));
+    getBinaryStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getBinaryStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getBinaryStream(1),
+        "Data type YEAR cannot be decoded as Stream");
+  }
+
+  @Test
+  public void getBytes() throws SQLException {
+    getBytes(get());
+  }
+
+  @Test
+  public void getBytesPrepare() throws SQLException {
+    getBytes(getPrepare(sharedConn));
+    getBytes(getPrepare(sharedConnBinary));
+  }
+
+  public void getBytes(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getBytes(1), "Data type YEAR cannot be decoded as byte[]");
+  }
+
+  @Test
+  public void getCharacterStream() throws SQLException {
+    getCharacterStream(get());
+  }
+
+  @Test
+  public void getCharacterStreamPrepare() throws SQLException {
+    getCharacterStream(getPrepare(sharedConn));
+    getCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getCharacterStream(1),
+        "Data type YEAR cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getNCharacterStream() throws SQLException {
+    getNCharacterStream(get());
+  }
+
+  @Test
+  public void getNCharacterStreamPrepare() throws SQLException {
+    getNCharacterStream(getPrepare(sharedConn));
+    getNCharacterStream(getPrepare(sharedConnBinary));
+  }
+
+  public void getNCharacterStream(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLDataException.class,
+        () -> rs.getNCharacterStream(1),
+        "Data type YEAR cannot be decoded as Reader");
+  }
+
+  @Test
+  public void getBlob() throws SQLException {
+    getBlob(get());
+  }
+
+  @Test
+  public void getBlobPrepare() throws SQLException {
+    getBlob(getPrepare(sharedConn));
+    getBlob(getPrepare(sharedConnBinary));
+  }
+
+  public void getBlob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getBlob(1), "Data type YEAR cannot be decoded as Blob");
+  }
+
+  @Test
+  public void getClob() throws SQLException {
+    getClob(get());
+  }
+
+  @Test
+  public void getClobPrepare() throws SQLException {
+    getClob(getPrepare(sharedConn));
+    getClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getClob(1), "Data type YEAR cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getNClob() throws SQLException {
+    getNClob(get());
+  }
+
+  @Test
+  public void getNClobPrepare() throws SQLException {
+    getNClob(getPrepare(sharedConn));
+    getNClob(getPrepare(sharedConnBinary));
+  }
+
+  public void getNClob(ResultSet rs) {
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.getNClob(1), "Data type YEAR cannot be decoded as Clob");
+  }
+
+  @Test
+  public void getMetaData() throws SQLException {
+    ResultSet rs = get();
+    ResultSetMetaData meta = rs.getMetaData();
+    assertEquals("YEAR", meta.getColumnTypeName(1));
+    assertEquals(sharedConn.getCatalog(), meta.getCatalogName(1));
+    assertEquals("java.sql.Date", meta.getColumnClassName(1));
+    assertEquals("t1alias", meta.getColumnLabel(1));
+    assertEquals("t1", meta.getColumnName(1));
+    assertEquals(Types.DATE, meta.getColumnType(1));
+    assertEquals(4, meta.getColumnCount());
+    assertEquals(isMariaDBServer() && !isXpand() ? 2 : 4, meta.getPrecision(1));
+    assertEquals(0, meta.getScale(1));
+    assertEquals("", meta.getSchemaName(1));
+    assertEquals(isMariaDBServer() && !isXpand() ? 2 : 4, meta.getColumnDisplaySize(1));
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/resultset/ReadResultSetTest.java b/src/test/java/org/mariadb/jdbc/integration/resultset/ReadResultSetTest.java
new file mode 100644
index 0000000..511b65b
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/resultset/ReadResultSetTest.java
@@ -0,0 +1,248 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.resultset;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.sql.*;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.integration.Common;
+
+public class ReadResultSetTest extends Common {
+
+  private final String NOT_SUPPORTED = "Not supported when using CONCUR_READ_ONLY concurrency";
+  private final Class<? extends java.lang.Exception> ns = SQLFeatureNotSupportedException.class;
+
+  @AfterAll
+  public static void after2() throws SQLException {
+    sharedConn.createStatement().execute("DROP TABLE ResultSetTest");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS ResultSetTest");
+    stmt.execute("CREATE TABLE ResultSetTest (t1 int not null primary key auto_increment, t2 int)");
+    stmt.execute("INSERT INTO ResultSetTest(t2) values (1),(2),(3),(4),(5),(6),(7),(8)");
+  }
+
+  @Test
+  public void nonUpdatableFields() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    Assertions.assertNull(rs.getWarnings());
+    rs.next();
+    Common.assertThrowsContains(ns, () -> rs.updateArray(1, null), "Array are not supported");
+    Common.assertThrowsContains(ns, () -> rs.updateNull(1), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBoolean(1, true), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateByte(1, (byte) 0x00), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateShort(1, (short) 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateInt(1, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateLong(1, 0L), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateFloat(1, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateDouble(1, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBigDecimal(1, BigDecimal.ZERO), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateString(1, ""), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNString(1, ""), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBytes(1, new byte[0]), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateDate(1, null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateTime(1, null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateTimestamp(1, null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateAsciiStream(1, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateAsciiStream(1, null, 0L), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBinaryStream(1, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateCharacterStream(1, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBinaryStream(1, null, 0L), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateCharacterStream(1, null, 0L), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateObject(1, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateObject(1, null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBlob(1, (Blob) null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateClob(1, (Clob) null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBlob(1, (InputStream) null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateClob(1, (Reader) null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNClob(1, (NClob) null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNClob(1, (Reader) null), NOT_SUPPORTED);
+
+    Common.assertThrowsContains(ns, () -> rs.updateArray("t1", null), "Array are not supported");
+
+    Common.assertThrowsContains(ns, () -> rs.updateNull("t1"), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBoolean("t1", true), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateByte("t1", (byte) 0x00), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateShort("t1", (short) 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateInt("t1", 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateLong("t1", 0L), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateFloat("t1", 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateDouble("t1", 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(
+        ns, () -> rs.updateBigDecimal("t1", BigDecimal.ZERO), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateString("t1", ""), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNString("t1", ""), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBytes("t1", new byte[0]), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateDate("t1", null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateTime("t1", null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateTimestamp("t1", null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateAsciiStream("t1", null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBinaryStream("t1", null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateCharacterStream("t1", null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateAsciiStream("t1", null, 0L), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBinaryStream("t1", null, 0L), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateCharacterStream("t1", null, 0L), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateObject("t1", null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateObject("t1", null), NOT_SUPPORTED);
+
+    Common.assertThrowsContains(ns, () -> rs.updateBlob("t1", (Blob) null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBlob("t1", (InputStream) null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateClob("t1", (Clob) null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateClob("t1", (Reader) null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNClob("t1", (NClob) null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNClob("t1", (Reader) null), NOT_SUPPORTED);
+
+    Common.assertThrowsContains(ns, rs::insertRow, NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, rs::updateRow, NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, rs::deleteRow, NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, rs::refreshRow, NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, rs::cancelRowUpdates, NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, rs::moveToInsertRow, NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, rs::moveToCurrentRow, NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, rs::rowUpdated, NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, rs::rowInserted, NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, rs::rowDeleted, NOT_SUPPORTED);
+
+    Common.assertThrowsContains(
+        ns, () -> rs.updateRef(1, null), "Method ResultSet.updateRef not supported");
+    Common.assertThrowsContains(
+        ns, () -> rs.updateRef("t1", null), "Method ResultSet.updateRef not supported");
+    Common.assertThrowsContains(ns, () -> rs.updateArray(1, null), "Array are not supported");
+    Common.assertThrowsContains(ns, () -> rs.updateArray("t1", null), "Array are not supported");
+    Common.assertThrowsContains(ns, () -> rs.updateRowId(1, null), "RowId are not supported");
+    Common.assertThrowsContains(ns, () -> rs.updateRowId("t1", null), "RowId are not supported");
+    Common.assertThrowsContains(ns, () -> rs.updateSQLXML(1, null), "SQLXML not supported");
+    Common.assertThrowsContains(ns, () -> rs.updateSQLXML("t1", null), "SQLXML not supported");
+
+    Common.assertThrowsContains(ns, () -> rs.updateNCharacterStream(1, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNCharacterStream("t1", null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateAsciiStream(1, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateAsciiStream("t1", null, 0), NOT_SUPPORTED);
+
+    Common.assertThrowsContains(ns, () -> rs.updateBinaryStream(1, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBinaryStream("t1", null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBlob(1, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBlob("t1", null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateClob(1, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateClob("t1", null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNClob(1, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNClob("t1", null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateCharacterStream(1, null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateCharacterStream("t1", null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNCharacterStream(1, null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateNCharacterStream("t1", null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateAsciiStream(1, null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateAsciiStream("t1", null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBinaryStream(1, null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateBinaryStream("t1", null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateObject(1, null, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateObject("t1", null, null, 0), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateObject(1, null, null), NOT_SUPPORTED);
+    Common.assertThrowsContains(ns, () -> rs.updateObject("t1", null, null), NOT_SUPPORTED);
+  }
+
+  @Test
+  public void notSupported() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    Common.assertThrowsContains(ns, () -> rs.getRowId(1), "RowId are not supported");
+    Common.assertThrowsContains(ns, () -> rs.getRowId("t1"), "RowId are not supported");
+    Map<String, Class<?>> map = new HashMap<>();
+    map.put("ddd", Integer.class);
+    Common.assertThrowsContains(
+        ns,
+        () -> rs.getObject(1, map),
+        "Method ResultSet.getObject(int columnIndex, Map<String, Class<?>> map) not supported");
+    Common.assertThrowsContains(
+        ns,
+        () -> rs.getObject("t1", map),
+        "Method ResultSet.getObject(String columnLabel, Map<String, Class<?>> map) not supported");
+    Common.assertThrowsContains(ns, rs::getCursorName, "Cursors are not supported");
+  }
+
+  @Test
+  public void staticMethod() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    Assertions.assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, rs.getHoldability());
+    rs.unwrap(ResultSet.class);
+
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.unwrap(String.class),
+        "The receiver is not a wrapper for java.lang.String");
+
+    assertEquals(ResultSet.FETCH_UNKNOWN, rs.getFetchDirection());
+    rs.setFetchDirection(ResultSet.FETCH_UNKNOWN);
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.setFetchDirection(ResultSet.FETCH_REVERSE),
+        "Invalid operation");
+    assertEquals(ResultSet.CONCUR_READ_ONLY, rs.getConcurrency());
+  }
+
+  @Test
+  public void type() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertEquals(ResultSet.TYPE_FORWARD_ONLY, rs.getType());
+    assertEquals(stmt, rs.getStatement());
+
+    stmt = sharedConn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
+    rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertEquals(ResultSet.TYPE_SCROLL_SENSITIVE, rs.getType());
+    assertEquals(stmt, rs.getStatement());
+  }
+
+  @Test
+  public void wrongIndex() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    rs.next();
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(-1),
+        "Wrong index position. Is -1 but must be in 1-2 range");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.getObject(10),
+        "Wrong index position. Is 10 but must be in 1-2 range");
+    Common.assertThrowsContains(
+        SQLException.class, () -> rs.findColumn(null), "null is not a valid label value");
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rs.findColumn("yy"),
+        "Unknown label 'yy'. Possible value [resultsettest.t1, t1, resultsettest.t2, t2]");
+  }
+
+  @Test
+  public void isBeforeFirstFetchTest() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.setFetchSize(1);
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertTrue(rs.isBeforeFirst());
+    while (rs.next()) {
+      assertFalse(rs.isBeforeFirst());
+    }
+    assertFalse(rs.isBeforeFirst());
+    rs.close();
+    Common.assertThrowsContains(
+        SQLException.class, rs::isBeforeFirst, "Operation not permit on a closed resultSet");
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/resultset/ResultSetMetadataTest.java b/src/test/java/org/mariadb/jdbc/integration/resultset/ResultSetMetadataTest.java
new file mode 100644
index 0000000..cee1a77
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/resultset/ResultSetMetadataTest.java
@@ -0,0 +1,244 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.resultset;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.integration.Common;
+
+public class ResultSetMetadataTest extends Common {
+
+  private static final Class<? extends java.lang.Exception> sqle = SQLException.class;
+
+  @AfterAll
+  public static void dropAll() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS ResultSetTest");
+    stmt.execute("DROP TABLE IF EXISTS test_rsmd");
+    stmt.execute("DROP TABLE IF EXISTS resultsetmetadatatest1");
+    stmt.execute("DROP TABLE IF EXISTS resultsetmetadatatest2");
+    stmt.execute("DROP TABLE IF EXISTS resultsetmetadatatest3");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    dropAll();
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("CREATE TABLE ResultSetTest (t1 int not null primary key auto_increment, t2 int)");
+    stmt.execute("INSERT INTO ResultSetTest(t2) values (1),(2),(3),(4),(5),(6),(7),(8)");
+    stmt.execute(
+        "CREATE TABLE test_rsmd(id_col int not null primary key auto_increment, nullable_col"
+            + " varchar(20), unikey_col int unique, char_col char(10), us smallint unsigned)");
+    stmt.execute("CREATE TABLE resultsetmetadatatest1(id int, name varchar(20))");
+    stmt.execute("CREATE TABLE resultsetmetadatatest2(id int, name varchar(20))");
+    stmt.execute("CREATE TABLE resultsetmetadatatest3(id int, name varchar(20))");
+  }
+
+  @Test
+  public void metaDataTest() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("insert into test_rsmd (id_col,nullable_col,unikey_col) values (null, 'hej', 9)");
+    ResultSet rs =
+        stmt.executeQuery(
+            "select id_col, nullable_col, unikey_col as something, char_col,us from test_rsmd");
+    assertTrue(rs.next());
+    ResultSetMetaData rsmd = rs.getMetaData();
+    if (!isXpand()) {
+      assertTrue(rsmd.isAutoIncrement(1));
+      assertFalse(rsmd.isAutoIncrement(2));
+    }
+    assertEquals(5, rsmd.getColumnCount());
+    assertEquals(ResultSetMetaData.columnNullable, rsmd.isNullable(2));
+    assertEquals(ResultSetMetaData.columnNoNulls, rsmd.isNullable(1));
+    assertEquals(String.class.getName(), rsmd.getColumnClassName(2));
+    assertEquals(Integer.class.getName(), rsmd.getColumnClassName(1));
+    assertEquals(Integer.class.getName(), rsmd.getColumnClassName(3));
+    assertEquals("id_col", rsmd.getColumnLabel(1));
+    assertEquals("nullable_col", rsmd.getColumnLabel(2));
+    assertEquals("something", rsmd.getColumnLabel(3));
+    assertEquals("unikey_col", rsmd.getColumnName(3));
+    if (!isXpand()) {
+      assertEquals(Types.CHAR, rsmd.getColumnType(4));
+    }
+    assertEquals(Types.INTEGER, rsmd.getColumnType(5));
+    assertFalse(rsmd.isReadOnly(1));
+    assertFalse(rsmd.isReadOnly(2));
+    assertTrue(rsmd.isWritable(1));
+    assertTrue(rsmd.isDefinitelyWritable(1));
+    assertTrue(rsmd.isCaseSensitive(1));
+    assertTrue(rsmd.isSearchable(1));
+    assertFalse(rsmd.isCurrency(1));
+    assertTrue(rsmd.isSigned(3));
+    assertFalse(rsmd.isSigned(5));
+
+    Common.assertThrowsContains(sqle, () -> rsmd.isAutoIncrement(6), "wrong column index 6");
+    Common.assertThrowsContains(sqle, () -> rsmd.isReadOnly(6), "wrong column index 6");
+    Common.assertThrowsContains(sqle, () -> rsmd.isReadOnly(-6), "wrong column index -6");
+    Common.assertThrowsContains(sqle, () -> rsmd.isWritable(6), "wrong column index 6");
+    Common.assertThrowsContains(sqle, () -> rsmd.isDefinitelyWritable(6), "wrong column index 6");
+
+    DatabaseMetaData md = sharedConn.getMetaData();
+    ResultSet cols = md.getColumns(null, null, "test\\_rsmd", null);
+    cols.next();
+    assertEquals("id_col", cols.getString("COLUMN_NAME"));
+    assertEquals(Types.INTEGER, cols.getInt("DATA_TYPE"));
+    cols.next(); /* nullable_col */
+    cols.next(); /* unikey_col */
+    cols.next(); /* char_col */
+    assertEquals("char_col", cols.getString("COLUMN_NAME"));
+    assertEquals(Types.CHAR, cols.getInt("DATA_TYPE"));
+    cols.next(); /* us */ // CONJ-96: SMALLINT UNSIGNED gives Types.SMALLINT
+    assertEquals("us", cols.getString("COLUMN_NAME"));
+    assertEquals(Types.SMALLINT, cols.getInt("DATA_TYPE"));
+
+    rs = stmt.executeQuery("select 1 from test_rsmd");
+    ResultSetMetaData rsmd2 = rs.getMetaData();
+    assertTrue(rsmd2.isReadOnly(1));
+    assertFalse(rsmd2.isWritable(1));
+    assertFalse(rsmd2.isDefinitelyWritable(1));
+  }
+
+  @Test
+  public void conj17() throws Exception {
+    ResultSet rs =
+        sharedConn
+            .createStatement()
+            .executeQuery("select count(*),1 from information_schema.tables");
+    assertTrue(rs.next());
+    assertEquals(rs.getMetaData().getColumnName(1), "count(*)");
+    assertEquals(rs.getMetaData().getColumnName(2), "1");
+  }
+
+  @Test
+  public void conj84() throws Exception {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("INSERT INTO resultsetmetadatatest1 VALUES (1, 'foo')");
+    stmt.execute("INSERT INTO resultsetmetadatatest2 VALUES (2, 'bar')");
+    ResultSet rs =
+        sharedConn
+            .createStatement()
+            .executeQuery(
+                "select resultsetmetadatatest1.*, resultsetmetadatatest2.* FROM"
+                    + " resultsetmetadatatest1 join resultsetmetadatatest2");
+    assertTrue(rs.next());
+    assertEquals(rs.findColumn("id"), 1);
+    assertEquals(rs.findColumn("name"), 2);
+    assertEquals(rs.findColumn("resultsetmetadatatest1.id"), 1);
+    assertEquals(rs.findColumn("resultsetmetadatatest1.name"), 2);
+    assertEquals(rs.findColumn("resultsetmetadatatest2.id"), 3);
+    assertEquals(rs.findColumn("resultsetmetadatatest2.name"), 4);
+  }
+
+  @Test
+  public void testAlias() throws Exception {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS testAlias");
+    stmt.execute("DROP TABLE IF EXISTS testAlias2");
+    stmt.execute("CREATE TABLE testAlias(id int, name varchar(20))");
+    stmt.execute("CREATE TABLE testAlias2(id2 int, name2 varchar(20))");
+
+    stmt.execute("INSERT INTO testAlias VALUES (1, 'foo')");
+    stmt.execute("INSERT INTO testAlias2 VALUES (2, 'bar')");
+    ResultSet rs =
+        sharedConn
+            .createStatement()
+            .executeQuery(
+                "select alias1.id as idalias1, "
+                    + "alias1.name as namealias1, "
+                    + "id2 as idalias2, "
+                    + "name2, "
+                    + "testAlias.id,"
+                    + "alias1.id "
+                    + "FROM testAlias as alias1 "
+                    + "join testAlias2 as alias2 "
+                    + "join testAlias");
+    assertTrue(rs.next());
+
+    assertEquals(rs.findColumn("idalias1"), 1);
+    assertEquals(rs.findColumn("alias1.idalias1"), 1);
+
+    Common.assertThrowsContains(sqle, () -> rs.findColumn("name"), "Unknown label 'name'");
+    assertEquals(rs.findColumn("namealias1"), 2);
+    assertEquals(rs.findColumn("alias1.namealias1"), 2);
+
+    Common.assertThrowsContains(sqle, () -> rs.findColumn("id2"), "Unknown label 'id2'");
+    assertEquals(rs.findColumn("idalias2"), 3);
+    assertEquals(rs.findColumn("alias2.idalias2"), 3);
+    Common.assertThrowsContains(
+        sqle, () -> rs.findColumn("testAlias2.id2"), "Unknown label 'testAlias2.id2'");
+
+    assertEquals(rs.findColumn("name2"), 4);
+    Common.assertThrowsContains(
+        sqle, () -> rs.findColumn("testAlias2.name2"), "Unknown label 'testAlias2.name2'");
+    assertEquals(rs.findColumn("alias2.name2"), 4);
+
+    assertEquals(rs.findColumn("id"), 5);
+    assertEquals(rs.findColumn("testAlias.id"), 5);
+    assertEquals(rs.findColumn("alias1.id"), 6);
+
+    Common.assertThrowsContains(
+        sqle, () -> rs.findColumn("alias2.name22"), "Unknown label 'alias2.name22'");
+    Common.assertThrowsContains(sqle, () -> rs.findColumn(""), "Unknown label ''");
+    Common.assertThrowsContains(sqle, () -> rs.findColumn(null), "null is not a valid label value");
+  }
+
+  @Test
+  public void blankTableNameMeta() throws Exception {
+    ResultSet rs =
+        sharedConn
+            .createStatement()
+            .executeQuery(
+                "SELECT id AS id_alias FROM resultsetmetadatatest3 AS"
+                    + " resultsetmetadatatest1_alias");
+    ResultSetMetaData rsmd = rs.getMetaData();
+
+    assertEquals("resultsetmetadatatest3", rsmd.getTableName(1));
+    assertEquals(rsmd.getColumnLabel(1), "id_alias");
+    assertEquals(rsmd.getColumnName(1), "id");
+
+    try (Connection connection = createCon("&blankTableNameMeta")) {
+      rs =
+          connection
+              .createStatement()
+              .executeQuery(
+                  "SELECT id AS id_alias FROM resultsetmetadatatest3 AS"
+                      + " resultsetmetadatatest1_alias");
+      rsmd = rs.getMetaData();
+
+      assertEquals("", rsmd.getTableName(1));
+      assertEquals("id_alias", rsmd.getColumnLabel(1));
+      assertEquals("id", rsmd.getColumnName(1));
+    }
+  }
+
+  @Test
+  public void staticMethod() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM resultsetmetadatatest3");
+    ResultSetMetaData rsmd = rs.getMetaData();
+
+    rsmd.unwrap(org.mariadb.jdbc.client.result.ResultSetMetaData.class);
+
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> rsmd.unwrap(String.class),
+        "The receiver is not a wrapper for java.lang.String");
+  }
+
+  @Test
+  public void databaseResultsetMeta() throws SQLException {
+    DatabaseMetaData meta = sharedConn.getMetaData();
+    ResultSet rs = meta.getTableTypes();
+    assertTrue(rs.next());
+    ResultSetMetaData rsMeta = rs.getMetaData();
+    assertEquals("TABLE_TYPE", rsMeta.getColumnName(1));
+    assertEquals("", rsMeta.getTableName(1));
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/resultset/RowChangeTest.java b/src/test/java/org/mariadb/jdbc/integration/resultset/RowChangeTest.java
new file mode 100644
index 0000000..f22c9c1
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/resultset/RowChangeTest.java
@@ -0,0 +1,247 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.resultset;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.integration.Common;
+
+public class RowChangeTest extends Common {
+
+  @AfterAll
+  public static void after2() throws SQLException {
+    sharedConn.createStatement().execute("DROP TABLE ResultSetTest");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS ResultSetTest");
+    stmt.execute("CREATE TABLE ResultSetTest (t1 int not null primary key auto_increment, t2 int)");
+    stmt.execute("INSERT INTO ResultSetTest(t2) values (1),(2),(3),(4),(5),(6),(7),(8)");
+  }
+
+  @Test
+  public void next() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    for (int i = 1; i < 9; i++) {
+      assertTrue(rs.next());
+      assertEquals(i, rs.getInt(1));
+    }
+    assertFalse(rs.next());
+  }
+
+  @Test
+  public void isAfterLast() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    for (int i = 1; i < 9; i++) {
+      assertTrue(rs.next());
+      assertFalse(rs.isAfterLast());
+      assertEquals(i, rs.getInt(1));
+    }
+    assertFalse(rs.isAfterLast());
+    assertFalse(rs.next());
+    assertTrue(rs.isAfterLast());
+  }
+
+  @Test
+  public void isFirst() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertFalse(rs.isFirst());
+    assertTrue(rs.next());
+    assertTrue(rs.isFirst());
+    while (rs.next()) {
+      assertFalse(rs.isFirst());
+    }
+    assertFalse(rs.isFirst());
+
+    rs = stmt.executeQuery("SELECT * FROM ResultSetTest where t1 = -1");
+    assertFalse(rs.isFirst());
+    assertFalse(rs.next());
+    assertFalse(rs.isFirst());
+  }
+
+  @Test
+  public void isLast() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertFalse(rs.isLast());
+    for (int i = 1; i < 8; i++) {
+      assertTrue(rs.next());
+      assertFalse(rs.isLast());
+      assertEquals(i, rs.getInt(1));
+    }
+    assertTrue(rs.next());
+    assertEquals(8, rs.getInt(1));
+    assertTrue(rs.isLast());
+    assertFalse(rs.next());
+    assertFalse(rs.isLast());
+
+    rs = stmt.executeQuery("SELECT * FROM ResultSetTest where t1 = -1");
+    assertFalse(rs.isLast());
+    assertFalse(rs.next());
+    assertFalse(rs.isLast());
+  }
+
+  @Test
+  public void isBeforeFirst() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertTrue(rs.isBeforeFirst());
+    while (rs.next()) {
+      assertFalse(rs.isBeforeFirst());
+    }
+    assertFalse(rs.isBeforeFirst());
+  }
+
+  @Test
+  public void beforeFirst() throws SQLException {
+    Statement stmt =
+        sharedConn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertTrue(rs.isBeforeFirst());
+    for (int i = 1; i < 9; i++) {
+      assertTrue(rs.next());
+      assertEquals(i, rs.getInt(1));
+    }
+    assertFalse(rs.next());
+    assertFalse(rs.isBeforeFirst());
+    rs.beforeFirst();
+    assertTrue(rs.isBeforeFirst());
+    assertTrue(rs.next());
+    assertEquals(1, rs.getInt(1));
+
+    rs = stmt.executeQuery("SELECT * FROM ResultSetTest where t1 = -1");
+    assertFalse(rs.isBeforeFirst());
+  }
+
+  @Test
+  public void afterLast() throws SQLException {
+    Statement stmt =
+        sharedConn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    rs.afterLast();
+    assertFalse(rs.next());
+    assertTrue(rs.previous());
+    assertEquals(8, rs.getInt(1));
+  }
+
+  @Test
+  public void first() throws SQLException {
+    Statement stmt =
+        sharedConn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    rs.afterLast();
+    assertTrue(rs.first());
+    assertEquals(1, rs.getInt(1));
+
+    rs = stmt.executeQuery("SELECT * FROM ResultSetTest WHERE t1 = -1");
+    assertFalse(rs.first());
+  }
+
+  @Test
+  public void last() throws SQLException {
+    Statement stmt =
+        sharedConn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertTrue(rs.last());
+    assertEquals(8, rs.getInt(1));
+
+    rs = stmt.executeQuery("SELECT * FROM ResultSetTest WHERE t1 = -1");
+    assertFalse(rs.last());
+  }
+
+  @Test
+  public void getRow() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertEquals(0, rs.getRow());
+    for (int i = 1; i < 9; i++) {
+      assertTrue(rs.next());
+      assertEquals(i, rs.getRow());
+    }
+    assertFalse(rs.next());
+    assertEquals(0, rs.getRow());
+
+    rs = stmt.executeQuery("SELECT * FROM ResultSetTest WHERE t1 = -1");
+    assertEquals(0, rs.getRow());
+    assertFalse(rs.next());
+    assertEquals(0, rs.getRow());
+  }
+
+  @Test
+  public void absolute() throws SQLException {
+    Statement stmt =
+        sharedConn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertFalse(rs.absolute(0));
+    assertFalse(rs.absolute(9));
+    assertTrue(rs.absolute(4));
+    assertEquals(4, rs.getRow());
+    assertTrue(rs.absolute(5));
+    assertEquals(5, rs.getRow());
+    assertFalse(rs.absolute(9));
+    assertTrue(rs.isAfterLast());
+    assertTrue(rs.absolute(1));
+    assertTrue(rs.isFirst());
+
+    assertFalse(rs.absolute(-9));
+    assertFalse(rs.isAfterLast());
+    assertTrue(rs.isBeforeFirst());
+    assertTrue(rs.absolute(-8));
+    assertTrue(rs.isFirst());
+    assertTrue(rs.absolute(-3));
+    assertEquals(6, rs.getInt(1));
+  }
+
+  @Test
+  public void relative() throws SQLException {
+    Statement stmt =
+        sharedConn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    rs.relative(2);
+    assertEquals(2, rs.getInt(1));
+    rs.relative(2);
+    assertEquals(4, rs.getInt(1));
+    rs.relative(10);
+    assertTrue(rs.isAfterLast());
+    rs.relative(-20);
+    assertTrue(rs.isBeforeFirst());
+    rs.relative(5);
+    assertEquals(5, rs.getInt(1));
+  }
+
+  @Test
+  public void previous() throws SQLException {
+    Statement stmt =
+        sharedConn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    rs.afterLast();
+    for (int i = 8; i > 0; i--) {
+      assertTrue(rs.previous());
+      assertEquals(i, rs.getInt(1));
+    }
+    assertFalse(rs.previous());
+    assertFalse(rs.previous());
+  }
+
+  @Test
+  public void getFetchSize() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertEquals(0, rs.getFetchSize());
+    rs.setFetchSize(0);
+    assertEquals(0, rs.getFetchSize());
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/resultset/StreamingRowChangeTest.java b/src/test/java/org/mariadb/jdbc/integration/resultset/StreamingRowChangeTest.java
new file mode 100644
index 0000000..a83faf9
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/resultset/StreamingRowChangeTest.java
@@ -0,0 +1,477 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.resultset;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Statement;
+import org.mariadb.jdbc.integration.Common;
+
+public class StreamingRowChangeTest extends Common {
+
+  private final String NOT_FORWARD = "Operation not permit on TYPE_FORWARD_ONLY resultSet";
+  private final Class<? extends java.lang.Exception> sqle = SQLException.class;
+
+  @AfterAll
+  public static void after2() throws SQLException {
+    sharedConn.createStatement().execute("DROP TABLE ResultSetTest");
+  }
+
+  @BeforeAll
+  public static void beforeAll2() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.execute("DROP TABLE IF EXISTS ResultSetTest");
+    stmt.execute("CREATE TABLE ResultSetTest (t1 int not null primary key auto_increment, t2 int)");
+    stmt.execute("INSERT INTO ResultSetTest(t2) values (1),(2),(3),(4),(5),(6),(7),(8)");
+  }
+
+  @Test
+  public void closedRes() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.setFetchSize(3);
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    rs.close();
+    String NOT_SUPPORTED = "Operation not permit on a closed resultSet";
+    Common.assertThrowsContains(sqle, rs::next, NOT_SUPPORTED);
+    Common.assertThrowsContains(sqle, rs::isBeforeFirst, NOT_SUPPORTED);
+    Common.assertThrowsContains(sqle, rs::isAfterLast, NOT_SUPPORTED);
+    Common.assertThrowsContains(sqle, rs::isFirst, NOT_SUPPORTED);
+    Common.assertThrowsContains(sqle, rs::isLast, NOT_SUPPORTED);
+    Common.assertThrowsContains(sqle, rs::beforeFirst, NOT_SUPPORTED);
+    Common.assertThrowsContains(sqle, rs::afterLast, NOT_SUPPORTED);
+    Common.assertThrowsContains(sqle, rs::first, NOT_SUPPORTED);
+    Common.assertThrowsContains(sqle, rs::last, NOT_SUPPORTED);
+    Common.assertThrowsContains(sqle, rs::getRow, NOT_SUPPORTED);
+    Common.assertThrowsContains(sqle, () -> rs.absolute(0), NOT_SUPPORTED);
+    Common.assertThrowsContains(sqle, () -> rs.relative(1), NOT_SUPPORTED);
+    Common.assertThrowsContains(sqle, rs::previous, NOT_SUPPORTED);
+  }
+
+  @Test
+  public void next() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.setFetchSize(3);
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    for (int i = 1; i < 9; i++) {
+      assertTrue(rs.next());
+      assertEquals(i, rs.getInt(1));
+    }
+    assertFalse(rs.next());
+
+    // next fetching no result
+    stmt.setFetchSize(8);
+    rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    for (int i = 1; i < 9; i++) {
+      assertTrue(rs.next());
+      assertEquals(i, rs.getInt(1));
+    }
+    assertFalse(rs.next());
+
+    // next fetching no result keeping all results
+    stmt = sharedConn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
+    stmt.setFetchSize(8);
+    rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    for (int i = 1; i < 9; i++) {
+      assertTrue(rs.next());
+      assertEquals(i, rs.getInt(1));
+    }
+    assertFalse(rs.next());
+  }
+
+  @Test
+  public void isAfterLast() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    isAfterLast(stmt);
+    stmt.setFetchSize(3);
+    isAfterLast(stmt);
+    stmt.setFetchSize(4);
+    isAfterLast(stmt);
+  }
+
+  public void isAfterLast(Statement stmt) throws SQLException {
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    for (int i = 1; i < 9; i++) {
+      assertTrue(rs.next());
+      assertFalse(rs.isAfterLast());
+      assertEquals(i, rs.getInt(1));
+    }
+    assertFalse(rs.isAfterLast());
+    assertFalse(rs.next());
+    assertTrue(rs.isAfterLast());
+
+    rs = stmt.executeQuery("SELECT * FROM ResultSetTest WHERE t1 < 0");
+    assertFalse(rs.isAfterLast());
+    assertFalse(rs.next());
+    assertFalse(rs.isAfterLast());
+  }
+
+  @Test
+  public void isFirst() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    Statement stmt2 =
+        sharedConn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
+    isFirst(stmt, stmt2);
+
+    stmt.setFetchSize(3);
+    stmt2.setFetchSize(3);
+    isFirst(stmt, stmt2);
+
+    stmt.setFetchSize(4);
+    stmt2.setFetchSize(4);
+    isFirst(stmt, stmt2);
+  }
+
+  private void isFirst(Statement stmt, Statement stmt2) throws SQLException {
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertFalse(rs.isFirst());
+    assertTrue(rs.next());
+    assertTrue(rs.isFirst());
+    while (rs.next()) {
+      assertFalse(rs.isFirst());
+    }
+    assertFalse(rs.isFirst());
+
+    rs = stmt.executeQuery("SELECT * FROM ResultSetTest Where 1 = -1");
+    assertFalse(rs.isFirst());
+    assertFalse(rs.next());
+    assertFalse(rs.isFirst());
+
+    rs = stmt2.executeQuery("SELECT * FROM ResultSetTest");
+    assertFalse(rs.isFirst());
+    assertTrue(rs.next());
+    assertTrue(rs.isFirst());
+    while (rs.next()) {
+      assertFalse(rs.isFirst());
+    }
+    assertFalse(rs.isFirst());
+
+    rs = stmt2.executeQuery("SELECT * FROM ResultSetTest Where 1 = -1");
+    assertFalse(rs.isFirst());
+    assertFalse(rs.next());
+    assertFalse(rs.isFirst());
+  }
+
+  @Test
+  public void isLast() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    isLast(stmt);
+    stmt.setFetchSize(3);
+    isLast(stmt);
+    stmt.setFetchSize(4);
+    isLast(stmt);
+  }
+
+  private void isLast(Statement stmt) throws SQLException {
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertFalse(rs.isLast());
+    for (int i = 1; i < 8; i++) {
+      assertTrue(rs.next());
+      assertFalse(rs.isLast());
+      assertEquals(i, rs.getInt(1));
+    }
+    assertTrue(rs.next());
+    assertEquals(8, rs.getInt(1));
+    assertTrue(rs.isLast());
+    assertFalse(rs.next());
+    assertFalse(rs.isLast());
+
+    rs = stmt.executeQuery("SELECT * FROM ResultSetTest Where 1 = -1");
+    rs.isLast();
+    assertFalse(rs.isLast());
+    assertFalse(rs.next());
+    assertFalse(rs.isLast());
+
+    stmt = sharedConn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
+    stmt.setFetchSize(3);
+    rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertFalse(rs.isLast());
+    for (int i = 1; i < 8; i++) {
+      assertTrue(rs.next());
+      assertFalse(rs.isLast());
+      assertEquals(i, rs.getInt(1));
+    }
+    assertTrue(rs.next());
+    assertEquals(8, rs.getInt(1));
+    assertTrue(rs.isLast());
+    assertFalse(rs.next());
+    assertFalse(rs.isLast());
+
+    rs = stmt.executeQuery("SELECT * FROM ResultSetTest Where 1 = -1");
+    assertFalse(rs.isLast());
+    assertFalse(rs.next());
+    assertFalse(rs.isLast());
+  }
+
+  @Test
+  public void isBeforeFirst() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    isBeforeFirst(stmt);
+    stmt.setFetchSize(3);
+    isBeforeFirst(stmt);
+  }
+
+  private void isBeforeFirst(Statement stmt) throws SQLException {
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertTrue(rs.isBeforeFirst());
+    while (rs.next()) {
+      assertFalse(rs.isBeforeFirst());
+    }
+    assertFalse(rs.isBeforeFirst());
+
+    stmt = sharedConn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
+    stmt.setFetchSize(3);
+    rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertTrue(rs.isBeforeFirst());
+    while (rs.next()) {
+      assertFalse(rs.isBeforeFirst());
+    }
+    assertFalse(rs.isBeforeFirst());
+  }
+
+  @Test
+  public void beforeFirst() throws SQLException {
+    Statement stmt =
+        sharedConn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+    beforeFirst(stmt);
+    stmt.setFetchSize(3);
+    beforeFirst(stmt);
+  }
+
+  private void beforeFirst(Statement stmt) throws SQLException {
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    for (int i = 1; i < 9; i++) {
+      assertTrue(rs.next());
+      assertEquals(i, rs.getInt(1));
+    }
+    assertFalse(rs.next());
+    assertFalse(rs.isBeforeFirst());
+    rs.beforeFirst();
+    assertTrue(rs.isBeforeFirst());
+    assertTrue(rs.next());
+    assertEquals(1, rs.getInt(1));
+
+    stmt = sharedConn.createStatement();
+    stmt.setFetchSize(3);
+    final ResultSet rs2 = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    Common.assertThrowsContains(sqle, rs2::beforeFirst, NOT_FORWARD);
+  }
+
+  @Test
+  public void afterLast() throws SQLException {
+    Statement stmt =
+        sharedConn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+    afterLast(stmt);
+    stmt.setFetchSize(3);
+    afterLast(stmt);
+  }
+
+  private void afterLast(Statement stmt) throws SQLException {
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    rs.afterLast();
+    assertFalse(rs.next());
+    assertTrue(rs.previous());
+    assertEquals(8, rs.getInt(1));
+
+    stmt = sharedConn.createStatement();
+    stmt.setFetchSize(3);
+    final ResultSet rs2 = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    Common.assertThrowsContains(sqle, rs2::afterLast, NOT_FORWARD);
+  }
+
+  @Test
+  public void first() throws SQLException {
+    Statement stmt =
+        sharedConn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+    first(stmt);
+    stmt.setFetchSize(3);
+    first(stmt);
+  }
+
+  private void first(Statement stmt) throws SQLException {
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    rs.afterLast();
+    assertTrue(rs.first());
+    assertEquals(1, rs.getInt(1));
+
+    rs = stmt.executeQuery("SELECT * FROM ResultSetTest WHERE t1 = -1");
+    assertFalse(rs.first());
+
+    stmt = sharedConn.createStatement();
+    stmt.setFetchSize(3);
+    final ResultSet rs2 = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    Common.assertThrowsContains(sqle, rs2::first, NOT_FORWARD);
+  }
+
+  @Test
+  public void last() throws SQLException {
+    Statement stmt =
+        sharedConn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+    last(stmt);
+    stmt.setFetchSize(3);
+    last(stmt);
+  }
+
+  private void last(Statement stmt) throws SQLException {
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertTrue(rs.last());
+    assertEquals(8, rs.getInt(1));
+
+    rs = stmt.executeQuery("SELECT * FROM ResultSetTest WHERE t1 = -1");
+    assertFalse(rs.last());
+
+    stmt = sharedConn.createStatement();
+    stmt.setFetchSize(3);
+    rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertTrue(rs.last());
+    assertEquals(8, rs.getInt(1));
+
+    rs = stmt.executeQuery("SELECT * FROM ResultSetTest WHERE t1 = -1");
+    assertFalse(rs.last());
+  }
+
+  @Test
+  public void getRow() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.setFetchSize(3);
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertEquals(0, rs.getRow());
+    for (int i = 1; i < 9; i++) {
+      assertTrue(rs.next());
+      assertEquals(0, rs.getRow());
+    }
+    assertFalse(rs.next());
+    assertEquals(0, rs.getRow());
+
+    rs = stmt.executeQuery("SELECT * FROM ResultSetTest WHERE t1 = -1");
+    assertEquals(0, rs.getRow());
+    assertFalse(rs.next());
+    assertEquals(0, rs.getRow());
+  }
+
+  @Test
+  public void absolute() throws SQLException {
+    Statement stmt =
+        sharedConn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+    stmt.setFetchSize(3);
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertFalse(rs.absolute(0));
+    assertTrue(rs.absolute(2));
+    assertEquals(2, rs.getRow());
+    assertEquals(2, rs.getInt(1));
+    assertTrue(rs.absolute(6));
+    assertEquals(6, rs.getRow());
+    assertEquals(6, rs.getInt(1));
+    assertFalse(rs.absolute(9));
+    assertTrue(rs.absolute(4));
+    assertEquals(4, rs.getRow());
+    assertEquals(4, rs.getInt(1));
+    assertTrue(rs.absolute(5));
+    assertEquals(5, rs.getRow());
+    assertEquals(5, rs.getInt(1));
+    assertFalse(rs.absolute(9));
+    assertTrue(rs.isAfterLast());
+    assertTrue(rs.absolute(1));
+    assertTrue(rs.isFirst());
+
+    assertFalse(rs.absolute(-9));
+    assertFalse(rs.isAfterLast());
+    assertTrue(rs.absolute(-8));
+    assertTrue(rs.isFirst());
+    assertTrue(rs.absolute(-3));
+    assertEquals(6, rs.getInt(1));
+
+    stmt = sharedConn.createStatement();
+    stmt.setFetchSize(3);
+    final ResultSet rs2 = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    Common.assertThrowsContains(sqle, () -> rs2.absolute(0), NOT_FORWARD);
+  }
+
+  @Test
+  public void relative() throws SQLException {
+    Statement stmt =
+        sharedConn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+    stmt.setFetchSize(3);
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    rs.relative(2);
+    assertEquals(2, rs.getInt(1));
+    rs.relative(2);
+    assertEquals(4, rs.getInt(1));
+    rs.relative(10);
+    assertTrue(rs.isAfterLast());
+    rs.relative(-20);
+    assertTrue(rs.isBeforeFirst());
+    rs.relative(5);
+    assertEquals(5, rs.getInt(1));
+
+    stmt = sharedConn.createStatement();
+    stmt.setFetchSize(3);
+    final ResultSet rs2 = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    Common.assertThrowsContains(sqle, () -> rs2.relative(0), NOT_FORWARD);
+  }
+
+  @Test
+  public void previous() throws SQLException {
+    Statement stmt =
+        sharedConn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+    stmt.setFetchSize(3);
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertFalse(rs.previous());
+    rs.afterLast();
+    for (int i = 8; i > 0; i--) {
+      assertTrue(rs.previous());
+      assertEquals(i, rs.getInt(1));
+    }
+    assertFalse(rs.previous());
+
+    stmt = sharedConn.createStatement();
+    stmt.setFetchSize(3);
+    final ResultSet rs2 = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    Common.assertThrowsContains(sqle, rs2::previous, NOT_FORWARD);
+  }
+
+  @Test
+  public void getFetchSize() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.setFetchSize(3);
+    final ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertEquals(3, rs.getFetchSize());
+
+    assertTrue(rs.next());
+    assertEquals(1, rs.getInt(1));
+    assertTrue(rs.next());
+    assertEquals(2, rs.getInt(1));
+
+    rs.setFetchSize(2);
+    assertEquals(2, rs.getFetchSize());
+
+    for (int i = 3; i < 9; i++) {
+      assertTrue(rs.next());
+      assertEquals(i, rs.getInt(1));
+    }
+    assertFalse(rs.next());
+    Common.assertThrowsContains(sqle, () -> rs.setFetchSize(-2), "invalid fetch size -2");
+  }
+
+  @Test
+  public void removeStreaming() throws SQLException {
+    Statement stmt = sharedConn.createStatement();
+    stmt.setFetchSize(3);
+    final ResultSet rs = stmt.executeQuery("SELECT * FROM ResultSetTest");
+    assertEquals(3, rs.getFetchSize());
+    rs.setFetchSize(0);
+    assertEquals(0, rs.getFetchSize());
+
+    for (int i = 1; i < 9; i++) {
+      assertTrue(rs.next());
+      assertEquals(i, rs.getInt(1));
+    }
+    assertFalse(rs.next());
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/tools/TcpProxy.java b/src/test/java/org/mariadb/jdbc/integration/tools/TcpProxy.java
new file mode 100644
index 0000000..56032fd
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/tools/TcpProxy.java
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.tools;
+
+import java.io.IOException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import org.mariadb.jdbc.util.log.Logger;
+import org.mariadb.jdbc.util.log.Loggers;
+
+public class TcpProxy {
+  private static final Logger logger = Loggers.getLogger(TcpProxy.class);
+
+  private final String host;
+  private final TcpProxySocket socket;
+
+  /**
+   * Initialise proxy.
+   *
+   * @param host host (ip / dns)
+   * @param remoteport port
+   * @throws IOException exception
+   */
+  public TcpProxy(String host, int remoteport) throws IOException {
+    this.host = host;
+    socket = new TcpProxySocket(host, remoteport);
+    Executors.newSingleThreadScheduledExecutor().schedule(socket, 0, TimeUnit.MILLISECONDS);
+  }
+
+  public void stop() {
+    socket.kill();
+  }
+
+  public void setDelay(int delay) {
+    socket.setDelay(delay);
+  }
+
+  public void removeDelay() {
+    socket.setDelay(1);
+  }
+
+  /**
+   * Stop proxy and restart after X milliseconds.
+   *
+   * @param sleepTime sleep time in milliseconds
+   */
+  public void restart(long sleepTime) {
+    socket.kill();
+    logger.trace("host proxy port " + socket.getLocalport() + " for " + host + " started");
+    Executors.newSingleThreadScheduledExecutor().schedule(socket, sleepTime, TimeUnit.MILLISECONDS);
+  }
+
+  public void forceClose() {
+    socket.sendRst();
+  }
+
+  /** Restart proxy. */
+  public void restart() {
+    Executors.newSingleThreadExecutor().execute(socket);
+    try {
+      Thread.sleep(10);
+    } catch (InterruptedException e) {
+      // eat Exception
+    }
+  }
+
+  public int getLocalPort() {
+    return socket.getLocalPort();
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/tools/TcpProxySocket.java b/src/test/java/org/mariadb/jdbc/integration/tools/TcpProxySocket.java
new file mode 100644
index 0000000..33367c0
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/tools/TcpProxySocket.java
@@ -0,0 +1,204 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.tools;
+
+import java.io.*;
+import java.net.BindException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import org.mariadb.jdbc.util.log.Logger;
+import org.mariadb.jdbc.util.log.Loggers;
+
+public class TcpProxySocket implements Runnable {
+  private static final Logger logger = Loggers.getLogger(TcpProxySocket.class);
+
+  private final String host;
+  private final int remoteport;
+  private final int localport;
+  private boolean stop = false;
+  private Socket client = null;
+  private Socket server = null;
+  private ServerSocket ss;
+  private int delay;
+
+  /**
+   * Creation of proxy.
+   *
+   * @param host database host
+   * @param remoteport database port
+   * @throws IOException exception
+   */
+  public TcpProxySocket(String host, int remoteport) throws IOException {
+    this.host = host;
+    this.remoteport = remoteport;
+    ss = new ServerSocket(0);
+    this.localport = ss.getLocalPort();
+  }
+
+  public int getLocalPort() {
+    return ss.getLocalPort();
+  }
+
+  public boolean isClosed() {
+    return ss.isClosed();
+  }
+
+  public void setDelay(int delay) {
+    this.delay = delay;
+  }
+
+  /** Kill proxy. */
+  public void kill() {
+    stop = true;
+    try {
+      if (server != null) {
+        server.close();
+      }
+    } catch (IOException e) {
+      // eat Exception
+    }
+    try {
+      if (client != null) {
+        client.close();
+      }
+    } catch (IOException e) {
+      // eat Exception
+    }
+    try {
+      ss.close();
+    } catch (IOException e) {
+      // eat Exception
+    }
+  }
+
+  public void sendRst() {
+
+    try {
+      if (client != null) {
+        // send an RST, not FIN
+        client.setSoLinger(true, 0);
+        client.close();
+      }
+    } catch (IOException e) {
+      // eat Exception
+    }
+    try {
+      if (server != null) {
+        server.close();
+      }
+    } catch (IOException e) {
+      // eat Exception
+    }
+
+    try {
+      ss.close();
+    } catch (IOException e) {
+      // eat Exception
+    }
+  }
+
+  @Override
+  public void run() {
+
+    logger.trace("host proxy port " + this.localport + " for " + host + " started");
+    stop = false;
+    try {
+      try {
+        if (ss.isClosed()) {
+          ss = new ServerSocket(localport);
+        }
+      } catch (BindException b) {
+        // in case for testing crash and reopen too quickly
+        try {
+          Thread.sleep(100);
+        } catch (InterruptedException i) {
+          // eat Exception
+        }
+        if (ss.isClosed()) {
+          ss = new ServerSocket(localport);
+        }
+      }
+      final byte[] request = new byte[1024];
+      byte[] reply = new byte[4096];
+      while (!stop) {
+        try {
+          client = ss.accept();
+          final InputStream fromClient = client.getInputStream();
+          final OutputStream toClient = client.getOutputStream();
+          try {
+            server = new Socket(host, remoteport);
+          } catch (IOException e) {
+            PrintWriter out = new PrintWriter(new OutputStreamWriter(toClient));
+            out.println("Proxy server cannot connect to " + host + ":" + remoteport + ":\n" + e);
+            out.flush();
+            client.close();
+            continue;
+          }
+          final InputStream fromServer = server.getInputStream();
+          final OutputStream toServer = server.getOutputStream();
+          new Thread(
+                  () -> {
+                    int bytesRead;
+                    try {
+                      while ((bytesRead = fromClient.read(request)) != -1) {
+                        if (delay > 0) {
+                          try {
+                            Thread.sleep(delay);
+                          } catch (InterruptedException e) {
+                            e.printStackTrace();
+                          }
+                        }
+                        toServer.write(request, 0, bytesRead);
+                        toServer.flush();
+                      }
+                    } catch (IOException e) {
+                      // eat exception
+                    }
+                    try {
+                      toServer.close();
+                    } catch (IOException e) {
+                      // eat exception
+                    }
+                  })
+              .start();
+          int bytesRead;
+          try {
+            while ((bytesRead = fromServer.read(reply)) != -1) {
+              try {
+                Thread.sleep(1);
+              } catch (InterruptedException e) {
+                e.printStackTrace();
+              }
+              toClient.write(reply, 0, bytesRead);
+              toClient.flush();
+            }
+          } catch (IOException e) {
+            // eat exception
+          }
+          toClient.close();
+        } catch (IOException e) {
+          // System.err.println("ERROR socket : "+e);
+        } finally {
+          try {
+            if (server != null) {
+              server.close();
+            }
+            if (client != null) {
+              client.close();
+            }
+          } catch (IOException e) {
+            // eat exception
+          }
+        }
+      }
+    } catch (IOException e) {
+      e.printStackTrace();
+    }
+  }
+
+  public int getLocalport() {
+    return localport;
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/util/Env2CredentialPlugin.java b/src/test/java/org/mariadb/jdbc/integration/util/Env2CredentialPlugin.java
new file mode 100644
index 0000000..bc26e52
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/util/Env2CredentialPlugin.java
@@ -0,0 +1,40 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.util;
+
+import org.mariadb.jdbc.plugin.Credential;
+import org.mariadb.jdbc.plugin.CredentialPlugin;
+
+/**
+ * Authentication using environment variable.
+ *
+ * <p>default implementation use environment variable MARIADB_USER and MARIADB_PWD
+ *
+ * <p>example : `jdbc:mariadb://host/db?credentialType=ENV`
+ *
+ * <p>2 options `userKey` and `pwdKey` permits indicating which environment variable to use.
+ */
+public class Env2CredentialPlugin implements CredentialPlugin {
+
+  @Override
+  public String type() {
+    return "ENVTEST";
+  }
+
+  @Override
+  public boolean mustUseSsl() {
+    return true;
+  }
+
+  @Override
+  public String defaultAuthenticationPluginType() {
+    return "mysql_native_password";
+  }
+
+  @Override
+  public Credential get() {
+    return new Credential(System.getenv("MARIADB2_USER"), System.getenv("MARIADB2_PWD"));
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/util/SocketFactoryBasicTest.java b/src/test/java/org/mariadb/jdbc/integration/util/SocketFactoryBasicTest.java
new file mode 100644
index 0000000..adc80c7
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/util/SocketFactoryBasicTest.java
@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.util;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import javax.net.SocketFactory;
+
+public class SocketFactoryBasicTest extends SocketFactory {
+  final SocketFactory socketFactory = SocketFactory.getDefault();
+
+  public SocketFactoryBasicTest() {}
+
+  @Override
+  public Socket createSocket() throws IOException {
+    return socketFactory.createSocket();
+  }
+
+  @Override
+  public Socket createSocket(String s, int i) throws IOException {
+    return socketFactory.createSocket(s, i);
+  }
+
+  @Override
+  public Socket createSocket(String s, int i, InetAddress inetAddress, int i1) throws IOException {
+    return socketFactory.createSocket(s, i, inetAddress, i1);
+  }
+
+  @Override
+  public Socket createSocket(InetAddress inetAddress, int i) throws IOException {
+    return socketFactory.createSocket(inetAddress, i);
+  }
+
+  @Override
+  public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress1, int i1)
+      throws IOException {
+    return socketFactory.createSocket(inetAddress, i, inetAddress1, i1);
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/integration/util/SocketFactoryTest.java b/src/test/java/org/mariadb/jdbc/integration/util/SocketFactoryTest.java
new file mode 100644
index 0000000..1f94f8a
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/integration/util/SocketFactoryTest.java
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.integration.util;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import javax.net.SocketFactory;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.util.ConfigurableSocketFactory;
+
+public class SocketFactoryTest extends ConfigurableSocketFactory {
+  final SocketFactory socketFactory = SocketFactory.getDefault();
+
+  public SocketFactoryTest() {}
+
+  @Override
+  public void setConfiguration(Configuration conf, String host) {}
+
+  @Override
+  public Socket createSocket() throws IOException {
+    return socketFactory.createSocket();
+  }
+
+  @Override
+  public Socket createSocket(String s, int i) throws IOException {
+    return socketFactory.createSocket(s, i);
+  }
+
+  @Override
+  public Socket createSocket(String s, int i, InetAddress inetAddress, int i1) throws IOException {
+    return socketFactory.createSocket(s, i, inetAddress, i1);
+  }
+
+  @Override
+  public Socket createSocket(InetAddress inetAddress, int i) throws IOException {
+    return socketFactory.createSocket(inetAddress, i);
+  }
+
+  @Override
+  public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress1, int i1)
+      throws IOException {
+    return socketFactory.createSocket(inetAddress, i, inetAddress1, i1);
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/client/HostnameVerifierTest.java b/src/test/java/org/mariadb/jdbc/unit/client/HostnameVerifierTest.java
new file mode 100644
index 0000000..e8958b6
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/client/HostnameVerifierTest.java
@@ -0,0 +1,534 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.unit.client;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.ByteArrayInputStream;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.SSLException;
+import javax.security.auth.x500.X500Principal;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.client.tls.HostnameVerifier;
+
+public class HostnameVerifierTest {
+
+  private static X509Certificate getCertificate(String certString) throws CertificateException {
+    CertificateFactory cf = CertificateFactory.getInstance("X.509");
+    return (X509Certificate)
+        cf.generateCertificate(new ByteArrayInputStream(certString.getBytes()));
+  }
+
+  private void verifyExceptionEqual(String host, X509Certificate cert, String exceptionMessage) {
+    Exception e =
+        Assertions.assertThrows(SSLException.class, () -> HostnameVerifier.verify(host, cert, -1));
+    Assertions.assertTrue(
+        e.getMessage().contains(exceptionMessage), "real message:" + e.getMessage());
+  }
+
+  // generating certificate example
+  //      openssl genrsa -out "/home/osboxes/key" 2048
+  //      openssl req -new -utf8 -sha1 \
+  //            -key /home/osboxes/key \
+  //            -subj "/C=US/ST=CA/O=Acme, Inc./CN=mariadb.org" \
+  //            -reqexts SAN \
+  //            -config <(cat /etc/ssl/openssl.cnf \
+  //                    <(printf
+  // "\n[SAN]\nsubjectAltName=DNS:mariadbtest.org,DNS:www.mariadbtest.org")) \
+  //            -out domain.csr
+
+  @Test
+  public void verifyCn() throws Exception {
+    // CN=test.com
+    X509Certificate cert =
+        getCertificate(
+            ""
+                + "-----BEGIN CERTIFICATE-----\n"
+                + "MIIC+zCCAeOgAwIBAgIJANin/585wAXHMA0GCSqGSIb3DQEBBQUAMBMxETAPBgNV\n"
+                + "BAMMCHRlc3QuY29tMCAXDTE3MDYyMzEzNTI1NloYDzIxMTcwNTMwMTM1MjU2WjAT\n"
+                + "MREwDwYDVQQDDAh0ZXN0LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\n"
+                + "ggEBAMQHS31pL/fMkcQBo5zZV2Hi1Jdc1vIIv65d+XeFmv+n/vv+X5Z5NKLc7i01\n"
+                + "SFPoTEr1HG7R6Xvl27UtGm9Z6fmgsCbCRImUCG1yPER20fAgWNhKkoGgvOM8PxZz\n"
+                + "AS0dWgaukBnG9EDVQQrLu+bHuHji8qysGiYQGvSBy/QLSMWjfkSjyFv8I2pT0jLi\n"
+                + "eghgROl3IprRcsiebC/Bv1iJ06s8BD1C9ErzmPxqHaChdzGFATm+G4opcnBxzPuN\n"
+                + "DVE9CaLUS4Q5SixB9TRTQ2LyryEtUOUnnDyoktrX3LzkTmr2dhT8MIgRMsNkJD5w\n"
+                + "CpITvLchBXCdj0lcn5NMb0Rt/AsCAwEAAaNQME4wHQYDVR0OBBYEFJMoFo+HhyIt\n"
+                + "WA6QZmedeN2/qBU/MB8GA1UdIwQYMBaAFJMoFo+HhyItWA6QZmedeN2/qBU/MAwG\n"
+                + "A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAGpBkEJ3nv1kCnqQbrU3WYmv\n"
+                + "zl+kNc5xVZdLWMarjvWwBE1cnDHkXzJpl5cyWcHuPyWAi40/edN7gRfpNINWfc0A\n"
+                + "9YWo2PVBlBdklqzsewDV3gipFHkCgBTlGXaPXjCLLNxphYwdsble1hu/XXNvNY8v\n"
+                + "9EPxgN0rTnBj85gme/+Hrjq2kH15jyqK5rdylOjCZELs5g8cc49M6sw/sY70GsGA\n"
+                + "UEjb+vAN7FxXzXzH4oqIeycnxP+/MA82iieew7nlOMlYrppM6igjP0CUzw4ys6lG\n"
+                + "8QdWBcm2Ybo4XFjOnC98VlQl+WBu4CiToxjGphDmsMIO3Hf5PSTRwTKxtuWn45Y=\n"
+                + "-----END CERTIFICATE-----\n");
+    HostnameVerifier.verify("test.com", cert, -1);
+    verifyExceptionEqual(
+        "a.test.com",
+        cert,
+        "DNS host \"a.test.com\" doesn't correspond to certificate CN \"test.com\"");
+    verifyExceptionEqual(
+        "other.com",
+        cert,
+        "DNS host \"other.com\" doesn't correspond to certificate CN \"test.com\"");
+  }
+
+  @Test
+  public void verifyNoSan() throws Exception {
+    // CN=*.mariadb.com
+    X509Certificate cert =
+        getCertificate(
+            ""
+                + "-----BEGIN CERTIFICATE-----\n"
+                + "MIIDLjCCAhYCFEWSUFTlr/qGo/jC5R1nNQdE5zmrMA0GCSqGSIb3DQEBCwUAMFIx\n"
+                + "CzAJBgNVBAYTAkNOMQswCQYDVQQIDAJHRDELMAkGA1UEBwwCU1oxEjAQBgNVBAoM\n"
+                + "CUFjbWUsSW5jLjEVMBMGA1UEAwwMQWNtZSBSb290IENBMCAXDTIxMDMzMDA5MjQ1\n"
+                + "OVoYDzIxMjEwMzA2MDkyNDU5WjBTMQswCQYDVQQGEwJDTjELMAkGA1UECAwCR0Qx\n"
+                + "CzAJBgNVBAcMAlNaMRIwEAYDVQQKDAlBY21lLEluYy4xFjAUBgNVBAMMDSoubWFy\n"
+                + "aWFkYi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDdKvfR3PBZ\n"
+                + "7oo4bYlj+Z2F+g2s1Pnurs7KUIu04T8x1KI0tiHniBTuTKL4TinWx734VjnK4pJf\n"
+                + "mwtcWpuxcXY+Btj/C47HMwpZjABI4PbHcB46IVb3gg8XTEDMxZ/OLYACYe+PEwvI\n"
+                + "8Bdmydtn/A6IqIOd2eQTSFCX695CNczSWd3yrirZwe6xZCp2xYemW2UJkrjxRK66\n"
+                + "IpoYWyYVPCe4CiGBL0f6dLrWE6HUPuA3Pb68o0IFGP4NlmXv9qHm8kXejqE216Q/\n"
+                + "Fx4sUS/icH50zs+QtpoKYR8XQnUVeAO8lEuI09mAxII5jtWdYOA/s2oSz5e7m0i2\n"
+                + "xsSHvDJD0P6bAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAKE/YucmKZldC2iE330u\n"
+                + "CiP9EvUJfd98j96clpzdqW2V/uDylJP96QDk0iY5rIfoGAu6a9xZP9Bju8F7crLz\n"
+                + "Ogx/Vdq//7uOHMe/kJ3kOjzI8jsdFKGA26BT8wBNZ/UT9ll0iFjYZ1YhsaURslX5\n"
+                + "+uW//GG6vpcMtWIMbkkHZnM5DKeZXF1VX55jrB01kxjaqrObhqNlQl887KQ/j5HF\n"
+                + "iAAiLwDQu171TQmCY7iesYv84cyUqLvRoAfLzOPb62AlLwGNWKeIOFJElnMSDx3y\n"
+                + "LzGv9zUUe1SdqqVASKJAAhzqGyu9eLkGYIKTHU7WdrPSyAhammdeq7C7AYLpMrPc\n"
+                + "CAM=\n"
+                + "-----END CERTIFICATE-----\n");
+    HostnameVerifier.verify("test.mariadb.org", cert, -1);
+    verifyExceptionEqual(
+        "test.org",
+        cert,
+        "DNS host \"test.org\" doesn't correspond to certificate CN \"*.mariadb.org\"");
+  }
+
+  @Test
+  public void verifyNonAsciiCn() throws Exception {
+    // CN=😎.com = "\uD83D\uDE0E"
+    X509Certificate cert =
+        getCertificate(
+            ""
+                + "-----BEGIN CERTIFICATE-----\n"
+                + "MIIDWTCCAkGgAwIBAgIJAI38v686DwcOMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNV\n"
+                + "BAYTAlVTMQswCQYDVQQIDAJDQTETMBEGA1UECgwKQWNtZSwgSW5jLjERMA8GA1UE\n"
+                + "AwwI8J+Yji5jb20wIBcNMTcwNjIzMTQyNzQ2WhgPMjExNzA1MzAxNDI3NDZaMEIx\n"
+                + "CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTETMBEGA1UECgwKQWNtZSwgSW5jLjER\n"
+                + "MA8GA1UEAwwI8J+Yji5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB\n"
+                + "AQDEB0t9aS/3zJHEAaOc2Vdh4tSXXNbyCL+uXfl3hZr/p/77/l+WeTSi3O4tNUhT\n"
+                + "6ExK9Rxu0el75du1LRpvWen5oLAmwkSJlAhtcjxEdtHwIFjYSpKBoLzjPD8WcwEt\n"
+                + "HVoGrpAZxvRA1UEKy7vmx7h44vKsrBomEBr0gcv0C0jFo35Eo8hb/CNqU9Iy4noI\n"
+                + "YETpdyKa0XLInmwvwb9YidOrPAQ9QvRK85j8ah2goXcxhQE5vhuKKXJwccz7jQ1R\n"
+                + "PQmi1EuEOUosQfU0U0Ni8q8hLVDlJ5w8qJLa19y85E5q9nYU/DCIETLDZCQ+cAqS\n"
+                + "E7y3IQVwnY9JXJ+TTG9EbfwLAgMBAAGjUDBOMB0GA1UdDgQWBBSTKBaPh4ciLVgO\n"
+                + "kGZnnXjdv6gVPzAfBgNVHSMEGDAWgBSTKBaPh4ciLVgOkGZnnXjdv6gVPzAMBgNV\n"
+                + "HRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBknxZ3ihZHmcyrV3H0pNdc+jxB\n"
+                + "xU0T1T1nOLVpgqh+N0m+WFyGdcZlwPcmuD2a5jFn7uIdh8qfq11T8R+OmJqrBxoo\n"
+                + "RSACSAgRSQjPfnN8wi4i8hFLKXOQw43UomsSuNixdGIsMWrDh02e1Q0/g/TD7S62\n"
+                + "JCRksTlBS/+qw+w384vEd4djq7HuT8/hs2RC6Hm9EQkipgNn9+2z40vJ/jgcuPIF\n"
+                + "x51XCozrD1yW9JK/YyBnjYk04iEfQLW7+pGMJOcsX7x9EGwpEg1gsDg2mM0EEIwU\n"
+                + "d6DHlYvpD9JkzyEScg8Supztoc2aGbGE4SHBKB1riTLBAHWqqwas4sGSgZxu\n"
+                + "-----END CERTIFICATE-----\n");
+    HostnameVerifier.verify("😎.com", cert, -1);
+    verifyExceptionEqual(
+        "a.😎.com", cert, "DNS host \"a.😎.com\" doesn't correspond to certificate CN \"😎.com\"");
+  }
+
+  @Test
+  public void verifySubjectAlt() throws Exception {
+    // CN=mariadb.org, subjectAlt=other.org,www.other.org
+    //        openssl genrsa -out ca.key 2048
+    //        openssl req -new -x509 -days 36500 -key ca.key -subj "/C=CN/ST=GD/L=SZ/O=Acme,
+    // Inc./CN=Acme Root CA" -out ca.crt
+    //
+    //        openssl req -newkey rsa:2048 -nodes -keyout server.key -subj "/C=CN/ST=GD/L=SZ/O=Acme,
+    // Inc./CN=*.mariadb.org" \
+    //                  -out server.csr
+    //        openssl x509 -req -extfile <(printf "subjectAltName=DNS:other.org,DNS:www.other.org")
+    // -days 36500 \
+    //                  -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt
+    X509Certificate cert =
+        getCertificate(
+            ""
+                + "-----BEGIN CERTIFICATE-----\n"
+                + "MIIDUzCCAjugAwIBAgIJAPMG38xrY9DcMA0GCSqGSIb3DQEBCwUAMFMxCzAJBgNV\n"
+                + "BAYTAkNOMQswCQYDVQQIDAJHRDELMAkGA1UEBwwCU1oxEzARBgNVBAoMCkFjbWUs\n"
+                + "IEluYy4xFTATBgNVBAMMDEFjbWUgUm9vdCBDQTAgFw0xNzA2MjMxNjEyNTlaGA8y\n"
+                + "MTE3MDUzMDE2MTI1OVowVDELMAkGA1UEBhMCQ04xCzAJBgNVBAgMAkdEMQswCQYD\n"
+                + "VQQHDAJTWjETMBEGA1UECgwKQWNtZSwgSW5jLjEWMBQGA1UEAwwNKi5tYXJpYWRi\n"
+                + "Lm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANZnnLoS2JKQNr/d\n"
+                + "ccRT1pNVKHykAVJHhZiIfjqqKEstjob30VZjll5exQ6iTHaS73qXG1/PfXhEl8Tc\n"
+                + "7R3VlE7dHxPE+FhWSCcdsJlSEpa9h0TOkJ6H4V1iD+bTwlfEesLqXCzLkaBz7hsw\n"
+                + "L6mzVDJ4Lucrstf2YgBEoXjzj8G+ECiz1Vx1GX1jU2yoRYk/LcGUgsbxMxZOFcKG\n"
+                + "JyCmjbRe7xJozhu3m/1bf8eCyHg/+Tpiw1VGwPNFe6mb2SI9pYnk9l0pjzFy5yxJ\n"
+                + "nFeYj5cLBZHwM5K2aHiOvvBeXvYz9RIrWI98zsXZFjzD00+Js3X/jC4nRtkHV/JC\n"
+                + "COvAwRUCAwEAAaMnMCUwIwYDVR0RBBwwGoIJb3RoZXIub3Jngg13d3cub3RoZXIu\n"
+                + "b3JnMA0GCSqGSIb3DQEBCwUAA4IBAQDEswEEw2VXv6+EKIz3ysN7kRNXs09TWiYd\n"
+                + "bhGIVWHK4oRMjbUkPQWNftD+VvRyW1mZPZ0Tn/kXPnUsYCuF/UFLautmIAa16/el\n"
+                + "WJc9EA4yM42CByW/DEUfvFVgaBoJysLNhA2O/1VC/UmC2TNjiwXAO3AOJTVgdS1/\n"
+                + "nj34C3SJgbtmMu/ToCILMcjkaKJPD2/1AaIioBOSxvwdseM399eVjZIhf9bQCSHU\n"
+                + "fDrV4El/nb5nr4j7AvHtIhbPtpJOmKCAbZRwKc+ZgrH6ZyapyZfpmNmlwZcuC4DM\n"
+                + "SJmVrJfl1GdaXyxsljClcXM9MDQYm9r9wcchc3dSVR+k6wz2+vbw\n"
+                + "-----END CERTIFICATE-----\n");
+
+    verifyExceptionEqual(
+        "mariadb.org",
+        cert,
+        "DNS host \"mariadb.org\" doesn't correspond to certificate "
+            + "CN \"*.mariadb.org\" and SAN[{DNS:\"other.org\"},{DNS:\"www.other.org\"}]");
+    HostnameVerifier.verify("a.mariadb.org", cert, -1);
+    verifyExceptionEqual(
+        "a.other2.org",
+        cert,
+        "DNS host \"a.other2.org\" doesn't correspond to certificate "
+            + "CN \"*.mariadb.org\" and SAN[{DNS:\"other.org\"},{DNS:\"www.other.org\"}]");
+    HostnameVerifier.verify("other.org", cert, -1);
+    verifyExceptionEqual(
+        "a.other.org",
+        cert,
+        "DNS host \"a.other.org\" doesn't correspond to certificate "
+            + "CN \"*.mariadb.org\" and SAN[{DNS:\"other.org\"},{DNS:\"www.other.org\"}]");
+    HostnameVerifier.verify("www.other.org", cert, -1);
+  }
+
+  @Test
+  public void verifySubjectAltOnly() throws Exception {
+    // subjectAlt=foo.com
+    X509Certificate cert =
+        getCertificate(
+            ""
+                + "-----BEGIN CERTIFICATE-----\n"
+                + "MIIESjCCAzKgAwIBAgIJAIz+EYMBU6aYMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+                + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+                + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+                + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+                + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE2MjYxMFoXDTI4MTEwNTE2MjYxMFowgZIx\n"
+                + "CzAJBgNVBAYTAlVTMREwDwYDVQQIDAhNYXJ5bGFuZDEUMBIGA1UEBwwLRm9yZXN0\n"
+                + "IEhpbGwxFzAVBgNVBAoMDmh0dHBjb21wb25lbnRzMRowGAYDVQQLDBF0ZXN0IGNl\n"
+                + "cnRpZmljYXRlczElMCMGCSqGSIb3DQEJARYWanVsaXVzZGF2aWVzQGdtYWlsLmNv\n"
+                + "bTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMhjr5aCPoyp0R1iroWA\n"
+                + "fnEyBMGYWoCidH96yGPFjYLowez5aYKY1IOKTY2BlYho4O84X244QrZTRl8kQbYt\n"
+                + "xnGh4gSCD+Z8gjZ/gMvLUlhqOb+WXPAUHMB39GRyzerA/ZtrlUqf+lKo0uWcocxe\n"
+                + "Rc771KN8cPH3nHZ0rV0Hx4ZAZy6U4xxObe4rtSVY07hNKXAb2odnVqgzcYiDkLV8\n"
+                + "ilvEmoNWMWrp8UBqkTcpEhYhCYp3cTkgJwMSuqv8BqnGd87xQU3FVZI4tbtkB+Kz\n"
+                + "jD9zz8QCDJAfDjZHR03KNQ5mxOgXwxwKw6lGMaiVJTxpTKqym93whYk93l3ocEe5\n"
+                + "5c0CAwEAAaOBkDCBjTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NM\n"
+                + "IEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUnxR3vz86tso4gkJIFiza\n"
+                + "0Mteh9gwHwYDVR0jBBgwFoAUe5raj5CZTlLSrNuzA1LKh6YNPg0wEgYDVR0RBAsw\n"
+                + "CYIHZm9vLmNvbTANBgkqhkiG9w0BAQUFAAOCAQEAjl78oMjzFdsMy6F1sGg/IkO8\n"
+                + "tF5yUgPgFYrs41yzAca7IQu6G9qtFDJz/7ehh/9HoG+oqCCIHPuIOmS7Sd0wnkyJ\n"
+                + "Y7Y04jVXIb3a6f6AgBkEFP1nOT0z6kjT7vkA5LJ2y3MiDcXuRNMSta5PYVnrX8aZ\n"
+                + "yiqVUNi40peuZ2R8mAUSBvWgD7z2qWhF8YgDb7wWaFjg53I36vWKn90ZEti3wNCw\n"
+                + "qAVqixM+J0qJmQStgAc53i2aTMvAQu3A3snvH/PHTBo+5UL72n9S1kZyNCsVf1Qo\n"
+                + "n8jKTiRriEM+fMFlcgQP284EBFzYHyCXFb9O/hMjK2+6mY9euMB1U1aFFzM/Bg==\n"
+                + "-----END CERTIFICATE-----\n");
+    HostnameVerifier.verify("foo.com", cert, -1);
+    verifyExceptionEqual(
+        "a.foo.com",
+        cert,
+        "CN not found in certificate principal \"EMAILADDRESS=juliusdavies@gmail.com, OU=test"
+            + " certificates, O=httpcomponents, L=Forest Hill, ST=Maryland, C=US\" and DNS host"
+            + " \"a.foo.com\" doesn't correspond to SAN[{DNS:\"foo.com\"}]");
+  }
+
+  @Test
+  public void noCn() throws Exception {
+    // subjectAlt=foo.com
+    X509Certificate cert =
+        getCertificate(
+            "-----BEGIN CERTIFICATE-----\n"
+                + "MIIDazCCAlOgAwIBAgIUFxeXaoK5VSZBV1UdBhcDIB0abUAwDQYJKoZIhvcNAQEF\n"
+                + "BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM\n"
+                + "GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTA0MTQxNjIxMTlaFw0zMTA0\n"
+                + "MTIxNjIxMTlaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw\n"
+                + "HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB\n"
+                + "AQUAA4IBDwAwggEKAoIBAQDRmmo+05f5oqKFidWhk6+EbCNJzxJ7fwrGV13ZfqqP\n"
+                + "HSyxZIcWI/bK6BCjb9BxJa/QaaThr6x7GxI6PaDqRWInpuHepqIBL2arJLq2H3ys\n"
+                + "2zErfA5n0rFkryYW6zQUW/TJTxz5dbagemYA4TvS5Tshm0fimtNDTcv6Vb7U3OXc\n"
+                + "pa42VeLgaaM+OeCQlFH4OEXzGqXwqU090D2aRp05uPJRCFhwvMI9QXG2R8zXogTx\n"
+                + "TAlmxm4piKmg123TLd2N1TxJHxskg4OR5guO/XaG/Zji4KCKJ7dJFHjvNztG0Nme\n"
+                + "dxOo/+I/AeWhLEq81fzGMg4/BYeU1cLv6wnqFi4pbyWLAgMBAAGjUzBRMB0GA1Ud\n"
+                + "DgQWBBTLvLm0GjjbZOg5fJYgt80FvCgxNzAfBgNVHSMEGDAWgBTLvLm0GjjbZOg5\n"
+                + "fJYgt80FvCgxNzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBp\n"
+                + "3Z8UMM8y++04mKRZEP7y88dmmLOt64TRjJhTFoHvZsC/VZi5glt3/FnYxhlf8sfQ\n"
+                + "sJB+TRlVcEWBff6yJd/uUScff56Zgy2PCCL4rjgBjxgq/kf28m6tC8nHlx88qz0Y\n"
+                + "CZR9eEwTN8xgRvgUx5OFNewDYpkY0QAHkzAddCl5uaO6Mi0E34gSECIQ/cJ75xhQ\n"
+                + "K3Qy/qj1Kl7r80WJtzmhpZbpKbVZXa3NpwTWUfaD6WhNW0H/BhAnQq3XkXVWK6sW\n"
+                + "rLfHdWz9hQ79AiNaTc1I3YDyrnNEQBvHAFZ87Y8XIk4RaPzttLAfL/IKHuJdb85M\n"
+                + "cMq0UjgrIuCJoKB8kcm/\n"
+                + "-----END CERTIFICATE-----");
+    verifyExceptionEqual(
+        "a.foo.com",
+        cert,
+        "CN not found in certificate principal \"O=Internet Widgits Pty Ltd, ST=Some-State, C=AU\""
+            + " and certificate doesn't contain SAN");
+  }
+
+  @Test
+  public void verifyMultipleCn() throws Exception {
+    // CN=test1.org, CN=test2.org
+    X509Certificate cert =
+        getCertificate(
+            ""
+                + "-----BEGIN CERTIFICATE-----\n"
+                + "MIIDgzCCAmugAwIBAgIJAPqfD+J8D4gqMA0GCSqGSIb3DQEBBQUAMFcxCzAJBgNV\n"
+                + "BAYTAlVTMQswCQYDVQQIDAJDQTETMBEGA1UECgwKQWNtZSwgSW5jLjESMBAGA1UE\n"
+                + "AwwJdGVzdDEub3JnMRIwEAYDVQQDDAl0ZXN0Mi5vcmcwIBcNMTcwNjIzMTYxNDIx\n"
+                + "WhgPMjExNzA1MzAxNjE0MjFaMFcxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTET\n"
+                + "MBEGA1UECgwKQWNtZSwgSW5jLjESMBAGA1UEAwwJdGVzdDEub3JnMRIwEAYDVQQD\n"
+                + "DAl0ZXN0Mi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEB0t9\n"
+                + "aS/3zJHEAaOc2Vdh4tSXXNbyCL+uXfl3hZr/p/77/l+WeTSi3O4tNUhT6ExK9Rxu\n"
+                + "0el75du1LRpvWen5oLAmwkSJlAhtcjxEdtHwIFjYSpKBoLzjPD8WcwEtHVoGrpAZ\n"
+                + "xvRA1UEKy7vmx7h44vKsrBomEBr0gcv0C0jFo35Eo8hb/CNqU9Iy4noIYETpdyKa\n"
+                + "0XLInmwvwb9YidOrPAQ9QvRK85j8ah2goXcxhQE5vhuKKXJwccz7jQ1RPQmi1EuE\n"
+                + "OUosQfU0U0Ni8q8hLVDlJ5w8qJLa19y85E5q9nYU/DCIETLDZCQ+cAqSE7y3IQVw\n"
+                + "nY9JXJ+TTG9EbfwLAgMBAAGjUDBOMB0GA1UdDgQWBBSTKBaPh4ciLVgOkGZnnXjd\n"
+                + "v6gVPzAfBgNVHSMEGDAWgBSTKBaPh4ciLVgOkGZnnXjdv6gVPzAMBgNVHRMEBTAD\n"
+                + "AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQANlc974MeEIjEG8PzjDuiCbImZU/vxmBu1\n"
+                + "QD4mOfTjoixx/o9w/TbtnYhlugH3Nb2biaIx+2VnQAjk6euNBdFXW1cIawstrYGn\n"
+                + "KKEbZgQ7rgWfqyXIUK5NgX5jqxv5iW2xQE9nFGum8ouy8t+Nwi5F5uPGlhw/POnZ\n"
+                + "SLdP5i67GJN/Ho2HCfYOWm8STo0S7jmxtGoLcZ/EPaM3DaqLQYTdjtNKuotw1YuF\n"
+                + "A94gKVaU6XS6EdDGc6oSfKAR/pqKnWAmDc0ofvYniojquzm4fUO3JgzXN/xTDPUc\n"
+                + "GiY3dV92GD9wZfbUWsQRzLizRzIrsvZfCn/LLeUvOQPuCCeLzIxD\n"
+                + "-----END CERTIFICATE-----\n");
+    HostnameVerifier.verify("test1.org", cert, -1);
+    verifyExceptionEqual(
+        "test2.org",
+        cert,
+        "DNS host \"test2.org\" doesn't correspond to certificate CN \"test1.org\"");
+  }
+
+  @Test
+  public void verifyWilcardCn() throws Exception {
+    // CN=*.foo.com
+    X509Certificate cert =
+        getCertificate(
+            ""
+                + "-----BEGIN CERTIFICATE-----\n"
+                + "MIIESDCCAzCgAwIBAgIJAIz+EYMBU6aUMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+                + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+                + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+                + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+                + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE2MTU1NVoXDTI4MTEwNTE2MTU1NVowgaYx\n"
+                + "CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEUMBIGA1UEBxMLRm9yZXN0\n"
+                + "IEhpbGwxFzAVBgNVBAoTDmh0dHBjb21wb25lbnRzMRowGAYDVQQLExF0ZXN0IGNl\n"
+                + "cnRpZmljYXRlczESMBAGA1UEAxQJKi5mb28uY29tMSUwIwYJKoZIhvcNAQkBFhZq\n"
+                + "dWxpdXNkYXZpZXNAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n"
+                + "CgKCAQEAyGOvloI+jKnRHWKuhYB+cTIEwZhagKJ0f3rIY8WNgujB7PlpgpjUg4pN\n"
+                + "jYGViGjg7zhfbjhCtlNGXyRBti3GcaHiBIIP5nyCNn+Ay8tSWGo5v5Zc8BQcwHf0\n"
+                + "ZHLN6sD9m2uVSp/6UqjS5ZyhzF5FzvvUo3xw8fecdnStXQfHhkBnLpTjHE5t7iu1\n"
+                + "JVjTuE0pcBvah2dWqDNxiIOQtXyKW8Sag1YxaunxQGqRNykSFiEJindxOSAnAxK6\n"
+                + "q/wGqcZ3zvFBTcVVkji1u2QH4rOMP3PPxAIMkB8ONkdHTco1DmbE6BfDHArDqUYx\n"
+                + "qJUlPGlMqrKb3fCFiT3eXehwR7nlzQIDAQABo3sweTAJBgNVHRMEAjAAMCwGCWCG\n"
+                + "SAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4E\n"
+                + "FgQUnxR3vz86tso4gkJIFiza0Mteh9gwHwYDVR0jBBgwFoAUe5raj5CZTlLSrNuz\n"
+                + "A1LKh6YNPg0wDQYJKoZIhvcNAQEFBQADggEBAH0ipG6J561UKUfgkeW7GvYwW98B\n"
+                + "N1ZooWX+JEEZK7+Pf/96d3Ij0rw9ACfN4bpfnCq0VUNZVSYB+GthQ2zYuz7tf/UY\n"
+                + "A6nxVgR/IjG69BmsBl92uFO7JTNtHztuiPqBn59pt+vNx4yPvno7zmxsfI7jv0ww\n"
+                + "yfs+0FNm7FwdsC1k47GBSOaGw38kuIVWqXSAbL4EX9GkryGGOKGNh0qvAENCdRSB\n"
+                + "G9Z6tyMbmfRY+dLSh3a9JwoEcBUso6EWYBakLbq4nG/nvYdYvG9ehrnLVwZFL82e\n"
+                + "l3Q/RK95bnA6cuRClGusLad0e6bjkBzx/VQ3VarDEpAkTLUGVAa0CLXtnyc=\n"
+                + "-----END CERTIFICATE-----\n");
+    verifyExceptionEqual(
+        "foo.com", cert, "DNS host \"foo.com\" doesn't correspond to certificate CN \"*.foo.com\"");
+    HostnameVerifier.verify("www.foo.com", cert, -1);
+    HostnameVerifier.verify("花子.foo.com", cert, -1);
+    verifyExceptionEqual(
+        "a.b.foo.com",
+        cert,
+        "DNS host \"a.b.foo.com\" doesn't correspond to certificate CN \"*.foo.com\"");
+  }
+
+  @Test
+  public void verifyWilcardCnOnTld() throws Exception {
+    // It's the CA's responsibility to not issue broad-matching certificates!
+    // CN=*.co.jp
+    X509Certificate cert =
+        getCertificate(
+            ""
+                + "-----BEGIN CERTIFICATE-----\n"
+                + "MIIERjCCAy6gAwIBAgIJAIz+EYMBU6aVMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+                + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+                + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+                + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+                + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE2MTYzMFoXDTI4MTEwNTE2MTYzMFowgaQx\n"
+                + "CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEUMBIGA1UEBxMLRm9yZXN0\n"
+                + "IEhpbGwxFzAVBgNVBAoTDmh0dHBjb21wb25lbnRzMRowGAYDVQQLExF0ZXN0IGNl\n"
+                + "cnRpZmljYXRlczEQMA4GA1UEAxQHKi5jby5qcDElMCMGCSqGSIb3DQEJARYWanVs\n"
+                + "aXVzZGF2aWVzQGdtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\n"
+                + "ggEBAMhjr5aCPoyp0R1iroWAfnEyBMGYWoCidH96yGPFjYLowez5aYKY1IOKTY2B\n"
+                + "lYho4O84X244QrZTRl8kQbYtxnGh4gSCD+Z8gjZ/gMvLUlhqOb+WXPAUHMB39GRy\n"
+                + "zerA/ZtrlUqf+lKo0uWcocxeRc771KN8cPH3nHZ0rV0Hx4ZAZy6U4xxObe4rtSVY\n"
+                + "07hNKXAb2odnVqgzcYiDkLV8ilvEmoNWMWrp8UBqkTcpEhYhCYp3cTkgJwMSuqv8\n"
+                + "BqnGd87xQU3FVZI4tbtkB+KzjD9zz8QCDJAfDjZHR03KNQ5mxOgXwxwKw6lGMaiV\n"
+                + "JTxpTKqym93whYk93l3ocEe55c0CAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgB\n"
+                + "hvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYE\n"
+                + "FJ8Ud78/OrbKOIJCSBYs2tDLXofYMB8GA1UdIwQYMBaAFHua2o+QmU5S0qzbswNS\n"
+                + "yoemDT4NMA0GCSqGSIb3DQEBBQUAA4IBAQA0sWglVlMx2zNGvUqFC73XtREwii53\n"
+                + "CfMM6mtf2+f3k/d8KXhLNySrg8RRlN11zgmpPaLtbdTLrmG4UdAHHYr8O4y2BBmE\n"
+                + "1cxNfGxxechgF8HX10QV4dkyzp6Z1cfwvCeMrT5G/V1pejago0ayXx+GPLbWlNeZ\n"
+                + "S+Kl0m3p+QplXujtwG5fYcIpaGpiYraBLx3Tadih39QN65CnAh/zRDhLCUzKyt9l\n"
+                + "UGPLEUDzRHMPHLnSqT1n5UU5UDRytbjJPXzF+l/+WZIsanefWLsxnkgAuZe/oMMF\n"
+                + "EJMryEzOjg4Tfuc5qM0EXoPcQ/JlheaxZ40p2IyHqbsWV4MRYuFH4bkM\n"
+                + "-----END CERTIFICATE-----\n");
+    HostnameVerifier.verify("foo.co.jp", cert, -1);
+    HostnameVerifier.verify("花子.co.jp", cert, -1);
+  }
+
+  @Test
+  public void subjectAltUsesLocalDomainAndIp() throws Exception {
+    // /C=CN/ST=GD/L=SZ/O=Acme, Inc./CN=*.mariadb.org,
+    // subjectAltName=DNS:localhost.localdomain,DNS:localhost,IP:127.0.0.1
+    X509Certificate cert =
+        getCertificate(
+            ""
+                + "-----BEGIN CERTIFICATE-----\n"
+                + "MIIDfDCCAmSgAwIBAgIURZJQVOWv+oaj+MLlHWc1B0TnOaowDQYJKoZIhvcNAQEL\n"
+                + "BQAwUjELMAkGA1UEBhMCQ04xCzAJBgNVBAgMAkdEMQswCQYDVQQHDAJTWjESMBAG\n"
+                + "A1UECgwJQWNtZSxJbmMuMRUwEwYDVQQDDAxBY21lIFJvb3QgQ0EwIBcNMjEwMzMw\n"
+                + "MDkwODAxWhgPMjEyMTAzMDYwOTA4MDFaMFMxCzAJBgNVBAYTAkNOMQswCQYDVQQI\n"
+                + "DAJHRDELMAkGA1UEBwwCU1oxEjAQBgNVBAoMCUFjbWUsSW5jLjEWMBQGA1UEAwwN\n"
+                + "Ki5tYXJpYWRiLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAJ\n"
+                + "xqbqTGmwO5n3kVd6QJPRSh+0M1HIQacyM/tkE7jLw3725/KtknuwuFbPpxKyTCLC\n"
+                + "IoNx4yaBbmx783OPP3pokXTWiMdrVZdLltBNamNzekNFN4YhR5oN479M5cKgrk94\n"
+                + "Ud+ql0NN5FscrSQ0fSdJf0idJMqThro1MJVp9rp5cdCba6/lKyDbdOybe5f7rmrg\n"
+                + "+37J+src67+rqwVT8ZwZgLTGDf4X9OSIzyw6+PCWYWr89aurrOuOyqA3QqXVRZa/\n"
+                + "IxOMHIdzXMgLN6+HduwdZ+DNv1NPT2MDlRQvOnDop3NoEVKWekOTv50LbKRgWTYO\n"
+                + "TK/dfcsDpZmdyHv7pb8CAwEAAaNHMEUwQwYDVR0RBDwwOoIVbG9jYWxob3N0Lmxv\n"
+                + "Y2FsZG9tYWlugglsb2NhbGhvc3SHBH8AAAGHECABDbg5AjRoAAAAAAAABEMwDQYJ\n"
+                + "KoZIhvcNAQELBQADggEBAHsiJz9cpmL8BTa/o10S+pmap3iOnYYuJT0llCRLJ+Ji\n"
+                + "msO2niyIwqCJHMLcEABCENJt0HDOEKlnunVgc+X/6K8DnPrYhfWQbYI/dwUBoSIQ\n"
+                + "siK/yKW0q+S+YjCVpNMA3iMfhJ9Qe9LDO+xdCBhzplgrV8YwG+J2FUNbZfvl5cML\n"
+                + "TjKLWrWo9dgZyH/7mjwryRzswfUfr/lRARCyrMotaXfYmjPjwTSRc0aPGrEjs3ns\n"
+                + "WMtimgh7Zw3Tbxc51miz9CRy767lq/9BGTdeBLmW0EXssIJb9uO0Ht3C/Pqy0ojk\n"
+                + "8e1eYtofjTsqWHZ1s2LhtT0HvXdL6BnWP9GWc/zxiKM=\n"
+                + "-----END CERTIFICATE-----\n");
+    assertEquals(
+        new X500Principal("CN=*.mariadb.org, O=\"Acme,Inc.\", L=SZ, ST=GD, C=CN"),
+        cert.getSubjectX500Principal());
+
+    HostnameVerifier.verify("localhost", cert, -1);
+    HostnameVerifier.verify("localhost.localdomain", cert, -1);
+    verifyExceptionEqual(
+        "local.host",
+        cert,
+        "DNS host \"local.host\" doesn't correspond to certificate CN \"*.mariadb.org\" and"
+            + " SAN[{DNS:\"localhost.localdomain\"},{DNS:\"localhost\"},{IP:\"127.0.0.1\"},{IP:\"2001:db8:3902:3468:0:0:0:443\"}]");
+
+    HostnameVerifier.verify("127.0.0.1", cert, -1);
+    verifyExceptionEqual(
+        "127.0.0.2",
+        cert,
+        "IPv4 host \"127.0.0.2\" doesn't correspond to certificate CN \"*.mariadb.org\" and"
+            + " SAN[{DNS:\"localhost.localdomain\"},{DNS:\"localhost\"},{IP:\"127.0.0.1\"},{IP:\"2001:db8:3902:3468:0:0:0:443\"}]");
+
+    HostnameVerifier.verify("2001:db8:3902:3468:0:0:0:443", cert, -1);
+    verifyExceptionEqual(
+        "2001:db8:1::",
+        cert,
+        "IPv6 host \"2001:db8:1::\" doesn't correspond to certificate CN \"*.mariadb.org\" and"
+            + " SAN[{DNS:\"localhost.localdomain\"},{DNS:\"localhost\"},{IP:\"127.0.0.1\"},{IP:\"2001:db8:3902:3468:0:0:0:443\"}]");
+  }
+
+  @Test
+  public void wildcardsCannotMatchIpAddresses() throws Exception {
+    // openssl req -x509 -nodes -days 36500 -subj '/CN=*.0.0.1' -newkey rsa:512 -out cert.pem
+    X509Certificate cert =
+        getCertificate(
+            ""
+                + "-----BEGIN CERTIFICATE-----\n"
+                + "MIIDVzCCAj+gAwIBAgIJAN9L/Y9e1F7dMA0GCSqGSIb3DQEBBQUAMEExCzAJBgNV\n"
+                + "BAYTAlVTMQswCQYDVQQIDAJDQTETMBEGA1UECgwKQWNtZSwgSW5jLjEQMA4GA1UE\n"
+                + "AwwHKi4wLjAuMTAgFw0xNzA2MjMxNTU3MTdaGA8yMTE3MDUzMDE1NTcxN1owQTEL\n"
+                + "MAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRMwEQYDVQQKDApBY21lLCBJbmMuMRAw\n"
+                + "DgYDVQQDDAcqLjAuMC4xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA\n"
+                + "xAdLfWkv98yRxAGjnNlXYeLUl1zW8gi/rl35d4Wa/6f++/5flnk0otzuLTVIU+hM\n"
+                + "SvUcbtHpe+XbtS0ab1np+aCwJsJEiZQIbXI8RHbR8CBY2EqSgaC84zw/FnMBLR1a\n"
+                + "Bq6QGcb0QNVBCsu75se4eOLyrKwaJhAa9IHL9AtIxaN+RKPIW/wjalPSMuJ6CGBE\n"
+                + "6XcimtFyyJ5sL8G/WInTqzwEPUL0SvOY/GodoKF3MYUBOb4biilycHHM+40NUT0J\n"
+                + "otRLhDlKLEH1NFNDYvKvIS1Q5SecPKiS2tfcvOROavZ2FPwwiBEyw2QkPnAKkhO8\n"
+                + "tyEFcJ2PSVyfk0xvRG38CwIDAQABo1AwTjAdBgNVHQ4EFgQUkygWj4eHIi1YDpBm\n"
+                + "Z5143b+oFT8wHwYDVR0jBBgwFoAUkygWj4eHIi1YDpBmZ5143b+oFT8wDAYDVR0T\n"
+                + "BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAAE4YYuS94g9EyIRvPeXcHlJsjG1n\n"
+                + "moSZIVgSuKjLAb31SOyY+2c534SUELED7ECEb4yKM7WmWctpF0anFZUDCczuo+zl\n"
+                + "uuTv1k/TE9GBWizxZgu/vX7+FAbxAgkA9Jxn2phJlks+PwnUClzVBzJ77zPNzIO8\n"
+                + "6s8waZr9ttnASBHVaeSKkknI+gas5KpvY+B4eRxZx0G8Fyher29yIiE44Z6RHzjI\n"
+                + "+EnURTvdjd2ZuY5QKvwlBQssqOHxDATg8pL6JmgnrvbYqh+FBpUN8sqwrXx6q8dz\n"
+                + "aUH7ncQGgwZBAUIiQaKlb0QYpcyrMlGWNri+RFt+Goz5S3BxxobwfiaBoA==\n"
+                + "-----END CERTIFICATE-----\n");
+    verifyExceptionEqual(
+        "127.0.0.1",
+        cert,
+        "IPv4 host \"127.0.0.1\" doesn't correspond to "
+            + "certificate CN \"*.0.0.1\" : wildcards not possible for IPs");
+  }
+
+  @Test
+  public void subjectAltNameWithWildcard() throws Exception {
+    // subjectAltName=DNS:*.other.org,DNS:a*b.other2.com
+    X509Certificate cert =
+        getCertificate(
+            ""
+                + "-----BEGIN CERTIFICATE-----\n"
+                + "MIIDVjCCAj6gAwIBAgIJAPMG38xrY9DaMA0GCSqGSIb3DQEBCwUAMFMxCzAJBgNV\n"
+                + "BAYTAkNOMQswCQYDVQQIDAJHRDELMAkGA1UEBwwCU1oxEzARBgNVBAoMCkFjbWUs\n"
+                + "IEluYy4xFTATBgNVBAMMDEFjbWUgUm9vdCBDQTAgFw0xNzA2MjMxNjA1MTlaGA8y\n"
+                + "MTE3MDUzMDE2MDUxOVowVDELMAkGA1UEBhMCQ04xCzAJBgNVBAgMAkdEMQswCQYD\n"
+                + "VQQHDAJTWjETMBEGA1UECgwKQWNtZSwgSW5jLjEWMBQGA1UEAwwNKi5tYXJpYWRi\n"
+                + "Lm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKqUYr7IHOuE1FD3\n"
+                + "4AX/23DZ/VnK3U/sZNLfdP9322pO5HP4yv3HzHNtkl+3s/jKnRBwOpGjvmeoVgro\n"
+                + "B8NK7Prs8tCMET9yfFXg/gSkoMAnR4g1jExB9bszTRN1+5dmLZK7xoKcRYtdKCLB\n"
+                + "AeGYAx6lSKFkc5sAuU8e9k9iAdD+j2w4s5UbP2QTK6N97+EMGTYjQ91ynsxzUltX\n"
+                + "5ueRLbg0M5WyRZpB4oH3J5T2L+NzpjU60Lhny/Kz5fCDizkwYjYBV+p9bhneklia\n"
+                + "ByBxGm/y+yrDm5RDRICws5UhjWEe5ztOrfLwjZGmkYgcr2iuVqR3yhXoQIrxK1OV\n"
+                + "8RnNJSECAwEAAaMqMCgwJgYDVR0RBB8wHYILKi5vdGhlci5vcmeCDmEqYi5vdGhl\n"
+                + "cjIuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQAAgqjYSzvgc+lUa/8gEpX9QJVvvDN9\n"
+                + "nKqsJIB8G7uSGQgjq1eA8LrklTo1X3uER2+dLfoHIvJJxzuqRF6ugDnHMW+ocITY\n"
+                + "yYkvb1Ok/aKo9e9sEKhndT47A9fjGoN94xhEEfVL8oc2g5gnNQ/+YHwO0vajdh2V\n"
+                + "CMpkFvvSClvomb91u/leWwu1C07dJWHM2OzldEmlQK9sm847YofEfXe5FZXt+Py2\n"
+                + "zpmwb3/djqBpSwdMgBB3us2wEiHN95EGRCT8BmTZ4gFtfdXt6uAZOd93NAoYlmpV\n"
+                + "Flo8jrfEOHRCrdYqXobC/YVuxk+1h+Q2Nu5mKzbc3XfpG1LGGZB98+FP\n"
+                + "-----END CERTIFICATE-----\n");
+
+    verifyExceptionEqual(
+        "other.org",
+        cert,
+        "DNS host \"other.org\" doesn't correspond to certificate CN \"*.mariadb.org\" and"
+            + " SAN[{DNS:\"*.other.org\"},{DNS:\"a*b.other2.com\"}]");
+    HostnameVerifier.verify("www.other.org", cert, -1);
+    verifyExceptionEqual(
+        "other2.org",
+        cert,
+        "DNS host \"other2.org\" doesn't correspond to certificate CN \"*.mariadb.org\" and"
+            + " SAN[{DNS:\"*.other.org\"},{DNS:\"a*b.other2.com\"}]");
+    verifyExceptionEqual(
+        "www.other2.org",
+        cert,
+        "DNS host \"www.other2.org\" doesn't correspond to certificate CN \"*.mariadb.org\" and"
+            + " SAN[{DNS:\"*.other.org\"},{DNS:\"a*b.other2.com\"}]");
+    HostnameVerifier.verify("ab.other2.com", cert, -1);
+    HostnameVerifier.verify("axxxxb.other2.com", cert, -1);
+    verifyExceptionEqual(
+        "axxxxbc.other2.org",
+        cert,
+        "DNS host \"axxxxbc.other2.org\" doesn't correspond to certificate CN \"*.mariadb.org\" and"
+            + " SAN[{DNS:\"*.other.org\"},{DNS:\"a*b.other2.com\"}]");
+    verifyExceptionEqual(
+        "caxxxxb.other2.org",
+        cert,
+        "DNS host \"caxxxxb.other2.org\" doesn't correspond to certificate CN \"*.mariadb.org\" and"
+            + " SAN[{DNS:\"*.other.org\"},{DNS:\"a*b.other2.com\"}]");
+    verifyExceptionEqual(
+        "a.axxxxb.other2.org",
+        cert,
+        "DNS host \"a.axxxxb.other2.org\" doesn't correspond to certificate CN \"*.mariadb.org\""
+            + " and SAN[{DNS:\"*.other.org\"},{DNS:\"a*b.other2.com\"}]");
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/client/PrepareCacheTest.java b/src/test/java/org/mariadb/jdbc/unit/client/PrepareCacheTest.java
new file mode 100644
index 0000000..4f06e34
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/client/PrepareCacheTest.java
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.unit.client;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.client.impl.PrepareCache;
+import org.mariadb.jdbc.message.server.PrepareResultPacket;
+
+public class PrepareCacheTest {
+
+  @Test
+  public void check() {
+    PrepareCache cache = new PrepareCache(20, null);
+    try {
+      cache.get("dd");
+      fail();
+    } catch (IllegalStateException s) {
+      assertTrue(s.getMessage().contains("not available method"));
+    }
+    try {
+      cache.put("dd", (PrepareResultPacket) null);
+      fail();
+    } catch (IllegalStateException s) {
+      assertTrue(s.getMessage().contains("not available method"));
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/client/ServerVersionUtilityTest.java b/src/test/java/org/mariadb/jdbc/unit/client/ServerVersionUtilityTest.java
new file mode 100644
index 0000000..2107533
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/client/ServerVersionUtilityTest.java
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.unit.client;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.message.server.util.ServerVersionUtility;
+
+public class ServerVersionUtilityTest {
+
+  @Test
+  public void check() {
+    ServerVersionUtility ver = new ServerVersionUtility("10.5.2", true);
+    assertEquals(10, ver.getMajorVersion());
+    assertEquals(5, ver.getMinorVersion());
+    assertEquals(2, ver.getPatchVersion());
+    assertEquals("10.5.2", ver.getVersion());
+    assertTrue(ver.isMariaDBServer());
+    assertTrue(ver.versionGreaterOrEqual(10, 5, 1));
+    assertTrue(ver.versionGreaterOrEqual(10, 4, 5));
+    assertTrue(ver.versionGreaterOrEqual(5, 6, 5));
+    assertTrue(ver.versionGreaterOrEqual(10, 5, 2));
+    assertFalse(ver.versionGreaterOrEqual(10, 5, 3));
+    assertFalse(ver.versionGreaterOrEqual(10, 6, 0));
+    assertFalse(ver.versionGreaterOrEqual(11, 0, 0));
+
+    ver = new ServerVersionUtility("10.5.2-MariaDB", true);
+    assertEquals(10, ver.getMajorVersion());
+    assertEquals(5, ver.getMinorVersion());
+    assertEquals(2, ver.getPatchVersion());
+    assertEquals("10.5.2-MariaDB", ver.getVersion());
+    assertTrue(ver.isMariaDBServer());
+    assertTrue(ver.versionGreaterOrEqual(10, 5, 1));
+    assertTrue(ver.versionGreaterOrEqual(10, 4, 5));
+    assertTrue(ver.versionGreaterOrEqual(5, 6, 5));
+    assertTrue(ver.versionGreaterOrEqual(10, 5, 2));
+    assertFalse(ver.versionGreaterOrEqual(10, 5, 3));
+    assertFalse(ver.versionGreaterOrEqual(10, 6, 0));
+    assertFalse(ver.versionGreaterOrEqual(11, 0, 0));
+
+    ver = new ServerVersionUtility("8.0.12-something", false);
+    assertEquals(8, ver.getMajorVersion());
+    assertEquals(0, ver.getMinorVersion());
+    assertEquals(12, ver.getPatchVersion());
+    assertEquals("8.0.12-something", ver.getVersion());
+    assertFalse(ver.isMariaDBServer());
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/client/result/CompleteResultTest.java b/src/test/java/org/mariadb/jdbc/unit/client/result/CompleteResultTest.java
new file mode 100644
index 0000000..a9e6159
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/client/result/CompleteResultTest.java
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.unit.client.result;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.result.CompleteResult;
+import org.mariadb.jdbc.integration.Common;
+
+public class CompleteResultTest extends Common {
+
+  /** SELECT query cannot be rewritable. */
+  @Test
+  public void metaQuery() throws SQLException {
+    // SELECT query cannot be rewritable
+    String[] columnNames =
+        new String[] {
+          "small",
+          "big123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"
+        };
+    DataType[] columnTypes = new DataType[] {DataType.STRING, DataType.STRING};
+    String[][] data =
+        new String[][] {
+          new String[] {
+            "small",
+            "big123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"
+          }
+        };
+    ResultSet rs =
+        CompleteResult.createResultSet(columnNames, columnTypes, data, sharedConn.getContext(), 0);
+    assertTrue(rs.next());
+    assertEquals(data[0][0], rs.getString(1));
+    assertEquals(data[0][1], rs.getString(2));
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/client/socket/PacketWriterTest.java b/src/test/java/org/mariadb/jdbc/unit/client/socket/PacketWriterTest.java
new file mode 100644
index 0000000..0b86e16
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/client/socket/PacketWriterTest.java
@@ -0,0 +1,25 @@
+package org.mariadb.jdbc.unit.client.socket;
+
+import java.io.IOException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.client.socket.impl.PacketWriter;
+
+public class PacketWriterTest {
+
+  @Test
+  public void growBuffer() throws IOException {
+    PacketWriter pw = new PacketWriter(null, 0, 0xffffff, null, null);
+    Assertions.assertEquals(4, pw.pos());
+    pw.writeBytes(new byte[8190], 0, 8190);
+    pw.writeAscii("abcdefghij");
+    Assertions.assertEquals(8200, pw.pos() - 4);
+
+    for (int i = 0; i < 8190; i++) {
+      Assertions.assertEquals(0, pw.buf()[i + 4]);
+    }
+    for (int i = 0; i < 10; i++) {
+      Assertions.assertEquals('a' + i, pw.buf()[i + 8194]);
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/client/tls/MariaDbX509KeyManagerTest.java b/src/test/java/org/mariadb/jdbc/unit/client/tls/MariaDbX509KeyManagerTest.java
new file mode 100644
index 0000000..b471f2a
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/client/tls/MariaDbX509KeyManagerTest.java
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.unit.client.tls;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.*;
+import java.security.cert.CertificateException;
+import javax.security.auth.x500.X500Principal;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.client.tls.MariaDbX509KeyManager;
+
+public class MariaDbX509KeyManagerTest {
+
+  @Test
+  public void check() throws Exception {
+    MariaDbX509KeyManager keyMger = get();
+    String[] aliases = keyMger.getClientAliases("RSA", null);
+    assertEquals(1, aliases.length);
+    assertEquals("mysqlalias", aliases[0]);
+
+    assertNull(
+        keyMger.getClientAliases(
+            "RSA", new Principal[] {new X500Principal("CN=Android Debug,O=Android,C=US")}));
+    aliases =
+        keyMger.getClientAliases(
+            "RSA",
+            new Principal[] {
+              new X500Principal("EMAILADDRESS=X, OU=X, CN=ca.example.com, L=X, O=X, ST=X, C=XX")
+            });
+    assertEquals(1, aliases.length);
+    assertEquals("mysqlalias", aliases[0]);
+
+    assertNull(keyMger.getPrivateKey("wrong"));
+    assertNotNull(keyMger.getPrivateKey("mysqlalias"));
+
+    assertNull(keyMger.getCertificateChain("wrong"));
+    assertNotNull(keyMger.getCertificateChain("mysqlalias"));
+
+    assertEquals(
+        "mysqlalias",
+        keyMger.chooseEngineClientAlias(
+            new String[] {"RSA"},
+            new Principal[] {
+              new X500Principal("EMAILADDRESS=X, OU=X, CN=ca.example.com, L=X, O=X, ST=X, C=XX")
+            },
+            null));
+    assertNull(
+        keyMger.chooseEngineClientAlias(
+            new String[] {},
+            new Principal[] {
+              new X500Principal("EMAILADDRESS=X, OU=X, CN=ca.example.com, L=X, O=X, ST=X, C=XX")
+            },
+            null));
+    assertNull(
+        keyMger.chooseEngineClientAlias(
+            null,
+            new Principal[] {
+              new X500Principal("EMAILADDRESS=X, OU=X, CN=ca.example.com, L=X, O=X, ST=X, C=XX")
+            },
+            null));
+    assertNull(keyMger.getServerAliases("RSA", null));
+    assertNull(keyMger.chooseServerAlias("RSA", null, null));
+    assertNull(keyMger.chooseEngineServerAlias("RSA", null, null));
+  }
+
+  private MariaDbX509KeyManager get()
+      throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException {
+    try (InputStream inStream =
+        MariaDbX509KeyManagerTest.class
+            .getClassLoader()
+            .getResourceAsStream("testclient-keystore.p12")) {
+      assertNotNull(inStream);
+      char[] keyStorePasswordChars = "kspass".toCharArray();
+      KeyStore ks = KeyStore.getInstance("PKCS12");
+      ks.load(inStream, keyStorePasswordChars);
+      return new MariaDbX509KeyManager(ks, keyStorePasswordChars);
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/client/tls/MariaDbX509TrustingManagerTest.java b/src/test/java/org/mariadb/jdbc/unit/client/tls/MariaDbX509TrustingManagerTest.java
new file mode 100644
index 0000000..b8dd9f5
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/client/tls/MariaDbX509TrustingManagerTest.java
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.unit.client.tls;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.client.tls.MariaDbX509TrustingManager;
+
+public class MariaDbX509TrustingManagerTest {
+
+  @Test
+  public void check() {
+    MariaDbX509TrustingManager trustingManager = new MariaDbX509TrustingManager();
+    assertNull(trustingManager.getAcceptedIssuers());
+    trustingManager.checkClientTrusted(null, null);
+    trustingManager.checkServerTrusted(null, null);
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/plugin/AuthenticationPluginLoaderTest.java b/src/test/java/org/mariadb/jdbc/unit/plugin/AuthenticationPluginLoaderTest.java
new file mode 100644
index 0000000..9e58c84
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/plugin/AuthenticationPluginLoaderTest.java
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.unit.plugin;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.Configuration;
+import org.mariadb.jdbc.integration.Common;
+import org.mariadb.jdbc.plugin.AuthenticationPlugin;
+import org.mariadb.jdbc.plugin.authentication.AuthenticationPluginLoader;
+import org.mariadb.jdbc.plugin.authentication.standard.NativePasswordPlugin;
+
+public class AuthenticationPluginLoaderTest extends Common {
+
+  @Test
+  public void authenticationPluginLoaderTest() throws SQLException {
+    Configuration conf = Configuration.parse("jdbc:mariadb://localhost/");
+    AuthenticationPlugin authenticationPlugin =
+        AuthenticationPluginLoader.get("mysql_native_password", conf);
+    assertTrue(authenticationPlugin instanceof NativePasswordPlugin);
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> AuthenticationPluginLoader.get("UNKNOWN", conf),
+        "Client does not support authentication protocol requested by server");
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/plugin/TlsSocketPluginLoaderTest.java b/src/test/java/org/mariadb/jdbc/unit/plugin/TlsSocketPluginLoaderTest.java
new file mode 100644
index 0000000..1b416ee
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/plugin/TlsSocketPluginLoaderTest.java
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.unit.plugin;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.integration.Common;
+import org.mariadb.jdbc.plugin.TlsSocketPlugin;
+import org.mariadb.jdbc.plugin.tls.TlsSocketPluginLoader;
+import org.mariadb.jdbc.plugin.tls.main.DefaultTlsSocketPlugin;
+
+public class TlsSocketPluginLoaderTest extends Common {
+
+  @Test
+  public void AuthenticationPluginLoaderTest() throws SQLException {
+    TlsSocketPlugin tlsSocketPlugin = TlsSocketPluginLoader.get("DEFAULT");
+    assertTrue(tlsSocketPlugin instanceof DefaultTlsSocketPlugin);
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> TlsSocketPluginLoader.get("UNKNOWN"),
+        "Client has not found any TLS factory plugin with name 'UNKNOWN'");
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/type/GeometryTest.java b/src/test/java/org/mariadb/jdbc/unit/type/GeometryTest.java
new file mode 100644
index 0000000..fb4b880
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/type/GeometryTest.java
@@ -0,0 +1,308 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.unit.type;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.client.ColumnDecoder;
+import org.mariadb.jdbc.client.DataType;
+import org.mariadb.jdbc.client.ReadableByteBuf;
+import org.mariadb.jdbc.client.impl.StandardReadableByteBuf;
+import org.mariadb.jdbc.type.*;
+
+public class GeometryTest {
+
+  /* s must be an even-length string. */
+  public static byte[] hexStringToByteArray(String s1) {
+    String s = s1.replace(" ", "");
+    int len = s.length();
+    byte[] data = new byte[len / 2];
+    for (int i = 0; i < len; i += 2) {
+      data[i / 2] =
+          (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
+    }
+    return data;
+  }
+
+  @Test
+  public void testPointEncoding() throws SQLException {
+    byte[] ptBytes = hexStringToByteArray("000000000140000000000000004010000000000000");
+    ReadableByteBuf readBuf = new StandardReadableByteBuf(ptBytes, ptBytes.length);
+    Geometry geo = Geometry.getGeometry(readBuf, ptBytes.length, null);
+    assertEquals("POINT(2.0 4.0)", geo.toString());
+    assertEquals(geo, geo);
+
+    assertEquals(new Point(2, 4), geo);
+    assertEquals(new Point(2, 4).hashCode(), geo.hashCode());
+    assertNotEquals("wrong", geo);
+    assertNotEquals(new Point(2, 5), geo);
+  }
+
+  @Test
+  public void testLineStringEncoding() throws SQLException {
+    String lineBigEndian =
+        "00"
+            + "00000002"
+            + "00000003"
+            + "0000000000000000"
+            + "0000000000000000"
+            + "00 00 00 00 00 00 00 00"
+            + "40 24 00 00 00 00 00 00"
+            + "40 24 00 00 00 00 00 00"
+            + "00 00 00 00 00 00 00 00";
+    byte[] lineBytes = hexStringToByteArray(lineBigEndian);
+    ReadableByteBuf readBuf = new StandardReadableByteBuf(lineBytes, lineBytes.length);
+    Geometry geo = Geometry.getGeometry(readBuf, lineBytes.length, null);
+    assertEquals("LINESTRING(0.0 0.0,0.0 10.0,10.0 0.0)", geo.toString());
+    assertEquals(geo, geo);
+    assertEquals(
+        new LineString(new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0)}, true),
+        geo);
+    assertEquals(
+        new LineString(new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0)}, true)
+            .hashCode(),
+        geo.hashCode());
+    assertNotEquals("wrong", geo);
+    assertNotEquals(
+        new LineString(new Point[] {new Point(1, 0), new Point(0, 10), new Point(10, 0)}, true),
+        geo);
+  }
+
+  @Test
+  public void testPolygonStringEncoding() throws SQLException {
+    String polygonBigEndian =
+        "00  "
+            + "00 00 00 03  "
+            + "00 00 00 01  "
+            + "00 00 00 07  "
+            + "3F F0 00 00 00 00 00 00  "
+            + "3F F0 00 00 00 00 00 00  "
+            + "3F F0 00 00 00 00 00 00  "
+            + "40 14 00 00 00 00 00 00  "
+            + "40 10 00 00 00 00 00 00  "
+            + "40 22 00 00 00 00 00 00  "
+            + "40 18 00 00 00 00 00 00  "
+            + "40 22 00 00 00 00 00 00  "
+            + "40 22 00 00 00 00 00 00  "
+            + "40 08 00 00 00 00 00 00  "
+            + "40 1C 00 00 00 00 00 00  "
+            + "40 00 00 00 00 00 00 00  "
+            + "3F F0 00 00 00 00 00 00  "
+            + "3F F0 00 00 00 00 00 00";
+    byte[] lineBytes = hexStringToByteArray(polygonBigEndian);
+    ReadableByteBuf readBuf = new StandardReadableByteBuf(lineBytes, lineBytes.length);
+    Geometry geo = Geometry.getGeometry(readBuf, lineBytes.length, null);
+    assertEquals(
+        "POLYGON((1.0 1.0,1.0 5.0,4.0 9.0,6.0 9.0,9.0 3.0,7.0 2.0,1.0 1.0))", geo.toString());
+    assertEquals(geo, geo);
+    assertEquals(
+        new Polygon(
+            new LineString[] {
+              new LineString(
+                  new Point[] {
+                    new Point(1, 1),
+                    new Point(1, 5),
+                    new Point(4, 9),
+                    new Point(6, 9),
+                    new Point(9, 3),
+                    new Point(7, 2),
+                    new Point(1, 1)
+                  },
+                  false)
+            }),
+        geo);
+    assertEquals(
+        new Polygon(
+                new LineString[] {
+                  new LineString(
+                      new Point[] {
+                        new Point(1, 1),
+                        new Point(1, 5),
+                        new Point(4, 9),
+                        new Point(6, 9),
+                        new Point(9, 3),
+                        new Point(7, 2),
+                        new Point(1, 1)
+                      },
+                      false)
+                })
+            .hashCode(),
+        geo.hashCode());
+    assertNotEquals("wrong", geo);
+    assertNotEquals(
+        new Polygon(
+            new LineString[] {
+              new LineString(new Point[] {new Point(1, 1), new Point(1, 5), new Point(1, 1)}, false)
+            }),
+        geo);
+  }
+
+  @Test
+  public void testMultiPointEncoding() throws SQLException {
+    byte[] ptBytes =
+        hexStringToByteArray(
+            "00"
+                + "00 00 00 04"
+                + "00 00 00 03 "
+                + "00 "
+                + "00 00 00 01"
+                + "00 00 00 00 00 00 00 00"
+                + "00 00 00 00 00 00 00 00"
+                + "00 "
+                + "00 00 00 01"
+                + "00 00 00 00 00 00 00 00"
+                + "40 24 00 00 00 00 00 00"
+                + "00 "
+                + "00 00 00 01"
+                + "40 24 00 00 00 00 00 00"
+                + "00 00 00 00 00 00 00 00 ");
+    ReadableByteBuf readBuf = new StandardReadableByteBuf(ptBytes, ptBytes.length);
+    Geometry geo = Geometry.getGeometry(readBuf, ptBytes.length, null);
+    assertEquals("MULTIPOINT(0.0 0.0,0.0 10.0,10.0 0.0)", geo.toString());
+    assertEquals(geo, geo);
+    assertEquals(
+        new MultiPoint(new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0)}), geo);
+    assertEquals(
+        new MultiPoint(new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0)})
+            .hashCode(),
+        geo.hashCode());
+    assertNotEquals("wrong", geo);
+    assertNotEquals(
+        new MultiPoint(new Point[] {new Point(0, 0), new Point(0, 11), new Point(10, 0)}), geo);
+  }
+
+  @Test
+  public void testMultiLinestringEncoding() throws SQLException {
+    byte[] ptBytes =
+        hexStringToByteArray(
+            "00"
+                + "00 00 00 05"
+                + "00 00 00 01 "
+                + "00 "
+                + "00000002"
+                + "00000003"
+                + "00 00 00 00 00 00 00 00"
+                + "00 00 00 00 00 00 00 00"
+                + "00 00 00 00 00 00 00 00"
+                + "40 24 00 00 00 00 00 00"
+                + "40 24 00 00 00 00 00 00"
+                + "00 00 00 00 00 00 00 00");
+    ReadableByteBuf readBuf = new StandardReadableByteBuf(ptBytes, ptBytes.length);
+    Geometry geo = Geometry.getGeometry(readBuf, ptBytes.length, null);
+    assertEquals("MULTILINESTRING((0.0 0.0,0.0 10.0,10.0 0.0))", geo.toString());
+    assertEquals(geo, geo);
+    MultiLineString ml =
+        new MultiLineString(
+            new LineString[] {
+              new LineString(
+                  new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0)}, true)
+            });
+    assertEquals(ml, geo);
+    assertEquals(ml.hashCode(), geo.hashCode());
+    assertFalse(geo.equals("wrong"));
+  }
+
+  @Test
+  public void testMultiPolygonEncoding() throws SQLException {
+    byte[] ptBytes =
+        hexStringToByteArray(
+            "00"
+                + "00 00 00 06"
+                + "00 00 00 01 "
+                + "00 "
+                + "00 00 00 03  "
+                + "00 00 00 01  "
+                + "00 00 00 07  "
+                + "3F F0 00 00 00 00 00 00  "
+                + "3F F0 00 00 00 00 00 00  "
+                + "3F F0 00 00 00 00 00 00  "
+                + "40 14 00 00 00 00 00 00  "
+                + "40 10 00 00 00 00 00 00  "
+                + "40 22 00 00 00 00 00 00  "
+                + "40 18 00 00 00 00 00 00  "
+                + "40 22 00 00 00 00 00 00  "
+                + "40 22 00 00 00 00 00 00  "
+                + "40 08 00 00 00 00 00 00  "
+                + "40 1C 00 00 00 00 00 00  "
+                + "40 00 00 00 00 00 00 00  "
+                + "3F F0 00 00 00 00 00 00  "
+                + "3F F0 00 00 00 00 00 00");
+    ReadableByteBuf readBuf = new StandardReadableByteBuf(ptBytes, ptBytes.length);
+    Geometry geo = Geometry.getGeometry(readBuf, ptBytes.length, null);
+    assertEquals(
+        "MULTIPOLYGON(((1.0 1.0,1.0 5.0,4.0 9.0,6.0 9.0,9.0 3.0,7.0 2.0,1.0 1.0)))",
+        geo.toString());
+    assertEquals(geo, geo);
+    MultiPolygon ml =
+        new MultiPolygon(
+            new Polygon[] {
+              new Polygon(
+                  new LineString[] {
+                    new LineString(
+                        new Point[] {
+                          new Point(1, 1),
+                          new Point(1, 5),
+                          new Point(4, 9),
+                          new Point(6, 9),
+                          new Point(9, 3),
+                          new Point(7, 2),
+                          new Point(1, 1)
+                        },
+                        false)
+                  })
+            });
+    assertEquals(ml, geo);
+    assertEquals(ml.hashCode(), geo.hashCode());
+    assertFalse(geo.equals("wrong"));
+  }
+
+  @Test
+  public void testMultiGeoEncoding() throws SQLException {
+    byte[] ptBytes =
+        hexStringToByteArray(
+            "00"
+                + "00 00 00 07"
+                + "00 00 00 02 "
+                + "00 "
+                + "00 00 00 01"
+                + "40 00 00 00 00 00 00 00 "
+                + "40 10 00 00 00 00 00 00"
+                + "00"
+                + "00000002"
+                + "00000003"
+                + "0000000000000000"
+                + "0000000000000000"
+                + "00 00 00 00 00 00 00 00"
+                + "40 24 00 00 00 00 00 00"
+                + "40 24 00 00 00 00 00 00"
+                + "00 00 00 00 00 00 00 00");
+    ReadableByteBuf readBuf = new StandardReadableByteBuf(ptBytes, ptBytes.length);
+    Geometry geo = Geometry.getGeometry(readBuf, ptBytes.length, null);
+    assertEquals(
+        "GEOMETRYCOLLECTION(POINT(2.0 4.0),LINESTRING(0.0 0.0,0.0 10.0,10.0 0.0))", geo.toString());
+    GeometryCollection geo1 =
+        new GeometryCollection(
+            new Geometry[] {
+              new Point(2, 4),
+              new LineString(
+                  new Point[] {new Point(0, 0), new Point(0, 10), new Point(10, 0)}, true)
+            });
+    assertEquals(geo1, geo);
+  }
+
+  @Test
+  public void testWrongEncoding() throws SQLException {
+    byte[] ptBytes = hexStringToByteArray("00 00 00 00 08");
+    ReadableByteBuf readBuf = new StandardReadableByteBuf(ptBytes, ptBytes.length);
+    assertThrows(
+        SQLException.class,
+        () ->
+            Geometry.getGeometry(
+                readBuf, ptBytes.length, ColumnDecoder.create("test", DataType.GEOMETRY, 0)));
+    assertNull(Geometry.getGeometry(new StandardReadableByteBuf(new byte[0], 0), 0, null));
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/util/ClientParserTest.java b/src/test/java/org/mariadb/jdbc/unit/util/ClientParserTest.java
new file mode 100644
index 0000000..824f0d2
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/util/ClientParserTest.java
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.unit.util;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.nio.charset.StandardCharsets;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.util.ClientParser;
+
+@SuppressWarnings("ConstantConditions")
+public class ClientParserTest {
+
+  private void parse(String sql, String[] expected, String[] expectedNoBackSlash) {
+    ClientParser parser = ClientParser.parameterParts(sql, false);
+    assertEquals(expected.length, parser.getParamCount() + 1, displayErr(parser, expected));
+
+    int pos = 0;
+    int paramPos = parser.getQuery().length;
+    for (int i = 0; i < parser.getParamCount(); i++) {
+      paramPos = parser.getParamPositions().get(i);
+      assertEquals(expected[i], new String(parser.getQuery(), pos, paramPos - pos));
+      pos = paramPos + 1;
+    }
+    assertEquals(expected[expected.length - 1], new String(parser.getQuery(), pos, paramPos - pos));
+
+    parser = ClientParser.parameterParts(sql, true);
+    assertEquals(
+        expectedNoBackSlash.length, parser.getParamCount() + 1, displayErr(parser, expected));
+    pos = 0;
+    paramPos = parser.getQuery().length;
+    for (int i = 0; i < parser.getParamCount(); i++) {
+      paramPos = parser.getParamPositions().get(i);
+      assertEquals(expectedNoBackSlash[i], new String(parser.getQuery(), pos, paramPos - pos));
+      pos = paramPos + 1;
+    }
+    assertEquals(
+        expectedNoBackSlash[expectedNoBackSlash.length - 1],
+        new String(parser.getQuery(), pos, paramPos - pos));
+  }
+
+  private String displayErr(ClientParser parser, String[] exp) {
+    StringBuilder sb = new StringBuilder();
+    sb.append("is:\n");
+
+    int pos = 0;
+    int paramPos = parser.getQuery().length;
+    for (int i = 0; i < parser.getParamCount(); i++) {
+      paramPos = parser.getParamPositions().get(i);
+      sb.append(new String(parser.getQuery(), pos, paramPos - pos, StandardCharsets.UTF_8))
+          .append("\n");
+      pos = paramPos + 1;
+    }
+    sb.append(new String(parser.getQuery(), pos, paramPos - pos));
+
+    sb.append("but was:\n");
+    for (String s : exp) {
+      sb.append(s).append("\n");
+    }
+    return sb.toString();
+  }
+
+  @Test
+  public void ClientParser() {
+    parse(
+        "SELECT '\\\\test' /*test* #/ ;`*/",
+        new String[] {"SELECT '\\\\test' /*test* #/ ;`*/"},
+        new String[] {"SELECT '\\\\test' /*test* #/ ;`*/"});
+    parse(
+        "DO '\\\"', \"\\'\"",
+        new String[] {"DO '\\\"', \"\\'\""},
+        new String[] {"DO '\\\"', \"\\'\""});
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/util/ConfigurationTest.java b/src/test/java/org/mariadb/jdbc/unit/util/ConfigurationTest.java
new file mode 100644
index 0000000..25737e4
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/util/ConfigurationTest.java
@@ -0,0 +1,888 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.unit.util;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.Properties;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.*;
+import org.mariadb.jdbc.export.HaMode;
+import org.mariadb.jdbc.export.SslMode;
+import org.mariadb.jdbc.integration.Common;
+
+@SuppressWarnings("ConstantConditions")
+public class ConfigurationTest {
+
+  @Test
+  public void testWrongFormat() {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> Configuration.parse("jdbc:mariadb:/localhost/test"),
+        "url parsing error : '//' is not present in the url");
+  }
+
+  @Test
+  public void testParseProps() throws SQLException {
+    Configuration conf = Configuration.parse("jdbc:mariadb://localhost/test", null);
+    assertEquals(0, conf.socketTimeout());
+
+    Properties props = new Properties();
+    props.setProperty("socketTimeout", "50");
+    conf = Configuration.parse("jdbc:mariadb://localhost/test", props);
+    assertEquals(50, conf.socketTimeout());
+  }
+
+  @Test
+  public void testCredentialType() throws SQLException {
+    Configuration conf = Configuration.parse("jdbc:mariadb://localhost/test?credentialType=");
+    assertNull(conf.credentialPlugin());
+  }
+
+  @Test
+  public void testWrongHostFormat() {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> Configuration.parse("jdbc:mariadb://localhost:wrongPort/test"),
+        "Incorrect port value : wrongPort");
+  }
+
+  @Test
+  public void testNoAdditionalPart() throws SQLException {
+    assertEquals(null, Configuration.parse("jdbc:mariadb://localhost/").database());
+    assertEquals(null, Configuration.parse("jdbc:mariadb://localhost/?socketTimeout=50").user());
+    assertEquals(null, Configuration.parse("jdbc:mariadb://localhost").database());
+    assertEquals(null, Configuration.parse("jdbc:mariadb://localhost").user());
+    assertEquals(
+        50,
+        Configuration.parse("jdbc:mariadb://localhost?socketTimeout=50&file=/tmp/test")
+            .socketTimeout());
+    assertEquals(null, Configuration.parse("jdbc:mariadb://localhost?").user());
+  }
+
+  @Test
+  public void testAliases() throws SQLException {
+    assertEquals(
+        "someCipher",
+        Configuration.parse("jdbc:mariadb://localhost/?enabledSSLCipherSuites=someCipher")
+            .enabledSslCipherSuites());
+    assertEquals(
+        "/tmp/path",
+        Configuration.parse("jdbc:mariadb://localhost/?serverRSAPublicKeyFile=/tmp/path")
+            .serverRsaPublicKeyFile());
+  }
+
+  @Test
+  public void testDatabaseOnly() throws SQLException {
+    assertEquals("DB", Configuration.parse("jdbc:mariadb://localhost/DB").database());
+    assertEquals(null, Configuration.parse("jdbc:mariadb://localhost/DB").user());
+  }
+
+  @Test
+  public void testUrl() throws SQLException {
+    Configuration conf =
+        new Configuration.Builder()
+            .database("DB")
+            .addHost("local", 3306, true)
+            .haMode(HaMode.REPLICATION)
+            .build();
+    assertEquals("jdbc:mariadb:replication://local/DB", conf.initialUrl());
+    assertEquals("jdbc:mariadb:replication://local/DB", conf.toString());
+    assertEquals(
+        Configuration.parse(
+            "jdbc:mariadb:replication://address=(host=local)(port=3306)(type=primary)/DB"),
+        conf);
+
+    conf =
+        new Configuration.Builder()
+            .database("DB")
+            .addresses(
+                HostAddress.from("local", 3306, true), HostAddress.from("host2", 3307, false))
+            .haMode(HaMode.REPLICATION)
+            .build();
+
+    assertEquals("jdbc:mariadb:replication://local,host2:3307/DB", conf.initialUrl());
+
+    conf =
+        new Configuration.Builder()
+            .database("DB")
+            .addHost("local", 3306, true)
+            .haMode(HaMode.REPLICATION)
+            .socketTimeout(50)
+            .build();
+    assertEquals("jdbc:mariadb:replication://local/DB?socketTimeout=50", conf.initialUrl());
+
+    conf =
+        new Configuration.Builder()
+            .database("DB")
+            .addHost("local", 3306)
+            .addHost("local", 3307)
+            .addHost("local", 3308)
+            .haMode(HaMode.REPLICATION)
+            .socketTimeout(50)
+            .build();
+    assertEquals(
+        "jdbc:mariadb:replication://local,local:3307,local:3308/DB?socketTimeout=50",
+        conf.initialUrl());
+
+    conf =
+        new Configuration.Builder()
+            .database("DB")
+            .addHost("local", 3306)
+            .addHost("local", 3307)
+            .addHost("local", 3308)
+            .haMode(HaMode.LOADBALANCE)
+            .socketTimeout(50)
+            .build();
+    assertEquals(
+        "jdbc:mariadb:loadbalance://address=(host=local)(port=3306)(type=primary),address=(host=local)(port=3307)(type=primary),address=(host=local)(port=3308)(type=primary)/DB?socketTimeout=50",
+        conf.initialUrl());
+
+    conf =
+        new Configuration.Builder()
+            .database("DB")
+            .addHost("local", 3306, true)
+            .haMode(HaMode.REPLICATION)
+            .autocommit(false)
+            .build();
+    assertEquals("jdbc:mariadb:replication://local/DB?autocommit=false", conf.initialUrl());
+  }
+
+  @Test
+  public void testAcceptsUrl() {
+    Driver driver = new Driver();
+    assertFalse(driver.acceptsURL(null));
+    assertTrue(driver.acceptsURL("jdbc:mariadb://localhost/test"));
+    assertFalse(driver.acceptsURL("jdbc:mysql://localhost/test"));
+    assertTrue(driver.acceptsURL("jdbc:mysql://localhost/test?permitMysqlScheme"));
+  }
+
+  @Test
+  public void testConfigurationIsolation() throws Throwable {
+    Configuration conf =
+        Configuration.parse("jdbc:mariadb://localhost/test?transactionIsolation=REPEATABLE-READ");
+    assertTrue(TransactionIsolation.REPEATABLE_READ == conf.transactionIsolation());
+
+    conf = Configuration.parse("jdbc:mariadb://localhost/test");
+    assertNull(conf.transactionIsolation());
+
+    conf =
+        Configuration.parse("jdbc:mariadb://localhost/test?transactionIsolation=repeatable-read");
+    assertTrue(TransactionIsolation.REPEATABLE_READ == conf.transactionIsolation());
+
+    conf = Configuration.parse("jdbc:mariadb://localhost/test?transactionIsolation=readCommitted");
+    assertTrue(TransactionIsolation.READ_COMMITTED == conf.transactionIsolation());
+
+    conf =
+        Configuration.parse("jdbc:mariadb://localhost/test?transactionIsolation=READ-UNCOMMITTED");
+    assertTrue(TransactionIsolation.READ_UNCOMMITTED == conf.transactionIsolation());
+
+    conf = Configuration.parse("jdbc:mariadb://localhost/test?transactionIsolation=SERIALIZABLE");
+    assertTrue(TransactionIsolation.SERIALIZABLE == conf.transactionIsolation());
+
+    try {
+      Configuration.parse("jdbc:mariadb://localhost/test?transactionIsolation=wrong_val");
+      Assertions.fail();
+    } catch (SQLException e) {
+      assertTrue(
+          e.getMessage().contains("Wrong argument value 'wrong_val' for TransactionIsolation"));
+    }
+
+    Assertions.assertNull(
+        Configuration.parse("jdbc:mysql://localhost/test?transactionIsolation=wrong_val"));
+
+    conf =
+        Configuration.parse(
+            "jdbc:mysql://localhost/test?transactionIsolation=SERIALIZABLE&permitMysqlScheme");
+    assertTrue(TransactionIsolation.SERIALIZABLE == conf.transactionIsolation());
+  }
+
+  @Test
+  public void testSslAlias() throws Throwable {
+    Configuration conf = Configuration.parse("jdbc:mariadb://localhost/test?sslMode=verify-full");
+    assertTrue(SslMode.VERIFY_FULL == conf.sslMode());
+
+    conf = Configuration.parse("jdbc:mariadb://localhost/test?sslMode=verify_full");
+    assertTrue(SslMode.VERIFY_FULL == conf.sslMode());
+
+    conf = Configuration.parse("jdbc:mariadb://localhost/test?sslMode=trust");
+    assertTrue(SslMode.TRUST == conf.sslMode());
+
+    try {
+      Configuration.parse("jdbc:mariadb://localhost/test?sslMode=wrong_trust");
+      Assertions.fail();
+    } catch (SQLException e) {
+      assertTrue(e.getMessage().contains("Wrong argument value 'wrong_trust' for SslMode"));
+    }
+
+    conf = Configuration.parse("jdbc:mariadb://localhost/test?sslMode=verify-ca");
+    assertTrue(SslMode.VERIFY_CA == conf.sslMode());
+
+    conf = Configuration.parse("jdbc:mariadb://localhost/test");
+    assertTrue(SslMode.DISABLE == conf.sslMode());
+
+    conf = Configuration.parse("jdbc:mariadb://localhost/test?sslMode");
+    assertTrue(SslMode.DISABLE == conf.sslMode());
+
+    conf = Configuration.parse("jdbc:mariadb://localhost/test?sslMode=0");
+    assertTrue(SslMode.DISABLE == conf.sslMode());
+
+    conf = Configuration.parse("jdbc:mariadb://localhost/test?sslMode=1");
+    assertTrue(SslMode.VERIFY_FULL == conf.sslMode());
+
+    conf = Configuration.parse("jdbc:mariadb://localhost/test?sslMode=true");
+    assertTrue(SslMode.VERIFY_FULL == conf.sslMode());
+  }
+
+  @Test
+  public void testSslCompatibility() throws Throwable {
+    assertEquals(
+        SslMode.VERIFY_FULL, Configuration.parse("jdbc:mariadb://localhost/test?useSsl").sslMode());
+    assertEquals(
+        SslMode.VERIFY_FULL,
+        Configuration.parse("jdbc:mariadb://localhost/test?useSsl=true").sslMode());
+    assertEquals(
+        SslMode.VERIFY_FULL,
+        Configuration.parse("jdbc:mariadb://localhost/test?useSsl=1").sslMode());
+    assertEquals(
+        SslMode.VERIFY_FULL,
+        Configuration.parse("jdbc:mariadb://localhost/test?useSSL=1").sslMode());
+    assertEquals(
+        SslMode.TRUST,
+        Configuration.parse("jdbc:mariadb://localhost/test?useSsl&trustServerCertificate")
+            .sslMode());
+    assertEquals(
+        SslMode.VERIFY_CA,
+        Configuration.parse("jdbc:mariadb://localhost/test?useSsl&disableSslHostnameVerification")
+            .sslMode());
+  }
+
+  @Test
+  public void testBooleanDefault() throws Throwable {
+    assertFalse(
+        Configuration.parse("jdbc:mariadb:///test").includeThreadDumpInDeadlockExceptions());
+    assertFalse(
+        Configuration.parse("jdbc:mariadb:///test?includeThreadDumpInDeadlockExceptions=false")
+            .includeThreadDumpInDeadlockExceptions());
+    assertTrue(
+        Configuration.parse("jdbc:mariadb:///test?includeThreadDumpInDeadlockExceptions=true")
+            .includeThreadDumpInDeadlockExceptions());
+    assertTrue(
+        Configuration.parse("jdbc:mariadb:///test?includeThreadDumpInDeadlockExceptions")
+            .includeThreadDumpInDeadlockExceptions());
+  }
+
+  @Test
+  public void testOptionTakeDefault() throws Throwable {
+    Configuration conf = Configuration.parse("jdbc:mariadb://localhost/test");
+    assertEquals(30_000, conf.connectTimeout());
+    assertEquals(250, conf.prepStmtCacheSize());
+    assertNull(conf.user());
+    assertEquals(0, conf.socketTimeout());
+    int initialLoginTimeout = DriverManager.getLoginTimeout();
+    DriverManager.setLoginTimeout(60);
+    conf = Configuration.parse("jdbc:mariadb://localhost/test");
+    assertEquals(60_000, conf.connectTimeout());
+    DriverManager.setLoginTimeout(initialLoginTimeout);
+  }
+
+  @Test
+  public void testOptionParse() throws Throwable {
+    Configuration conf =
+        Configuration.parse(
+            "jdbc:mariadb://localhost/test?user=root&password=toto&createDB=true"
+                + "&autoReconnect=true&prepStmtCacheSize=2&connectTimeout=5&socketTimeout=20");
+    assertEquals(5, conf.connectTimeout());
+    assertEquals(20, conf.socketTimeout());
+    assertEquals(2, conf.prepStmtCacheSize());
+    assertEquals("true", conf.nonMappedOptions().get("createDB"));
+    assertEquals("true", conf.nonMappedOptions().get("autoReconnect"));
+    assertEquals("root", conf.user());
+    assertEquals("toto", conf.password());
+  }
+
+  @Test
+  public void nonCaseSensitiveOptions() throws Throwable {
+    Configuration conf =
+        Configuration.parse(
+            "jdbc:mariadb://localhost/test?useR=root&paSsword=toto&createdb=true"
+                + "&autoReConnect=true&prepStMtCacheSize=2&ConnectTimeout=5&socketTimeout=20");
+    assertEquals(5, conf.connectTimeout());
+    assertEquals(20, conf.socketTimeout());
+    assertEquals(2, conf.prepStmtCacheSize());
+    assertEquals("true", conf.nonMappedOptions().get("createdb"));
+    assertEquals("true", conf.nonMappedOptions().get("autoReConnect"));
+    assertEquals("root", conf.user());
+    assertEquals("toto", conf.password());
+  }
+
+  @Test
+  public void wrongTypeParsing() {
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> Configuration.parse("jdbc:mariadb://localhost/test?socketTimeout=20aa"),
+        "Optional parameter socketTimeout must be Integer, was '20aa'");
+  }
+
+  @Test
+  public void testOptionParseSlash() throws Throwable {
+    Configuration jdbc =
+        Configuration.parse(
+            "jdbc:mariadb://127.0.0.1:3306/colleo?user=root&password=toto"
+                + "&localSocket=/var/run/mysqld/mysqld.sock");
+    assertEquals("/var/run/mysqld/mysqld.sock", jdbc.localSocket());
+    assertEquals("root", jdbc.user());
+    assertEquals("toto", jdbc.password());
+  }
+
+  @Test
+  public void testOptionParseIntegerMinimum() throws Throwable {
+    Configuration jdbc =
+        Configuration.parse(
+            "jdbc:mariadb://localhost/test?user=root&autoReconnect=true"
+                + "&prepStmtCacheSize=240&connectTimeout=5");
+    assertEquals(5, jdbc.connectTimeout());
+    assertEquals(240, jdbc.prepStmtCacheSize());
+  }
+
+  @Test
+  public void testWithoutDb() throws Throwable {
+    Configuration jdbc =
+        Configuration.parse("jdbc:mariadb://localhost/?user=root&tcpKeepAlive=true");
+    assertTrue(jdbc.tcpKeepAlive());
+    assertNull(jdbc.database());
+
+    Configuration jdbc2 =
+        Configuration.parse("jdbc:mariadb://localhost?user=root&tcpKeepAlive=true");
+    assertTrue(jdbc2.tcpKeepAlive());
+    assertNull(jdbc2.database());
+  }
+
+  @Test
+  public void testOptionParseIntegerNotPossible() {
+    assertThrows(
+        SQLException.class,
+        () ->
+            Configuration.parse(
+                "jdbc:mariadb://localhost/test?user=root&autoReconnect=true&prepStmtCacheSize=-2"
+                    + "&connectTimeout=5"));
+  }
+
+  @Test()
+  public void testJdbcParserSimpleIpv4basic() throws SQLException {
+    String url = "jdbc:mariadb://master:3306,slave1:3307,slave2:3308/database";
+    Configuration conf = Configuration.parse(url);
+    assertEquals("jdbc:mariadb://master,slave1:3307,slave2:3308/database", conf.initialUrl());
+    url =
+        "jdbc:mariadb://address=(host=master)(port=3306)(type=primary),address=(host=slave1)(port=3307)(type=replica),address=(host=slave2)(port=3308)(type=replica)/database";
+    conf = Configuration.parse(url);
+    assertEquals(
+        "jdbc:mariadb://master,address=(host=slave1)(port=3307)(type=replica),address=(host=slave2)(port=3308)(type=replica)/database",
+        conf.initialUrl());
+    url =
+        "jdbc:mariadb://address=(host=master)(port=3306)(type=master),address=(host=slave1)(port=3307)(type=replica),address=(host=slave2)(port=3308)(type=replica)/database";
+    conf = Configuration.parse(url);
+    assertEquals(
+        "jdbc:mariadb://master,address=(host=slave1)(port=3307)(type=replica),address=(host=slave2)(port=3308)(type=replica)/database",
+        conf.initialUrl());
+    url = "jdbc:mariadb:replication://master:3306,slave1:3307,slave2:3308/database";
+    conf = Configuration.parse(url);
+    assertEquals(
+        "jdbc:mariadb:replication://master,slave1:3307,slave2:3308/database", conf.initialUrl());
+  }
+
+  @Test
+  public void testJdbcParserSimpleIpv4basicError() throws SQLException {
+    Configuration Configuration = org.mariadb.jdbc.Configuration.parse(null);
+    assertTrue(Configuration == null);
+  }
+
+  @Test
+  public void testJdbcParserSimpleIpv4basicwithoutDatabase() throws SQLException {
+    String url = "jdbc:mariadb://master:3306,slave1:3307,slave2:3308/";
+    Configuration conf = org.mariadb.jdbc.Configuration.parse(url);
+    assertNull(conf.database());
+    assertNull(conf.user());
+    assertNull(conf.password());
+    assertEquals(3, conf.addresses().size());
+    assertEquals(HostAddress.from("master", 3306, true), conf.addresses().get(0));
+    assertEquals(HostAddress.from("slave1", 3307, true), conf.addresses().get(1));
+    assertEquals(HostAddress.from("slave2", 3308, true), conf.addresses().get(2));
+  }
+
+  @Test
+  public void testJdbcParserWithoutDatabaseWithProperties() throws SQLException {
+    String url = "jdbc:mariadb://master:3306,slave1:3307,slave2:3308?autoReconnect=true";
+    Configuration conf = org.mariadb.jdbc.Configuration.parse(url);
+    assertNull(conf.database());
+    assertNull(conf.user());
+    assertNull(conf.password());
+    assertEquals(3, conf.addresses().size());
+    assertEquals(HostAddress.from("master", 3306, true), conf.addresses().get(0));
+    assertEquals(HostAddress.from("slave1", 3307, true), conf.addresses().get(1));
+    assertEquals(HostAddress.from("slave2", 3308, true), conf.addresses().get(2));
+  }
+
+  @Test
+  public void testJdbcParserWithoutDatabase2WithProperties() throws SQLException {
+    String url = "jdbc:mariadb://master:3306,slave1:3307,slave2:3308/?autoReconnect=true";
+    Configuration conf = org.mariadb.jdbc.Configuration.parse(url);
+    assertNull(conf.database());
+    assertNull(conf.user());
+    assertNull(conf.password());
+    assertEquals(3, conf.addresses().size());
+    assertEquals(HostAddress.from("master", 3306, true), conf.addresses().get(0));
+    assertEquals(HostAddress.from("slave1", 3307, true), conf.addresses().get(1));
+    assertEquals(HostAddress.from("slave2", 3308, true), conf.addresses().get(2));
+  }
+
+  @Test
+  public void testJdbcParserSimpleIpv4Properties() throws SQLException {
+    String url = "jdbc:mariadb://master:3306,slave1:3307,slave2:3308/database?autoReconnect=true";
+
+    Properties prop = new Properties();
+    prop.setProperty("user", "greg");
+    prop.setProperty("password", "pass");
+    prop.setProperty("allowMultiQueries", "true");
+
+    Configuration conf = org.mariadb.jdbc.Configuration.parse(url, prop);
+    assertEquals("database", conf.database());
+    assertEquals("greg", conf.user());
+    assertEquals("pass", conf.password());
+    assertTrue(conf.allowMultiQueries());
+    assertEquals(3, conf.addresses().size());
+    assertEquals(HostAddress.from("master", 3306, true), conf.addresses().get(0));
+    assertEquals(HostAddress.from("slave1", 3307, true), conf.addresses().get(1));
+    assertEquals(HostAddress.from("slave2", 3308, true), conf.addresses().get(2));
+
+    prop = new Properties();
+    prop.put("user", "greg");
+    prop.put("password", "pass");
+    prop.put("allowMultiQueries", true);
+
+    conf = org.mariadb.jdbc.Configuration.parse(url, prop);
+    assertEquals("database", conf.database());
+    assertEquals("greg", conf.user());
+    assertEquals("pass", conf.password());
+    assertTrue(conf.allowMultiQueries());
+    assertEquals(3, conf.addresses().size());
+    assertEquals(HostAddress.from("master", 3306, true), conf.addresses().get(0));
+    assertEquals(HostAddress.from("slave1", 3307, true), conf.addresses().get(1));
+    assertEquals(HostAddress.from("slave2", 3308, true), conf.addresses().get(2));
+  }
+
+  @Test
+  public void testJdbcParserBooleanOption() {
+    String url = "jdbc:mariadb://master:3306,slave1:3307,slave2:3308?autoReconnect=truee";
+    Properties prop = new Properties();
+    prop.setProperty("user", "greg");
+    prop.setProperty("password", "pass");
+    try {
+      Configuration.parse(url, prop);
+    } catch (SQLException sqle) {
+      assertTrue(
+          sqle.getMessage()
+              .contains(
+                  "Optional parameter autoReconnect must be boolean (true/false or 0/1) was"
+                      + " \"truee\""));
+    }
+  }
+
+  @Test
+  public void testJdbcParserSimpleIpv4() throws SQLException {
+    String url =
+        "jdbc:mariadb://master:3306,slave1:3307,slave2:3308/database?user=greg&password=pass";
+    Configuration conf = org.mariadb.jdbc.Configuration.parse(url);
+    assertEquals("database", conf.database());
+    assertEquals("greg", conf.user());
+    assertEquals("pass", conf.password());
+    assertEquals(3, conf.addresses().size());
+    assertEquals(HostAddress.from("master", 3306, true), conf.addresses().get(0));
+    assertEquals(HostAddress.from("slave1", 3307, true), conf.addresses().get(1));
+    assertEquals(HostAddress.from("slave2", 3308, true), conf.addresses().get(2));
+  }
+
+  @Test
+  public void testJdbcParserSimpleIpv6() throws SQLException {
+    String url =
+        "jdbc:mariadb://[2001:0660:7401:0200:0000:0000:0edf:bdd7],[2001:660:7401:200::edf:bdd7]:3307,[2001:660:7401:200::edf:bdd7]-test"
+            + "/database?user=greg&password=pass";
+    Configuration conf = org.mariadb.jdbc.Configuration.parse(url);
+    assertEquals("database", conf.database());
+    assertEquals("greg", conf.user());
+    assertEquals("pass", conf.password());
+    assertEquals(3, conf.addresses().size());
+    assertEquals(
+        HostAddress.from("2001:0660:7401:0200:0000:0000:0edf:bdd7", 3306, true),
+        conf.addresses().get(0));
+    assertEquals(
+        HostAddress.from("2001:660:7401:200::edf:bdd7", 3307, true), conf.addresses().get(1));
+    assertEquals(
+        HostAddress.from("2001:660:7401:200::edf:bdd7", 3306, true), conf.addresses().get(2));
+  }
+
+  @Test
+  public void testJdbcParserParameter() throws SQLException {
+    String url =
+        "jdbc:mariadb://address=(type=primary)(port=3306)(host=master1),address=(port=3307)(type=primary)"
+            + "(host=master2)(type=replica),address=(type=slave)(host=slave1)(port=3308)/database?user=greg&password=pass";
+    Configuration conf = org.mariadb.jdbc.Configuration.parse(url);
+    assertEquals("database", conf.database());
+    assertEquals("greg", conf.user());
+    assertEquals("pass", conf.password());
+    assertEquals(3, conf.addresses().size());
+    assertEquals(HostAddress.from("master1", 3306, true), conf.addresses().get(0));
+    assertEquals(HostAddress.from("master2", 3307, false), conf.addresses().get(1));
+    assertEquals(HostAddress.from("slave1", 3308, false), conf.addresses().get(2));
+
+    url =
+        "jdbc:mariadb://address=(port=3306)(host=master1),address=(port=3307)"
+            + "(host=master2),address=(host=master3)(port=3308)/database?user=greg&password=pass";
+    conf = org.mariadb.jdbc.Configuration.parse(url);
+    assertEquals("database", conf.database());
+    assertEquals("greg", conf.user());
+    assertEquals("pass", conf.password());
+    assertEquals(3, conf.addresses().size());
+    assertEquals(HostAddress.from("master1", 3306, true), conf.addresses().get(0));
+    assertEquals(HostAddress.from("master2", 3307, true), conf.addresses().get(1));
+    assertEquals(HostAddress.from("master3", 3308, true), conf.addresses().get(2));
+
+    url =
+        "jdbc:mariadb:replication://address=(port=3306)(host=master1),address=(port=3307)(host=slave1)"
+            + " ,address=(host=slave2)(port=3308)(other=5/database?user=greg&password=pass";
+    conf = org.mariadb.jdbc.Configuration.parse(url);
+    assertEquals("database", conf.database());
+    assertEquals("greg", conf.user());
+    assertEquals("pass", conf.password());
+    assertEquals(3, conf.addresses().size());
+    assertEquals(HostAddress.from("master1", 3306, true), conf.addresses().get(0));
+    assertEquals(HostAddress.from("slave1", 3307, false), conf.addresses().get(1));
+    assertEquals(HostAddress.from("slave2", 3308, false), conf.addresses().get(2));
+  }
+
+  @Test
+  public void address() {
+    assertEquals("address=(host=test)(port=3306)", HostAddress.from("test", 3306).toString());
+    assertEquals(
+        "address=(host=test)(port=3306)(type=replica)",
+        HostAddress.from("test", 3306, false).toString());
+    assertEquals(
+        "address=(host=test)(port=3306)(type=primary)",
+        HostAddress.from("test", 3306, true).toString());
+  }
+
+  @Test
+  public void hostAddressEqual() {
+    HostAddress host = HostAddress.from("test", 3306);
+    assertEquals(host, host);
+    assertNotEquals(null, host);
+    assertEquals(HostAddress.from("test", 3306), host);
+    assertNotEquals("", host);
+    assertNotEquals(HostAddress.from("test2", 3306, true), host);
+    assertNotEquals(HostAddress.from("test", 3306, true), host);
+    assertNotEquals(HostAddress.from("test", 3306, false), host);
+  }
+
+  @Test
+  public void testJdbcParserParameterErrorEqual() {
+    String wrongIntVal = "jdbc:mariadb://localhost?socketTimeout=blabla";
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> Configuration.parse(wrongIntVal),
+        "Optional parameter socketTimeout must be Integer, was 'blabla'");
+    String wrongBoolVal = "jdbc:mariadb://localhost?autocommit=blabla";
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> Configuration.parse(wrongBoolVal),
+        "Optional parameter autocommit must be boolean (true/false or 0/1)");
+    String url =
+        "jdbc:mariadb://address=(type=)(port=3306)(host=master1),address=(port=3307)(type=primary)"
+            + "(host=master2),address=(type=replica)(host=slave1)(port=3308)/database?user=greg&password=pass";
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> Configuration.parse(url),
+        "Invalid connection URL, expected key=value pairs, found (type=)");
+    String url2 =
+        "jdbc:mariadb://address=(type=wrong)(port=3306)(host=master1),address=(port=3307)(type=primary)"
+            + "(host=master2),address=(type=replica)(host=slave1)(port=3308)/database?user=greg&password=pass";
+    Common.assertThrowsContains(
+        SQLException.class,
+        () -> Configuration.parse(url2),
+        "Wrong type value (type=wrong) (possible value primary/replica)");
+  }
+
+  @Test
+  public void testJdbcParserHaModeNone() throws SQLException {
+    String url = "jdbc:mariadb://localhost/database";
+    Configuration jdbc = Configuration.parse(url);
+    assertTrue(jdbc.haMode().equals(HaMode.NONE));
+  }
+
+  @Test
+  public void testJdbcParserHaModeLoadReplication() throws SQLException {
+    String url = "jdbc:mariadb:replication://localhost/database";
+    Configuration jdbc = Configuration.parse(url);
+    assertTrue(jdbc.haMode().equals(HaMode.REPLICATION));
+  }
+
+  @Test
+  public void testJdbcParserReplicationParameter() throws SQLException {
+    String url =
+        "jdbc:mariadb:replication://address=(type=primary)(port=3306)(host=master1),address=(port=3307)"
+            + "(type=primary)(host=master2),address=(type=replica)(host=slave1)(port=3308)/database"
+            + "?user=greg&password=pass&pinGlobalTxToPhysicalConnection&servicePrincipalName=BLA"
+            + "&allowPublicKeyRetrieval&serverRSAPublicKeyFile=/tmp/path";
+    Configuration conf = org.mariadb.jdbc.Configuration.parse(url);
+    assertEquals("database", conf.database());
+    assertEquals("greg", conf.user());
+    assertEquals("pass", conf.password());
+    assertEquals("BLA", conf.servicePrincipalName());
+    assertTrue(conf.allowPublicKeyRetrieval());
+    assertEquals("/tmp/path", conf.serverRsaPublicKeyFile());
+    assertEquals(3, conf.addresses().size());
+    assertEquals(HostAddress.from("master1", 3306, true), conf.addresses().get(0));
+    assertEquals(HostAddress.from("master2", 3307, true), conf.addresses().get(1));
+    assertEquals(HostAddress.from("slave1", 3308, false), conf.addresses().get(2));
+  }
+
+  @Test
+  public void testJdbcParserReplicationParameterWithoutType() throws SQLException {
+    String url = "jdbc:mariadb:replication://master1,slave1,slave2/database";
+    Configuration conf = org.mariadb.jdbc.Configuration.parse(url);
+    assertEquals(3, conf.addresses().size());
+    assertEquals(HostAddress.from("master1", 3306, true), conf.addresses().get(0));
+    assertEquals(HostAddress.from("slave1", 3306, false), conf.addresses().get(1));
+    assertEquals(HostAddress.from("slave2", 3306, false), conf.addresses().get(2));
+  }
+
+  /**
+   * Conj-167 : Driver is throwing IllegalArgumentException instead of returning null.
+   *
+   * @throws SQLException if any exception occur
+   */
+  @Test
+  public void checkOtherDriverCompatibility() throws SQLException {
+    Configuration jdbc = Configuration.parse("jdbc:h2:mem:RZM;DB_CLOSE_DELAY=-1");
+    assertTrue(jdbc == null);
+  }
+
+  @Test
+  public void checkDisable() throws SQLException {
+    Configuration jdbc = Configuration.parse("jdbc:mysql://localhost/test");
+    assertTrue(jdbc == null);
+  }
+
+  @Test
+  public void loginTimeout() throws SQLException {
+    Configuration jdbc = Configuration.parse("jdbc:mariadb://localhost/test");
+    assertEquals(30000, jdbc.connectTimeout());
+
+    DriverManager.setLoginTimeout(10);
+    jdbc = Configuration.parse("jdbc:mariadb://localhost/test");
+    assertEquals(10000, jdbc.connectTimeout());
+
+    jdbc = Configuration.parse("jdbc:mariadb://localhost/test?connectTimeout=5000");
+    assertEquals(5000, jdbc.connectTimeout());
+    DriverManager.setLoginTimeout(0);
+
+    jdbc = Configuration.parse("jdbc:mariadb://localhost/test?connectTimeout=5000");
+    assertEquals(5000, jdbc.connectTimeout());
+  }
+
+  @Test
+  public void checkHaMode() throws SQLException {
+    checkHaMode("jdbc:mariadb://localhost/test", HaMode.NONE);
+    checkHaMode("jdbc:mariadb:replication://localhost/test", HaMode.REPLICATION);
+    checkHaMode("jdbc:mariadb:replication//localhost/test", HaMode.REPLICATION);
+    checkHaMode("jdbc:mariadb:failover://localhost:3306/test", HaMode.LOADBALANCE);
+    checkHaMode("jdbc:mariadb:loadbalance://localhost:3306/test", HaMode.LOADBALANCE);
+
+    try {
+      checkHaMode("jdbc:mariadb:replicati//localhost/test", HaMode.REPLICATION);
+      fail();
+    } catch (SQLException sqle) {
+      assertTrue(
+          sqle.getMessage().contains("wrong failover parameter format in connection String"));
+    }
+  }
+
+  private void checkHaMode(String url, HaMode expectedHaMode) throws SQLException {
+    Configuration jdbc = Configuration.parse(url);
+    assertEquals(expectedHaMode, jdbc.haMode());
+  }
+
+  /**
+   * CONJ-452 : correcting line break in connection url.
+   *
+   * @throws SQLException if any exception occur
+   */
+  @Test
+  public void checkInfileCertificate() throws SQLException {
+    String url =
+        "jdbc:mariadb://1.2.3.4/testj?user=diego"
+            + "&autocommit=true&serverSslCert="
+            + "-----BEGIN CERTIFICATE-----\n"
+            + "MIIDITCCAgmgAwIBAgIBADANBgkqhkiG9w0BAQUFADBIMSMwIQYDVQQDExpHb29n\n"
+            + "bGUgQ2xvdWQgU1FMIFNlcnZlciBDQTEUMBIGA1UEChMLR29vZ2xlLCBJbmMxCzAJ\n"
+            + "BgNVBAYTAlVTMB4XDTE3MDQyNzEyMjcyNFoXDTE5MDQyNzEyMjgyNFowSDEjMCEG\n"
+            + "A1UEAxMaR29vZ2xlIENsb3VkIFNRTCBTZXJ2ZXIgQ0ExFDASBgNVBAoTC0dvb2ds\n"
+            + "ZSwgSW5jMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\n"
+            + "ggEBANA6vS37//gvYOsEXKA9Cnlt/C1Ef/a5zJNahpVxx8HbJn6DF+pQbhHm3o4P\n"
+            + "TeZp1HoRg5TRiXOEkNTBmgSQbR2+otM2q2gmkn4XAh0+yXkNW3hr2IydJyg9C26v\n"
+            + "/OzFvuLcw9iDBvrn433pDa6vjYDU+wiQaVtr1ItzsoE/kgW2IkgFVQB+CrkpAmwm\n"
+            + "omwEze3QFUUznP0PHy3P7g7UVD9u5x3APY6kVt2dq8mnOiLZkyfHHR2j6+j0E73I\n"
+            + "k3HQv7D0yRIv3kuNpFgJbITVgDIq9ukWU2XinDHUjguCDH+yQAoQH7hOQlWUHIz8\n"
+            + "/TtfZjrlUQf2uLzOWCn5KxfEqTkCAwEAAaMWMBQwEgYDVR0TAQH/BAgwBgEB/wIB\n"
+            + "ADANBgkqhkiG9w0BAQUFAAOCAQEArYkBkUvMvteT4fN6lxUkzmp8R7clLPkA2HnJ\n"
+            + "7IUK3c5GJ0/blffxj/9Oe2g+etga15SIO73GNAnTxhxIJllKRmmh6SR+dwNMkAvE\n"
+            + "xp87/Y6cSeJp5d4HhZUvxpFjaUDsWIC8tpbriUJoqGIirprLVcsPgDjKyuvVOlbK\n"
+            + "aQf3fOoBPLspGWHgic8Iw1O4kRcStUGCSCwOtYcgMJEhVqTgX0sTX5BgatZhr8FY\n"
+            + "Mnoceo2vzzxgHJU9qZuPkpYDs+ipQjzhoIJaY4HU2Uz4jMptqxSdzsPpC6PAKwuN\n"
+            + "+LBCR0B194YbRn6726vWwUUE05yskVN6gllGSCgZ/G8y98DhjQ==\n"
+            + "-----END CERTIFICATE-----&sslMode&password=testj&password=pwd2";
+    Configuration jdbc = Configuration.parse(url);
+    assertEquals("diego", jdbc.user());
+    assertEquals(true, jdbc.autocommit());
+    assertEquals(
+        "-----BEGIN CERTIFICATE-----\n"
+            + "MIIDITCCAgmgAwIBAgIBADANBgkqhkiG9w0BAQUFADBIMSMwIQYDVQQDExpHb29n\n"
+            + "bGUgQ2xvdWQgU1FMIFNlcnZlciBDQTEUMBIGA1UEChMLR29vZ2xlLCBJbmMxCzAJ\n"
+            + "BgNVBAYTAlVTMB4XDTE3MDQyNzEyMjcyNFoXDTE5MDQyNzEyMjgyNFowSDEjMCEG\n"
+            + "A1UEAxMaR29vZ2xlIENsb3VkIFNRTCBTZXJ2ZXIgQ0ExFDASBgNVBAoTC0dvb2ds\n"
+            + "ZSwgSW5jMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\n"
+            + "ggEBANA6vS37//gvYOsEXKA9Cnlt/C1Ef/a5zJNahpVxx8HbJn6DF+pQbhHm3o4P\n"
+            + "TeZp1HoRg5TRiXOEkNTBmgSQbR2+otM2q2gmkn4XAh0+yXkNW3hr2IydJyg9C26v\n"
+            + "/OzFvuLcw9iDBvrn433pDa6vjYDU+wiQaVtr1ItzsoE/kgW2IkgFVQB+CrkpAmwm\n"
+            + "omwEze3QFUUznP0PHy3P7g7UVD9u5x3APY6kVt2dq8mnOiLZkyfHHR2j6+j0E73I\n"
+            + "k3HQv7D0yRIv3kuNpFgJbITVgDIq9ukWU2XinDHUjguCDH+yQAoQH7hOQlWUHIz8\n"
+            + "/TtfZjrlUQf2uLzOWCn5KxfEqTkCAwEAAaMWMBQwEgYDVR0TAQH/BAgwBgEB/wIB\n"
+            + "ADANBgkqhkiG9w0BAQUFAAOCAQEArYkBkUvMvteT4fN6lxUkzmp8R7clLPkA2HnJ\n"
+            + "7IUK3c5GJ0/blffxj/9Oe2g+etga15SIO73GNAnTxhxIJllKRmmh6SR+dwNMkAvE\n"
+            + "xp87/Y6cSeJp5d4HhZUvxpFjaUDsWIC8tpbriUJoqGIirprLVcsPgDjKyuvVOlbK\n"
+            + "aQf3fOoBPLspGWHgic8Iw1O4kRcStUGCSCwOtYcgMJEhVqTgX0sTX5BgatZhr8FY\n"
+            + "Mnoceo2vzzxgHJU9qZuPkpYDs+ipQjzhoIJaY4HU2Uz4jMptqxSdzsPpC6PAKwuN\n"
+            + "+LBCR0B194YbRn6726vWwUUE05yskVN6gllGSCgZ/G8y98DhjQ==\n"
+            + "-----END CERTIFICATE-----",
+        jdbc.serverSslCert());
+    assertEquals(SslMode.DISABLE, jdbc.sslMode());
+    assertEquals("pwd2", jdbc.password());
+  }
+
+  @Test
+  public void builder() throws SQLException {
+    Configuration conf =
+        new Configuration.Builder()
+            .addresses(
+                HostAddress.from("host1", 3305, true), HostAddress.from("host2", 3307, false))
+            .user("me")
+            .password("pwd")
+            .database("db")
+            .socketFactory("someSocketFactory")
+            .connectTimeout(22)
+            .restrictedAuth("mysql_native_password,client_ed25519")
+            .pipe("pipeName")
+            .localSocket("localSocket")
+            .tcpKeepAlive(false)
+            .uuidAsString(true)
+            .tcpAbortiveClose(true)
+            .localSocketAddress("localSocketAddress")
+            .socketTimeout(1000)
+            .allowMultiQueries(true)
+            .allowLocalInfile(false)
+            .useCompression(true)
+            .blankTableNameMeta(true)
+            .credentialType("ENV")
+            .sslMode("REQUIRED")
+            .enabledSslCipherSuites("myCipher,cipher2")
+            .sessionVariables("blabla")
+            .tinyInt1isBit(false)
+            .yearIsDateType(false)
+            .timezone("UTC")
+            .dumpQueriesOnException(true)
+            .prepStmtCacheSize(2)
+            .useAffectedRows(true)
+            .useServerPrepStmts(true)
+            .connectionAttributes("bla=bla")
+            .useBulkStmts(false)
+            .autocommit(false)
+            .includeInnodbStatusInDeadlockExceptions(true)
+            .includeThreadDumpInDeadlockExceptions(true)
+            .servicePrincipalName("SPN")
+            .defaultFetchSize(10)
+            .tlsSocketType("TLStype")
+            .maxQuerySizeToLog(100)
+            .retriesAllDown(10)
+            .galeraAllowedState("A,B")
+            .enabledSslProtocolSuites("TLSv1.2")
+            .transactionReplay(true)
+            .pool(true)
+            .poolName("myPool")
+            .maxPoolSize(16)
+            .minPoolSize(12)
+            .maxIdleTime(25000)
+            .transactionIsolation("REPEATABLE-READ")
+            .keyStore("/tmp")
+            .keyStorePassword("MyPWD")
+            .keyStoreType("JKS")
+            .geometryDefaultType("default")
+            .registerJmxPool(false)
+            .tcpKeepCount(50)
+            .tcpKeepIdle(10)
+            .tcpKeepInterval(50)
+            .poolValidMinDelay(260)
+            .useResetConnection(true)
+            .useReadAheadInput(true)
+            .cachePrepStmts(false)
+            .serverSslCert("mycertPath")
+            .serverRsaPublicKeyFile("RSAPath")
+            .allowPublicKeyRetrieval(true)
+            .createDatabaseIfNotExist(true)
+            .disablePipeline(true)
+            .maxAllowedPacket(8000)
+            .initSql("SET @@a='10'")
+            .build();
+    assertEquals(
+        "jdbc:mariadb://host1:3305,address=(host=host2)(port=3307)(type=replica)/db?user=me&password=***&timezone=UTC&autocommit=false&createDatabaseIfNotExist=true&transactionIsolation=REPEATABLE_READ&defaultFetchSize=10&maxQuerySizeToLog=100&maxAllowedPacket=8000&geometryDefaultType=default&restrictedAuth=mysql_native_password,client_ed25519&initSql=SET"
+            + " @@a='10'&socketFactory=someSocketFactory&connectTimeout=22&pipe=pipeName&localSocket=localSocket&uuidAsString=true&tcpKeepAlive=false&tcpKeepIdle=10&tcpKeepCount=50&tcpKeepInterval=50&tcpAbortiveClose=true&localSocketAddress=localSocketAddress&socketTimeout=1000&useReadAheadInput=true&tlsSocketType=TLStype&sslMode=TRUST&serverSslCert=mycertPath&keyStore=/tmp&keyStorePassword=MyPWD&keyStoreType=JKS&enabledSslCipherSuites=myCipher,cipher2&enabledSslProtocolSuites=TLSv1.2&allowMultiQueries=true&allowLocalInfile=false&useCompression=true&useAffectedRows=true&useBulkStmts=false&disablePipeline=true&cachePrepStmts=false&prepStmtCacheSize=2&useServerPrepStmts=true&credentialType=ENV&sessionVariables=blabla&connectionAttributes=bla=bla&servicePrincipalName=SPN&blankTableNameMeta=true&tinyInt1isBit=false&yearIsDateType=false&dumpQueriesOnException=true&includeInnodbStatusInDeadlockExceptions=true&includeThreadDumpInDeadlockExceptions=true&retriesAllDown=10&galeraAllowedState=A,B&transactionReplay=true&pool=true&poolName=myPool&maxPoolSize=16&minPoolSize=12&maxIdleTime=25000&registerJmxPool=false&poolValidMinDelay=260&useResetConnection=true&serverRsaPublicKeyFile=RSAPath&allowPublicKeyRetrieval=true",
+        conf.toString());
+  }
+
+  @Test
+  public void equal() throws SQLException {
+    Configuration conf = Configuration.parse("jdbc:mariadb://localhost/test");
+    assertEquals(conf, conf);
+    assertEquals(Configuration.parse("jdbc:mariadb://localhost/test"), conf);
+    assertNotEquals(null, conf);
+    assertNotEquals("", conf);
+    assertNotEquals(Configuration.parse("jdbc:mariadb://localhost/test2"), conf);
+  }
+
+  @Test
+  public void useMysqlMetadata() throws SQLException {
+    assertTrue(
+        new Configuration.Builder()
+            .database("DB")
+            .useMysqlMetadata(true)
+            .build()
+            .useMysqlMetadata());
+
+    assertFalse(
+        new Configuration.Builder()
+            .database("DB")
+            .useMysqlMetadata(false)
+            .build()
+            .useMysqlMetadata());
+
+    assertFalse(
+        new Configuration.Builder()
+            .database("DB")
+            .useMysqlMetadata(null)
+            .build()
+            .useMysqlMetadata());
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/util/ParameterListTest.java b/src/test/java/org/mariadb/jdbc/unit/util/ParameterListTest.java
new file mode 100644
index 0000000..a2aa2f9
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/util/ParameterListTest.java
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.unit.util;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.codec.Parameter;
+import org.mariadb.jdbc.plugin.codec.StringCodec;
+import org.mariadb.jdbc.util.ParameterList;
+
+public class ParameterListTest {
+
+  @Test
+  public void sizeLimit() {
+    ParameterList p = new ParameterList();
+    p.set(2, new Parameter<>(StringCodec.INSTANCE, "test2"));
+    assertNull(p.get(1));
+    assertNotNull(p.get(2));
+    assertThrows(ArrayIndexOutOfBoundsException.class, () -> p.get(3));
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/util/SecurityTest.java b/src/test/java/org/mariadb/jdbc/unit/util/SecurityTest.java
new file mode 100644
index 0000000..0495834
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/util/SecurityTest.java
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.unit.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.util.Security;
+
+@SuppressWarnings("ConstantConditions")
+public class SecurityTest {
+
+  @Test
+  public void parser() {
+    assertEquals(
+        "wait_timeout=5,some='bla;'", Security.parseSessionVariables("wait_timeout=5,some='bla;'"));
+    assertEquals(
+        "wait_timeout=5,some='bla\"\\n=;'",
+        Security.parseSessionVariables("wait_timeout=5;some='bla\"\\n=;'"));
+    assertEquals(
+        "wait_timeout=5,some=\"bla;'\"",
+        Security.parseSessionVariables("wait_timeout=5;some=\"bla;'\""));
+    assertEquals("wait_timeout", Security.parseSessionVariables("wait_timeout"));
+    assertEquals("wait_timeout=", Security.parseSessionVariables("wait_timeout="));
+    assertEquals("wait_timeout=1,t=", Security.parseSessionVariables("wait_timeout=1,t="));
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/util/VersionFactoryTest.java b/src/test/java/org/mariadb/jdbc/unit/util/VersionFactoryTest.java
new file mode 100644
index 0000000..38e6352
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/util/VersionFactoryTest.java
@@ -0,0 +1,17 @@
+package org.mariadb.jdbc.unit.util;

+

+import static org.junit.jupiter.api.Assertions.assertNotNull;

+

+import org.junit.jupiter.api.Test;

+import org.mariadb.jdbc.util.Version;

+import org.mariadb.jdbc.util.VersionFactory;

+

+class VersionFactoryTest {

+

+  @Test

+  public void testGetInstance() {

+    Version actual = VersionFactory.getInstance();

+

+    assertNotNull(actual);

+  }

+}

diff --git a/src/test/java/org/mariadb/jdbc/unit/util/VersionTest.java b/src/test/java/org/mariadb/jdbc/unit/util/VersionTest.java
new file mode 100644
index 0000000..8ff3816
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/util/VersionTest.java
@@ -0,0 +1,35 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.unit.util;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.util.Version;
+
+@SuppressWarnings("ConstantConditions")
+public class VersionTest {
+
+  @Test
+  public void testValue() {
+    Version v = new Version("3.0.0-alpha-SNAPSHOT");
+    assertEquals(3, v.getMajorVersion());
+    assertEquals(0, v.getMinorVersion());
+    assertEquals(0, v.getPatchVersion());
+    assertEquals("-alpha-SNAPSHOT", v.getQualifier());
+
+    v = new Version("3.0.0=alpha-SNAPSHOT");
+    assertEquals(3, v.getMajorVersion());
+    assertEquals(0, v.getMinorVersion());
+    assertEquals(0, v.getPatchVersion());
+    assertEquals("=alpha-SNAPSHOT", v.getQualifier());
+
+    v = new Version("3.0.1");
+    assertEquals(3, v.getMajorVersion());
+    assertEquals(0, v.getMinorVersion());
+    assertEquals(1, v.getPatchVersion());
+    assertEquals("", v.getQualifier());
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/util/constant/HaModeTest.java b/src/test/java/org/mariadb/jdbc/unit/util/constant/HaModeTest.java
new file mode 100644
index 0000000..d72c79c
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/util/constant/HaModeTest.java
@@ -0,0 +1,177 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.unit.util.constant;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.HostAddress;
+import org.mariadb.jdbc.client.impl.ConnectionHelper;
+import org.mariadb.jdbc.client.socket.impl.SocketUtility;
+import org.mariadb.jdbc.export.HaMode;
+import org.mariadb.jdbc.plugin.authentication.AuthenticationPluginLoader;
+import org.mariadb.jdbc.plugin.credential.CredentialPluginLoader;
+import org.mariadb.jdbc.plugin.tls.TlsSocketPluginLoader;
+import org.mariadb.jdbc.pool.Pools;
+import org.mariadb.jdbc.util.CharsetEncodingLength;
+import org.mariadb.jdbc.util.NativeSql;
+import org.mariadb.jdbc.util.Security;
+import org.mariadb.jdbc.util.VersionFactory;
+import org.mariadb.jdbc.util.constants.*;
+import org.mariadb.jdbc.util.log.LoggerHelper;
+import org.mariadb.jdbc.util.log.Loggers;
+import org.mariadb.jdbc.util.options.OptionAliases;
+
+public class HaModeTest {
+  @Test
+  public void instantiateStaticOnlyClass() {
+    new Capabilities();
+    new ColumnFlags();
+    new ConnectionState();
+    new ServerStatus();
+    new StateChange();
+    new CharsetEncodingLength();
+    new NativeSql();
+    new Security();
+    new OptionAliases();
+    new CredentialPluginLoader();
+    new AuthenticationPluginLoader();
+    new TlsSocketPluginLoader();
+    new LoggerHelper();
+    new ConnectionHelper();
+    new Pools();
+    new Loggers();
+    new VersionFactory();
+    new SocketUtility();
+  }
+
+  @Test
+  public void replicationEndOfBlacklistTest() {
+    HostAddress host1 = HostAddress.from("1", 3306, true);
+    HostAddress host2 = HostAddress.from("2", 3306, false);
+    HostAddress host3 = HostAddress.from("3", 3306, false);
+
+    List<HostAddress> available = new ArrayList<>();
+    available.add(host1);
+    available.add(host2);
+    available.add(host3);
+
+    ConcurrentMap<HostAddress, Long> denyList = new ConcurrentHashMap<>();
+    denyList.putIfAbsent(host1, System.currentTimeMillis() - 10);
+
+    Optional<HostAddress> res = HaMode.REPLICATION.getAvailableHost(available, denyList, true);
+    Assertions.assertTrue(res.isPresent());
+    Assertions.assertEquals(host1, res.get());
+
+    int replica1 = 0;
+    int replica2 = 0;
+    for (int i = 0; i < 1000; i++) {
+      res = HaMode.REPLICATION.getAvailableHost(available, denyList, false);
+      Assertions.assertTrue(res.isPresent());
+      if (host2.equals(res.get())) replica1++;
+      if (host3.equals(res.get())) replica2++;
+    }
+    assertTrue(replica1 > 350 && replica2 > 350, "bad distribution :" + replica1 + "/" + replica2);
+
+    replica1 = 0;
+    replica2 = 0;
+    denyList.putIfAbsent(host2, System.currentTimeMillis() - 10);
+    for (int i = 0; i < 1000; i++) {
+      res = HaMode.REPLICATION.getAvailableHost(available, denyList, false);
+      Assertions.assertTrue(res.isPresent());
+      if (host2.equals(res.get())) replica1++;
+      if (host3.equals(res.get())) replica2++;
+    }
+    assertTrue(replica1 > 350 && replica2 > 350, "bad distribution :" + replica1 + "/" + replica2);
+
+    for (int i = 0; i < 1000; i++) {
+      denyList.putIfAbsent(host2, System.currentTimeMillis() + 1000);
+      res = HaMode.REPLICATION.getAvailableHost(available, denyList, false);
+      Assertions.assertTrue(res.isPresent());
+      Assertions.assertEquals(host3, res.get());
+    }
+  }
+
+  @Test
+  public void loadBalanceTest() throws InterruptedException {
+    HostAddress host1 = HostAddress.from("1", 3306, true);
+    HostAddress host2 = HostAddress.from("2", 3306, true);
+    HostAddress host3 = HostAddress.from("3", 3306, true);
+    HostAddress host4 = HostAddress.from("4", 3306, false);
+    HostAddress host5 = HostAddress.from("5", 3306, false);
+    HostAddress host6 = HostAddress.from("6", 3306, false);
+
+    List<HostAddress> available = new ArrayList<>();
+    available.add(host1);
+    available.add(host2);
+    available.add(host3);
+    available.add(host4);
+    available.add(host5);
+    available.add(host6);
+
+    ConcurrentMap<HostAddress, Long> denyList = new ConcurrentHashMap<>();
+    Map<HostAddress, Integer> res = loopPercReturn(available, denyList, true);
+    Assertions.assertEquals(334, res.get(host1));
+    Assertions.assertEquals(333, res.get(host2));
+    Assertions.assertEquals(333, res.get(host3));
+
+    denyList.putIfAbsent(host1, System.currentTimeMillis() + 1000000);
+
+    res = loopPercReturn(available, denyList, true);
+    Assertions.assertEquals(null, res.get(host1));
+    Assertions.assertEquals(500, res.get(host2));
+    Assertions.assertEquals(500, res.get(host3));
+
+    denyList.clear();
+    denyList.putIfAbsent(host1, System.currentTimeMillis() - 1000000);
+
+    res = loopPercReturn(available, denyList, true);
+    Assertions.assertEquals(334, res.get(host1));
+    Assertions.assertEquals(333, res.get(host2));
+    Assertions.assertEquals(333, res.get(host3));
+
+    res = loopPercReturn(available, denyList, false);
+    Assertions.assertEquals(334, res.get(host4));
+    Assertions.assertEquals(333, res.get(host5));
+    Assertions.assertEquals(333, res.get(host6));
+  }
+
+  @Test
+  public void noneTest() {
+    HostAddress host1 = HostAddress.from("1", 3306, true);
+
+    List<HostAddress> available = new ArrayList<>();
+    available.add(host1);
+
+    ConcurrentMap<HostAddress, Long> denyList = new ConcurrentHashMap<>();
+
+    Optional<HostAddress> res = HaMode.NONE.getAvailableHost(available, denyList, true);
+    Assertions.assertTrue(res.isPresent());
+    Assertions.assertEquals(host1, res.get());
+
+    res = HaMode.NONE.getAvailableHost(new ArrayList<>(), denyList, true);
+    Assertions.assertFalse(res.isPresent());
+  }
+
+  private Map<HostAddress, Integer> loopPercReturn(
+      List<HostAddress> available, ConcurrentMap<HostAddress, Long> denyList, boolean primary) {
+    Map<HostAddress, Integer> resMap = new HashMap<>();
+    for (int i = 0; i < 1000; i++) {
+      Optional<HostAddress> res = HaMode.LOADBALANCE.getAvailableHost(available, denyList, primary);
+      if (res.isPresent()) {
+        if (resMap.containsKey(res.get())) {
+          resMap.put(res.get(), resMap.get(res.get()) + 1);
+        } else {
+          resMap.put(res.get(), 1);
+        }
+      }
+    }
+    return resMap;
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/util/log/ConsoleLoggerTest.java b/src/test/java/org/mariadb/jdbc/unit/util/log/ConsoleLoggerTest.java
new file mode 100644
index 0000000..a2e2870
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/util/log/ConsoleLoggerTest.java
@@ -0,0 +1,95 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.unit.util.log;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.util.log.ConsoleLogger;
+
+public class ConsoleLoggerTest {
+
+  @Test
+  public void logger() throws IOException {
+    logger(true);
+    logger(false);
+  }
+
+  public void logger(boolean logDebug) throws IOException {
+    try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+      try (ByteArrayOutputStream err = new ByteArrayOutputStream()) {
+
+        ConsoleLogger logger =
+            new ConsoleLogger("test", new PrintStream(out), new PrintStream(err), logDebug);
+
+        assertEquals("test", logger.getName());
+        assertTrue(logger.isErrorEnabled());
+        logger.error("error msg");
+        logger.error("error msg3 {} {}", 1, "t");
+        logger.error("error msg4 {}", (String) null);
+        logger.error(null, (String) null);
+        logger.error("error msg2", new SQLException("test"));
+
+        assertTrue(logger.isInfoEnabled());
+        logger.info("info msg");
+        logger.info("info msg3 {} {}", 1, "t");
+        logger.info("info msg2", new SQLException("test"));
+
+        assertTrue(logger.isWarnEnabled());
+        logger.warn("warn msg");
+        logger.warn("warn msg3 {} {}", 1, "t");
+        logger.warn("warn msg2", new SQLException("test"));
+
+        assertEquals(logDebug, logger.isDebugEnabled());
+        logger.debug("debug msg");
+        logger.debug("debug msg3 {} {}", 1, "t");
+        logger.debug("debug msg2", new SQLException("test"));
+
+        assertEquals(logDebug, logger.isTraceEnabled());
+        logger.trace("trace msg");
+        logger.trace("trace msg3 {} {}", 1, "t");
+        logger.trace("trace msg2", new SQLException("test"));
+
+        String errSt = err.toString();
+        String outSt = out.toString();
+
+        assertTrue(
+            errSt.contains(
+                "[ERROR] (main) error msg\n"
+                    + "[ERROR] (main) error msg3 1 t\n"
+                    + "[ERROR] (main) error msg4 null\n"
+                    + "[ERROR] (main) null\n"
+                    + "[ERROR] (main) error msg2 - java.sql.SQLException: test"));
+        assertTrue(
+            errSt.contains(
+                "[ WARN] (main) warn msg\n"
+                    + "[ WARN] (main) warn msg3 1 t\n"
+                    + "[ WARN] (main) warn msg2 - java.sql.SQLException: test"));
+        assertTrue(
+            outSt.contains(
+                "[ INFO] (main) info msg\n"
+                    + "[ INFO] (main) info msg3 1 t\n"
+                    + "[ INFO] (main) info msg2 - java.sql.SQLException: test"));
+        if (logDebug) {
+          assertTrue(
+              outSt.contains(
+                  "[DEBUG] (main) debug msg\n"
+                      + "[DEBUG] (main) debug msg3 1 t\n"
+                      + "[DEBUG] (main) debug msg2 - java.sql.SQLException: test"));
+          assertTrue(
+              outSt.contains(
+                  "[TRACE] (main) trace msg\n"
+                      + "[TRACE] (main) trace msg3 1 t\n"
+                      + "[TRACE] (main) trace msg2 - java.sql.SQLException: test"));
+        } else {
+          assertFalse(outSt.contains("[DEBUG]"));
+          assertFalse(outSt.contains("[TRACE]"));
+        }
+      }
+    }
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/util/log/JdkLoggerTest.java b/src/test/java/org/mariadb/jdbc/unit/util/log/JdkLoggerTest.java
new file mode 100644
index 0000000..9bbaa4b
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/util/log/JdkLoggerTest.java
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.unit.util.log;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.sql.SQLException;
+import java.time.ZonedDateTime;
+import java.util.logging.*;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.util.log.JdkLogger;
+
+public class JdkLoggerTest {
+  final ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+  public class BufHandler extends StreamHandler {
+    public BufHandler() {
+      super(out, new MySimpleFormatter());
+    }
+
+    public void publish(LogRecord record) {
+      super.publish(record);
+      this.flush();
+    }
+
+    public void close() {
+      this.flush();
+    }
+  }
+
+  public static class MySimpleFormatter extends Formatter {
+
+    public MySimpleFormatter() {}
+
+    public String format(LogRecord record) {
+      ZonedDateTime zdt = ZonedDateTime.now();
+      String source;
+      if (record.getSourceClassName() != null) {
+        source = record.getSourceClassName();
+        if (record.getSourceMethodName() != null) {
+          source = source + " " + record.getSourceMethodName();
+        }
+      } else {
+        source = record.getLoggerName();
+      }
+
+      String message = this.formatMessage(record);
+      String throwable = "";
+      if (record.getThrown() != null) {
+        StringWriter sw = new StringWriter();
+        PrintWriter pw = new PrintWriter(sw);
+        pw.println();
+        record.getThrown().printStackTrace(pw);
+        pw.close();
+        throwable = sw.toString();
+      }
+
+      return String.format(
+          "[%4$-7s] %5$s %n",
+          zdt, source, record.getLoggerName(), Level.FINEST, message, throwable);
+    }
+  }
+
+  @Test
+  public void logger() {
+
+    java.util.logging.Logger log = Logger.getLogger("logger");
+    log.addHandler(new BufHandler());
+    log.setLevel(Level.FINEST);
+    JdkLogger logger = new JdkLogger(log);
+
+    assertEquals("logger", logger.getName());
+    assertTrue(logger.isErrorEnabled());
+    logger.error("error msg");
+    logger.error("error msg3 {} {}", 1, "t");
+    logger.error("error msg4 {}", (String) null);
+    logger.error(null, (String) null);
+    logger.error("error msg2", new SQLException("test"));
+
+    assertTrue(logger.isInfoEnabled());
+    logger.info("info msg");
+    logger.info("info msg3 {} {}", 1, "t");
+    logger.info("info msg2", new SQLException("test"));
+
+    assertTrue(logger.isWarnEnabled());
+    logger.warn("warn msg");
+    logger.warn("warn msg3 {} {}", 1, "t");
+    logger.warn("warn msg2", new SQLException("test"));
+
+    assertTrue(logger.isDebugEnabled());
+    logger.debug("debug msg");
+    logger.debug("debug msg3 {} {}", 1, "t");
+    logger.debug("debug msg2", new SQLException("test"));
+
+    assertTrue(logger.isTraceEnabled());
+    logger.trace("trace msg");
+    logger.trace("trace msg3 {} {}", 1, "t");
+    logger.trace("trace msg3 {} {}", (String) null);
+    logger.trace("trace msg2", new SQLException("test"));
+
+    String outSt = out.toString();
+    String expected =
+        "[FINEST ] error msg \n"
+            + "[FINEST ] error msg3 1 t \n"
+            + "[FINEST ] error msg4 null \n"
+            + "[FINEST ] null \n"
+            + "[FINEST ] error msg2 \n"
+            + "[FINEST ] info msg \n"
+            + "[FINEST ] info msg3 1 t \n"
+            + "[FINEST ] info msg2 \n"
+            + "[FINEST ] warn msg \n"
+            + "[FINEST ] warn msg3 1 t \n"
+            + "[FINEST ] warn msg2 ";
+    assertTrue(outSt.contains(expected) || outSt.replace("\r\n", "\n").contains(expected));
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/util/log/LoggerFactoryTest.java b/src/test/java/org/mariadb/jdbc/unit/util/log/LoggerFactoryTest.java
new file mode 100644
index 0000000..d359fbf
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/util/log/LoggerFactoryTest.java
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.unit.util.log;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.util.log.ConsoleLogger;
+import org.mariadb.jdbc.util.log.JdkLogger;
+import org.mariadb.jdbc.util.log.Loggers;
+import org.mariadb.jdbc.util.log.Slf4JLogger;
+
+public class LoggerFactoryTest {
+
+  @AfterAll
+  public static void drop() {
+    System.clearProperty(Loggers.TEST_ENABLE_SLF4J);
+    System.clearProperty(Loggers.FALLBACK_PROPERTY);
+    System.clearProperty(Loggers.CONSOLE_DEBUG_PROPERTY);
+    Loggers.init();
+  }
+
+  @Test
+  public void defaultSlf4j() {
+    System.clearProperty(Loggers.TEST_ENABLE_SLF4J);
+    System.clearProperty(Loggers.FALLBACK_PROPERTY);
+    Loggers.init();
+    assertTrue(Loggers.getLogger("test") instanceof Slf4JLogger);
+  }
+
+  @Test
+  public void commonLogger() {
+    System.setProperty(Loggers.TEST_ENABLE_SLF4J, "false");
+    System.setProperty(Loggers.FALLBACK_PROPERTY, "JDK");
+    Loggers.init();
+    assertTrue(Loggers.getLogger("test") instanceof JdkLogger);
+  }
+
+  @Test
+  public void consoleLogger() {
+    System.setProperty(Loggers.TEST_ENABLE_SLF4J, "false");
+    System.clearProperty(Loggers.FALLBACK_PROPERTY);
+    Loggers.init();
+    assertTrue(Loggers.getLogger("test") instanceof ConsoleLogger);
+    System.setProperty(Loggers.CONSOLE_DEBUG_PROPERTY, "true");
+    Loggers.init();
+    assertTrue(Loggers.getLogger("test") instanceof ConsoleLogger);
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/util/log/LoggerHelperTest.java b/src/test/java/org/mariadb/jdbc/unit/util/log/LoggerHelperTest.java
new file mode 100644
index 0000000..af94bfa
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/util/log/LoggerHelperTest.java
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.unit.util.log;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.util.log.LoggerHelper;
+
+class LoggerHelperTest {
+
+  @Test
+  void hex() {
+    byte[] bb =
+        new byte[] {
+          0x4A, 0x00, 0x00, 0x00, 0x03, 0x53, 0x45, 0x4C, 0x45,
+          0x43, 0x54, 0x20, 0x40, 0x40, 0x6D, 0x61, 0x78, 0x5F,
+          0x61, 0x6C, 0x6C, 0x6F, 0x77, 0x65, 0x64, 0x5F, 0x70,
+          0x61, 0x63, 0x6B, 0x65, 0x74, 0x20, 0x2C, 0x20, 0x40,
+          0x40, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6D, 0x5F, 0x74,
+          0x69, 0x6D, 0x65, 0x5F, 0x7A, 0x6F, 0x6E, 0x65, 0x2C,
+          0x20, 0x40, 0x40, 0x74, 0x69, 0x6D, 0x65, 0x5F, 0x7A,
+          0x6F, 0x6E, 0x65, 0x2C, 0x20, 0x40, 0x40, 0x73, 0x71,
+          0x6C, 0x5F, 0x6D, 0x6F, 0x64, 0x65, 0x64, 0x65
+        };
+
+    Assertions.assertEquals("", LoggerHelper.hex(null, 0, 2));
+    Assertions.assertEquals("", LoggerHelper.hex(new byte[0], 0, 2));
+
+    Assertions.assertEquals(
+        "       +--------------------------------------------------+\n"
+            + "       |  0  1  2  3  4  5  6  7   8  9  a  b  c  d  e  f |\n"
+            + "+------+--------------------------------------------------+------------------+\n"
+            + "|000000| 4A 00 00 00 03 53 45 4C  45 43 54 20 40 40 6D 61 | J....SELECT @@ma |\n"
+            + "|000010| 78 5F 61 6C 6C 6F 77 65  64 5F 70 61 63 6B 65 74 | x_allowed_packet |\n"
+            + "|000020| 20 2C 20 40 40 73 79 73  74 65 6D 5F 74 69 6D 65 |  , @@system_time |\n"
+            + "|000030| 5F 7A 6F 6E 65 2C 20 40  40 74 69 6D 65 5F 7A 6F | _zone, @@time_zo |\n"
+            + "|000040| 6E 65 2C 20 40 40 73 71  6C 5F 6D 6F 64 65       | ne, @@sql_mode   |\n"
+            + "+------+--------------------------------------------------+------------------+\n",
+        LoggerHelper.hex(bb, 0, bb.length - 2));
+
+    Assertions.assertEquals(
+        "       +--------------------------------------------------+\n"
+            + "       |  0  1  2  3  4  5  6  7   8  9  a  b  c  d  e  f |\n"
+            + "+------+--------------------------------------------------+------------------+\n"
+            + "|000000| 4A 00 00 00 03 53 45 4C  45 43 54 20 40 40 6D 61 | J....SELECT @@ma |\n"
+            + "+------+-------------------truncated----------------------+------------------+\n",
+        LoggerHelper.hex(bb, 0, bb.length - 2, 16));
+
+    assertEquals(
+        "       +--------------------------------------------------+\n"
+            + "       |  0  1  2  3  4  5  6  7   8  9  a  b  c  d  e  f |\n"
+            + "+------+--------------------------------------------------+------------------+\n"
+            + "|000000| 4A 00 00 00 03 53 45 4C  45 43 54 20 40 40 6D 61 | J....SELECT @@ma |\n"
+            + "|000010| 78 5F 61 6C 6C 6F 77 65  64 5F 70 61 63 6B 65 74 | x_allowed_packet |\n"
+            + "|000020| 20 2C 20 40 40 73 79 73  74 65 6D 5F 74 69 6D 65 |  , @@system_time |\n"
+            + "|000030| 5F 7A 6F 6E 65 2C 20 40  40 74 69 6D 65 5F 7A 6F | _zone, @@time_zo |\n"
+            + "|000040| 6E 65 2C 20 40 40 73 71  6C 5F 6D 6F 64 65 64 65 | ne, @@sql_modede |\n"
+            + "+------+--------------------------------------------------+------------------+\n",
+        LoggerHelper.hex(bb, 0, bb.length));
+
+    assertEquals(
+        "       +--------------------------------------------------+\n"
+            + "       |  0  1  2  3  4  5  6  7   8  9  a  b  c  d  e  f |\n"
+            + "+------+--------------------------------------------------+------------------+\n"
+            + "|000000| 00 00 00 03 53 45 4C 45  43 54 20 40 40 6D 61 78 | ....SELECT @@max |\n"
+            + "+------+--------------------------------------------------+------------------+\n",
+        LoggerHelper.hex(bb, 1, 16));
+
+    assertEquals(
+        "       +--------------------------------------------------+\n"
+            + "       |  0  1  2  3  4  5  6  7   8  9  a  b  c  d  e  f |\n"
+            + "+------+--------------------------------------------------+------------------+\n"
+            + "|000000| 00 00 00 03 53 45 4C 45  43 54 20 40 40 6D 61 78 | ....SELECT @@max |\n"
+            + "|000010| 5F                                               | _                |\n"
+            + "+------+--------------------------------------------------+------------------+\n",
+        LoggerHelper.hex(bb, 1, 17));
+    byte[] header = new byte[] {1, 2, 3, 4};
+
+    assertEquals(
+        "       +--------------------------------------------------+\n"
+            + "       |  0  1  2  3  4  5  6  7   8  9  a  b  c  d  e  f |\n"
+            + "+------+--------------------------------------------------+------------------+\n"
+            + "|000000| 01 02 03 04 00 00 00 03  53 45 4C 45 43 54 20 40 | ........SELECT @ |\n"
+            + "|000010| 40 6D 61 78                                      | @max             |\n"
+            + "+------+--------------------------------------------------+------------------+\n",
+        LoggerHelper.hex(header, bb, 1, 16, Integer.MAX_VALUE));
+    assertEquals(
+        "       +--------------------------------------------------+\n"
+            + "       |  0  1  2  3  4  5  6  7   8  9  a  b  c  d  e  f |\n"
+            + "+------+--------------------------------------------------+------------------+\n"
+            + "|000000| 01 02 03 04 00 00 00 03  53 45 4C 45 43 54 20 40 | ........SELECT @ |\n"
+            + "|000010| 40                                               | @                |\n"
+            + "+------+-------------------truncated----------------------+------------------+\n",
+        LoggerHelper.hex(header, bb, 1, 16, 17));
+  }
+}
diff --git a/src/test/java/org/mariadb/jdbc/unit/util/log/LoggersTest.java b/src/test/java/org/mariadb/jdbc/unit/util/log/LoggersTest.java
new file mode 100644
index 0000000..42cb7b5
--- /dev/null
+++ b/src/test/java/org/mariadb/jdbc/unit/util/log/LoggersTest.java
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// Copyright (c) 2012-2014 Monty Program Ab
+// Copyright (c) 2015-2021 MariaDB Corporation Ab
+
+package org.mariadb.jdbc.unit.util.log;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.FileAppender;
+import java.io.*;
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.mariadb.jdbc.util.log.Logger;
+import org.mariadb.jdbc.util.log.Loggers;
+import org.slf4j.LoggerFactory;
+
+public class LoggersTest {
+
+  @Test
+  public void Slf4JLogger() throws IOException {
+    ch.qos.logback.classic.Logger root =
+        (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(this.getClass());
+    root.setLevel(Level.TRACE);
+
+    LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
+    PatternLayoutEncoder ple = new PatternLayoutEncoder();
+    ple.setPattern("%level %logger{10} [%file] %msg%n");
+    ple.setContext(lc);
+    ple.start();
+
+    File tempfile = File.createTempFile("debug-", ".log");
+    FileAppender<ILoggingEvent> fileAppender = new FileAppender<>();
+    fileAppender.setFile(tempfile.getCanonicalPath());
+    fileAppender.setEncoder(ple);
+    fileAppender.setContext(lc);
+    fileAppender.start();
+
+    root.addAppender(fileAppender);
+    Logger logger = Loggers.getLogger(LoggersTest.class);
+    assertTrue(logger.isErrorEnabled());
+    assertTrue(logger.isWarnEnabled());
+    assertTrue(logger.isInfoEnabled());
+    assertTrue(logger.isDebugEnabled());
+    assertTrue(logger.isTraceEnabled());
+    assertEquals("org.mariadb.jdbc.unit.util.log.LoggersTest", logger.getName());
+    Logger logger2 = Loggers.getLogger("bla");
+    assertEquals("bla", logger2.getName());
+
+    logger.error("Test err1");
+    logger.error("Test err2", new SQLException("test exception"));
+    logger.error("Test err3 {}", "param");
+
+    logger.warn("Test warn 1");
+    logger.warn("Test warn 2", new SQLException("test exception"));
+    logger.warn("Test warn 3 {}", "param");
+
+    logger.info("Test info 1");
+    logger.info("Test info 2", new SQLException("test exception"));
+    logger.info("Test info 3 {}", "param");
+
+    logger.debug("Test debug 1");
+    logger.debug("Test debug 2", new SQLException("test exception"));
+    logger.debug("Test debug 3 {}", "param");
+
+    logger.trace("Test trace 1");
+    logger.trace("Test trace 2", new SQLException("test exception"));
+    logger.trace("Test trace 3 {}", "param");
+
+    BufferedReader reader = new BufferedReader(new FileReader(tempfile.getCanonicalPath()));
+    assertEquals("ERROR o.m.j.u.u.l.LoggersTest [Slf4JLogger.java] Test err1", reader.readLine());
+    assertEquals("ERROR o.m.j.u.u.l.LoggersTest [Slf4JLogger.java] Test err2", reader.readLine());
+    assertEquals("java.sql.SQLException: test exception", reader.readLine());
+    String line;
+    do {
+      line = reader.readLine();
+    } while (line != null && line.startsWith("\tat "));
+
+    assertEquals("ERROR o.m.j.u.u.l.LoggersTest [Slf4JLogger.java] Test err3 param", line);
+    assertEquals("WARN o.m.j.u.u.l.LoggersTest [Slf4JLogger.java] Test warn 1", reader.readLine());
+    assertEquals("WARN o.m.j.u.u.l.LoggersTest [Slf4JLogger.java] Test warn 2", reader.readLine());
+    assertEquals("java.sql.SQLException: test exception", reader.readLine());
+
+    do {
+      line = reader.readLine();
+    } while (line != null && line.startsWith("\tat "));
+
+    assertEquals("WARN o.m.j.u.u.l.LoggersTest [Slf4JLogger.java] Test warn 3 param", line);
+    assertEquals("INFO o.m.j.u.u.l.LoggersTest [Slf4JLogger.java] Test info 1", reader.readLine());
+    assertEquals("INFO o.m.j.u.u.l.LoggersTest [Slf4JLogger.java] Test info 2", reader.readLine());
+    assertEquals("java.sql.SQLException: test exception", reader.readLine());
+
+    do {
+      line = reader.readLine();
+    } while (line != null && line.startsWith("\tat "));
+
+    assertEquals("INFO o.m.j.u.u.l.LoggersTest [Slf4JLogger.java] Test info 3 param", line);
+    assertEquals(
+        "DEBUG o.m.j.u.u.l.LoggersTest [Slf4JLogger.java] Test debug 1", reader.readLine());
+    assertEquals(
+        "DEBUG o.m.j.u.u.l.LoggersTest [Slf4JLogger.java] Test debug 2", reader.readLine());
+    assertEquals("java.sql.SQLException: test exception", reader.readLine());
+
+    do {
+      line = reader.readLine();
+    } while (line != null && line.startsWith("\tat "));
+
+    assertEquals("DEBUG o.m.j.u.u.l.LoggersTest [Slf4JLogger.java] Test debug 3 param", line);
+    assertEquals(
+        "TRACE o.m.j.u.u.l.LoggersTest [Slf4JLogger.java] Test trace 1", reader.readLine());
+    assertEquals(
+        "TRACE o.m.j.u.u.l.LoggersTest [Slf4JLogger.java] Test trace 2", reader.readLine());
+    assertEquals("java.sql.SQLException: test exception", reader.readLine());
+
+    reader.close();
+  }
+}
diff --git a/src/test/resources/META-INF/services/org.mariadb.jdbc.plugin.CredentialPlugin b/src/test/resources/META-INF/services/org.mariadb.jdbc.plugin.CredentialPlugin
new file mode 100644
index 0000000..6311e36
--- /dev/null
+++ b/src/test/resources/META-INF/services/org.mariadb.jdbc.plugin.CredentialPlugin
@@ -0,0 +1,4 @@
+org.mariadb.jdbc.plugin.credential.aws.AwsIamCredentialPlugin
+org.mariadb.jdbc.plugin.credential.env.EnvCredentialPlugin
+org.mariadb.jdbc.plugin.credential.system.PropertiesCredentialPlugin
+org.mariadb.jdbc.integration.util.Env2CredentialPlugin
diff --git a/src/test/resources/conf.properties b/src/test/resources/conf.properties
new file mode 100644
index 0000000..59ad289
--- /dev/null
+++ b/src/test/resources/conf.properties
@@ -0,0 +1,7 @@
+DB_HOST=localhost
+DB_PORT=3306
+DB_DATABASE=testj
+DB_USER=root
+DB_PASSWORD=
+DB_OTHER=
+
diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..5587c71
--- /dev/null
+++ b/src/test/resources/logback-test.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+
+  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+    <layout class="ch.qos.logback.classic.PatternLayout">
+      <Pattern>
+        %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
+      </Pattern>
+    </layout>
+  </appender>
+
+<!--  <logger name="org.mariadb.jdbc" level="trace" additivity="false">-->
+<!--    <appender-ref ref="CONSOLE"/>-->
+<!--  </logger>-->
+
+  <root level="error">
+    <appender-ref ref="CONSOLE"/>
+  </root>
+
+</configuration>
diff --git a/src/test/resources/testclient-keystore.p12 b/src/test/resources/testclient-keystore.p12
new file mode 100644
index 0000000..88544e3
--- /dev/null
+++ b/src/test/resources/testclient-keystore.p12
Binary files differ