diff --git a/src/main/java/org/mariadb/jdbc/Configuration.java b/src/main/java/org/mariadb/jdbc/Configuration.java index 10b2cab38..8978a348b 100644 --- a/src/main/java/org/mariadb/jdbc/Configuration.java +++ b/src/main/java/org/mariadb/jdbc/Configuration.java @@ -63,6 +63,7 @@ public class Configuration { // various private String timezone = null; + private String connectionCollation = null; private String connectionTimeZone = null; private Boolean forceConnectionTimeZoneToSession = null; private boolean preserveInstants; @@ -174,6 +175,7 @@ private Configuration( Properties nonMappedOptions, String timezone, String connectionTimeZone, + String connectionCollation, boolean forceConnectionTimeZoneToSession, boolean preserveInstants, Boolean autocommit, @@ -261,6 +263,7 @@ private Configuration( this.nonMappedOptions = nonMappedOptions; this.timezone = timezone; this.connectionTimeZone = connectionTimeZone; + this.connectionCollation = connectionCollation; this.forceConnectionTimeZoneToSession = forceConnectionTimeZoneToSession; this.preserveInstants = preserveInstants; this.autocommit = autocommit; @@ -378,6 +381,7 @@ private Configuration( Boolean yearIsDateType, String timezone, String connectionTimeZone, + String connectionCollation, Boolean forceConnectionTimeZoneToSession, Boolean preserveInstants, Boolean dumpQueriesOnException, @@ -475,6 +479,7 @@ private Configuration( if (yearIsDateType != null) this.yearIsDateType = yearIsDateType; this.timezone = timezone; if (connectionTimeZone != null) this.connectionTimeZone = connectionTimeZone; + if (connectionCollation != null) this.connectionCollation = connectionCollation; if (forceConnectionTimeZoneToSession != null) this.forceConnectionTimeZoneToSession = forceConnectionTimeZoneToSession; if (preserveInstants != null) this.preserveInstants = preserveInstants; @@ -618,6 +623,24 @@ private Configuration( // option value verification // ************************************************************* + // ensure connection collation format + if (connectionCollation != null) { + if ("".equals(connectionCollation.trim())) { + this.connectionCollation = null; + } else { + // ensure this is an utf8mb4 collation + if (!connectionCollation.toLowerCase(Locale.ROOT).startsWith("utf8mb4_")) { + throw new SQLException( + String.format( + "wrong connection collation '%s' only utf8mb4 collation are accepted", + connectionCollation)); + } else if (!connectionCollation.matches("^[a-zA-Z0-9_]+$")) { + throw new SQLException( + String.format("wrong connection collation '%s' name", connectionCollation)); + } + } + } + // int fields must all be positive Field[] fields = Configuration.class.getDeclaredFields(); try { @@ -651,6 +674,7 @@ public Builder toBuilder() { .haMode(this.haMode) .timezone(this.timezone) .connectionTimeZone(this.connectionTimeZone) + .connectionCollation(this.connectionCollation) .forceConnectionTimeZoneToSession(this.forceConnectionTimeZoneToSession) .preserveInstants(this.preserveInstants) .autocommit(this.autocommit) @@ -1640,6 +1664,15 @@ public String connectionTimeZone() { return connectionTimeZone; } + /** + * get connectionCollation + * + * @return connectionCollation + */ + public String connectionCollation() { + return connectionCollation; + } + /** * forceConnectionTimeZoneToSession must connection timezone be forced * @@ -2129,6 +2162,7 @@ public static final class Builder implements Cloneable { // various private String timezone; private String connectionTimeZone; + private String connectionCollation; private Boolean forceConnectionTimeZoneToSession; private Boolean preserveInstants; private Boolean autocommit; @@ -2808,6 +2842,18 @@ public Builder connectionTimeZone(String connectionTimeZone) { return this; } + /** + * indicate what utf8mb4 collation to use. if not set, server default collation for utf8mb4 will + * be used + * + * @param connectionCollation utf8mb4 collation to use + * @return this {@link Builder} + */ + public Builder connectionCollation(String connectionCollation) { + this.connectionCollation = nullOrEmpty(connectionCollation); + return this; + } + /** * Indicate if connectionTimeZone must be forced to session * @@ -3350,6 +3396,7 @@ public Configuration build() throws SQLException { this.yearIsDateType, this.timezone, this.connectionTimeZone, + this.connectionCollation, this.forceConnectionTimeZoneToSession, this.preserveInstants, this.dumpQueriesOnException, diff --git a/src/main/java/org/mariadb/jdbc/client/impl/StandardClient.java b/src/main/java/org/mariadb/jdbc/client/impl/StandardClient.java index dbaeaf019..2694c5576 100644 --- a/src/main/java/org/mariadb/jdbc/client/impl/StandardClient.java +++ b/src/main/java/org/mariadb/jdbc/client/impl/StandardClient.java @@ -817,9 +817,16 @@ public String createSessionVariableQuery(Context context) { context.canUseTransactionIsolation() ? "transaction_read_only" : "tx_read_only")); } - if (context.getCharset() == null || !"utf8mb4".equals(context.getCharset())) { - sessionCommands.add("NAMES utf8mb4"); + if (context.getCharset() == null + || !"utf8mb4".equals(context.getCharset()) + || conf.connectionCollation() != null) { + String defaultCharsetSet = "NAMES utf8mb4"; + if (conf.connectionCollation() != null) { + defaultCharsetSet += " COLLATE " + conf.connectionCollation(); + } + sessionCommands.add(defaultCharsetSet); } + if (!sessionCommands.isEmpty()) { return "set " + sessionCommands.stream().collect(Collectors.joining(",")); } diff --git a/src/main/resources/driver.properties b/src/main/resources/driver.properties index bb6ab2446..24627b070 100644 --- a/src/main/resources/driver.properties +++ b/src/main/resources/driver.properties @@ -81,4 +81,5 @@ nullDatabaseMeansCurrent=When enable, in DatabaseMetadata, will handle null data preserveInstants= This option controls whether the connector converts Timestamp values to the connection's time zone. connectionTimeZone=This option defines the connection's time zone. LOCAL retrieves the JVM's default time zone, SERVER fetches the server's global time zone upon connection creation, and allows specifying a server time zone without requesting it during connection establishment. forceConnectionTimeZoneToSession=This setting dictates whether the connector enforces the connection time zone for the session. -pinGlobalTxToPhysicalConnection=When set, will reuse previous connection used for XID. \ No newline at end of file +pinGlobalTxToPhysicalConnection=When set, will reuse previous connection used for XID. +connectionCollation=indicate what utf8mb4 collation to use. if not set, server default collation for utf8mb4 will be used diff --git a/src/test/java/org/mariadb/jdbc/integration/ConfigurationTest.java b/src/test/java/org/mariadb/jdbc/integration/ConfigurationTest.java index f41ada7ed..510c99acb 100644 --- a/src/test/java/org/mariadb/jdbc/integration/ConfigurationTest.java +++ b/src/test/java/org/mariadb/jdbc/integration/ConfigurationTest.java @@ -155,4 +155,38 @@ public void jdbcCompliantTruncation() throws SQLException { stmt.execute("SET @@global.sql_mode = '" + sqlMode + "'"); } } + + @Test + public void connectionCollationTest() throws SQLException { + try (org.mariadb.jdbc.Connection conn = + createCon("&connectionCollation=utf8mb4_vietnamese_ci")) { + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT @@session.COLLATION_CONNECTION"); + rs.next(); + assertEquals("utf8mb4_vietnamese_ci", rs.getString(1)); + } + + try (org.mariadb.jdbc.Connection conn = createCon("&connectionCollation=")) { + Statement stmt = conn.createStatement(); + ResultSet rs = + stmt.executeQuery("SELECT @@global.COLLATION_CONNECTION, @@session.COLLATION_CONNECTION"); + rs.next(); + assertEquals(rs.getString(2), rs.getString(1)); + } + + Statement stmt = sharedConn.createStatement(); + ResultSet rs = + stmt.executeQuery("SELECT @@global.COLLATION_CONNECTION, @@session.COLLATION_CONNECTION"); + rs.next(); + assertEquals(rs.getString(2), rs.getString(1)); + + assertThrowsContains( + SQLException.class, + () -> createCon("&connectionCollation=utf8_vietnamese_ci"), + "wrong connection collation 'utf8_vietnamese_ci' only utf8mb4 collation are accepted"); + assertThrowsContains( + SQLException.class, + () -> createCon("&connectionCollation=utf8mb4_vietnamese_ci;SELECT"), + "wrong connection collation 'utf8mb4_vietnamese_ci;SELECT' name"); + } } diff --git a/src/test/java/org/mariadb/jdbc/unit/util/ConfigurationTest.java b/src/test/java/org/mariadb/jdbc/unit/util/ConfigurationTest.java index 053b036ce..8c377b9b3 100644 --- a/src/test/java/org/mariadb/jdbc/unit/util/ConfigurationTest.java +++ b/src/test/java/org/mariadb/jdbc/unit/util/ConfigurationTest.java @@ -904,9 +904,10 @@ public void builder() throws SQLException { .preserveInstants(true) .connectionTimeZone("SERVER") .forceConnectionTimeZoneToSession(false) + .connectionCollation("utf8mb4_vietnamese_ci") .build(); String expected = - "jdbc:mariadb://host1:3305,address=(host=host2)(port=3307)(type=replica)/db?user=me&password=***&timezone=UTC&connectionTimeZone=SERVER&forceConnectionTimeZoneToSession=false&preserveInstants=true&autocommit=false&nullDatabaseMeansCurrent=true&useCatalogTerm=SCHEMA&createDatabaseIfNotExist=true&useLocalSessionState=true&returnMultiValuesGeneratedIds=true&permitRedirect=false&transactionIsolation=REPEATABLE_READ&defaultFetchSize=10&maxQuerySizeToLog=100&maxAllowedPacket=8000&geometryDefaultType=default&restrictedAuth=mysql_native_password,client_ed25519&initSql=SET" + "jdbc:mariadb://host1:3305,address=(host=host2)(port=3307)(type=replica)/db?user=me&password=***&timezone=UTC&connectionCollation=utf8mb4_vietnamese_ci&connectionTimeZone=SERVER&forceConnectionTimeZoneToSession=false&preserveInstants=true&autocommit=false&nullDatabaseMeansCurrent=true&useCatalogTerm=SCHEMA&createDatabaseIfNotExist=true&useLocalSessionState=true&returnMultiValuesGeneratedIds=true&permitRedirect=false&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&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&trustStoreType=JKS&enabledSslCipherSuites=myCipher,cipher2&enabledSslProtocolSuites=TLSv1.2&fallbackToSystemKeyStore=false&fallbackToSystemTrustStore=false&allowMultiQueries=true&allowLocalInfile=false&useCompression=true&useAffectedRows=true&useBulkStmts=true&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®isterJmxPool=false&poolValidMinDelay=260&useResetConnection=true&serverRsaPublicKeyFile=RSAPath&allowPublicKeyRetrieval=true"; assertEquals(expected, conf.toString()); assertEquals(expected, conf.toBuilder().build().toString());