Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.prestosql.plugin.jdbc.BaseJdbcClient Maven / Gradle / Ivy
/*
* 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.jdbc;
import com.google.common.base.CharMatcher;
import com.google.common.base.VerifyException;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import io.airlift.log.Logger;
import io.airlift.units.Duration;
import io.prestosql.spi.PrestoException;
import io.prestosql.spi.connector.ColumnHandle;
import io.prestosql.spi.connector.ColumnMetadata;
import io.prestosql.spi.connector.ConnectorSession;
import io.prestosql.spi.connector.ConnectorSplitSource;
import io.prestosql.spi.connector.ConnectorTableMetadata;
import io.prestosql.spi.connector.FixedSplitSource;
import io.prestosql.spi.connector.SchemaTableName;
import io.prestosql.spi.connector.TableNotFoundException;
import io.prestosql.spi.predicate.TupleDomain;
import io.prestosql.spi.statistics.TableStatistics;
import io.prestosql.spi.type.CharType;
import io.prestosql.spi.type.DecimalType;
import io.prestosql.spi.type.Type;
import io.prestosql.spi.type.VarcharType;
import javax.annotation.Nullable;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.Set;
import java.util.UUID;
import java.util.function.BiFunction;
import java.util.function.Function;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.emptyToNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.base.Verify.verify;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Iterables.getOnlyElement;
import static io.prestosql.plugin.jdbc.JdbcErrorCode.JDBC_ERROR;
import static io.prestosql.plugin.jdbc.PredicatePushdownController.DISABLE_PUSHDOWN;
import static io.prestosql.plugin.jdbc.StandardColumnMappings.bigintWriteFunction;
import static io.prestosql.plugin.jdbc.StandardColumnMappings.booleanWriteFunction;
import static io.prestosql.plugin.jdbc.StandardColumnMappings.charWriteFunction;
import static io.prestosql.plugin.jdbc.StandardColumnMappings.dateWriteFunction;
import static io.prestosql.plugin.jdbc.StandardColumnMappings.doubleWriteFunction;
import static io.prestosql.plugin.jdbc.StandardColumnMappings.integerWriteFunction;
import static io.prestosql.plugin.jdbc.StandardColumnMappings.jdbcTypeToPrestoType;
import static io.prestosql.plugin.jdbc.StandardColumnMappings.longDecimalWriteFunction;
import static io.prestosql.plugin.jdbc.StandardColumnMappings.realWriteFunction;
import static io.prestosql.plugin.jdbc.StandardColumnMappings.shortDecimalWriteFunction;
import static io.prestosql.plugin.jdbc.StandardColumnMappings.smallintWriteFunction;
import static io.prestosql.plugin.jdbc.StandardColumnMappings.tinyintWriteFunction;
import static io.prestosql.plugin.jdbc.StandardColumnMappings.varbinaryWriteFunction;
import static io.prestosql.plugin.jdbc.StandardColumnMappings.varcharReadFunction;
import static io.prestosql.plugin.jdbc.StandardColumnMappings.varcharWriteFunction;
import static io.prestosql.plugin.jdbc.TypeHandlingJdbcSessionProperties.getUnsupportedTypeHandling;
import static io.prestosql.plugin.jdbc.UnsupportedTypeHandling.CONVERT_TO_VARCHAR;
import static io.prestosql.plugin.jdbc.UnsupportedTypeHandling.IGNORE;
import static io.prestosql.spi.StandardErrorCode.NOT_FOUND;
import static io.prestosql.spi.StandardErrorCode.NOT_SUPPORTED;
import static io.prestosql.spi.type.BigintType.BIGINT;
import static io.prestosql.spi.type.BooleanType.BOOLEAN;
import static io.prestosql.spi.type.DateType.DATE;
import static io.prestosql.spi.type.DoubleType.DOUBLE;
import static io.prestosql.spi.type.IntegerType.INTEGER;
import static io.prestosql.spi.type.RealType.REAL;
import static io.prestosql.spi.type.SmallintType.SMALLINT;
import static io.prestosql.spi.type.TinyintType.TINYINT;
import static io.prestosql.spi.type.VarbinaryType.VARBINARY;
import static io.prestosql.spi.type.VarcharType.createUnboundedVarcharType;
import static java.lang.String.CASE_INSENSITIVE_ORDER;
import static java.lang.String.format;
import static java.lang.String.join;
import static java.sql.DatabaseMetaData.columnNoNulls;
import static java.util.Collections.emptyMap;
import static java.util.Locale.ENGLISH;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.stream.Collectors.joining;
public abstract class BaseJdbcClient
implements JdbcClient
{
private static final Logger log = Logger.get(BaseJdbcClient.class);
/**
* @deprecated To be removed after {{@link #legacyToWriteMapping} is removed.
*/
@Deprecated
private static final Map WRITE_MAPPINGS = ImmutableMap.builder()
.put(BOOLEAN, WriteMapping.booleanMapping("boolean", booleanWriteFunction()))
.put(BIGINT, WriteMapping.longMapping("bigint", bigintWriteFunction()))
.put(INTEGER, WriteMapping.longMapping("integer", integerWriteFunction()))
.put(SMALLINT, WriteMapping.longMapping("smallint", smallintWriteFunction()))
.put(TINYINT, WriteMapping.longMapping("tinyint", tinyintWriteFunction()))
.put(DOUBLE, WriteMapping.doubleMapping("double precision", doubleWriteFunction()))
.put(REAL, WriteMapping.longMapping("real", realWriteFunction()))
.put(VARBINARY, WriteMapping.sliceMapping("varbinary", varbinaryWriteFunction()))
.put(DATE, WriteMapping.longMapping("date", dateWriteFunction()))
.build();
protected final ConnectionFactory connectionFactory;
protected final String identifierQuote;
protected final Set jdbcTypesMappedToVarchar;
protected final boolean caseInsensitiveNameMatching;
protected final Cache> remoteSchemaNames;
protected final Cache> remoteTableNames;
public BaseJdbcClient(BaseJdbcConfig config, String identifierQuote, ConnectionFactory connectionFactory)
{
this(
identifierQuote,
connectionFactory,
config.getJdbcTypesMappedToVarchar(),
requireNonNull(config, "config is null").isCaseInsensitiveNameMatching(),
config.getCaseInsensitiveNameMatchingCacheTtl());
}
public BaseJdbcClient(
String identifierQuote,
ConnectionFactory connectionFactory,
Set jdbcTypesMappedToVarchar,
boolean caseInsensitiveNameMatching,
Duration caseInsensitiveNameMatchingCacheTtl)
{
this.identifierQuote = requireNonNull(identifierQuote, "identifierQuote is null");
this.connectionFactory = requireNonNull(connectionFactory, "connectionFactory is null");
this.jdbcTypesMappedToVarchar = ImmutableSortedSet.orderedBy(CASE_INSENSITIVE_ORDER)
.addAll(requireNonNull(jdbcTypesMappedToVarchar, "jdbcTypesMappedToVarchar is null"))
.build();
requireNonNull(caseInsensitiveNameMatchingCacheTtl, "caseInsensitiveNameMatchingCacheTtl is null");
this.caseInsensitiveNameMatching = caseInsensitiveNameMatching;
CacheBuilder remoteNamesCacheBuilder = CacheBuilder.newBuilder()
.expireAfterWrite(caseInsensitiveNameMatchingCacheTtl.toMillis(), MILLISECONDS);
this.remoteSchemaNames = remoteNamesCacheBuilder.build();
this.remoteTableNames = remoteNamesCacheBuilder.build();
}
@Override
public final Set getSchemaNames(ConnectorSession session)
{
try (Connection connection = connectionFactory.openConnection(session)) {
return listSchemas(connection).stream()
.map(schemaName -> schemaName.toLowerCase(ENGLISH))
.collect(toImmutableSet());
}
catch (SQLException e) {
throw new PrestoException(JDBC_ERROR, e);
}
}
protected Collection listSchemas(Connection connection)
{
try (ResultSet resultSet = connection.getMetaData().getSchemas(connection.getCatalog(), null)) {
ImmutableSet.Builder schemaNames = ImmutableSet.builder();
while (resultSet.next()) {
String schemaName = resultSet.getString("TABLE_SCHEM");
// skip internal schemas
if (filterSchema(schemaName)) {
schemaNames.add(schemaName);
}
}
return schemaNames.build();
}
catch (SQLException e) {
throw new PrestoException(JDBC_ERROR, e);
}
}
protected boolean filterSchema(String schemaName)
{
if (schemaName.equalsIgnoreCase("information_schema")) {
return false;
}
return true;
}
@Override
public List getTableNames(ConnectorSession session, Optional schema)
{
try (Connection connection = connectionFactory.openConnection(session)) {
JdbcIdentity identity = JdbcIdentity.from(session);
Optional remoteSchema = schema.map(schemaName -> toRemoteSchemaName(identity, connection, schemaName));
if (remoteSchema.isPresent() && !filterSchema(remoteSchema.get())) {
return ImmutableList.of();
}
try (ResultSet resultSet = getTables(connection, remoteSchema, Optional.empty())) {
ImmutableList.Builder list = ImmutableList.builder();
while (resultSet.next()) {
String tableSchema = getTableSchemaName(resultSet);
String tableName = resultSet.getString("TABLE_NAME");
if (filterSchema(tableSchema)) {
list.add(new SchemaTableName(tableSchema, tableName));
}
}
return list.build();
}
}
catch (SQLException e) {
throw new PrestoException(JDBC_ERROR, e);
}
}
@Override
public Optional getTableHandle(ConnectorSession session, SchemaTableName schemaTableName)
{
try (Connection connection = connectionFactory.openConnection(session)) {
JdbcIdentity identity = JdbcIdentity.from(session);
String remoteSchema = toRemoteSchemaName(identity, connection, schemaTableName.getSchemaName());
String remoteTable = toRemoteTableName(identity, connection, remoteSchema, schemaTableName.getTableName());
try (ResultSet resultSet = getTables(connection, Optional.of(remoteSchema), Optional.of(remoteTable))) {
List tableHandles = new ArrayList<>();
while (resultSet.next()) {
tableHandles.add(new JdbcTableHandle(schemaTableName, getRemoteTable(resultSet)));
}
if (tableHandles.isEmpty()) {
return Optional.empty();
}
if (tableHandles.size() > 1) {
throw new PrestoException(NOT_SUPPORTED, "Multiple tables matched: " + schemaTableName);
}
return Optional.of(getOnlyElement(tableHandles));
}
}
catch (SQLException e) {
throw new PrestoException(JDBC_ERROR, e);
}
}
@Override
public List getColumns(ConnectorSession session, JdbcTableHandle tableHandle)
{
if (tableHandle.getColumns().isPresent()) {
return tableHandle.getColumns().get();
}
try (Connection connection = connectionFactory.openConnection(session);
ResultSet resultSet = getColumns(tableHandle, connection.getMetaData())) {
int allColumns = 0;
List columns = new ArrayList<>();
while (resultSet.next()) {
// skip if table doesn't match expected
if (!(Objects.equals(tableHandle.getRemoteTableName(), getRemoteTable(resultSet)))) {
continue;
}
allColumns++;
String columnName = resultSet.getString("COLUMN_NAME");
JdbcTypeHandle typeHandle = new JdbcTypeHandle(
getInteger(resultSet, "DATA_TYPE").orElseThrow(() -> new IllegalStateException("DATA_TYPE is null")),
Optional.ofNullable(resultSet.getString("TYPE_NAME")),
getInteger(resultSet, "COLUMN_SIZE"),
getInteger(resultSet, "DECIMAL_DIGITS"),
Optional.empty(),
Optional.empty());
Optional columnMapping = toPrestoType(session, connection, typeHandle);
log.debug("Mapping data type of '%s' column '%s': %s mapped to %s", tableHandle.getSchemaTableName(), columnName, typeHandle, columnMapping);
// skip unsupported column types
boolean nullable = (resultSet.getInt("NULLABLE") != columnNoNulls);
// Note: some databases (e.g. SQL Server) do not return column remarks/comment here.
Optional comment = Optional.ofNullable(emptyToNull(resultSet.getString("REMARKS")));
if (columnMapping.isPresent()) {
columns.add(JdbcColumnHandle.builder()
.setColumnName(columnName)
.setJdbcTypeHandle(typeHandle)
.setColumnType(columnMapping.get().getType())
.setNullable(nullable)
.setComment(comment)
.build());
}
if (columnMapping.isEmpty()) {
UnsupportedTypeHandling unsupportedTypeHandling = getUnsupportedTypeHandling(session);
verify(
unsupportedTypeHandling == IGNORE,
"Unsupported type handling is set to %s, but toPrestoType() returned empty for %s",
unsupportedTypeHandling,
typeHandle);
}
}
if (columns.isEmpty()) {
// A table may have no supported columns. In rare cases (e.g. PostgreSQL) a table might have no columns at all.
throw new TableNotFoundException(
tableHandle.getSchemaTableName(),
format("Table '%s' has no supported columns (all %s columns are not supported)", tableHandle.getSchemaTableName(), allColumns));
}
return ImmutableList.copyOf(columns);
}
catch (SQLException e) {
throw new PrestoException(JDBC_ERROR, e);
}
}
protected static Optional getInteger(ResultSet resultSet, String columnLabel)
throws SQLException
{
int value = resultSet.getInt(columnLabel);
if (resultSet.wasNull()) {
return Optional.empty();
}
return Optional.of(value);
}
protected ResultSet getColumns(JdbcTableHandle tableHandle, DatabaseMetaData metadata)
throws SQLException
{
return metadata.getColumns(
tableHandle.getRemoteTableName().getCatalogName().orElse(null),
escapeNamePattern(tableHandle.getRemoteTableName().getSchemaName(), metadata.getSearchStringEscape()).orElse(null),
escapeNamePattern(Optional.of(tableHandle.getRemoteTableName().getTableName()), metadata.getSearchStringEscape()).orElse(null),
null);
}
/**
* @deprecated Each connector should provide its own explicit type mapping, along with respective tests.
*/
@Deprecated
protected Optional legacyToPrestoType(ConnectorSession session, @SuppressWarnings("unused") Connection connection, JdbcTypeHandle typeHandle)
{
Optional mapping = getForcedMappingToVarchar(typeHandle);
if (mapping.isPresent()) {
return mapping;
}
Optional connectorMapping = jdbcTypeToPrestoType(typeHandle);
if (connectorMapping.isPresent()) {
return connectorMapping;
}
if (getUnsupportedTypeHandling(session) == CONVERT_TO_VARCHAR) {
return mapToUnboundedVarchar(typeHandle);
}
return Optional.empty();
}
@Override
public List getColumnMappings(ConnectorSession session, List typeHandles)
{
try (Connection connection = connectionFactory.openConnection(session)) {
return typeHandles.stream()
.map(typeHandle -> toPrestoType(session, connection, typeHandle)
.orElseThrow(() -> new VerifyException(format("Unsupported type handle %s", typeHandle))))
.collect(toImmutableList());
}
catch (SQLException e) {
throw new PrestoException(JDBC_ERROR, e);
}
}
protected Optional getForcedMappingToVarchar(JdbcTypeHandle typeHandle)
{
if (typeHandle.getJdbcTypeName().isPresent() && jdbcTypesMappedToVarchar.contains(typeHandle.getJdbcTypeName().get())) {
return mapToUnboundedVarchar(typeHandle);
}
return Optional.empty();
}
protected static Optional mapToUnboundedVarchar(JdbcTypeHandle typeHandle)
{
VarcharType unboundedVarcharType = createUnboundedVarcharType();
return Optional.of(ColumnMapping.sliceMapping(
unboundedVarcharType,
varcharReadFunction(unboundedVarcharType),
(statement, index, value) -> {
throw new PrestoException(
NOT_SUPPORTED,
"Underlying type that is mapped to VARCHAR is not supported for INSERT: " + typeHandle.getJdbcTypeName().get());
},
DISABLE_PUSHDOWN));
}
@Override
public ConnectorSplitSource getSplits(ConnectorSession session, JdbcTableHandle tableHandle)
{
return new FixedSplitSource(ImmutableList.of(new JdbcSplit(Optional.empty())));
}
@Override
public Connection getConnection(ConnectorSession session, JdbcSplit split)
throws SQLException
{
Connection connection = connectionFactory.openConnection(session);
try {
connection.setReadOnly(true);
}
catch (SQLException e) {
connection.close();
throw e;
}
return connection;
}
@Override
public PreparedStatement buildSql(ConnectorSession session, Connection connection, JdbcSplit split, JdbcTableHandle table, List columns)
throws SQLException
{
return new QueryBuilder(this).buildSql(
session,
connection,
table.getRemoteTableName(),
table.getGroupingSets(),
columns,
table.getConstraint(),
split.getAdditionalPredicate(),
tryApplyLimit(table.getLimit()));
}
@Override
public void createTable(ConnectorSession session, ConnectorTableMetadata tableMetadata)
{
try {
createTable(session, tableMetadata, tableMetadata.getTable().getTableName());
}
catch (SQLException e) {
throw new PrestoException(JDBC_ERROR, e);
}
}
@Override
public JdbcOutputTableHandle beginCreateTable(ConnectorSession session, ConnectorTableMetadata tableMetadata)
{
try {
return createTable(session, tableMetadata, generateTemporaryTableName());
}
catch (SQLException e) {
throw new PrestoException(JDBC_ERROR, e);
}
}
protected JdbcOutputTableHandle createTable(ConnectorSession session, ConnectorTableMetadata tableMetadata, String tableName)
throws SQLException
{
SchemaTableName schemaTableName = tableMetadata.getTable();
JdbcIdentity identity = JdbcIdentity.from(session);
if (!getSchemaNames(session).contains(schemaTableName.getSchemaName())) {
throw new PrestoException(NOT_FOUND, "Schema not found: " + schemaTableName.getSchemaName());
}
try (Connection connection = connectionFactory.openConnection(session)) {
boolean uppercase = connection.getMetaData().storesUpperCaseIdentifiers();
String remoteSchema = toRemoteSchemaName(identity, connection, schemaTableName.getSchemaName());
String remoteTable = toRemoteTableName(identity, connection, remoteSchema, schemaTableName.getTableName());
if (uppercase) {
tableName = tableName.toUpperCase(ENGLISH);
}
String catalog = connection.getCatalog();
ImmutableList.Builder columnNames = ImmutableList.builder();
ImmutableList.Builder columnTypes = ImmutableList.builder();
ImmutableList.Builder columnList = ImmutableList.builder();
for (ColumnMetadata column : tableMetadata.getColumns()) {
String columnName = column.getName();
if (uppercase) {
columnName = columnName.toUpperCase(ENGLISH);
}
columnNames.add(columnName);
columnTypes.add(column.getType());
columnList.add(getColumnDefinitionSql(session, column, columnName));
}
RemoteTableName remoteTableName = new RemoteTableName(Optional.ofNullable(catalog), Optional.ofNullable(remoteSchema), tableName);
String sql = createTableSql(remoteTableName, columnList.build(), tableMetadata);
execute(connection, sql);
return new JdbcOutputTableHandle(
catalog,
remoteSchema,
remoteTable,
columnNames.build(),
columnTypes.build(),
Optional.empty(),
tableName);
}
}
protected String createTableSql(RemoteTableName remoteTableName, List columns, ConnectorTableMetadata tableMetadata)
{
checkArgument(tableMetadata.getProperties().isEmpty(), "Unsupported table properties: %s", tableMetadata.getProperties());
return format("CREATE TABLE %s (%s)", quoted(remoteTableName), join(", ", columns));
}
protected String getColumnDefinitionSql(ConnectorSession session, ColumnMetadata column, String columnName)
{
StringBuilder sb = new StringBuilder()
.append(quoted(columnName))
.append(" ")
.append(toWriteMapping(session, column.getType()).getDataType());
if (!column.isNullable()) {
sb.append(" NOT NULL");
}
return sb.toString();
}
@Override
public JdbcOutputTableHandle beginInsertTable(ConnectorSession session, JdbcTableHandle tableHandle, List columns)
{
SchemaTableName schemaTableName = tableHandle.getSchemaTableName();
JdbcIdentity identity = JdbcIdentity.from(session);
try (Connection connection = connectionFactory.openConnection(session)) {
boolean uppercase = connection.getMetaData().storesUpperCaseIdentifiers();
String remoteSchema = toRemoteSchemaName(identity, connection, schemaTableName.getSchemaName());
String remoteTable = toRemoteTableName(identity, connection, remoteSchema, schemaTableName.getTableName());
String tableName = generateTemporaryTableName();
if (uppercase) {
tableName = tableName.toUpperCase(ENGLISH);
}
String catalog = connection.getCatalog();
ImmutableList.Builder columnNames = ImmutableList.builder();
ImmutableList.Builder columnTypes = ImmutableList.builder();
ImmutableList.Builder jdbcColumnTypes = ImmutableList.builder();
for (JdbcColumnHandle column : columns) {
columnNames.add(column.getColumnName());
columnTypes.add(column.getColumnType());
jdbcColumnTypes.add(column.getJdbcTypeHandle());
}
copyTableSchema(connection, catalog, remoteSchema, remoteTable, tableName, columnNames.build());
return new JdbcOutputTableHandle(
catalog,
remoteSchema,
remoteTable,
columnNames.build(),
columnTypes.build(),
Optional.of(jdbcColumnTypes.build()),
tableName);
}
catch (SQLException e) {
throw new PrestoException(JDBC_ERROR, e);
}
}
protected void copyTableSchema(Connection connection, String catalogName, String schemaName, String tableName, String newTableName, List columnNames)
{
String sql = format(
"CREATE TABLE %s AS SELECT %s FROM %s WHERE 0 = 1",
quoted(catalogName, schemaName, newTableName),
columnNames.stream()
.map(this::quoted)
.collect(joining(", ")),
quoted(catalogName, schemaName, tableName));
execute(connection, sql);
}
protected String generateTemporaryTableName()
{
return "tmp_presto_" + UUID.randomUUID().toString().replace("-", "");
}
@Override
public void commitCreateTable(ConnectorSession session, JdbcOutputTableHandle handle)
{
renameTable(
session,
handle.getCatalogName(),
handle.getSchemaName(),
handle.getTemporaryTableName(),
new SchemaTableName(handle.getSchemaName(), handle.getTableName()));
}
@Override
public void renameTable(ConnectorSession session, JdbcTableHandle handle, SchemaTableName newTableName)
{
renameTable(session, handle.getCatalogName(), handle.getSchemaName(), handle.getTableName(), newTableName);
}
protected void renameTable(ConnectorSession session, String catalogName, String schemaName, String tableName, SchemaTableName newTable)
{
try (Connection connection = connectionFactory.openConnection(session)) {
String newSchemaName = newTable.getSchemaName();
String newTableName = newTable.getTableName();
if (connection.getMetaData().storesUpperCaseIdentifiers()) {
newSchemaName = newSchemaName.toUpperCase(ENGLISH);
newTableName = newTableName.toUpperCase(ENGLISH);
}
String sql = format(
"ALTER TABLE %s RENAME TO %s",
quoted(catalogName, schemaName, tableName),
quoted(catalogName, newSchemaName, newTableName));
execute(connection, sql);
}
catch (SQLException e) {
throw new PrestoException(JDBC_ERROR, e);
}
}
@Override
public void finishInsertTable(ConnectorSession session, JdbcOutputTableHandle handle)
{
String temporaryTable = quoted(handle.getCatalogName(), handle.getSchemaName(), handle.getTemporaryTableName());
String targetTable = quoted(handle.getCatalogName(), handle.getSchemaName(), handle.getTableName());
String columnNames = handle.getColumnNames().stream()
.map(this::quoted)
.collect(joining(", "));
String insertSql = format("INSERT INTO %s (%s) SELECT %s FROM %s", targetTable, columnNames, columnNames, temporaryTable);
String cleanupSql = "DROP TABLE " + temporaryTable;
try (Connection connection = getConnection(session, handle)) {
execute(connection, insertSql);
}
catch (SQLException e) {
throw new PrestoException(JDBC_ERROR, e);
}
try (Connection connection = getConnection(session, handle)) {
execute(connection, cleanupSql);
}
catch (SQLException e) {
log.warn(e, "Failed to cleanup temporary table: %s", temporaryTable);
}
}
@Override
public void addColumn(ConnectorSession session, JdbcTableHandle handle, ColumnMetadata column)
{
try (Connection connection = connectionFactory.openConnection(session)) {
String columnName = column.getName();
if (connection.getMetaData().storesUpperCaseIdentifiers()) {
columnName = columnName.toUpperCase(ENGLISH);
}
String sql = format(
"ALTER TABLE %s ADD %s",
quoted(handle.getRemoteTableName()),
getColumnDefinitionSql(session, column, columnName));
execute(connection, sql);
}
catch (SQLException e) {
throw new PrestoException(JDBC_ERROR, e);
}
}
@Override
public void renameColumn(ConnectorSession session, JdbcTableHandle handle, JdbcColumnHandle jdbcColumn, String newColumnName)
{
try (Connection connection = connectionFactory.openConnection(session)) {
if (connection.getMetaData().storesUpperCaseIdentifiers()) {
newColumnName = newColumnName.toUpperCase(ENGLISH);
}
String sql = format(
"ALTER TABLE %s RENAME COLUMN %s TO %s",
quoted(handle.getRemoteTableName()),
jdbcColumn.getColumnName(),
newColumnName);
execute(connection, sql);
}
catch (SQLException e) {
throw new PrestoException(JDBC_ERROR, e);
}
}
@Override
public void dropColumn(ConnectorSession session, JdbcTableHandle handle, JdbcColumnHandle column)
{
String sql = format(
"ALTER TABLE %s DROP COLUMN %s",
quoted(handle.getRemoteTableName()),
column.getColumnName());
execute(session, sql);
}
@Override
public void dropTable(ConnectorSession session, JdbcTableHandle handle)
{
String sql = "DROP TABLE " + quoted(handle.getRemoteTableName());
execute(session, sql);
}
@Override
public void rollbackCreateTable(ConnectorSession session, JdbcOutputTableHandle handle)
{
dropTable(session, new JdbcTableHandle(
new SchemaTableName(handle.getSchemaName(), handle.getTemporaryTableName()),
handle.getCatalogName(),
handle.getSchemaName(),
handle.getTemporaryTableName()));
}
@Override
public String buildInsertSql(JdbcOutputTableHandle handle, List columnWriters)
{
checkArgument(handle.getColumnNames().size() == columnWriters.size(), "handle and columnWriters mismatch: %s, %s", handle, columnWriters);
return format(
"INSERT INTO %s (%s) VALUES (%s)",
quoted(handle.getCatalogName(), handle.getSchemaName(), handle.getTemporaryTableName()),
handle.getColumnNames().stream()
.map(this::quoted)
.collect(joining(", ")),
columnWriters.stream()
.map(WriteFunction::getBindExpression)
.collect(joining(",")));
}
@Override
public Connection getConnection(ConnectorSession session, JdbcOutputTableHandle handle)
throws SQLException
{
return connectionFactory.openConnection(session);
}
@Override
public PreparedStatement getPreparedStatement(Connection connection, String sql)
throws SQLException
{
return connection.prepareStatement(sql);
}
protected ResultSet getTables(Connection connection, Optional schemaName, Optional tableName)
throws SQLException
{
DatabaseMetaData metadata = connection.getMetaData();
return metadata.getTables(
connection.getCatalog(),
escapeNamePattern(schemaName, metadata.getSearchStringEscape()).orElse(null),
escapeNamePattern(tableName, metadata.getSearchStringEscape()).orElse(null),
new String[] {"TABLE", "VIEW"});
}
protected String getTableSchemaName(ResultSet resultSet)
throws SQLException
{
return resultSet.getString("TABLE_SCHEM");
}
protected String toRemoteSchemaName(JdbcIdentity identity, Connection connection, String schemaName)
{
requireNonNull(schemaName, "schemaName is null");
verify(CharMatcher.forPredicate(Character::isUpperCase).matchesNoneOf(schemaName), "Expected schema name from internal metadata to be lowercase: %s", schemaName);
if (caseInsensitiveNameMatching) {
try {
Map mapping = remoteSchemaNames.getIfPresent(identity);
if (mapping != null && !mapping.containsKey(schemaName)) {
// This might be a schema that has just been created. Force reload.
mapping = null;
}
if (mapping == null) {
mapping = listSchemasByLowerCase(connection);
remoteSchemaNames.put(identity, mapping);
}
String remoteSchema = mapping.get(schemaName);
if (remoteSchema != null) {
return remoteSchema;
}
}
catch (RuntimeException e) {
throw new PrestoException(JDBC_ERROR, "Failed to find remote schema name: " + firstNonNull(e.getMessage(), e), e);
}
}
try {
DatabaseMetaData metadata = connection.getMetaData();
if (metadata.storesUpperCaseIdentifiers()) {
return schemaName.toUpperCase(ENGLISH);
}
return schemaName;
}
catch (SQLException e) {
throw new PrestoException(JDBC_ERROR, e);
}
}
protected Map listSchemasByLowerCase(Connection connection)
{
return listSchemas(connection).stream()
.collect(toImmutableMap(schemaName -> schemaName.toLowerCase(ENGLISH), schemaName -> schemaName));
}
protected String toRemoteTableName(JdbcIdentity identity, Connection connection, String remoteSchema, String tableName)
{
requireNonNull(remoteSchema, "remoteSchema is null");
requireNonNull(tableName, "tableName is null");
verify(CharMatcher.forPredicate(Character::isUpperCase).matchesNoneOf(tableName), "Expected table name from internal metadata to be lowercase: %s", tableName);
if (caseInsensitiveNameMatching) {
try {
RemoteTableNameCacheKey cacheKey = new RemoteTableNameCacheKey(identity, remoteSchema);
Map mapping = remoteTableNames.getIfPresent(cacheKey);
if (mapping != null && !mapping.containsKey(tableName)) {
// This might be a table that has just been created. Force reload.
mapping = null;
}
if (mapping == null) {
mapping = listTablesByLowerCase(connection, remoteSchema);
remoteTableNames.put(cacheKey, mapping);
}
String remoteTable = mapping.get(tableName);
if (remoteTable != null) {
return remoteTable;
}
}
catch (RuntimeException e) {
throw new PrestoException(JDBC_ERROR, "Failed to find remote table name: " + firstNonNull(e.getMessage(), e), e);
}
}
try {
DatabaseMetaData metadata = connection.getMetaData();
if (metadata.storesUpperCaseIdentifiers()) {
return tableName.toUpperCase(ENGLISH);
}
return tableName;
}
catch (SQLException e) {
throw new PrestoException(JDBC_ERROR, e);
}
}
protected Map listTablesByLowerCase(Connection connection, String remoteSchema)
{
try (ResultSet resultSet = getTables(connection, Optional.of(remoteSchema), Optional.empty())) {
ImmutableMap.Builder map = ImmutableMap.builder();
while (resultSet.next()) {
String tableName = resultSet.getString("TABLE_NAME");
map.put(tableName.toLowerCase(ENGLISH), tableName);
}
return map.build();
}
catch (SQLException e) {
throw new PrestoException(JDBC_ERROR, e);
}
}
@Override
public TableStatistics getTableStatistics(ConnectorSession session, JdbcTableHandle handle, TupleDomain tupleDomain)
{
return TableStatistics.empty();
}
@Override
public void createSchema(ConnectorSession session, String schemaName)
{
execute(session, "CREATE SCHEMA " + quoted(schemaName));
}
@Override
public void dropSchema(ConnectorSession session, String schemaName)
{
execute(session, "DROP SCHEMA " + quoted(schemaName));
}
protected void execute(ConnectorSession session, String query)
{
try (Connection connection = connectionFactory.openConnection(session)) {
execute(connection, query);
}
catch (SQLException e) {
throw new PrestoException(JDBC_ERROR, e);
}
}
protected void execute(Connection connection, String query)
{
try (Statement statement = connection.createStatement()) {
log.debug("Execute: %s", query);
statement.execute(query);
}
catch (SQLException e) {
PrestoException exception = new PrestoException(JDBC_ERROR, e);
exception.addSuppressed(new RuntimeException("Query: " + query));
throw exception;
}
}
/**
* @deprecated Each connector should provide its own explicit type mapping, along with respective tests.
*/
@Deprecated
protected WriteMapping legacyToWriteMapping(@SuppressWarnings("unused") ConnectorSession session, Type type)
{
if (type instanceof VarcharType) {
VarcharType varcharType = (VarcharType) type;
String dataType;
if (varcharType.isUnbounded()) {
dataType = "varchar";
}
else {
dataType = "varchar(" + varcharType.getBoundedLength() + ")";
}
return WriteMapping.sliceMapping(dataType, varcharWriteFunction());
}
if (type instanceof CharType) {
return WriteMapping.sliceMapping("char(" + ((CharType) type).getLength() + ")", charWriteFunction());
}
if (type instanceof DecimalType) {
DecimalType decimalType = (DecimalType) type;
String dataType = format("decimal(%s, %s)", decimalType.getPrecision(), decimalType.getScale());
if (decimalType.isShort()) {
return WriteMapping.longMapping(dataType, shortDecimalWriteFunction(decimalType));
}
return WriteMapping.sliceMapping(dataType, longDecimalWriteFunction(decimalType));
}
WriteMapping writeMapping = WRITE_MAPPINGS.get(type);
if (writeMapping != null) {
return writeMapping;
}
throw new PrestoException(NOT_SUPPORTED, "Unsupported column type: " + type.getDisplayName());
}
protected Function tryApplyLimit(OptionalLong limit)
{
if (limit.isEmpty()) {
return Function.identity();
}
return limitFunction()
.map(limitFunction -> (Function) sql -> limitFunction.apply(sql, limit.getAsLong()))
.orElseGet(Function::identity);
}
@Override
public boolean supportsLimit()
{
return limitFunction().isPresent();
}
protected Optional> limitFunction()
{
return Optional.empty();
}
@Override
public boolean isLimitGuaranteed(ConnectorSession session)
{
throw new PrestoException(JDBC_ERROR, "limitFunction() is implemented without isLimitGuaranteed()");
}
@Override
public String quoted(String name)
{
name = name.replace(identifierQuote, identifierQuote + identifierQuote);
return identifierQuote + name + identifierQuote;
}
@Override
public String quoted(RemoteTableName remoteTableName)
{
return quoted(
remoteTableName.getCatalogName().orElse(null),
remoteTableName.getSchemaName().orElse(null),
remoteTableName.getTableName());
}
@Override
public Map getTableProperties(ConnectorSession session, JdbcTableHandle tableHandle)
{
return emptyMap();
}
protected String quoted(@Nullable String catalog, @Nullable String schema, String table)
{
StringBuilder sb = new StringBuilder();
if (!isNullOrEmpty(catalog)) {
sb.append(quoted(catalog)).append(".");
}
if (!isNullOrEmpty(schema)) {
sb.append(quoted(schema)).append(".");
}
sb.append(quoted(table));
return sb.toString();
}
protected static Optional escapeNamePattern(Optional name, String escape)
{
return name.map(string -> escapeNamePattern(string, escape));
}
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);
name = name.replace("_", escape + "_");
name = name.replace("%", escape + "%");
return name;
}
private static RemoteTableName getRemoteTable(ResultSet resultSet)
throws SQLException
{
return new RemoteTableName(
Optional.ofNullable(resultSet.getString("TABLE_CAT")),
Optional.ofNullable(resultSet.getString("TABLE_SCHEM")),
resultSet.getString("TABLE_NAME"));
}
}