SQL: Prefer resultSets over exceptions in metadata (#40641)

Changed the JDBC metadata to return empty results sets instead of
throwing SQLFeatureNotSupported as it seems a more safer/compatible
approach for consumers.

Fix #40533

(cherry picked from commit ef2d2527c2b5140556fd477e7ff6ea36966684da)
This commit is contained in:
Costin Leau 2019-04-08 16:45:25 +03:00 committed by Costin Leau
parent 3e078b2026
commit af874635ba
7 changed files with 273 additions and 42 deletions

View File

@ -162,7 +162,7 @@ class JdbcConfiguration extends ConnectionConfiguration {
}
@Override
protected Collection<? extends String> extraOptions() {
protected Collection<String> extraOptions() {
return OPTION_NAMES;
}
@ -192,9 +192,8 @@ class JdbcConfiguration extends ConnectionConfiguration {
public DriverPropertyInfo[] driverPropertyInfo() {
List<DriverPropertyInfo> info = new ArrayList<>();
for (String option : OPTION_NAMES) {
String value = null;
DriverPropertyInfo prop = new DriverPropertyInfo(option, value);
for (String option : optionNames()) {
DriverPropertyInfo prop = new DriverPropertyInfo(option, null);
info.add(prop);
}

View File

@ -45,9 +45,12 @@ class JdbcConnection implements Connection, JdbcWrapper {
* If we remove it, we need to make sure no other types of Exceptions (runtime or otherwise) are thrown
*/
JdbcConnection(JdbcConfiguration connectionInfo) throws SQLException {
cfg = connectionInfo;
client = new JdbcHttpClient(connectionInfo);
this(connectionInfo, true);
}
JdbcConnection(JdbcConfiguration connectionInfo, boolean checkServer) throws SQLException {
cfg = connectionInfo;
client = new JdbcHttpClient(connectionInfo, checkServer);
url = connectionInfo.connectionString();
userName = connectionInfo.authUser();
}

View File

@ -10,15 +10,17 @@ import org.elasticsearch.xpack.sql.client.Version;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverPropertyInfo;
import java.sql.JDBCType;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.RowIdLifetime;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.ArrayList;
import java.util.List;
import static java.sql.JDBCType.BIGINT;
import static java.sql.JDBCType.BOOLEAN;
import static java.sql.JDBCType.INTEGER;
import static java.sql.JDBCType.SMALLINT;
import static org.elasticsearch.xpack.sql.client.StringUtils.EMPTY;
@ -209,7 +211,7 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
@Override
public String getSystemFunctions() throws SQLException {
// https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/system-functions?view=sql-server-2017
return "DATABASE, IFNULL, USER";
return "DATABASE,IFNULL,USER";
}
@Override
@ -663,8 +665,7 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
// https://www.postgresql.org/docs/9.0/static/infoschema-routines.html
@Override
public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) throws SQLException {
return emptySet(con.cfg,
"ROUTINES",
return emptySet(con.cfg, "ROUTINES",
"PROCEDURE_CAT",
"PROCEDURE_SCHEM",
"PROCEDURE_NAME",
@ -679,8 +680,7 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
@Override
public ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern)
throws SQLException {
return emptySet(con.cfg,
"PARAMETERS",
return emptySet(con.cfg, "ROUTINES_COLUMNS",
"PROCEDURE_CAT",
"PROCEDURE_SCHEM",
"PROCEDURE_NAME",
@ -774,14 +774,14 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
public ResultSet getCatalogs() throws SQLException {
// TABLE_CAT is the first column
Object[][] data = queryColumn(con, "SYS TABLES CATALOG LIKE '%' LIKE ''", 1);
return memorySet(con.cfg, columnInfo("", "TABLE_CAT"), data);
return memorySet(con.cfg, columnInfo("CATALOGS", "TABLE_CAT"), data);
}
@Override
public ResultSet getTableTypes() throws SQLException {
// TABLE_TYPE (4)
Object[][] data = queryColumn(con, "SYS TABLES CATALOG LIKE '' LIKE '' TYPE '%'", 4);
return memorySet(con.cfg, columnInfo("", "TABLE_TYPE"), data);
return memorySet(con.cfg, columnInfo("TABLE_TYPES", "TABLE_TYPE"), data);
}
@Override
@ -797,43 +797,128 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
@Override
public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) throws SQLException {
throw new SQLFeatureNotSupportedException("Privileges not supported");
return emptySet(con.cfg, "",
"TABLE_CAT",
"TABLE_SCHEM",
"TABLE_NAME",
"COLUMN_NAME",
"GRANTOR",
"GRANTEE",
"PRIVILEGE",
"IS_GRANTABLE");
}
@Override
public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) throws SQLException {
throw new SQLFeatureNotSupportedException("Privileges not supported");
return emptySet(con.cfg, "",
"TABLE_CAT",
"TABLE_SCHEM",
"TABLE_NAME",
"GRANTOR",
"GRANTEE",
"PRIVILEGE",
"IS_GRANTABLE");
}
@Override
public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) throws SQLException {
throw new SQLFeatureNotSupportedException("Row identifiers not supported");
return emptySet(con.cfg, "",
"SCOPE", SMALLINT,
"COLUMN_NAME",
"DATA_TYPE", INTEGER,
"TYPE_NAME",
"COLUMN_SIZE", INTEGER,
"BUFFER_LENGTH", INTEGER,
"DECIMAL_DIGITS", SMALLINT,
"PSEUDO_COLUMN", SMALLINT);
}
@Override
public ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException {
throw new SQLFeatureNotSupportedException("Version column not supported yet");
return emptySet(con.cfg, "",
"SCOPE", SMALLINT,
"COLUMN_NAME",
"DATA_TYPE", INTEGER,
"TYPE_NAME",
"COLUMN_SIZE", INTEGER,
"BUFFER_LENGTH", INTEGER,
"DECIMAL_DIGITS", SMALLINT,
"PSEUDO_COLUMN", SMALLINT);
}
@Override
public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException {
throw new SQLFeatureNotSupportedException("Primary keys not supported");
return emptySet(con.cfg, "",
"TABLE_CAT",
"TABLE_SCHEM",
"TABLE_NAME",
"COLUMN_NAME",
"KEY_SEQ", SMALLINT,
"PK_NAME");
}
@Override
public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException {
throw new SQLFeatureNotSupportedException("Imported keys not supported");
return emptySet(con.cfg, "",
"PKTABLE_CAT",
"PKTABLE_SCHEM",
"PKTABLE_NAME",
"PKCOLUMN_NAME",
"FKTABLE_CAT",
"FKTABLE_SCHEM",
"FKTABLE_NAME",
"FKCOLUMN_NAME",
"KEY_SEQ", SMALLINT,
"UPDATE_RULE ", SMALLINT,
"DELETE_RULE ", SMALLINT,
"FK_NAME",
"PK_NAME ",
"DEFERRABILITY", SMALLINT,
"IS_NULLABLE"
);
}
@Override
public ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException {
throw new SQLFeatureNotSupportedException("Exported keys not supported");
return emptySet(con.cfg, "",
"PKTABLE_CAT",
"PKTABLE_SCHEM",
"PKTABLE_NAME",
"PKCOLUMN_NAME",
"FKTABLE_CAT",
"FKTABLE_SCHEM",
"FKTABLE_NAME",
"FKCOLUMN_NAME",
"KEY_SEQ", SMALLINT,
"UPDATE_RULE ", SMALLINT,
"DELETE_RULE ", SMALLINT,
"FK_NAME",
"PK_NAME ",
"DEFERRABILITY", SMALLINT,
"IS_NULLABLE"
);
}
@Override
public ResultSet getCrossReference(String parentCatalog, String parentSchema, String parentTable, String foreignCatalog,
String foreignSchema, String foreignTable) throws SQLException {
throw new SQLFeatureNotSupportedException("Cross reference not supported");
return emptySet(con.cfg, "",
"PKTABLE_CAT",
"PKTABLE_SCHEM",
"PKTABLE_NAME",
"PKCOLUMN_NAME",
"FKTABLE_CAT",
"FKTABLE_SCHEM",
"FKTABLE_NAME",
"FKCOLUMN_NAME",
"KEY_SEQ", SMALLINT,
"UPDATE_RULE ", SMALLINT,
"DELETE_RULE ", SMALLINT,
"FK_NAME",
"PK_NAME ",
"DEFERRABILITY", SMALLINT,
"IS_NULLABLE"
);
}
@Override
@ -843,7 +928,22 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
@Override
public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) throws SQLException {
throw new SQLFeatureNotSupportedException("Indicies not supported");
return emptySet(con.cfg, "",
"TABLE_CAT",
"TABLE_SCHEM",
"TABLE_NAME",
"NON_UNIQUE", BOOLEAN,
"INDEX_QUALIFIER",
"INDEX_NAME",
"TYPE", SMALLINT,
"ORDINAL_POSITION", SMALLINT,
"COLUMN_NAME",
"ASC_OR_DESC",
"CARDINALITY", BIGINT,
"PAGES", BIGINT,
"FILTER_CONDITION",
"TYPE_NAME"
);
}
@Override
@ -908,7 +1008,7 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
@Override
public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) throws SQLException {
return emptySet(con.cfg,
return emptySet(con.cfg, "",
"USER_DEFINED_TYPES",
"TYPE_CAT",
"TYPE_SCHEM",
@ -946,7 +1046,7 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
@Override
public ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern) throws SQLException {
return emptySet(con.cfg,
return emptySet(con.cfg, "",
"SUPER_TYPES",
"TYPE_CAT",
"TYPE_SCHEM",
@ -959,7 +1059,7 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
@Override
public ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern) throws SQLException {
return emptySet(con.cfg, "SUPER_TABLES",
return emptySet(con.cfg, "",
"TABLE_CAT",
"TABLE_SCHEM",
"TABLE_NAME",
@ -969,7 +1069,7 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
@Override
public ResultSet getAttributes(String catalog, String schemaPattern, String typeNamePattern, String attributeNamePattern)
throws SQLException {
return emptySet(con.cfg,
return emptySet(con.cfg, "",
"ATTRIBUTES",
"TYPE_CAT",
"TYPE_SCHEM",
@ -1056,12 +1156,27 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
@Override
public ResultSet getClientInfoProperties() throws SQLException {
throw new SQLException("Client info not implemented yet");
DriverPropertyInfo[] info = con.cfg.driverPropertyInfo();
Object[][] data = new Object[info.length][];
for (int i = 0; i < data.length; i++) {
data[i] = new Object[4];
data[i][0] = info[i].name;
data[i][1] = Integer.valueOf(-1);
data[i][2] = EMPTY;
data[i][3] = EMPTY;
}
return memorySet(con.cfg, columnInfo("",
"NAME",
"MAX_LEN", INTEGER,
"DEFAULT_VALUE",
"DESCRIPTION"), data);
}
@Override
public ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) throws SQLException {
return emptySet(con.cfg,
return emptySet(con.cfg, "",
"FUNCTIONS",
"FUNCTION_CAT",
"FUNCTION_SCHEM",
@ -1074,7 +1189,7 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
@Override
public ResultSet getFunctionColumns(String catalog, String schemaPattern, String functionNamePattern, String columnNamePattern)
throws SQLException {
return emptySet(con.cfg,
return emptySet(con.cfg, "",
"FUNCTION_COLUMNS",
"FUNCTION_CAT",
"FUNCTION_SCHEM",
@ -1097,7 +1212,7 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
@Override
public ResultSet getPseudoColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern)
throws SQLException {
return emptySet(con.cfg,
return emptySet(con.cfg, "",
"PSEUDO_COLUMNS",
"TABLE_CAT",
"TABLE_SCHEM",
@ -1212,7 +1327,7 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
@Override
public int batchSize() {
return data.length;
return ObjectUtils.isEmpty(data) ? 0 : data.length;
}
@Override

View File

@ -30,18 +30,24 @@ import static org.elasticsearch.xpack.sql.client.StringUtils.EMPTY;
class JdbcHttpClient {
private final HttpClient httpClient;
private final JdbcConfiguration conCfg;
private final InfoResponse serverInfo;
private InfoResponse serverInfo;
/**
* The SQLException is the only type of Exception the JDBC API can throw (and that the user expects).
* If we remove it, we need to make sure no other types of Exceptions (runtime or otherwise) are thrown
*/
JdbcHttpClient(JdbcConfiguration conCfg) throws SQLException {
this(conCfg, true);
}
JdbcHttpClient(JdbcConfiguration conCfg, boolean checkServer) throws SQLException {
httpClient = new HttpClient(conCfg);
this.conCfg = conCfg;
if (checkServer) {
this.serverInfo = fetchServerInfo();
checkServerVersion();
}
}
boolean ping(long timeoutInMs) throws SQLException {
return httpClient.ping(timeoutInMs);
@ -78,6 +84,9 @@ class JdbcHttpClient {
}
InfoResponse serverInfo() throws SQLException {
if (serverInfo == null) {
serverInfo = fetchServerInfo();
}
return serverInfo;
}

View File

@ -1224,6 +1224,7 @@ class JdbcResultSet implements ResultSet, JdbcWrapper {
@Override
public String toString() {
return format(Locale.ROOT, "%s:row %d", getClass().getSimpleName(), rowNumber);
return format(Locale.ROOT, "%s:row %d:cursor size %d:%s", getClass().getSimpleName(), rowNumber, cursor.batchSize(),
cursor.columns());
}
}

View File

@ -6,15 +6,119 @@
package org.elasticsearch.xpack.sql.jdbc;
import org.elasticsearch.common.CheckedSupplier;
import org.elasticsearch.test.ESTestCase;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;
public class JdbcDatabaseMetaDataTests extends ESTestCase {
private JdbcDatabaseMetaData md = new JdbcDatabaseMetaData(null);
private JdbcDatabaseMetaData md = null;
{
try {
md = new JdbcDatabaseMetaData(
new JdbcConnection(JdbcConfiguration.create("jdbc:es://localhost:9200/", new Properties(), 10), false));
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
public void testSeparators() throws Exception {
assertEquals(":", md.getCatalogSeparator());
assertEquals("\"", md.getIdentifierQuoteString());
assertEquals("\\", md.getSearchStringEscape());
}
public void testGetProcedures() throws Exception {
testEmptySet(() -> md.getProcedures(null, null, null));
}
public void testGetProcedureColumns() throws Exception {
testEmptySet(() -> md.getProcedureColumns(null, null, null, null));
}
public void testGetColumnPrivileges() throws Exception {
testEmptySet(() -> md.getColumnPrivileges(null, null, null, null));
}
public void testGetTablePrivileges() throws Exception {
testEmptySet(() -> md.getTablePrivileges(null, null, null));
}
public void testGetBestRowIdentifier() throws Exception {
testEmptySet(() -> md.getBestRowIdentifier(null, null, null, 0, false));
}
public void testGetVersionColumns() throws Exception {
testEmptySet(() -> md.getVersionColumns(null, null, null));
}
public void testGetPrimaryKeys() throws Exception {
testEmptySet(() -> md.getPrimaryKeys(null, null, null));
}
public void testGetImportedKeys() throws Exception {
testEmptySet(() -> md.getImportedKeys(null, null, null));
}
public void testGetExportedKeys() throws Exception {
testEmptySet(() -> md.getExportedKeys(null, null, null));
}
public void testGetCrossReference() throws Exception {
testEmptySet(() -> md.getCrossReference(null, null, null, null, null, null));
}
public void testGetIndexInfo() throws Exception {
testEmptySet(() -> md.getIndexInfo(null, null, null, false, false));
}
public void testGetUDTs() throws Exception {
testEmptySet(() -> md.getUDTs(null, null, null, null));
}
public void testGetSuperTypes() throws Exception {
testEmptySet(() -> md.getSuperTypes(null, null, null));
}
public void testGetSuperTables() throws Exception {
testEmptySet(() -> md.getSuperTables(null, null, null));
}
public void testGetAttributes() throws Exception {
testEmptySet(() -> md.getAttributes(null, null, null, null));
}
public void testGetFunctions() throws Exception {
testEmptySet(() -> md.getFunctions(null, null, null));
}
public void testGetFunctionColumns() throws Exception {
testEmptySet(() -> md.getFunctionColumns(null, null, null, null));
}
public void testGetPseudoColumns() throws Exception {
testEmptySet(() -> md.getPseudoColumns(null, null, null, null));
}
private static void testEmptySet(CheckedSupplier<ResultSet, SQLException> supplier) throws SQLException {
try (ResultSet result = supplier.get()) {
assertNotNull(result);
assertFalse(result.next());
}
}
public void testGetClientInfoProperties() throws Exception {
try (ResultSet result = md.getClientInfoProperties()) {
assertNotNull(result);
assertTrue(result.next());
assertNotNull(result.getString(1));
assertEquals(-1, result.getInt(2));
assertEquals("", result.getString(3));
assertEquals("", result.getString(4));
}
}
}

View File

@ -7,13 +7,13 @@ package org.elasticsearch.xpack.sql.client;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@ -148,13 +148,13 @@ public class ConnectionConfiguration {
}
}
private Collection<String> optionNames() {
Collection<String> options = new ArrayList<>(OPTION_NAMES);
protected Collection<String> optionNames() {
Set<String> options = new TreeSet<>(OPTION_NAMES);
options.addAll(extraOptions());
return options;
}
protected Collection<? extends String> extraOptions() {
protected Collection<String> extraOptions() {
return emptyList();
}