diff --git a/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java b/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java index 4f90afcc4f..c2c34c4882 100644 --- a/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java +++ b/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java @@ -552,6 +552,7 @@ public final class DbxJdbcPlugin { JdbcUrlCredentials urlCredentials = extractJdbcUrlCredentials(url); url = urlCredentials.url; Properties properties = new Properties(); + applyPhoenixUrlProperties(url, properties); String username = optionalText(connection, "username"); String password = optionalText(connection, "password"); if (username == null) { @@ -586,13 +587,47 @@ public final class DbxJdbcPlugin { } private static boolean isPhoenixConnection(JsonNode connection, String url) { - if (urlMatchesPrefix(url, "jdbc:phoenix:")) { + if (isPhoenixUrl(url)) { return true; } String driverClass = optionalText(connection, "jdbc_driver_class"); return driverClass != null && driverClass.equalsIgnoreCase("org.apache.phoenix.jdbc.PhoenixDriver"); } + private static void applyPhoenixUrlProperties(String url, Properties properties) { + if (!isPhoenixDirectUrl(url)) { + return; + } + int propertiesStart = url.indexOf(';'); + if (propertiesStart < 0) { + return; + } + // Phoenix builds its HBase client configuration from Driver.connect Properties, + // while arbitrary semicolon URL attributes are not merged into QueryServices. + for (String part : url.substring(propertiesStart + 1).split(";")) { + int equals = part.indexOf('='); + if (equals <= 0) { + continue; + } + String key = part.substring(0, equals).trim(); + if (!key.isEmpty()) { + properties.setProperty(key, part.substring(equals + 1).trim()); + } + } + } + + private static boolean isPhoenixDirectUrl(String url) { + return isPhoenixUrl(url) && !urlMatchesPrefix(url, "jdbc:phoenix:thin:"); + } + + private static boolean isPhoenixUrl(String url) { + String prefix = "jdbc:phoenix"; + if (url == null || !url.regionMatches(true, 0, prefix, 0, prefix.length())) { + return false; + } + return url.length() == prefix.length() || url.charAt(prefix.length()) == ':' || url.charAt(prefix.length()) == ';'; + } + private static void applyJdbcxExtensionSecurity(JsonNode connection, String url, Properties properties) { if (isJdbcxUrl(url) && !jdbcxHighPrivilegeExtensionsEnabled(connection)) { properties.setProperty(JDBCX_EXTENSION_WHITELIST_PROPERTY, JDBCX_SAFE_EXTENSION_WHITELIST); @@ -2923,7 +2958,11 @@ public final class DbxJdbcPlugin { } private static String jdbcUrlParamSeparator(String base) { - if (urlMatchesPrefix(base, "jdbc:sqlserver:") || urlMatchesPrefix(base, "jdbc:dremio:")) { + if ( + urlMatchesPrefix(base, "jdbc:sqlserver:") || + urlMatchesPrefix(base, "jdbc:dremio:") || + isPhoenixUrl(base) + ) { return base.endsWith(";") ? "" : ";"; } if (jdbcUrlUsesColonProperties(base)) { diff --git a/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java b/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java index 4ebfb43599..3a1529873b 100644 --- a/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java +++ b/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java @@ -953,6 +953,117 @@ final class DbxJdbcPluginTest { assertEquals(List.of("getAutoCommit", "setAutoCommit:true"), calls); } + @Test + void phoenixDirectUrlPropertiesArePassedToTheDriver() throws Exception { + RecordingConnectDriver driver = new RecordingConnectDriver("jdbc:phoenix:"); + DriverManager.registerDriver(driver); + String connection = """ + { + "connection_string": "jdbc:phoenix:ambari01,ambari02,ambari03:2181:/hbase-unsecure;phoenix.schema.isNamespaceMappingEnabled=true;phoenix.schema.mapSystemTablesToNamespace=true;user=url-user", + "username": "phoenix-user", + "connect_timeout_secs": 30 + } + """; + try { + JsonNode response = request("testConnection", """ + { "connection": %s } + """.formatted(connection)); + + assertFalse(response.has("error"), response.toString()); + assertEquals( + "jdbc:phoenix:ambari01,ambari02,ambari03:2181:/hbase-unsecure;phoenix.schema.isNamespaceMappingEnabled=true;phoenix.schema.mapSystemTablesToNamespace=true;user=url-user", + driver.urls.get(0) + ); + assertEquals("true", driver.properties.get(0).getProperty("phoenix.schema.isNamespaceMappingEnabled")); + assertEquals("true", driver.properties.get(0).getProperty("phoenix.schema.mapSystemTablesToNamespace")); + assertEquals("phoenix-user", driver.properties.get(0).getProperty("user")); + } finally { + closeAndDeregister(connection, driver); + } + } + + @Test + void phoenixConnectionUrlParamsUseSemicolonAndReachDriverProperties() throws Exception { + RecordingConnectDriver driver = new RecordingConnectDriver("jdbc:phoenix:"); + DriverManager.registerDriver(driver); + String connection = """ + { + "connection_string": "jdbc:phoenix:zk1,zk2:2181:/hbase-unsecure", + "url_params": "phoenix.schema.isNamespaceMappingEnabled=true;phoenix.schema.mapSystemTablesToNamespace=true", + "connect_timeout_secs": 30 + } + """; + try { + JsonNode response = request("testConnection", """ + { "connection": %s } + """.formatted(connection)); + + assertFalse(response.has("error"), response.toString()); + assertEquals( + "jdbc:phoenix:zk1,zk2:2181:/hbase-unsecure;phoenix.schema.isNamespaceMappingEnabled=true;phoenix.schema.mapSystemTablesToNamespace=true", + driver.urls.get(0) + ); + assertEquals("true", driver.properties.get(0).getProperty("phoenix.schema.isNamespaceMappingEnabled")); + assertEquals("true", driver.properties.get(0).getProperty("phoenix.schema.mapSystemTablesToNamespace")); + } finally { + closeAndDeregister(connection, driver); + } + } + + @Test + void phoenixUrlPropertyParsingIgnoresMalformedSegmentsAndPreservesEqualsInValue() throws Exception { + Method method = DbxJdbcPlugin.class.getDeclaredMethod("applyPhoenixUrlProperties", String.class, Properties.class); + method.setAccessible(true); + Properties properties = new Properties(); + + method.invoke( + null, + "jdbc:phoenix;broken;=ignored;phoenix.query.custom=a=b", + properties + ); + + assertEquals("a=b", properties.getProperty("phoenix.query.custom")); + assertFalse(properties.containsKey("broken")); + assertFalse(properties.containsKey("")); + } + + @Test + void phoenixThinUrlParamsUseSemicolonSyntax() throws Exception { + JsonNode connection = MAPPER.readTree(""" + { + "connection_string": "jdbc:phoenix:thin:url=http://127.0.0.1:8765;serialization=PROTOBUF", + "url_params": "authentication=SPNEGO" + } + """); + + assertEquals( + "jdbc:phoenix:thin:url=http://127.0.0.1:8765;serialization=PROTOBUF;authentication=SPNEGO", + DbxJdbcPlugin.jdbcUrl(connection) + ); + } + + @Test + void nonPhoenixSemicolonUrlPropertiesAreNotCopiedToDriverProperties() throws Exception { + RecordingConnectDriver driver = new RecordingConnectDriver("jdbc:dbx-semicolon:"); + DriverManager.registerDriver(driver); + String connection = """ + { + "connection_string": "jdbc:dbx-semicolon:demo;custom.option=true", + "connect_timeout_secs": 30 + } + """; + try { + JsonNode response = request("testConnection", """ + { "connection": %s } + """.formatted(connection)); + + assertFalse(response.has("error"), response.toString()); + assertFalse(driver.properties.get(0).containsKey("custom.option")); + } finally { + closeAndDeregister(connection, driver); + } + } + @Test void phoenixAutoCommitConfigurationSkipsNonPhoenixConnections() throws Exception { Method method = DbxJdbcPlugin.class.getDeclaredMethod(