blob: a4f45c6bcbdb43d872aeaa7e5ff67739a16804f7 [file] [log] [blame]
/*
* 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;
import org.apache.calcite.avatica.proto.Common;
import org.apache.calcite.avatica.remote.TypedValue;
import org.apache.calcite.avatica.util.FilteredConstants;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.google.protobuf.Descriptors.FieldDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.stream.Collectors;
/**
* Command handler for getting various metadata. Should be implemented by each
* driver.
*
* <p>Also holds other abstract methods that are not related to metadata
* that each provider must implement. This is not ideal.</p>
*/
public interface Meta {
/**
* Returns a map of static database properties.
*
* <p>The provider can omit properties whose value is the same as the
* default.
*/
Map<DatabaseProperty, Object> getDatabaseProperties(ConnectionHandle ch);
/** Per {@link DatabaseMetaData#getTables(String, String, String, String[])}. */
MetaResultSet getTables(ConnectionHandle ch,
String catalog,
Pat schemaPattern,
Pat tableNamePattern,
List<String> typeList);
/** Per {@link DatabaseMetaData#getColumns(String, String, String, String)}. */
MetaResultSet getColumns(ConnectionHandle ch,
String catalog,
Pat schemaPattern,
Pat tableNamePattern,
Pat columnNamePattern);
MetaResultSet getSchemas(ConnectionHandle ch, String catalog, Pat schemaPattern);
/** Per {@link DatabaseMetaData#getCatalogs()}. */
MetaResultSet getCatalogs(ConnectionHandle ch);
/** Per {@link DatabaseMetaData#getTableTypes()}. */
MetaResultSet getTableTypes(ConnectionHandle ch);
/** Per {@link DatabaseMetaData#getProcedures(String, String, String)}. */
MetaResultSet getProcedures(ConnectionHandle ch,
String catalog,
Pat schemaPattern,
Pat procedureNamePattern);
/** Per {@link DatabaseMetaData#getProcedureColumns(String, String, String, String)}. */
MetaResultSet getProcedureColumns(ConnectionHandle ch,
String catalog,
Pat schemaPattern,
Pat procedureNamePattern,
Pat columnNamePattern);
/** Per {@link DatabaseMetaData#getColumnPrivileges(String, String, String, String)}. */
MetaResultSet getColumnPrivileges(ConnectionHandle ch,
String catalog,
String schema,
String table,
Pat columnNamePattern);
/** Per {@link DatabaseMetaData#getTablePrivileges(String, String, String)}. */
MetaResultSet getTablePrivileges(ConnectionHandle ch,
String catalog,
Pat schemaPattern,
Pat tableNamePattern);
/** Per
* {@link DatabaseMetaData#getBestRowIdentifier(String, String, String, int, boolean)}. */
MetaResultSet getBestRowIdentifier(ConnectionHandle ch,
String catalog,
String schema,
String table,
int scope,
boolean nullable);
/** Per {@link DatabaseMetaData#getVersionColumns(String, String, String)}. */
MetaResultSet getVersionColumns(ConnectionHandle ch, String catalog, String schema, String table);
/** Per {@link DatabaseMetaData#getPrimaryKeys(String, String, String)}. */
MetaResultSet getPrimaryKeys(ConnectionHandle ch, String catalog, String schema, String table);
/** Per {@link DatabaseMetaData#getImportedKeys(String, String, String)}. */
MetaResultSet getImportedKeys(ConnectionHandle ch, String catalog, String schema, String table);
/** Per {@link DatabaseMetaData#getExportedKeys(String, String, String)}. */
MetaResultSet getExportedKeys(ConnectionHandle ch, String catalog, String schema, String table);
/** Per
* {@link DatabaseMetaData#getCrossReference(String, String, String, String, String, String)}. */
MetaResultSet getCrossReference(ConnectionHandle ch,
String parentCatalog,
String parentSchema,
String parentTable,
String foreignCatalog,
String foreignSchema,
String foreignTable);
/** Per {@link DatabaseMetaData#getTypeInfo()}. */
MetaResultSet getTypeInfo(ConnectionHandle ch);
/** Per {@link DatabaseMetaData#getIndexInfo(String, String, String, boolean, boolean)}. */
MetaResultSet getIndexInfo(ConnectionHandle ch, String catalog,
String schema,
String table,
boolean unique,
boolean approximate);
/** Per {@link DatabaseMetaData#getUDTs(String, String, String, int[])}. */
MetaResultSet getUDTs(ConnectionHandle ch,
String catalog,
Pat schemaPattern,
Pat typeNamePattern,
int[] types);
/** Per {@link DatabaseMetaData#getSuperTypes(String, String, String)}. */
MetaResultSet getSuperTypes(ConnectionHandle ch,
String catalog,
Pat schemaPattern,
Pat typeNamePattern);
/** Per {@link DatabaseMetaData#getSuperTables(String, String, String)}. */
MetaResultSet getSuperTables(ConnectionHandle ch,
String catalog,
Pat schemaPattern,
Pat tableNamePattern);
/** Per {@link DatabaseMetaData#getAttributes(String, String, String, String)}. */
MetaResultSet getAttributes(ConnectionHandle ch,
String catalog,
Pat schemaPattern,
Pat typeNamePattern,
Pat attributeNamePattern);
/** Per {@link DatabaseMetaData#getClientInfoProperties()}. */
MetaResultSet getClientInfoProperties(ConnectionHandle ch);
/** Per {@link DatabaseMetaData#getFunctions(String, String, String)}. */
MetaResultSet getFunctions(ConnectionHandle ch,
String catalog,
Pat schemaPattern,
Pat functionNamePattern);
/** Per {@link DatabaseMetaData#getFunctionColumns(String, String, String, String)}. */
MetaResultSet getFunctionColumns(ConnectionHandle ch,
String catalog,
Pat schemaPattern,
Pat functionNamePattern,
Pat columnNamePattern);
/** Per {@link DatabaseMetaData#getPseudoColumns(String, String, String, String)}. */
MetaResultSet getPseudoColumns(ConnectionHandle ch,
String catalog,
Pat schemaPattern,
Pat tableNamePattern,
Pat columnNamePattern);
/** Creates an iterable for a result set.
*
* <p>The default implementation just returns {@code iterable}, which it
* requires to be not null; derived classes may instead choose to execute the
* relational expression in {@code signature}. */
Iterable<Object> createIterable(StatementHandle stmt, QueryState state, Signature signature,
List<TypedValue> parameters, Frame firstFrame);
/** Prepares a statement.
*
* @param ch Connection handle
* @param sql SQL query
* @param maxRowCount Negative for no limit (different meaning than JDBC)
* @return Signature of prepared statement
*/
StatementHandle prepare(ConnectionHandle ch, String sql, long maxRowCount);
/** Prepares and executes a statement.
*
* @param h Statement handle
* @param sql SQL query
* @param maxRowCount Negative for no limit (different meaning than JDBC)
* @param callback Callback to lock, clear and assign cursor
*
* @return Result containing statement ID, and if a query, a result set and
* first frame of data
* @deprecated See {@link #prepareAndExecute(StatementHandle, String, long, int, PrepareCallback)}
*/
@Deprecated // to be removed before 2.0
ExecuteResult prepareAndExecute(StatementHandle h, String sql,
long maxRowCount, PrepareCallback callback) throws NoSuchStatementException;
/** Prepares and executes a statement.
*
* @param h Statement handle
* @param sql SQL query
* @param maxRowCount Maximum number of rows for the entire query. Negative for no limit
* (different meaning than JDBC).
* @param maxRowsInFirstFrame Maximum number of rows for the first frame. This value should
* always be less than or equal to {@code maxRowCount} as the number of results are guaranteed
* to be restricted by {@code maxRowCount} and the underlying database.
* @param callback Callback to lock, clear and assign cursor
*
* @return Result containing statement ID, and if a query, a result set and
* first frame of data
*/
ExecuteResult prepareAndExecute(StatementHandle h, String sql,
long maxRowCount, int maxRowsInFirstFrame, PrepareCallback callback)
throws NoSuchStatementException;
/** Prepares a statement and then executes a number of SQL commands in one pass.
*
* @param h Statement handle
* @param sqlCommands SQL commands to run
* @return An array of update counts containing one element for each command in the batch.
*/
ExecuteBatchResult prepareAndExecuteBatch(StatementHandle h, List<String> sqlCommands)
throws NoSuchStatementException;
/** Executes a collection of bound parameter values on a prepared statement.
*
* @param h Statement handle
* @param parameterValues A collection of list of typed values, one list per batch
* @return An array of update counts containing one element for each command in the batch.
*/
ExecuteBatchResult executeBatch(StatementHandle h, List<List<TypedValue>> parameterValues)
throws NoSuchStatementException;
/** Returns a frame of rows.
*
* <p>The frame describes whether there may be another frame. If there is not
* another frame, the current iteration is done when we have finished the
* rows in the this frame.
*
* <p>The default implementation always returns null.
*
* @param h Statement handle
* @param offset Zero-based offset of first row in the requested frame
* @param fetchMaxRowCount Maximum number of rows to return; negative means
* no limit
* @return Frame, or null if there are no more
*/
Frame fetch(StatementHandle h, long offset, int fetchMaxRowCount) throws
NoSuchStatementException, MissingResultsException;
/** Executes a prepared statement.
*
* @param h Statement handle
* @param parameterValues A list of parameter values; may be empty, not null
* @param maxRowCount Maximum number of rows to return; negative means
* no limit
* @return Execute result
* @deprecated See {@link #execute(StatementHandle, List, int)}
*/
@Deprecated // to be removed before 2.0
ExecuteResult execute(StatementHandle h, List<TypedValue> parameterValues,
long maxRowCount) throws NoSuchStatementException;
/** Executes a prepared statement.
*
* @param h Statement handle
* @param parameterValues A list of parameter values; may be empty, not null
* @param maxRowsInFirstFrame Maximum number of rows to return in the Frame.
* @return Execute result
*/
ExecuteResult execute(StatementHandle h, List<TypedValue> parameterValues,
int maxRowsInFirstFrame) throws NoSuchStatementException;
/** Called during the creation of a statement to allocate a new handle.
*
* @param ch Connection handle
*/
StatementHandle createStatement(ConnectionHandle ch);
/** Closes a statement.
*
* <p>If the statement handle is not known, or is already closed, does
* nothing.
*
* @param h Statement handle
*/
void closeStatement(StatementHandle h);
/**
* Opens (creates) a connection. The client allocates its own connection ID which the server is
* then made aware of through the {@link ConnectionHandle}. The Map {@code info} argument is
* analogous to the {@link Properties} typically passed to a "normal" JDBC Driver. Avatica
* specific properties should not be included -- only properties for the underlying driver.
*
* @param ch A ConnectionHandle encapsulates information about the connection to be opened
* as provided by the client.
* @param info A Map corresponding to the Properties typically passed to a JDBC Driver.
*/
void openConnection(ConnectionHandle ch, Map<String, String> info);
/** Closes a connection */
void closeConnection(ConnectionHandle ch);
/**
* Re-sets the {@link ResultSet} on a Statement. Not a JDBC method.
*
* @return True if there are results to fetch after resetting to the given offset. False otherwise
*/
boolean syncResults(StatementHandle sh, QueryState state, long offset)
throws NoSuchStatementException;
/**
* Makes all changes since the last commit/rollback permanent. Analogous to
* {@link Connection#commit()}.
*
* @param ch A reference to the real JDBC Connection
*/
void commit(ConnectionHandle ch);
/**
* Undoes all changes since the last commit/rollback. Analogous to
* {@link Connection#rollback()};
*
* @param ch A reference to the real JDBC Connection
*/
void rollback(ConnectionHandle ch);
/** Synchronizes client and server view of connection properties.
*
* <p>Note: this interface is considered "experimental" and may undergo further changes as this
* functionality is extended to other aspects of state management for
* {@link java.sql.Connection}, {@link java.sql.Statement}, and {@link java.sql.ResultSet}.</p>
*/
ConnectionProperties connectionSync(ConnectionHandle ch, ConnectionProperties connProps);
/** Factory to create instances of {@link Meta}. */
interface Factory {
Meta create(List<String> args);
}
/** Wrapper to remind API calls that a parameter is a pattern (allows '%' and
* '_' wildcards, per the JDBC spec) rather than a string to be matched
* exactly. */
class Pat {
public final String s;
private Pat(String s) {
this.s = s;
}
@Override public String toString() {
return "Pat[" + s + "]";
}
@JsonCreator
public static Pat of(@JsonProperty("s") String name) {
return new Pat(name);
}
}
/** Database property.
*
* <p>Values exist for methods, such as
* {@link DatabaseMetaData#getSQLKeywords()}, which always return the same
* value at all times and across connections.
*
* @see #getDatabaseProperties(Meta.ConnectionHandle)
*/
enum DatabaseProperty {
/** Database property containing the value of
* {@link DatabaseMetaData#getNumericFunctions()}. */
GET_NUMERIC_FUNCTIONS(""),
/** Database property containing the value of
* {@link DatabaseMetaData#getStringFunctions()}. */
GET_STRING_FUNCTIONS(""),
/** Database property containing the value of
* {@link DatabaseMetaData#getSystemFunctions()}. */
GET_SYSTEM_FUNCTIONS(""),
/** Database property containing the value of
* {@link DatabaseMetaData#getTimeDateFunctions()}. */
GET_TIME_DATE_FUNCTIONS(""),
/** Database property containing the value of
* {@link DatabaseMetaData#getSQLKeywords()}. */
GET_S_Q_L_KEYWORDS(""),
/** Database property containing the value of
* {@link DatabaseMetaData#getDefaultTransactionIsolation()}. */
GET_DEFAULT_TRANSACTION_ISOLATION(Connection.TRANSACTION_NONE),
/** Database property which is the Avatica version */
AVATICA_VERSION(FilteredConstants.VERSION),
/** Database property containing the value of
* {@link DatabaseMetaData#getDriverVersion()}. */
GET_DRIVER_VERSION(""),
/** Database property containing the value of
* {@link DatabaseMetaData#getDriverMinorVersion()}. */
GET_DRIVER_MINOR_VERSION(-1),
/** Database property containing the value of
* {@link DatabaseMetaData#getDriverMajorVersion()}. */
GET_DRIVER_MAJOR_VERSION(-1),
/** Database property containing the value of
* {@link DatabaseMetaData#getDriverName()}. */
GET_DRIVER_NAME(""),
/** Database property containing the value of
* {@link DatabaseMetaData#getDatabaseMinorVersion()}. */
GET_DATABASE_MINOR_VERSION(-1),
/** Database property containing the value of
* {@link DatabaseMetaData#getDatabaseMajorVersion()}. */
GET_DATABASE_MAJOR_VERSION(-1),
/** Database property containing the value of
* {@link DatabaseMetaData#getDatabaseProductName()}. */
GET_DATABASE_PRODUCT_NAME(""),
/** Database property containing the value of
* {@link DatabaseMetaData#getDatabaseProductVersion()}. */
GET_DATABASE_PRODUCT_VERSION("");
public final Class<?> type;
public final Object defaultValue;
public final Method method;
public final boolean isJdbc;
<T> DatabaseProperty(T defaultValue) {
this.defaultValue = defaultValue;
final String methodName = AvaticaUtils.toCamelCase(name());
Method localMethod = null;
try {
localMethod = DatabaseMetaData.class.getMethod(methodName);
} catch (NoSuchMethodException e) {
// Pass, localMethod stays null.
}
if (null == localMethod) {
this.method = null;
this.type = null;
this.isJdbc = false;
} else {
this.method = localMethod;
this.type = AvaticaUtils.box(method.getReturnType());
this.isJdbc = true;
}
// It's either: 1) not a JDBC method, 2) has no default value,
// 3) the defaultValue is of the expected type
assert !isJdbc || defaultValue == null || defaultValue.getClass() == type;
}
/** Returns a value of this property, using the default value if the map
* does not contain an explicit value. */
public <T> T getProp(Meta meta, ConnectionHandle ch, Class<T> aClass) {
return getProp(meta.getDatabaseProperties(ch), aClass);
}
/** Returns a value of this property, using the default value if the map
* does not contain an explicit value. */
public <T> T getProp(Map<DatabaseProperty, Object> map, Class<T> aClass) {
assert aClass == type;
Object v = map.get(this);
if (v == null) {
v = defaultValue;
}
return aClass.cast(v);
}
public static DatabaseProperty fromProto(Common.DatabaseProperty proto) {
return DatabaseProperty.valueOf(proto.getName());
}
public Common.DatabaseProperty toProto() {
return Common.DatabaseProperty.newBuilder().setName(name()).build();
}
}
/** Response from execute.
*
* <p>Typically a query will have a result set and rowCount = -1;
* a DML statement will have a rowCount and no result sets.
*/
class ExecuteResult {
public final List<MetaResultSet> resultSets;
public ExecuteResult(List<MetaResultSet> resultSets) {
this.resultSets = resultSets;
}
}
/**
* Response from a collection of SQL commands or parameter values in a single batch.
*/
class ExecuteBatchResult {
public final long[] updateCounts;
public ExecuteBatchResult(long[] updateCounts) {
this.updateCounts = Objects.requireNonNull(updateCounts);
}
}
/** Meta data from which a result set can be constructed.
*
* <p>If {@code updateCount} is not -1, the result is just a count. A result
* set cannot be constructed. */
class MetaResultSet {
public final String connectionId;
public final int statementId;
public final boolean ownStatement;
public final Frame firstFrame;
public final Signature signature;
public final long updateCount;
@Deprecated // to be removed before 2.0
protected MetaResultSet(String connectionId, int statementId,
boolean ownStatement, Signature signature, Frame firstFrame,
int updateCount) {
this(connectionId, statementId, ownStatement, signature, firstFrame,
(long) updateCount);
}
protected MetaResultSet(String connectionId, int statementId,
boolean ownStatement, Signature signature, Frame firstFrame,
long updateCount) {
this.signature = signature;
this.connectionId = connectionId;
this.statementId = statementId;
this.ownStatement = ownStatement;
this.firstFrame = firstFrame; // may be null even if signature is not null
this.updateCount = updateCount;
}
public static MetaResultSet create(String connectionId, int statementId,
boolean ownStatement, Signature signature, Frame firstFrame) {
return new MetaResultSet(connectionId, statementId, ownStatement,
Objects.requireNonNull(signature), firstFrame, -1L);
}
public static MetaResultSet create(String connectionId, int statementId,
boolean ownStatement, Signature signature, Frame firstFrame, long updateCount) {
return new MetaResultSet(connectionId, statementId, ownStatement,
Objects.requireNonNull(signature), firstFrame, updateCount);
}
public static MetaResultSet count(String connectionId, int statementId,
long updateCount) {
assert updateCount >= 0
: "Meta.count(" + connectionId + ", " + statementId + ", "
+ updateCount + ")";
return new MetaResultSet(connectionId, statementId, false, null, null,
updateCount);
}
}
/** Information necessary to convert an {@link Iterable} into a
* {@link org.apache.calcite.avatica.util.Cursor}. */
final class CursorFactory {
private static final FieldDescriptor CLASS_NAME_DESCRIPTOR = Common.CursorFactory.
getDescriptor().findFieldByNumber(Common.CursorFactory.CLASS_NAME_FIELD_NUMBER);
public final Style style;
public final Class clazz;
@JsonIgnore
public final List<Field> fields;
public final List<String> fieldNames;
private CursorFactory(Style style, Class clazz, List<Field> fields,
List<String> fieldNames) {
assert (fieldNames != null) == (style == Style.RECORD || style == Style.MAP);
assert (fields != null) == (style == Style.RECORD);
this.style = Objects.requireNonNull(style);
this.clazz = clazz;
this.fields = fields;
this.fieldNames = fieldNames;
}
@JsonCreator
public static CursorFactory create(@JsonProperty("style") Style style,
@JsonProperty("clazz") Class clazz,
@JsonProperty("fieldNames") List<String> fieldNames) {
switch (style) {
case OBJECT:
return OBJECT;
case ARRAY:
return ARRAY;
case LIST:
return LIST;
case RECORD:
return record(clazz, null, fieldNames);
case MAP:
return map(fieldNames);
default:
throw new AssertionError("unknown style: " + style);
}
}
public static final CursorFactory OBJECT =
new CursorFactory(Style.OBJECT, null, null, null);
public static final CursorFactory ARRAY =
new CursorFactory(Style.ARRAY, null, null, null);
public static final CursorFactory LIST =
new CursorFactory(Style.LIST, null, null, null);
/**
*
* @deprecated Use {@link #record(Class, List, List)}
*/
@Deprecated // to be removed before 1.19.0
public static CursorFactory record(Class resultClazz) {
List<Field> fields = Arrays.asList(resultClazz.getFields());
return new CursorFactory(Style.RECORD, resultClazz, fields, null);
}
public static CursorFactory record(Class resultClass, List<Field> fields,
List<String> fieldNames) {
if (fields == null) {
fields = new ArrayList<>();
for (String fieldName : fieldNames) {
try {
fields.add(resultClass.getField(fieldName));
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
}
return new CursorFactory(Style.RECORD, resultClass, fields, fieldNames);
}
public static CursorFactory map(List<String> fieldNames) {
return new CursorFactory(Style.MAP, null, null, fieldNames);
}
/**
* Deduces the appropriate {@code CursorFactory} for accessing the underlying
* result set. For result sets composed by records, {@code resultClazz} must
* be not null, and each field name in {@code columns} must match one of its
* public fields.
* @param columns The columns metadata for the result set
* @param resultClazz The class representing the records, if any
* @return the appropriate {@code CursorFactory} for the underlying result set
* @throws RuntimeException if the field name validation fails
*/
public static CursorFactory deduce(List<ColumnMetaData> columns,
Class resultClazz) {
if (columns.size() == 1) {
return OBJECT;
}
if (resultClazz == null) {
return ARRAY;
}
if (resultClazz.isArray()) {
return ARRAY;
}
if (List.class.isAssignableFrom(resultClazz)) {
return LIST;
}
return record(resultClazz, null,
columns.stream().map(c -> c.columnName).collect(Collectors.toList()));
}
public Common.CursorFactory toProto() {
Common.CursorFactory.Builder builder = Common.CursorFactory.newBuilder();
if (null != clazz) {
builder.setClassName(clazz.getName());
}
builder.setStyle(style.toProto());
if (null != fieldNames) {
builder.addAllFieldNames(fieldNames);
}
return builder.build();
}
public static CursorFactory fromProto(Common.CursorFactory proto) {
// Reconstruct CursorFactory
Class<?> clz = null;
if (proto.hasField(CLASS_NAME_DESCRIPTOR)) {
try {
clz = Class.forName(proto.getClassName());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
return CursorFactory.create(Style.fromProto(proto.getStyle()), clz,
proto.getFieldNamesList());
}
@Override public int hashCode() {
return Objects.hash(clazz, fieldNames, fields, style);
}
@Override public boolean equals(Object o) {
return o == this
|| o instanceof CursorFactory
&& Objects.equals(clazz, ((CursorFactory) o).clazz)
&& Objects.equals(fieldNames, ((CursorFactory) o).fieldNames)
&& Objects.equals(fields, ((CursorFactory) o).fields)
&& style == ((CursorFactory) o).style;
}
}
/** How logical fields are represented in the objects returned by the
* iterator. */
enum Style {
OBJECT,
RECORD,
ARRAY,
LIST,
MAP;
public Common.CursorFactory.Style toProto() {
return Common.CursorFactory.Style.valueOf(name());
}
public static Style fromProto(Common.CursorFactory.Style proto) {
return Style.valueOf(proto.name());
}
}
/** Result of preparing a statement. */
class Signature {
private static final FieldDescriptor SQL_DESCRIPTOR = Common.Signature
.getDescriptor().findFieldByNumber(Common.Signature.SQL_FIELD_NUMBER);
private static final FieldDescriptor CURSOR_FACTORY_DESCRIPTOR = Common.Signature
.getDescriptor().findFieldByNumber(Common.Signature.CURSOR_FACTORY_FIELD_NUMBER);
public final List<ColumnMetaData> columns;
public final String sql;
public final List<AvaticaParameter> parameters;
public final transient Map<String, Object> internalParameters;
public final CursorFactory cursorFactory;
public final Meta.StatementType statementType;
/** Creates a Signature. */
public Signature(List<ColumnMetaData> columns,
String sql,
List<AvaticaParameter> parameters,
Map<String, Object> internalParameters,
CursorFactory cursorFactory,
Meta.StatementType statementType) {
this.columns = columns;
this.sql = sql;
this.parameters = parameters;
this.internalParameters = internalParameters;
this.cursorFactory = cursorFactory;
this.statementType = statementType;
}
/** Used by Jackson to create a Signature by de-serializing JSON. */
@JsonCreator
public static Signature create(
@JsonProperty("columns") List<ColumnMetaData> columns,
@JsonProperty("sql") String sql,
@JsonProperty("parameters") List<AvaticaParameter> parameters,
@JsonProperty("cursorFactory") CursorFactory cursorFactory,
@JsonProperty("statementType") Meta.StatementType statementType) {
return new Signature(columns, sql, parameters,
Collections.<String, Object>emptyMap(), cursorFactory, statementType);
}
/** Returns a copy of this Signature, substituting given CursorFactory. */
public Signature setCursorFactory(CursorFactory cursorFactory) {
return new Signature(columns, sql, parameters, internalParameters,
cursorFactory, statementType);
}
/** Creates a copy of this Signature with null lists and maps converted to
* empty. */
public Signature sanitize() {
if (columns == null || parameters == null || internalParameters == null
|| statementType == null) {
return new Signature(sanitize(columns), sql, sanitize(parameters),
sanitize(internalParameters), cursorFactory,
Meta.StatementType.SELECT);
}
return this;
}
private <E> List<E> sanitize(List<E> list) {
return list == null ? Collections.<E>emptyList() : list;
}
private <K, V> Map<K, V> sanitize(Map<K, V> map) {
return map == null ? Collections.<K, V>emptyMap() : map;
}
public Common.Signature toProto() {
Common.Signature.Builder builder = Common.Signature.newBuilder();
if (null != sql) {
builder.setSql(sql);
}
if (null != cursorFactory) {
builder.setCursorFactory(cursorFactory.toProto());
}
if (null != columns) {
for (ColumnMetaData column : columns) {
builder.addColumns(column.toProto());
}
}
if (null != parameters) {
for (AvaticaParameter parameter : parameters) {
builder.addParameters(parameter.toProto());
}
}
return builder.build();
}
public static Signature fromProto(Common.Signature protoSignature) {
List<ColumnMetaData> metadata = new ArrayList<>(protoSignature.getColumnsCount());
for (Common.ColumnMetaData protoMetadata : protoSignature.getColumnsList()) {
metadata.add(ColumnMetaData.fromProto(protoMetadata));
}
List<AvaticaParameter> parameters = new ArrayList<>(protoSignature.getParametersCount());
for (Common.AvaticaParameter protoParam : protoSignature.getParametersList()) {
parameters.add(AvaticaParameter.fromProto(protoParam));
}
String sql = null;
if (protoSignature.hasField(SQL_DESCRIPTOR)) {
sql = protoSignature.getSql();
}
CursorFactory cursorFactory = null;
if (protoSignature.hasField(CURSOR_FACTORY_DESCRIPTOR)) {
cursorFactory = CursorFactory.fromProto(protoSignature.getCursorFactory());
}
final Meta.StatementType statementType =
Meta.StatementType.fromProto(protoSignature.getStatementType());
return Signature.create(metadata, sql, parameters, cursorFactory, statementType);
}
@Override public int hashCode() {
return Objects.hash(columns, cursorFactory, parameters, sql);
}
@Override public boolean equals(Object o) {
return o == this
|| o instanceof Signature
&& Objects.equals(columns, ((Signature) o).columns)
&& Objects.equals(cursorFactory, ((Signature) o).cursorFactory)
&& Objects.equals(parameters, ((Signature) o).parameters)
&& Objects.equals(sql, ((Signature) o).sql);
}
}
/** A collection of rows. */
class Frame {
private static final FieldDescriptor HAS_ARRAY_VALUE_DESCRIPTOR = Common.ColumnValue
.getDescriptor().findFieldByNumber(Common.ColumnValue.HAS_ARRAY_VALUE_FIELD_NUMBER);
private static final FieldDescriptor SCALAR_VALUE_DESCRIPTOR = Common.ColumnValue
.getDescriptor().findFieldByNumber(Common.ColumnValue.SCALAR_VALUE_FIELD_NUMBER);
/** Frame that has zero rows and is the last frame. */
public static final Frame EMPTY =
new Frame(0, true, Collections.emptyList());
/** Frame that has zero rows but may have another frame. */
public static final Frame MORE =
new Frame(0, false, Collections.emptyList());
/** Zero-based offset of first row. */
public final long offset;
/** Whether this is definitely the last frame of rows.
* If true, there are no more rows.
* If false, there may or may not be more rows. */
public final boolean done;
/** The rows. */
public final Iterable<Object> rows;
public Frame(long offset, boolean done, Iterable<Object> rows) {
this.offset = offset;
this.done = done;
this.rows = rows;
}
@JsonCreator
public static Frame create(@JsonProperty("offset") long offset,
@JsonProperty("done") boolean done,
@JsonProperty("rows") List<Object> rows) {
if (offset == 0 && done && rows.isEmpty()) {
return EMPTY;
}
return new Frame(offset, done, rows);
}
public Common.Frame toProto() {
Common.Frame.Builder builder = Common.Frame.newBuilder();
builder.setDone(done).setOffset(offset);
for (Object row : this.rows) {
if (null == row) {
// Does this need to be persisted for some reason?
continue;
}
final Common.Row.Builder rowBuilder = Common.Row.newBuilder();
if (row instanceof Object[]) {
// If only Object[] was also Iterable.
for (Object element : (Object[]) row) {
parseColumn(rowBuilder, element);
}
} else if (row instanceof Iterable) {
for (Object element : (Iterable<?>) row) {
parseColumn(rowBuilder, element);
}
} else {
// Can a "row" be a primitive? A struct? Only an Array?
throw new RuntimeException("Only arrays are supported");
}
// Collect all rows
builder.addRows(rowBuilder.build());
}
return builder.build();
}
static void parseColumn(Common.Row.Builder rowBuilder, Object column) {
final Common.ColumnValue.Builder columnBuilder = Common.ColumnValue.newBuilder();
if (column instanceof List) {
columnBuilder.setHasArrayValue(true);
List<?> list = (List<?>) column;
// Add each element in the list/array to the column's value
for (Object listItem : list) {
final Common.TypedValue scalarListItem = serializeScalar(listItem);
columnBuilder.addArrayValue(scalarListItem);
// Add the deprecated 'value' repeated attribute for backwards compat
columnBuilder.addValue(scalarListItem);
}
} else {
// The default value, but still explicit.
columnBuilder.setHasArrayValue(false);
// Only one value for this column, a scalar.
final Common.TypedValue scalarVal = serializeScalar(column);
columnBuilder.setScalarValue(scalarVal);
// Add the deprecated 'value' repeated attribute for backwards compat
columnBuilder.addValue(scalarVal);
}
// Add value to row
rowBuilder.addValue(columnBuilder.build());
}
static Common.TypedValue serializeScalar(Object element) {
final Common.TypedValue.Builder valueBuilder = Common.TypedValue.newBuilder();
// Let TypedValue handle the serialization for us.
TypedValue.toProto(valueBuilder, element);
return valueBuilder.build();
}
public static Frame fromProto(Common.Frame proto) {
List<Object> parsedRows = new ArrayList<>(proto.getRowsCount());
for (Common.Row protoRow : proto.getRowsList()) {
ArrayList<Object> row = new ArrayList<>(protoRow.getValueCount());
for (Common.ColumnValue protoColumn : protoRow.getValueList()) {
final Object value;
if (!isNewStyleColumn(protoColumn)) {
// Backward compatibility
value = parseOldStyleColumn(protoColumn);
} else {
// Current style parsing (separate scalar and array values)
value = parseColumn(protoColumn);
}
row.add(value);
}
parsedRows.add(row);
}
return new Frame(proto.getOffset(), proto.getDone(), parsedRows);
}
/**
* Determines whether this message contains the new attributes in the
* message. We can't directly test for the negative because our
* {@code hasField} trick does not work on repeated fields.
*
* @param column The protobuf column object
* @return True if the message is the new style, false otherwise.
*/
static boolean isNewStyleColumn(Common.ColumnValue column) {
return column.hasField(HAS_ARRAY_VALUE_DESCRIPTOR)
|| column.hasField(SCALAR_VALUE_DESCRIPTOR);
}
/**
* For Calcite 1.5, we made the mistake of using array length to determine when the value for a
* column is a scalar or an array. This method performs the old parsing for backwards
* compatibility.
*
* @param column The protobuf ColumnValue object
* @return The parsed value for this column
*/
static Object parseOldStyleColumn(Common.ColumnValue column) {
if (column.getValueCount() > 1) {
List<Object> array = new ArrayList<>(column.getValueCount());
for (Common.TypedValue columnValue : column.getValueList()) {
array.add(deserializeScalarValue(columnValue));
}
return array;
} else {
return deserializeScalarValue(column.getValue(0));
}
}
/**
* Parses the value for a ColumnValue using the separated array and scalar attributes.
*
* @param column The protobuf ColumnValue object
* @return The parse value for this column
*/
static Object parseColumn(Common.ColumnValue column) {
// Verify that we have one or the other (scalar or array)
validateColumnValue(column);
if (!column.hasField(SCALAR_VALUE_DESCRIPTOR)) {
// The column in this row is an Array (has multiple values)
List<Object> array = new ArrayList<>(column.getArrayValueCount());
for (Common.TypedValue arrayValue : column.getArrayValueList()) {
// Duplicative because of the ColumnValue/TypedValue difference.
if (Common.Rep.ARRAY == arrayValue.getType()) {
// Each element in this Array is an Array.
array.add(parseArray(arrayValue));
} else {
// The array element is a scalar.
array.add(deserializeScalarValue(arrayValue));
}
}
return array;
} else {
// Scalar
return deserializeScalarValue(column.getScalarValue());
}
}
/**
* Recursively parses a TypedValue while it is an array.
*/
static Object parseArray(Common.TypedValue array) {
List<Object> convertedArray = new ArrayList<>(array.getArrayValueCount());
for (Common.TypedValue arrayElement : array.getArrayValueList()) {
if (Common.Rep.ARRAY == arrayElement.getType()) {
// Recurse
convertedArray.add(parseArray(arrayElement));
} else {
// The component type of this array is a scalar.
convertedArray.add(deserializeScalarValue(arrayElement));
}
}
return convertedArray;
}
/**
* Verifies that a ColumnValue has only a scalar or array value, not both and not neither.
*
* @param column The protobuf ColumnValue object
* @throws IllegalArgumentException When the above condition is not met
*/
static void validateColumnValue(Common.ColumnValue column) {
final boolean hasScalar = column.hasField(SCALAR_VALUE_DESCRIPTOR);
final boolean hasArrayValue = column.getHasArrayValue();
// These should always be different
if (hasScalar == hasArrayValue) {
throw new IllegalArgumentException("A column must have a scalar or array value, not "
+ (hasScalar ? "both" : "neither"));
}
}
static Object deserializeScalarValue(Common.TypedValue protoElement) {
// ByteString is a single case where TypedValue is representing the data differently
// (in its "local" form) than Frame does. We need to unwrap the Base64 encoding.
if (Common.Rep.BYTE_STRING == protoElement.getType()) {
// Protobuf is sending native bytes (not b64) across the wire. B64 bytes is only for
// TypedValue's benefit
return protoElement.getBytesValue().toByteArray();
}
// Again, let TypedValue deserialize things for us.
return TypedValue.fromProto(protoElement).value;
}
@Override public int hashCode() {
return Objects.hash(done, offset, rows);
}
@Override public boolean equals(Object o) {
return o == this
|| o instanceof Frame
&& equalRows(rows, ((Frame) o).rows)
&& offset == ((Frame) o).offset
&& done == ((Frame) o).done;
}
private static boolean equalRows(Iterable<Object> rows, Iterable<Object> otherRows) {
if (null == rows) {
if (null != otherRows) {
return false;
}
} else {
Iterator<Object> iter1 = rows.iterator();
Iterator<Object> iter2 = otherRows.iterator();
while (iter1.hasNext() && iter2.hasNext()) {
Object obj1 = iter1.next();
Object obj2 = iter2.next();
// Can't just call equals on an array
if (obj1 instanceof Object[]) {
if (obj2 instanceof Object[]) {
// Compare array and array
if (!Arrays.equals((Object[]) obj1, (Object[]) obj2)) {
return false;
}
} else if (obj2 instanceof List) {
// compare array and list
@SuppressWarnings("unchecked")
List<Object> obj2List = (List<Object>) obj2;
if (!Arrays.equals((Object[]) obj1, obj2List.toArray())) {
return false;
}
} else {
// compare array and something that isn't an array will always fail
return false;
}
} else if (obj1 instanceof List) {
if (obj2 instanceof Object[]) {
// Compare list and array
@SuppressWarnings("unchecked")
List<Object> obj1List = (List<Object>) obj1;
if (!Arrays.equals(obj1List.toArray(), (Object[]) obj2)) {
return false;
}
} else if (!obj1.equals(obj2)) {
// compare list and something else, let it fall to equals()
return false;
}
} else if (!obj1.equals(obj2)) {
// Not an array, leave it to equals()
return false;
}
}
// More elements in one of the iterables
if (iter1.hasNext() || iter2.hasNext()) {
return false;
}
}
return true;
}
}
/** Connection handle. */
class ConnectionHandle {
public final String id;
@Override public String toString() {
return id;
}
@JsonCreator
public ConnectionHandle(@JsonProperty("id") String id) {
this.id = id;
}
}
/** Statement handle. */
// Visible for testing
class StatementHandle {
private static final FieldDescriptor SIGNATURE_DESCRIPTOR = Common.StatementHandle
.getDescriptor().findFieldByNumber(Common.StatementHandle.SIGNATURE_FIELD_NUMBER);
public final String connectionId;
public final int id;
// not final because LocalService#apply(PrepareRequest)
/** Only present for PreparedStatement handles, null otherwise. */
public Signature signature;
@Override public String toString() {
return connectionId + "::" + Integer.toString(id);
}
@JsonCreator
public StatementHandle(
@JsonProperty("connectionId") String connectionId,
@JsonProperty("id") int id,
@JsonProperty("signature") Signature signature) {
this.connectionId = connectionId;
this.id = id;
this.signature = signature;
}
public Common.StatementHandle toProto() {
Common.StatementHandle.Builder builder = Common.StatementHandle.newBuilder()
.setConnectionId(connectionId).setId(id);
if (null != signature) {
builder.setSignature(signature.toProto());
}
return builder.build();
}
public static StatementHandle fromProto(Common.StatementHandle protoHandle) {
// Signature is optional in the update path for executes.
Signature signature = null;
if (protoHandle.hasField(SIGNATURE_DESCRIPTOR)) {
signature = Signature.fromProto(protoHandle.getSignature());
}
return new StatementHandle(protoHandle.getConnectionId(), protoHandle.getId(), signature);
}
@Override public int hashCode() {
return Objects.hash(connectionId, id, signature);
}
@Override public boolean equals(Object o) {
return o == this
|| o instanceof StatementHandle
&& Objects.equals(connectionId, ((StatementHandle) o).connectionId)
&& Objects.equals(signature, ((StatementHandle) o).signature)
&& id == ((StatementHandle) o).id;
}
}
/** A pojo containing various client-settable {@link java.sql.Connection} properties.
*
* <p>{@code java.lang} types are used here so that {@code null} can be used to indicate
* a value has no been set.</p>
*
* <p>Note: this interface is considered "experimental" and may undergo further changes as this
* functionality is extended to other aspects of state management for
* {@link java.sql.Connection}, {@link java.sql.Statement}, and {@link java.sql.ResultSet}.</p>
*/
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "connProps",
defaultImpl = ConnectionPropertiesImpl.class)
@JsonSubTypes({
@JsonSubTypes.Type(value = ConnectionPropertiesImpl.class, name = "connPropsImpl")
})
interface ConnectionProperties {
/** Overwrite fields in {@code this} with any non-null fields in {@code that}
*
* @return {@code this}
*/
ConnectionProperties merge(ConnectionProperties that);
/** @return {@code true} when no properties have been set, {@code false} otherwise. */
@JsonIgnore
boolean isEmpty();
/** Set {@code autoCommit} status.
*
* @return {@code this}
*/
ConnectionProperties setAutoCommit(boolean val);
Boolean isAutoCommit();
/** Set {@code readOnly} status.
*
* @return {@code this}
*/
ConnectionProperties setReadOnly(boolean val);
Boolean isReadOnly();
/** Set {@code transactionIsolation} status.
*
* @return {@code this}
*/
ConnectionProperties setTransactionIsolation(int val);
Integer getTransactionIsolation();
/** Set {@code catalog}.
*
* @return {@code this}
*/
ConnectionProperties setCatalog(String val);
String getCatalog();
/** Set {@code schema}.
*
* @return {@code this}
*/
ConnectionProperties setSchema(String val);
String getSchema();
Common.ConnectionProperties toProto();
}
/** API to put a result set into a statement, being careful to enforce
* thread-safety and not to overwrite existing open result sets. */
interface PrepareCallback {
Object getMonitor();
void clear() throws SQLException;
void assign(Signature signature, Frame firstFrame, long updateCount)
throws SQLException;
void execute() throws SQLException;
}
/** Type of statement. */
enum StatementType {
SELECT, INSERT, UPDATE, DELETE, UPSERT, MERGE, OTHER_DML, IS_DML,
CREATE, DROP, ALTER, OTHER_DDL, CALL;
public boolean canUpdate() {
switch (this) {
case INSERT:
return true;
case IS_DML:
return true;
default:
return false;
}
}
public Common.StatementType toProto() {
return Common.StatementType.valueOf(name());
}
public static StatementType fromProto(Common.StatementType proto) {
return StatementType.valueOf(proto.name());
}
}
}
// End Meta.java