org.apache.calcite.avatica.jdbc.JdbcMeta Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you 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 org.apache.calcite.avatica.jdbc;
import org.apache.calcite.avatica.AvaticaParameter;
import org.apache.calcite.avatica.AvaticaPreparedStatement;
import org.apache.calcite.avatica.AvaticaUtils;
import org.apache.calcite.avatica.ColumnMetaData;
import org.apache.calcite.avatica.ConnectionPropertiesImpl;
import org.apache.calcite.avatica.Meta;
import org.apache.calcite.avatica.MetaImpl;
import org.apache.calcite.avatica.MissingResultsException;
import org.apache.calcite.avatica.NoSuchConnectionException;
import org.apache.calcite.avatica.NoSuchStatementException;
import org.apache.calcite.avatica.QueryState;
import org.apache.calcite.avatica.SqlType;
import org.apache.calcite.avatica.metrics.Gauge;
import org.apache.calcite.avatica.metrics.MetricsSystem;
import org.apache.calcite.avatica.metrics.noop.NoopMetricsSystem;
import org.apache.calcite.avatica.proto.Common;
import org.apache.calcite.avatica.proto.Requests;
import org.apache.calcite.avatica.remote.ProtobufMeta;
import org.apache.calcite.avatica.remote.TypedValue;
import org.apache.calcite.avatica.util.Unsafe;
import com.google.common.base.Optional;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.apache.calcite.avatica.remote.MetricsHelper.concat;
/** Implementation of {@link Meta} upon an existing JDBC data source. */
public class JdbcMeta implements ProtobufMeta {
private static final Logger LOG = LoggerFactory.getLogger(JdbcMeta.class);
private static final String CONN_CACHE_KEY_BASE = "avatica.connectioncache";
private static final String STMT_CACHE_KEY_BASE = "avatica.statementcache";
/** Special value for {@code Statement#getLargeMaxRows()} that means fetch
* an unlimited number of rows in a single batch.
*
* Any other negative value will return an unlimited number of rows but
* will do it in the default batch size, namely 100. */
public static final int UNLIMITED_COUNT = -2;
// End of constants, start of member variables
final Calendar calendar = Unsafe.localCalendar();
/** Generates ids for statements. The ids are unique across all connections
* created by this JdbcMeta. */
private final AtomicInteger statementIdGenerator = new AtomicInteger();
private final String url;
private final Properties info;
private final Cache connectionCache;
private final Cache statementCache;
private final MetricsSystem metrics;
/**
* Creates a JdbcMeta.
*
* @param url a database url of the form
* jdbc:subprotocol:subname
*/
public JdbcMeta(String url) throws SQLException {
this(url, new Properties());
}
/**
* Creates a JdbcMeta.
*
* @param url a database url of the form
* jdbc:subprotocol:subname
* @param user the database user on whose behalf the connection is being
* made
* @param password the user's password
*/
public JdbcMeta(final String url, final String user, final String password)
throws SQLException {
this(url, new Properties() {
{
put("user", user);
put("password", password);
}
});
}
public JdbcMeta(String url, Properties info) throws SQLException {
this(url, info, NoopMetricsSystem.getInstance());
}
/**
* Creates a JdbcMeta.
*
* @param url a database url of the form
* jdbc:subprotocol:subname
* @param info a list of arbitrary string tag/value pairs as
* connection arguments; normally at least a "user" and
* "password" property should be included
*/
public JdbcMeta(String url, Properties info, MetricsSystem metrics)
throws SQLException {
this.url = url;
this.info = info;
this.metrics = Objects.requireNonNull(metrics);
int concurrencyLevel = Integer.parseInt(
info.getProperty(ConnectionCacheSettings.CONCURRENCY_LEVEL.key(),
ConnectionCacheSettings.CONCURRENCY_LEVEL.defaultValue()));
int initialCapacity = Integer.parseInt(
info.getProperty(ConnectionCacheSettings.INITIAL_CAPACITY.key(),
ConnectionCacheSettings.INITIAL_CAPACITY.defaultValue()));
long maxCapacity = Long.parseLong(
info.getProperty(ConnectionCacheSettings.MAX_CAPACITY.key(),
ConnectionCacheSettings.MAX_CAPACITY.defaultValue()));
long connectionExpiryDuration = Long.parseLong(
info.getProperty(ConnectionCacheSettings.EXPIRY_DURATION.key(),
ConnectionCacheSettings.EXPIRY_DURATION.defaultValue()));
TimeUnit connectionExpiryUnit = TimeUnit.valueOf(
info.getProperty(ConnectionCacheSettings.EXPIRY_UNIT.key(),
ConnectionCacheSettings.EXPIRY_UNIT.defaultValue()));
this.connectionCache = CacheBuilder.newBuilder()
.concurrencyLevel(concurrencyLevel)
.initialCapacity(initialCapacity)
.maximumSize(maxCapacity)
.expireAfterAccess(connectionExpiryDuration, connectionExpiryUnit)
.removalListener(new ConnectionExpiryHandler())
.build();
LOG.debug("instantiated connection cache: {}", connectionCache.stats());
concurrencyLevel = Integer.parseInt(
info.getProperty(StatementCacheSettings.CONCURRENCY_LEVEL.key(),
StatementCacheSettings.CONCURRENCY_LEVEL.defaultValue()));
initialCapacity = Integer.parseInt(
info.getProperty(StatementCacheSettings.INITIAL_CAPACITY.key(),
StatementCacheSettings.INITIAL_CAPACITY.defaultValue()));
maxCapacity = Long.parseLong(
info.getProperty(StatementCacheSettings.MAX_CAPACITY.key(),
StatementCacheSettings.MAX_CAPACITY.defaultValue()));
connectionExpiryDuration = Long.parseLong(
info.getProperty(StatementCacheSettings.EXPIRY_DURATION.key(),
StatementCacheSettings.EXPIRY_DURATION.defaultValue()));
connectionExpiryUnit = TimeUnit.valueOf(
info.getProperty(StatementCacheSettings.EXPIRY_UNIT.key(),
StatementCacheSettings.EXPIRY_UNIT.defaultValue()));
this.statementCache = CacheBuilder.newBuilder()
.concurrencyLevel(concurrencyLevel)
.initialCapacity(initialCapacity)
.maximumSize(maxCapacity)
.expireAfterAccess(connectionExpiryDuration, connectionExpiryUnit)
.removalListener(new StatementExpiryHandler())
.build();
LOG.debug("instantiated statement cache: {}", statementCache.stats());
// Register some metrics
this.metrics.register(concat(JdbcMeta.class, "ConnectionCacheSize"), new Gauge() {
@Override public Long getValue() {
return connectionCache.size();
}
});
this.metrics.register(concat(JdbcMeta.class, "StatementCacheSize"), new Gauge() {
@Override public Long getValue() {
return statementCache.size();
}
});
}
// For testing purposes
protected AtomicInteger getStatementIdGenerator() {
return statementIdGenerator;
}
// For testing purposes
protected Cache getConnectionCache() {
return connectionCache;
}
// For testing purposes
protected Cache getStatementCache() {
return statementCache;
}
/**
* Converts from JDBC metadata to Avatica columns.
*/
protected static List
columns(ResultSetMetaData metaData) throws SQLException {
if (metaData == null) {
return Collections.emptyList();
}
final List columns = new ArrayList<>();
for (int i = 1; i <= metaData.getColumnCount(); i++) {
final SqlType sqlType = SqlType.valueOf(metaData.getColumnType(i));
final ColumnMetaData.Rep rep = ColumnMetaData.Rep.of(sqlType.internal);
final ColumnMetaData.AvaticaType t;
if (sqlType == SqlType.ARRAY || sqlType == SqlType.STRUCT || sqlType == SqlType.MULTISET) {
ColumnMetaData.AvaticaType arrayValueType = ColumnMetaData.scalar(Types.JAVA_OBJECT,
metaData.getColumnTypeName(i), ColumnMetaData.Rep.OBJECT);
t = ColumnMetaData.array(arrayValueType, metaData.getColumnTypeName(i), rep);
} else {
t = ColumnMetaData.scalar(metaData.getColumnType(i), metaData.getColumnTypeName(i), rep);
}
ColumnMetaData md =
new ColumnMetaData(i - 1, metaData.isAutoIncrement(i),
metaData.isCaseSensitive(i), metaData.isSearchable(i),
metaData.isCurrency(i), metaData.isNullable(i),
metaData.isSigned(i), metaData.getColumnDisplaySize(i),
metaData.getColumnLabel(i), metaData.getColumnName(i),
metaData.getSchemaName(i), metaData.getPrecision(i),
metaData.getScale(i), metaData.getTableName(i),
metaData.getCatalogName(i), t, metaData.isReadOnly(i),
metaData.isWritable(i), metaData.isDefinitelyWritable(i),
metaData.getColumnClassName(i));
columns.add(md);
}
return columns;
}
/**
* Converts from JDBC metadata to Avatica parameters
*/
protected static List parameters(ParameterMetaData metaData)
throws SQLException {
if (metaData == null) {
return Collections.emptyList();
}
final List params = new ArrayList<>();
for (int i = 1; i <= metaData.getParameterCount(); i++) {
params.add(
new AvaticaParameter(metaData.isSigned(i), metaData.getPrecision(i),
metaData.getScale(i), metaData.getParameterType(i),
metaData.getParameterTypeName(i),
metaData.getParameterClassName(i), "?" + i));
}
return params;
}
protected static Signature signature(ResultSetMetaData metaData,
ParameterMetaData parameterMetaData, String sql,
Meta.StatementType statementType) throws SQLException {
final CursorFactory cf = CursorFactory.LIST; // because JdbcResultSet#frame
return new Signature(columns(metaData), sql, parameters(parameterMetaData),
null, cf, statementType);
}
protected static Signature signature(ResultSetMetaData metaData)
throws SQLException {
return signature(metaData, null, null, null);
}
public Map getDatabaseProperties(ConnectionHandle ch) {
try {
final Map map = new HashMap<>();
final Connection conn = getConnection(ch.id);
final DatabaseMetaData metaData = conn.getMetaData();
for (DatabaseProperty p : DatabaseProperty.values()) {
addProperty(map, metaData, p);
}
return map;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private static Object addProperty(Map map,
DatabaseMetaData metaData, DatabaseProperty p) throws SQLException {
Object propertyValue;
if (p.isJdbc) {
try {
propertyValue = p.method.invoke(metaData);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
} else {
propertyValue = p.defaultValue;
}
return map.put(p, propertyValue);
}
public MetaResultSet getTables(ConnectionHandle ch, String catalog, Pat schemaPattern,
Pat tableNamePattern, List typeList) {
try {
final ResultSet rs =
getConnection(ch.id).getMetaData().getTables(catalog, schemaPattern.s,
tableNamePattern.s, toArray(typeList));
int stmtId = registerMetaStatement(rs);
return JdbcResultSet.create(ch.id, stmtId, rs);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* Registers a StatementInfo for the given ResultSet, returning the id under
* which it is registered. This should be used for metadata ResultSets, which
* have an implicit statement created.
*/
private int registerMetaStatement(ResultSet rs) throws SQLException {
final int id = statementIdGenerator.getAndIncrement();
StatementInfo statementInfo = new StatementInfo(rs.getStatement());
statementInfo.setResultSet(rs);
statementCache.put(id, statementInfo);
return id;
}
public MetaResultSet getColumns(ConnectionHandle ch, String catalog, Pat schemaPattern,
Pat tableNamePattern, Pat columnNamePattern) {
try {
final ResultSet rs =
getConnection(ch.id).getMetaData().getColumns(catalog, schemaPattern.s,
tableNamePattern.s, columnNamePattern.s);
int stmtId = registerMetaStatement(rs);
return JdbcResultSet.create(ch.id, stmtId, rs);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public MetaResultSet getSchemas(ConnectionHandle ch, String catalog, Pat schemaPattern) {
try {
final ResultSet rs =
getConnection(ch.id).getMetaData().getSchemas(catalog, schemaPattern.s);
int stmtId = registerMetaStatement(rs);
return JdbcResultSet.create(ch.id, stmtId, rs);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public MetaResultSet getCatalogs(ConnectionHandle ch) {
try {
final ResultSet rs = getConnection(ch.id).getMetaData().getCatalogs();
int stmtId = registerMetaStatement(rs);
return JdbcResultSet.create(ch.id, stmtId, rs);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public MetaResultSet getTableTypes(ConnectionHandle ch) {
try {
final ResultSet rs = getConnection(ch.id).getMetaData().getTableTypes();
int stmtId = registerMetaStatement(rs);
return JdbcResultSet.create(ch.id, stmtId, rs);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public MetaResultSet getProcedures(ConnectionHandle ch, String catalog, Pat schemaPattern,
Pat procedureNamePattern) {
try {
final ResultSet rs =
getConnection(ch.id).getMetaData().getProcedures(catalog, schemaPattern.s,
procedureNamePattern.s);
int stmtId = registerMetaStatement(rs);
return JdbcResultSet.create(ch.id, stmtId, rs);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public MetaResultSet getProcedureColumns(ConnectionHandle ch, String catalog, Pat schemaPattern,
Pat procedureNamePattern, Pat columnNamePattern) {
try {
final ResultSet rs =
getConnection(ch.id).getMetaData().getProcedureColumns(catalog,
schemaPattern.s, procedureNamePattern.s, columnNamePattern.s);
int stmtId = registerMetaStatement(rs);
return JdbcResultSet.create(ch.id, stmtId, rs);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public MetaResultSet getColumnPrivileges(ConnectionHandle ch, String catalog, String schema,
String table, Pat columnNamePattern) {
try {
final ResultSet rs =
getConnection(ch.id).getMetaData().getColumnPrivileges(catalog, schema,
table, columnNamePattern.s);
int stmtId = registerMetaStatement(rs);
return JdbcResultSet.create(ch.id, stmtId, rs);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public MetaResultSet getTablePrivileges(ConnectionHandle ch, String catalog, Pat schemaPattern,
Pat tableNamePattern) {
try {
final ResultSet rs =
getConnection(ch.id).getMetaData().getTablePrivileges(catalog,
schemaPattern.s, tableNamePattern.s);
int stmtId = registerMetaStatement(rs);
return JdbcResultSet.create(ch.id, stmtId, rs);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public MetaResultSet getBestRowIdentifier(ConnectionHandle ch, String catalog, String schema,
String table, int scope, boolean nullable) {
LOG.trace("getBestRowIdentifier catalog:{} schema:{} table:{} scope:{} nullable:{}", catalog,
schema, table, scope, nullable);
try {
final ResultSet rs =
getConnection(ch.id).getMetaData().getBestRowIdentifier(catalog, schema,
table, scope, nullable);
int stmtId = registerMetaStatement(rs);
return JdbcResultSet.create(ch.id, stmtId, rs);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public MetaResultSet getVersionColumns(ConnectionHandle ch, String catalog, String schema,
String table) {
LOG.trace("getVersionColumns catalog:{} schema:{} table:{}", catalog, schema, table);
try {
final ResultSet rs =
getConnection(ch.id).getMetaData().getVersionColumns(catalog, schema, table);
int stmtId = registerMetaStatement(rs);
return JdbcResultSet.create(ch.id, stmtId, rs);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public MetaResultSet getPrimaryKeys(ConnectionHandle ch, String catalog, String schema,
String table) {
LOG.trace("getPrimaryKeys catalog:{} schema:{} table:{}", catalog, schema, table);
try {
final ResultSet rs =
getConnection(ch.id).getMetaData().getPrimaryKeys(catalog, schema, table);
int stmtId = registerMetaStatement(rs);
return JdbcResultSet.create(ch.id, stmtId, rs);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public MetaResultSet getImportedKeys(ConnectionHandle ch, String catalog, String schema,
String table) {
return null;
}
public MetaResultSet getExportedKeys(ConnectionHandle ch, String catalog, String schema,
String table) {
return null;
}
public MetaResultSet getCrossReference(ConnectionHandle ch, String parentCatalog,
String parentSchema, String parentTable, String foreignCatalog,
String foreignSchema, String foreignTable) {
return null;
}
public MetaResultSet getTypeInfo(ConnectionHandle ch) {
try {
final ResultSet rs = getConnection(ch.id).getMetaData().getTypeInfo();
int stmtId = registerMetaStatement(rs);
return JdbcResultSet.create(ch.id, stmtId, rs);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public MetaResultSet getIndexInfo(ConnectionHandle ch, String catalog, String schema,
String table, boolean unique, boolean approximate) {
return null;
}
public MetaResultSet getUDTs(ConnectionHandle ch, String catalog, Pat schemaPattern,
Pat typeNamePattern, int[] types) {
return null;
}
public MetaResultSet getSuperTypes(ConnectionHandle ch, String catalog, Pat schemaPattern,
Pat typeNamePattern) {
return null;
}
public MetaResultSet getSuperTables(ConnectionHandle ch, String catalog, Pat schemaPattern,
Pat tableNamePattern) {
return null;
}
public MetaResultSet getAttributes(ConnectionHandle ch, String catalog, Pat schemaPattern,
Pat typeNamePattern, Pat attributeNamePattern) {
return null;
}
public MetaResultSet getClientInfoProperties(ConnectionHandle ch) {
return null;
}
public MetaResultSet getFunctions(ConnectionHandle ch, String catalog, Pat schemaPattern,
Pat functionNamePattern) {
return null;
}
public MetaResultSet getFunctionColumns(ConnectionHandle ch, String catalog, Pat schemaPattern,
Pat functionNamePattern, Pat columnNamePattern) {
return null;
}
public MetaResultSet getPseudoColumns(ConnectionHandle ch, String catalog, Pat schemaPattern,
Pat tableNamePattern, Pat columnNamePattern) {
return null;
}
public Iterable