Skip to content

Commit

Permalink
BREAKING CHANGE: more robust handling of connection urls (#7)
Browse files Browse the repository at this point in the history
Trying to make small steps towards passing around an object that
represents the connection string with its various details as opposed to
implicitly setting properties on the property bag provided by the
client.
The breaking change listed here is moving public methods about what urls
are handled by this driver out of the `DataCloudConnection : Connection`
implementation
  • Loading branch information
jschneidereit authored Jan 8, 2025
1 parent ff56e4c commit 2e2520f
Show file tree
Hide file tree
Showing 13 changed files with 393 additions and 133 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import com.salesforce.datacloud.jdbc.config.DriverVersion;
import com.salesforce.datacloud.jdbc.core.DataCloudConnection;
import com.salesforce.datacloud.jdbc.core.DataCloudConnectionString;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
Expand Down Expand Up @@ -66,7 +67,7 @@ public Connection connect(String url, Properties info) throws SQLException {

@Override
public boolean acceptsURL(String url) {
return DataCloudConnection.acceptsUrl(url);
return DataCloudConnectionString.acceptsUrl(url);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package com.salesforce.datacloud.jdbc.core;

import static com.salesforce.datacloud.jdbc.util.Constants.CONNECTION_PROTOCOL;
import static com.salesforce.datacloud.jdbc.util.Constants.LOGIN_URL;
import static com.salesforce.datacloud.jdbc.util.Constants.USER;
import static com.salesforce.datacloud.jdbc.util.Constants.USER_NAME;
Expand All @@ -31,11 +30,8 @@
import com.salesforce.datacloud.jdbc.interceptor.HyperWorkloadHeaderInterceptor;
import com.salesforce.datacloud.jdbc.interceptor.MaxMetadataSizeHeaderInterceptor;
import com.salesforce.datacloud.jdbc.interceptor.TracingHeadersInterceptor;
import com.salesforce.datacloud.jdbc.util.Messages;
import com.salesforce.datacloud.jdbc.util.StringCompatibility;
import io.grpc.ClientInterceptor;
import io.grpc.ManagedChannelBuilder;
import java.net.URI;
import java.sql.Array;
import java.sql.Blob;
import java.sql.CallableStatement;
Expand Down Expand Up @@ -68,10 +64,8 @@
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.commons.lang3.StringUtils;

@Slf4j
@Getter
@Builder(access = AccessLevel.PACKAGE)
public class DataCloudConnection implements Connection, AutoCloseable {
private static final int DEFAULT_PORT = 443;
Expand All @@ -80,13 +74,18 @@ public class DataCloudConnection implements Connection, AutoCloseable {

private final TokenProcessor tokenProcessor;

private final DataCloudConnectionString connectionString;

@Getter(AccessLevel.PACKAGE)
@NonNull @Builder.Default
private final Properties properties = new Properties();

@Getter(AccessLevel.PACKAGE)
@Setter
@Builder.Default
private List<ClientInterceptor> interceptors = new ArrayList<>();

@Getter(AccessLevel.PACKAGE)
@NonNull private final HyperGrpcClientExecutor executor;

public static DataCloudConnection fromChannel(@NonNull ManagedChannelBuilder<?> builder, Properties properties)
Expand Down Expand Up @@ -136,9 +135,10 @@ static List<ClientInterceptor> getClientInterceptors(
}

public static DataCloudConnection of(String url, Properties properties) throws SQLException {
val serviceRootUrl = getServiceRootUrl(url);
properties.put(LOGIN_URL, serviceRootUrl);
val connectionString = DataCloudConnectionString.of(url);
addClientUsernameIfRequired(properties);
connectionString.withParameters(properties);
properties.setProperty(LOGIN_URL, connectionString.getLoginUrl());

if (!AuthenticationSettings.hasAny(properties)) {
throw new DataCloudJDBCException("No authentication settings provided");
Expand All @@ -147,7 +147,6 @@ public static DataCloudConnection of(String url, Properties properties) throws S
val tokenProcessor = DataCloudTokenProcessor.of(properties);

val host = tokenProcessor.getDataCloudToken().getTenantUrl();

val builder = ManagedChannelBuilder.forAddress(host, DEFAULT_PORT);
val authInterceptor = AuthorizationHeaderInterceptor.of(tokenProcessor);

Expand All @@ -158,6 +157,7 @@ public static DataCloudConnection of(String url, Properties properties) throws S
.tokenProcessor(tokenProcessor)
.executor(executor)
.properties(properties)
.connectionString(connectionString)
.build();
}

Expand Down Expand Up @@ -219,9 +219,12 @@ public boolean isClosed() {
public DatabaseMetaData getMetaData() {
val client = ClientBuilder.buildOkHttpClient(properties);
val userName = this.properties.getProperty("userName");
val loginUrl = this.properties.getProperty("loginURL");
return new DataCloudDatabaseMetadata(
getQueryStatement(), Optional.ofNullable(tokenProcessor), client, loginUrl, userName);
getQueryStatement(),
Optional.ofNullable(tokenProcessor),
client,
Optional.ofNullable(connectionString),
userName);
}

private @NonNull DataCloudStatement getQueryStatement() {
Expand Down Expand Up @@ -425,42 +428,6 @@ public boolean isWrapperFor(Class<?> iface) {
return iface.isInstance(this);
}

public static boolean acceptsUrl(String url) {
return url != null && url.startsWith(CONNECTION_PROTOCOL) && urlDoesNotContainScheme(url);
}

private static boolean urlDoesNotContainScheme(String url) {
val suffix = url.substring(CONNECTION_PROTOCOL.length());
return !suffix.startsWith("http://") && !suffix.startsWith("https://");
}

/**
* Returns the extracted service url from given jdbc endpoint
*
* @param url of the form jdbc:salesforce-datacloud://login.salesforce.com
* @return service url
* @throws SQLException when given url doesn't belong with required datasource
*/
static String getServiceRootUrl(String url) throws SQLException {
if (!acceptsUrl(url)) {
throw new DataCloudJDBCException(Messages.ILLEGAL_CONNECTION_PROTOCOL);
}

val serviceRootUrl = url.substring(CONNECTION_PROTOCOL.length());
val noTrailingSlash = StringUtils.removeEnd(serviceRootUrl, "/");
val host = StringUtils.removeStart(noTrailingSlash, "//");

return StringCompatibility.isBlank(host) ? host : createURI(host).toString();
}

private static URI createURI(String host) throws SQLException {
try {
return URI.create("https://" + host);
} catch (IllegalArgumentException e) {
throw new DataCloudJDBCException(Messages.ILLEGAL_CONNECTION_PROTOCOL, e);
}
}

static void addClientUsernameIfRequired(Properties properties) {
if (properties.containsKey(USER)) {
properties.computeIfAbsent(USER_NAME, p -> properties.get(USER));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright (c) 2024, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.salesforce.datacloud.jdbc.core;

import com.salesforce.datacloud.jdbc.exception.DataCloudJDBCException;
import com.salesforce.datacloud.jdbc.util.Messages;
import com.salesforce.datacloud.jdbc.util.StringCompatibility;
import java.net.URI;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.stream.Collectors;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.val;
import org.apache.commons.lang3.StringUtils;

@Builder(access = AccessLevel.PRIVATE)
public class DataCloudConnectionString {
public static final String CONNECTION_PROTOCOL = "jdbc:salesforce-datacloud:";

public static DataCloudConnectionString of(String url) throws SQLException {
if (!acceptsUrl(url)) {
throw new DataCloudJDBCException(Messages.ILLEGAL_CONNECTION_PROTOCOL);
}

val database = getDatabaseUrl(url);
val login = getAuthenticationUrl(url);
val parameters = parseParameters(url);

return DataCloudConnectionString.builder()
.databaseUrl(database)
.loginUrl(login)
.parameters(parameters)
.build();
}

@Getter
private final String databaseUrl;

@Getter
private final String loginUrl;

private final Map<String, String> parameters;

public static boolean acceptsUrl(String url) {
return url != null && url.startsWith(CONNECTION_PROTOCOL) && urlDoesNotContainScheme(url);
}

private static boolean urlDoesNotContainScheme(String url) {
val suffix = url.substring(CONNECTION_PROTOCOL.length());
return !suffix.startsWith("http://") && !suffix.startsWith("https://");
}

/**
* Returns the extracted service url from given jdbc endpoint
*
* @param url of the form jdbc:salesforce-datacloud://login.salesforce.com
* @return service url
* @throws SQLException when given url doesn't belong with required datasource
*/
static String getAuthenticationUrl(String url) throws SQLException {
if (!acceptsUrl(url)) {
throw new DataCloudJDBCException(Messages.ILLEGAL_CONNECTION_PROTOCOL);
}

val withoutParameters = withoutParameters(url);
val serviceRootUrl = withoutParameters.substring(CONNECTION_PROTOCOL.length());
val noTrailingSlash = StringUtils.removeEnd(serviceRootUrl, "/");
val host = StringUtils.removeStart(noTrailingSlash, "//");

return StringCompatibility.isBlank(host) ? host : createURI(host).toString();
}

static String getDatabaseUrl(String url) throws SQLException {
if (!acceptsUrl(url)) {
throw new DataCloudJDBCException(Messages.ILLEGAL_CONNECTION_PROTOCOL);
}

return withoutParameters(url);
}

private static String withoutParameters(String url) {
return url.split(";")[0];
}

private static Map<String, String> parseParameters(String connectionString) {
val parts = connectionString.split(";");

return Arrays.stream(parts)
.skip(1)
.filter(pair -> pair.contains("="))
.map(pair -> {
val split = pair.split("=");
return split.length != 2 ? null : split;
})
.filter(Objects::nonNull)
.collect(Collectors.toMap(p -> p[0], p -> p[1]));
}

private static URI createURI(String host) throws SQLException {
try {
return URI.create("https://" + host);
} catch (IllegalArgumentException e) {
throw new DataCloudJDBCException(Messages.ILLEGAL_CONNECTION_PROTOCOL, e);
}
}

void withParameters(Properties properties) {
properties.putAll(parameters);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,20 @@
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;

@Slf4j
@AllArgsConstructor
public class DataCloudDatabaseMetadata implements DatabaseMetaData {
private final DataCloudStatement dataCloudStatement;
private final Optional<TokenProcessor> tokenProcessor;
private final OkHttpClient client;
private final String loginURL;
private final Optional<DataCloudConnectionString> connectionString;
private final String userName;

public DataCloudDatabaseMetadata(
DataCloudStatement dataCloudStatement,
Optional<TokenProcessor> tokenProcessor,
OkHttpClient client,
String loginURL,
String userName) {
this.dataCloudStatement = dataCloudStatement;
this.tokenProcessor = tokenProcessor;
this.client = client;
this.loginURL = loginURL;
this.userName = userName;
}

@Override
public boolean allProceduresAreCallable() {
return false;
Expand All @@ -60,9 +50,10 @@ public boolean allTablesAreSelectable() {
return true;
}

@SneakyThrows
@Override
public String getURL() {
return loginURL;
return connectionString.map(DataCloudConnectionString::getDatabaseUrl).orElse(null);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import java.sql.SQLException;
import java.util.Collections;
import java.util.TimeZone;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
Expand All @@ -34,7 +33,6 @@
import org.apache.calcite.avatica.Meta;
import org.apache.calcite.avatica.QueryState;

@Getter
@Slf4j
public class StreamingResultSet extends AvaticaResultSet implements DataCloudResultSet {
private static final int ROOT_ALLOCATOR_MB_FROM_V2 = 100 * 1024 * 1024;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
package com.salesforce.datacloud.jdbc.core.accessor.impl;

import static com.salesforce.datacloud.jdbc.core.accessor.impl.TimeStampVectorGetter.createGetter;
import static com.salesforce.datacloud.jdbc.util.Constants.ISO_DATE_TIME_FORMAT;
import static com.salesforce.datacloud.jdbc.util.Constants.ISO_DATE_TIME_SEC_FORMAT;

import com.salesforce.datacloud.jdbc.core.accessor.QueryJDBCAccessor;
import com.salesforce.datacloud.jdbc.core.accessor.QueryJDBCAccessorFactory;
Expand All @@ -39,7 +37,8 @@
import org.apache.arrow.vector.util.DateUtility;

public class TimeStampVectorAccessor extends QueryJDBCAccessor {

private static final String ISO_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
private static final String ISO_DATE_TIME_SEC_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
private static final String INVALID_UNIT_ERROR_RESPONSE = "Invalid Arrow time unit";

@FunctionalInterface
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ public final class Constants {
public static final String CDP_URL = "/api/v1";
public static final String METADATA_URL = "/metadata";

public static final String CONNECTION_PROTOCOL = "jdbc:salesforce-datacloud:";
public static final String HYPER_LAKEHOUSE_ALIAS = "lakehouse";
public static final String HYPER_LAKEHOUSE_PATH_PREFIX = "lakehouse:";

// authentication constants
public static final String LOGIN_URL = "loginURL";

// Property constants
Expand All @@ -54,7 +49,6 @@ public final class Constants {
public static final String DRIVER_VERSION = "3.0";

// Date Time constants
public static final String ISO_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
public static final String ISO_DATE_TIME_SEC_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";

public static final String ISO_TIME_FORMAT = "HH:mm:ss";
}
Loading

0 comments on commit 2e2520f

Please sign in to comment.