diff --git a/pom.xml b/pom.xml
index 05b921635949c..377f5b34e31bc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -76,81 +76,79 @@
- presto-accumulo
- presto-accumulo-iterators
- presto-array
presto-atop
- presto-base-jdbc
- presto-benchmark
- presto-benchmark-driver
- presto-benchto-benchmarks
- presto-bigquery
- presto-blackhole
- presto-cassandra
- presto-cli
- presto-client
- presto-docs
- presto-druid
- presto-elasticsearch
- presto-example-http
- presto-geospatial
- presto-geospatial-toolkit
- presto-google-sheets
- presto-hive
- presto-hive-hadoop2
- presto-iceberg
- presto-jdbc
+ presto-spi
+ presto-array
presto-jmx
+ presto-record-decoder
presto-kafka
presto-kinesis
- presto-kudu
- presto-local-file
- presto-main
- presto-matching
+ presto-redis
+ presto-accumulo-iterators
+ presto-accumulo
+ presto-cassandra
+ presto-blackhole
presto-memory
- presto-memory-context
- presto-memsql
- presto-ml
- presto-mongodb
- presto-mysql
- presto-oracle
presto-orc
presto-parquet
- presto-parser
- presto-password-authenticators
+ presto-rcfile
+ presto-hive
+ presto-hive-hadoop2
+ presto-teradata-functions
+ presto-example-http
+ presto-local-file
+ presto-tpch
+ presto-tpcds
+ presto-raptor-legacy
+ presto-base-jdbc
+ presto-mysql
+ presto-memsql
presto-phoenix
- presto-pinot
- presto-plugin-toolkit
presto-postgresql
+ presto-redshift
+ presto-sqlserver
+ presto-mongodb
+ presto-client
+ presto-parser
+ presto-main
+ presto-server-main
+ presto-ml
+ presto-geospatial
+ presto-geospatial-toolkit
+ presto-benchmark
+ presto-testing
+ presto-tests
presto-product-tests
presto-product-tests-launcher
- presto-prometheus
- presto-proxy
- presto-raptor-legacy
- presto-rcfile
- presto-record-decoder
- presto-redis
- presto-redshift
- presto-resource-group-managers
+ presto-jdbc
+ presto-cli
+ presto-benchmark-driver
+ presto-salesforce
presto-server
- presto-server-main
presto-server-rpm
+ presto-docs
+ presto-verifier
+ presto-testing-server-launcher
+ presto-plugin-toolkit
+ presto-resource-group-managers
+ presto-password-authenticators
presto-session-property-managers
- presto-spi
- presto-sqlserver
- presto-teradata-functions
- presto-test-jdbc-compatibility-old-driver
- presto-test-jdbc-compatibility-old-server
- presto-testing
- presto-testing-kafka
- presto-testng-services
- presto-tests
- presto-thrift
+ presto-benchto-benchmarks
presto-thrift-api
presto-thrift-testing-server
- presto-tpcds
- presto-tpch
- presto-verifier
+ presto-thrift
+ presto-matching
+ presto-memory-context
+ presto-prometheus
+ presto-proxy
+ presto-kudu
+ presto-elasticsearch
+ presto-iceberg
+ presto-google-sheets
+ presto-bigquery
+ presto-pinot
+ presto-oracle
+ presto-druid
diff --git a/presto-base-jdbc/src/main/java/io/prestosql/plugin/jdbc/BaseJdbcClient.java b/presto-base-jdbc/src/main/java/io/prestosql/plugin/jdbc/BaseJdbcClient.java
index c3fff09c93803..80445e5721ca4 100644
--- a/presto-base-jdbc/src/main/java/io/prestosql/plugin/jdbc/BaseJdbcClient.java
+++ b/presto-base-jdbc/src/main/java/io/prestosql/plugin/jdbc/BaseJdbcClient.java
@@ -982,7 +982,6 @@ private static String escapeNamePattern(String name, String escape)
{
requireNonNull(name, "name is null");
requireNonNull(escape, "escape is null");
- checkArgument(!escape.isEmpty(), "Escape string must not be empty");
checkArgument(!escape.equals("_"), "Escape string must not be '_'");
checkArgument(!escape.equals("%"), "Escape string must not be '%'");
name = name.replace(escape, escape + escape);
diff --git a/presto-docs/src/main/sphinx/connector.rst b/presto-docs/src/main/sphinx/connector.rst
index 76a76872cc33f..bcc70d5989638 100644
--- a/presto-docs/src/main/sphinx/connector.rst
+++ b/presto-docs/src/main/sphinx/connector.rst
@@ -33,6 +33,7 @@ from different data sources.
Prometheus
Redis
Redshift
+ Salesforce
SQL Server
System
Thrift
diff --git a/presto-docs/src/main/sphinx/connector/salesforce.rst b/presto-docs/src/main/sphinx/connector/salesforce.rst
new file mode 100644
index 0000000000000..b155553a5370c
--- /dev/null
+++ b/presto-docs/src/main/sphinx/connector/salesforce.rst
@@ -0,0 +1,53 @@
+====================
+Salesforce Connector
+====================
+
+The Salesforce connector allows querying and creating tables in an
+external Salesforce instance. This can be used to join data between
+different systems like Salesforce and Hive, or between two different
+Salesforce instances.
+
+Configuration
+-------------
+
+To configure the Salesforce connector, create a catalog properties file
+in ``etc/catalog`` named, for example, ``salesforce.properties``, to
+mount the Salesforce connector as the ``salesforce`` catalog.
+Create the file with the following contents, replacing the
+connection properties as appropriate for your setup:
+
+.. code-block:: none
+
+ connector.name=salesforce
+ connection-url=jdbc:salesforce://
+ connection-user=admin
+ connection-password=secret
+ salesforce.security-token=abc
+
+Querying Salesforce
+-------------------
+
+The Salesforce connector provides single a schema named ``salesforce``.
+
+ SHOW TABLES FROM salesforce.salesforce;
+
+You can see a list of the columns in the ``account`` table in the ``salesforce`` database
+using either of the following::
+
+ DESCRIBE salesforce.salesforce.account;
+ SHOW COLUMNS FROM salesforce.salesforce.account;
+
+Finally, you can access the ``account`` table::
+
+ SELECT * FROM salesforce.salesforce.account;
+
+If you used a different name for your catalog properties file, use
+that catalog name instead of ``salesforce`` in the above examples.
+
+Salesforce Connector Limitations
+--------------------------------
+
+At this time this connector is read-only. Furthermore, it fetches data
+using the Salesforce synchronous API, which offers limited performance.
+
+Queries on the information schema can be especially expensive.
diff --git a/presto-salesforce/pom.xml b/presto-salesforce/pom.xml
new file mode 100644
index 0000000000000..b9a3bf66f5b1a
--- /dev/null
+++ b/presto-salesforce/pom.xml
@@ -0,0 +1,216 @@
+
+
+ 4.0.0
+
+
+ io.prestosql
+ presto-root
+ 339-SNAPSHOT
+
+
+ presto-salesforce
+ Presto - Salesforce Connector
+ presto-plugin
+
+
+ ${project.parent.basedir}
+ true
+
+ 1.25.0
+ 3.8
+ 4.1
+
+ 4.12
+ 1.3
+
+ 3.7.0
+ 2.5
+ 2.21.0
+ 3.0.1
+ 3.1.0
+ 3.1.0
+ 2.8.2
+ 2.0
+ 43.0.0
+ 3.0.5
+ 4.2
+ 1.2.2
+
+
+
+
+
+ mulesoft-releases
+ MuleSoft Releases Repository
+ http://repository.mulesoft.org/releases/
+ default
+
+
+
+
+
+ io.prestosql
+ presto-base-jdbc
+
+
+
+ io.airlift
+ configuration
+
+
+
+ com.google.guava
+ guava
+
+
+
+ com.google.inject
+ guice
+
+
+
+ javax.inject
+ javax.inject
+
+
+
+
+ io.prestosql
+ presto-spi
+ provided
+
+
+
+ io.airlift
+ slice
+ provided
+
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ provided
+
+
+
+ org.openjdk.jol
+ jol-core
+ provided
+
+
+
+
+ org.testng
+ testng
+ test
+
+
+
+ io.prestosql
+ presto-main
+ test
+
+
+
+
+ com.google.oauth-client
+ google-oauth-client
+ ${google-oauth-client.version}
+
+
+ org.apache.httpcomponents
+ httpclient
+
+
+
+
+
+ com.google.http-client
+ google-http-client-jackson2
+ ${google-oauth-client.version}
+
+
+
+ org.mule.tools
+ salesforce-soql-parser
+ 2.0
+
+
+ com.force.api
+ force-partner-api
+ ${force-partner-api.version}
+
+
+ org.mapdb
+ mapdb
+ ${mapdb.version}
+
+
+ org.apache.commons
+ commons-lang3
+ ${commons-lang3.version}
+
+
+ org.apache.commons
+ commons-collections4
+ ${commons-collections4.version}
+
+
+ commons-io
+ commons-io
+ 2.4
+
+
+ com.google.http-client
+ google-http-client
+ 1.25.0
+
+
+ com.force.api
+ force-wsc
+ 47.0.0
+
+
+
+
+ junit
+ junit
+ ${junit.version}
+ test
+
+
+ com.opencsv
+ opencsv
+ ${opencsv.version}
+ test
+
+
+ com.thoughtworks.xstream
+ xstream
+ ${xstream.version}
+ test
+
+
+ io.airlift
+ log
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+
+
+
+
+ org.antlr:ST4
+
+
+
+
+
+
+
+
diff --git a/presto-salesforce/src/main/java/io/prestosql/plugin/salesforce/SalesforceClient.java b/presto-salesforce/src/main/java/io/prestosql/plugin/salesforce/SalesforceClient.java
new file mode 100644
index 0000000000000..569bd839eafbc
--- /dev/null
+++ b/presto-salesforce/src/main/java/io/prestosql/plugin/salesforce/SalesforceClient.java
@@ -0,0 +1,166 @@
+/*
+ * 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 io.prestosql.plugin.salesforce;
+
+import io.prestosql.plugin.jdbc.BaseJdbcClient;
+import io.prestosql.plugin.jdbc.BaseJdbcConfig;
+import io.prestosql.plugin.jdbc.ColumnMapping;
+import io.prestosql.plugin.jdbc.ConnectionFactory;
+import io.prestosql.plugin.jdbc.JdbcColumnHandle;
+import io.prestosql.plugin.jdbc.JdbcSplit;
+import io.prestosql.plugin.jdbc.JdbcTableHandle;
+import io.prestosql.plugin.jdbc.JdbcTypeHandle;
+import io.prestosql.plugin.jdbc.ObjectReadFunction;
+import io.prestosql.plugin.jdbc.ObjectWriteFunction;
+import io.prestosql.plugin.jdbc.SliceReadFunction;
+import io.prestosql.spi.PrestoException;
+import io.prestosql.spi.block.Block;
+import io.prestosql.spi.block.BlockBuilder;
+import io.prestosql.spi.connector.ConnectorSession;
+import io.prestosql.spi.type.ArrayType;
+import io.prestosql.spi.type.Type;
+import io.prestosql.spi.type.VarcharType;
+
+import javax.inject.Inject;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.BiFunction;
+
+import static io.airlift.slice.Slices.utf8Slice;
+import static io.prestosql.plugin.jdbc.ColumnMapping.objectMapping;
+import static io.prestosql.plugin.jdbc.JdbcErrorCode.JDBC_ERROR;
+import static io.prestosql.plugin.jdbc.StandardColumnMappings.doubleColumnMapping;
+import static io.prestosql.plugin.jdbc.StandardColumnMappings.varcharColumnMapping;
+import static io.prestosql.plugin.jdbc.StandardColumnMappings.varcharWriteFunction;
+import static io.prestosql.spi.type.VarcharType.createUnboundedVarcharType;
+import static io.prestosql.spi.type.VarcharType.createVarcharType;
+
+public class SalesforceClient
+ extends BaseJdbcClient
+{
+ private final SalesforceConfig salesforceConfig;
+
+ @Inject
+ public SalesforceClient(BaseJdbcConfig baseConfig, SalesforceConfig salesforceConfig, ConnectionFactory connectionFactory)
+ {
+ super(baseConfig, "", connectionFactory);
+
+ this.salesforceConfig = salesforceConfig;
+ }
+
+ @Override
+ public PreparedStatement getPreparedStatement(Connection connection, String sql)
+ throws SQLException
+ {
+ connection.setAutoCommit(false);
+ PreparedStatement statement = connection.prepareStatement(sql);
+
+ statement.setFetchSize(salesforceConfig.getFetchSize().orElse(2000));
+
+ return statement;
+ }
+
+ @Override
+ protected Optional> limitFunction()
+ {
+ return Optional.of((sql, limit) -> sql + " LIMIT " + limit);
+ }
+
+ @Override
+ protected String getTableSchemaName(ResultSet resultSet)
+ throws SQLException
+ {
+ return "salesforce";
+ }
+
+ @Override
+ public Optional toPrestoType(ConnectorSession session, Connection connection, JdbcTypeHandle typeHandle)
+ {
+ int columnSize = typeHandle.getColumnSize();
+
+ String jdbcTypeName = typeHandle.getJdbcTypeName()
+ .orElseThrow(() -> new PrestoException(JDBC_ERROR, "Type name is missing: " + typeHandle));
+
+ Optional mapping = getForcedMappingToVarchar(typeHandle);
+ if (mapping.isPresent()) {
+ return mapping;
+ }
+
+ if (jdbcTypeName.equals("multipicklist")) {
+ VarcharType type = createVarcharType(typeHandle.getColumnSize());
+
+ if (typeHandle.getColumnSize() == 0 || typeHandle.getColumnSize() > VarcharType.MAX_LENGTH) {
+ type = createUnboundedVarcharType();
+ }
+
+ return Optional.of(objectMapping(new ArrayType(type), multiPicklistReadFunction(type), multiPicklistWriteFunction(type)));
+ }
+
+ switch (typeHandle.getJdbcType()) {
+ case Types.VARCHAR:
+ case Types.NVARCHAR:
+ case Types.LONGVARCHAR:
+ case Types.LONGNVARCHAR:
+ if (columnSize == 0 || typeHandle.getColumnSize() > VarcharType.MAX_LENGTH) {
+ return Optional.of(varcharColumnMapping(createUnboundedVarcharType()));
+ }
+ return Optional.of(varcharColumnMapping(createVarcharType(columnSize)));
+ case Types.NUMERIC:
+ case Types.DECIMAL:
+ return Optional.of(doubleColumnMapping());
+ case Types.OTHER:
+ return Optional.of(ColumnMapping.sliceMapping(VarcharType.VARCHAR, otherReadFunction(), varcharWriteFunction()));
+ default:
+ return super.toPrestoType(session, connection, typeHandle);
+ }
+ }
+
+ @Override
+ public PreparedStatement buildSql(ConnectorSession session, Connection connection, JdbcSplit split, JdbcTableHandle table, List columns)
+ throws SQLException
+ {
+ return new SoqlQueryBuilder(identifierQuote).buildSql(this, session, connection, table.getCatalogName(), table.getSchemaName(), table.getTableName(), columns, table.getConstraint(), split.getAdditionalPredicate(), tryApplyLimit(table.getLimit()));
+ }
+
+ private static ObjectReadFunction multiPicklistReadFunction(Type type)
+ {
+ return ObjectReadFunction.of(Block.class, (resultSet, columnIndex) -> {
+ BlockBuilder builder = createUnboundedVarcharType().createBlockBuilder(null, 1);
+
+ for (String value : resultSet.getString(columnIndex).split(";")) {
+ type.writeSlice(builder, utf8Slice(value));
+ }
+
+ return builder.build();
+ });
+ }
+
+ private static ObjectWriteFunction multiPicklistWriteFunction(Type type)
+ {
+ return ObjectWriteFunction.of(Block.class, (statement, index, block) -> {
+ // Not implemented
+ });
+ }
+
+ public static SliceReadFunction otherReadFunction()
+ {
+ return (resultSet, columnIndex) -> utf8Slice(resultSet.getObject(columnIndex).toString());
+ }
+}
diff --git a/presto-salesforce/src/main/java/io/prestosql/plugin/salesforce/SalesforceClientModule.java b/presto-salesforce/src/main/java/io/prestosql/plugin/salesforce/SalesforceClientModule.java
new file mode 100644
index 0000000000000..e7d52a083f3f6
--- /dev/null
+++ b/presto-salesforce/src/main/java/io/prestosql/plugin/salesforce/SalesforceClientModule.java
@@ -0,0 +1,67 @@
+/*
+ * 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 io.prestosql.plugin.salesforce;
+
+import com.google.inject.Binder;
+import com.google.inject.Module;
+import com.google.inject.Provides;
+import com.google.inject.Scopes;
+import com.google.inject.Singleton;
+import io.prestosql.plugin.jdbc.BaseJdbcConfig;
+import io.prestosql.plugin.jdbc.ConnectionFactory;
+import io.prestosql.plugin.jdbc.DriverConnectionFactory;
+import io.prestosql.plugin.jdbc.ForBaseJdbc;
+import io.prestosql.plugin.jdbc.JdbcClient;
+import io.prestosql.plugin.jdbc.TypeHandlingJdbcConfig;
+import io.prestosql.plugin.jdbc.credential.CredentialProvider;
+import io.prestosql.plugin.salesforce.driver.ForceDriver;
+import io.prestosql.plugin.salesforce.driver.ForceModule;
+
+import java.util.Properties;
+
+import static io.airlift.configuration.ConfigBinder.configBinder;
+
+public class SalesforceClientModule
+ implements Module
+{
+ @Override
+ public void configure(Binder binder)
+ {
+ binder.install(new ForceModule());
+
+ binder.bind(JdbcClient.class)
+ .annotatedWith(ForBaseJdbc.class)
+ .to(SalesforceClient.class)
+ .in(Scopes.SINGLETON);
+
+ configBinder(binder).bindConfig(BaseJdbcConfig.class);
+ configBinder(binder).bindConfig(TypeHandlingJdbcConfig.class);
+ configBinder(binder).bindConfig(SalesforceConfig.class);
+ }
+
+ @Singleton
+ @Provides
+ @ForBaseJdbc
+ public ConnectionFactory getConnectionFactory(
+ BaseJdbcConfig baseConfig,
+ SalesforceConfig salesforceConfig,
+ CredentialProvider credentialProvider)
+ {
+ Properties properties = new Properties();
+
+ salesforceConfig.getSecurityToken().ifPresent(token -> properties.setProperty("securityToken", token));
+
+ return new DriverConnectionFactory(new ForceDriver(), baseConfig.getConnectionUrl(), properties, credentialProvider);
+ }
+}
diff --git a/presto-salesforce/src/main/java/io/prestosql/plugin/salesforce/SalesforceConfig.java b/presto-salesforce/src/main/java/io/prestosql/plugin/salesforce/SalesforceConfig.java
new file mode 100644
index 0000000000000..cc6249338f01b
--- /dev/null
+++ b/presto-salesforce/src/main/java/io/prestosql/plugin/salesforce/SalesforceConfig.java
@@ -0,0 +1,50 @@
+/*
+ * 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 io.prestosql.plugin.salesforce;
+
+import io.airlift.configuration.Config;
+import io.airlift.configuration.ConfigSecuritySensitive;
+
+import java.util.Optional;
+
+public class SalesforceConfig
+{
+ private String securityToken;
+ private Integer fetchSize;
+
+ @Config("salesforce.security-token")
+ @ConfigSecuritySensitive
+ public SalesforceConfig setSecurityToken(String securityToken)
+ {
+ this.securityToken = securityToken;
+ return this;
+ }
+
+ @Config("salesforce.fetch-size")
+ public SalesforceConfig setFetchSize(Integer fetchSize)
+ {
+ this.fetchSize = fetchSize;
+ return this;
+ }
+
+ public Optional getSecurityToken()
+ {
+ return Optional.ofNullable(securityToken);
+ }
+
+ public Optional getFetchSize()
+ {
+ return Optional.ofNullable(fetchSize);
+ }
+}
diff --git a/presto-salesforce/src/main/java/io/prestosql/plugin/salesforce/SalesforcePlugin.java b/presto-salesforce/src/main/java/io/prestosql/plugin/salesforce/SalesforcePlugin.java
new file mode 100644
index 0000000000000..7061ade414461
--- /dev/null
+++ b/presto-salesforce/src/main/java/io/prestosql/plugin/salesforce/SalesforcePlugin.java
@@ -0,0 +1,25 @@
+/*
+ * 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 io.prestosql.plugin.salesforce;
+
+import io.prestosql.plugin.jdbc.JdbcPlugin;
+
+public class SalesforcePlugin
+ extends JdbcPlugin
+{
+ public SalesforcePlugin()
+ {
+ super("salesforce", new SalesforceClientModule());
+ }
+}
diff --git a/presto-salesforce/src/main/java/io/prestosql/plugin/salesforce/SoqlQueryBuilder.java b/presto-salesforce/src/main/java/io/prestosql/plugin/salesforce/SoqlQueryBuilder.java
new file mode 100644
index 0000000000000..4d7a1b28bc60e
--- /dev/null
+++ b/presto-salesforce/src/main/java/io/prestosql/plugin/salesforce/SoqlQueryBuilder.java
@@ -0,0 +1,298 @@
+/*
+ * 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 io.prestosql.plugin.salesforce;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.VerifyException;
+import com.google.common.collect.ImmutableList;
+import io.airlift.log.Logger;
+import io.airlift.slice.Slice;
+import io.prestosql.plugin.jdbc.BooleanWriteFunction;
+import io.prestosql.plugin.jdbc.DoubleWriteFunction;
+import io.prestosql.plugin.jdbc.JdbcClient;
+import io.prestosql.plugin.jdbc.JdbcColumnHandle;
+import io.prestosql.plugin.jdbc.JdbcTypeHandle;
+import io.prestosql.plugin.jdbc.LongWriteFunction;
+import io.prestosql.plugin.jdbc.ObjectWriteFunction;
+import io.prestosql.plugin.jdbc.SliceWriteFunction;
+import io.prestosql.plugin.jdbc.WriteFunction;
+import io.prestosql.spi.connector.ColumnHandle;
+import io.prestosql.spi.connector.ConnectorSession;
+import io.prestosql.spi.predicate.Domain;
+import io.prestosql.spi.predicate.Range;
+import io.prestosql.spi.predicate.TupleDomain;
+import io.prestosql.spi.type.Type;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static java.lang.String.format;
+import static java.util.Collections.nCopies;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
+
+public class SoqlQueryBuilder
+{
+ private static final Logger log = Logger.get(SoqlQueryBuilder.class);
+
+ // not all databases support booleans, so use 1=1 and 1=0 instead
+ private static final String ALWAYS_TRUE = "1=1";
+ private static final String ALWAYS_FALSE = "1=0";
+
+ private final String identifierQuote;
+
+ private static class TypeAndValue
+ {
+ private final Type type;
+ private final JdbcTypeHandle typeHandle;
+ private final Object value;
+
+ public TypeAndValue(Type type, JdbcTypeHandle typeHandle, Object value)
+ {
+ this.type = requireNonNull(type, "type is null");
+ this.typeHandle = requireNonNull(typeHandle, "typeHandle is null");
+ this.value = requireNonNull(value, "value is null");
+ }
+
+ public Type getType()
+ {
+ return type;
+ }
+
+ public JdbcTypeHandle getTypeHandle()
+ {
+ return typeHandle;
+ }
+
+ public Object getValue()
+ {
+ return value;
+ }
+ }
+
+ public SoqlQueryBuilder(String identifierQuote)
+ {
+ this.identifierQuote = requireNonNull(identifierQuote, "identifierQuote is null");
+ }
+
+ public PreparedStatement buildSql(
+ JdbcClient client,
+ ConnectorSession session,
+ Connection connection,
+ String catalog,
+ String schema,
+ String table,
+ List columns,
+ TupleDomain tupleDomain,
+ Optional additionalPredicate,
+ Function sqlFunction)
+ throws SQLException
+ {
+ StringBuilder sql = new StringBuilder();
+
+ String columnNames = columns.stream()
+ .map(JdbcColumnHandle::getColumnName)
+ .map(this::quote)
+ .collect(joining(", "));
+
+ sql.append("SELECT ");
+ sql.append(columnNames);
+ if (columns.isEmpty()) {
+ sql.append("null");
+ }
+
+ sql.append(" FROM ");
+ if (!isNullOrEmpty(catalog)) {
+ sql.append(quote(catalog)).append('.');
+ }
+ if (!isNullOrEmpty(schema)) {
+ sql.append(quote(schema)).append('.');
+ }
+ sql.append(quote(table));
+
+ List accumulator = new ArrayList<>();
+
+ List clauses = toConjuncts(client, session, connection, columns, tupleDomain, accumulator);
+ if (additionalPredicate.isPresent()) {
+ clauses = ImmutableList.builder()
+ .addAll(clauses)
+ .add(additionalPredicate.get())
+ .build();
+ }
+ if (!clauses.isEmpty()) {
+ sql.append(" WHERE ")
+ .append(Joiner.on(" AND ").join(clauses));
+ }
+
+ String query = sqlFunction.apply(sql.toString());
+ log.debug("Preparing query: %s", query);
+ PreparedStatement statement = client.getPreparedStatement(connection, query);
+
+ for (int i = 0; i < accumulator.size(); i++) {
+ TypeAndValue typeAndValue = accumulator.get(i);
+ int parameterIndex = i + 1;
+ Type type = typeAndValue.getType();
+ WriteFunction writeFunction = client.toPrestoType(session, connection, typeAndValue.getTypeHandle())
+ .orElseThrow(() -> new VerifyException(format("Unsupported type %s with handle %s", type, typeAndValue.getTypeHandle())))
+ .getWriteFunction();
+ Class> javaType = type.getJavaType();
+ Object value = typeAndValue.getValue();
+ if (javaType == boolean.class) {
+ ((BooleanWriteFunction) writeFunction).set(statement, parameterIndex, (boolean) value);
+ }
+ else if (javaType == long.class) {
+ ((LongWriteFunction) writeFunction).set(statement, parameterIndex, (long) value);
+ }
+ else if (javaType == double.class) {
+ ((DoubleWriteFunction) writeFunction).set(statement, parameterIndex, (double) value);
+ }
+ else if (javaType == Slice.class) {
+ ((SliceWriteFunction) writeFunction).set(statement, parameterIndex, (Slice) value);
+ }
+ else {
+ ((ObjectWriteFunction) writeFunction).set(statement, parameterIndex, value);
+ }
+ }
+
+ return statement;
+ }
+
+ private static Domain pushDownDomain(JdbcClient client, ConnectorSession session, Connection connection, JdbcColumnHandle column, Domain domain)
+ {
+ return client.toPrestoType(session, connection, column.getJdbcTypeHandle())
+ .orElseThrow(() -> new IllegalStateException(format("Unsupported type %s with handle %s", column.getColumnType(), column.getJdbcTypeHandle())))
+ .getPushdownConverter().apply(domain);
+ }
+
+ private List toConjuncts(
+ JdbcClient client,
+ ConnectorSession session,
+ Connection connection,
+ List columns,
+ TupleDomain tupleDomain,
+ List accumulator)
+ {
+ if (tupleDomain.isNone()) {
+ return ImmutableList.of(ALWAYS_FALSE);
+ }
+ ImmutableList.Builder builder = ImmutableList.builder();
+ for (JdbcColumnHandle column : columns) {
+ Domain domain = tupleDomain.getDomains().get().get(column);
+ if (domain != null) {
+ domain = pushDownDomain(client, session, connection, column, domain);
+ builder.add(toPredicate(column.getColumnName(), domain, column, accumulator));
+ }
+ }
+ return builder.build();
+ }
+
+ private String toPredicate(String columnName, Domain domain, JdbcColumnHandle column, List accumulator)
+ {
+ if (domain.getValues().isNone()) {
+ return domain.isNullAllowed() ? quote(columnName) + " = NULL" : ALWAYS_FALSE;
+ }
+
+ if (domain.getValues().isAll()) {
+ return domain.isNullAllowed() ? ALWAYS_TRUE : quote(columnName) + " != NULL";
+ }
+
+ List disjuncts = new ArrayList<>();
+ List