package org.opencb.opencga.storage.hadoop.variant.index.phoenix; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.util.Bytes; import org.apache.phoenix.schema.PTable; import org.apache.phoenix.schema.PTableType; import org.apache.phoenix.schema.TableNotFoundException; import org.apache.phoenix.schema.types.PArrayDataType; import org.apache.phoenix.schema.types.PDataType; import org.apache.phoenix.schema.types.PhoenixArray; import org.apache.phoenix.util.ColumnInfo; import org.apache.phoenix.util.PhoenixRuntime; import org.apache.phoenix.util.QueryUtil; import org.apache.phoenix.util.SchemaUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.*; import java.util.*; import java.util.function.BiConsumer; import java.util.stream.Collectors; /** * Created on 11/10/16. * * @author Jacobo Coll <jacobo167@gmail.com> */ public class PhoenixHelper { public static final PTableType DEFAULT_TABLE_TYPE = PTableType.TABLE; // Server offset, for server pagination, is only available in Phoenix4.8 or Phoenix4.7.0.2.5.0 (from HDP2.5.0) // See https://issues.apache.org/jira/browse/PHOENIX-2722 public static final String PHOENIX_SERVER_OFFSET_AVAILABLE = "phoenix.server.offset.available"; private final Configuration conf; private static Logger logger = LoggerFactory.getLogger(PhoenixHelper.class); public PhoenixHelper(Configuration conf) { this.conf = conf; } public boolean execute(Connection con, String sql) throws SQLException { logger.debug(sql); try (Statement statement = con.createStatement()) { return statement.execute(sql); } catch (Exception e) { logger.error("Error executing '{}'", sql); throw e; } } public String explain(Connection con, String sql, BiConsumer<Logger, String> loggerMethod) throws SQLException { String explain = explain(con, sql); for (String s : explain.split("\n")) { loggerMethod.accept(logger, " | " + s); } return explain; } public String explain(Connection con, String sql) throws SQLException { if (!sql.startsWith("explain") && !sql.startsWith("EXPLAIN")) { sql = "EXPLAIN " + sql; } try (Statement statement = con.createStatement(); ResultSet resultSet = statement.executeQuery(sql)) { // Close both - Statement and RS to make sure return QueryUtil.getExplainPlan(resultSet); } } public String buildAlterAddColumn(String tableName, String column, String type) { return buildAlterAddColumn(tableName, column, type, true); } public String buildAlterAddColumn(String tableName, String column, String type, boolean ifNotExists) { return buildAlterAddColumn(tableName, column, type, ifNotExists, DEFAULT_TABLE_TYPE); } public String buildAlterTableAddColumn(String tableName, String column, String type, boolean ifNotExists) { return buildAlterAddColumn(tableName, column, type, ifNotExists, PTableType.TABLE); } public String buildAlterViewAddColumn(String tableName, String column, String type, boolean ifNotExists) { return buildAlterAddColumn(tableName, column, type, ifNotExists, PTableType.VIEW); } private String buildAlterAddColumn(String tableName, String column, String type, boolean ifNotExists, PTableType tableType) { return "ALTER " + tableType.toString() + " " + SchemaUtil.getEscapedFullTableName(tableName) + " ADD " + (ifNotExists ? "IF NOT EXISTS " : "") + "\"" + column + "\" " + type; } public String buildAlterAddColumns(String tableName, Collection<Column> columns, boolean ifNotExists) { StringBuilder sb = new StringBuilder(); sb.append("ALTER ").append(DEFAULT_TABLE_TYPE).append(" ").append(SchemaUtil.getEscapedFullTableName(tableName)) .append(" ADD ").append(ifNotExists ? "IF NOT EXISTS " : ""); Iterator<Column> iterator = columns.iterator(); while (iterator.hasNext()) { Column column = iterator.next(); sb.append("\"").append(column.column()).append("\" ").append(column.sqlType()); if (iterator.hasNext()) { sb.append(", "); } } return sb.toString(); } public String buildDropTable(String tableName, boolean ifExists, boolean cascade) { StringBuilder sb = new StringBuilder().append("DROP TABLE "); if (ifExists) { sb.append("IF EXISTS "); } sb.append(SchemaUtil.getEscapedFullTableName(tableName)); if (cascade) { sb.append(" CASCADE"); } return sb.toString(); } public void dropTable(Connection con, String tableName, boolean ifExists, boolean cascade) throws SQLException { execute(con, buildDropTable(tableName, ifExists, cascade)); } public void addMissingColumns(Connection con, String tableName, Collection<Column> annotColumns, boolean oneCall) throws SQLException { Set<String> columns = getColumns(con, tableName).stream().map(Column::column).collect(Collectors.toSet()); List<Column> missingColumns = annotColumns.stream() .filter(column -> !columns.contains(column.column())) .collect(Collectors.toList()); if (!missingColumns.isEmpty()) { logger.info("Adding missing columns: " + missingColumns); if (oneCall) { String sql = buildAlterAddColumns(tableName, missingColumns, true); logger.info(sql); execute(con, sql); } else { for (Column column : missingColumns) { String sql = buildAlterAddColumn(tableName, column.column(), column.sqlType(), true); logger.info(sql); execute(con, sql); } } } } public Connection newJdbcConnection() throws SQLException, ClassNotFoundException { return newJdbcConnection(conf); } public Connection newJdbcConnection(Configuration conf) throws SQLException, ClassNotFoundException { Connection connection = QueryUtil.getConnection(conf); StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); logger.info("Opened Phoenix DB connection {} called from {}", connection, Arrays.toString(stackTrace)); return connection; } public static byte[] toBytes(Collection collection, PArrayDataType arrayType) { PDataType pDataType = PDataType.arrayBaseType(arrayType); Object[] elements = collection.toArray(); PhoenixArray phoenixArray = new PhoenixArray(pDataType, elements); return arrayType.toBytes(phoenixArray); } public void createIndexes(Connection con, String tableName, List<PhoenixHelper.Index> indices, boolean async) throws SQLException { for (PhoenixHelper.Index index : indices) { String sql = createIndexSql(tableName, index, async); execute(con, sql); } } public void createLocalIndex(Connection con, String tableName, String indexName, List<String> columns, List<String> include) throws SQLException { String sql = PhoenixHelper.createIndexSql(PTable.IndexType.LOCAL, tableName, indexName, columns, include, false); execute(con, sql); } public void createGlobalIndex(Connection con, String indexName, String tableName, List<String> columns, List<String> include) throws SQLException { String sql = PhoenixHelper.createIndexSql(PTable.IndexType.GLOBAL, tableName, indexName, columns, include, false); execute(con, sql); } public static String createIndexSql(String tableName, Index index, boolean async) { return createIndexSql(index.indexType, tableName, index.indexName, index.columns, index.include, async); } public static String createIndexSql(PTable.IndexType type, String tableName, String indexName, List<String> columns, List<String> include, boolean async) { Objects.requireNonNull(indexName); Objects.requireNonNull(tableName); if (columns == null || columns.isEmpty()) { throw new IllegalArgumentException("Columns can not be empty"); } StringBuilder sb = new StringBuilder(); sb.append("CREATE"); if (type.equals(PTable.IndexType.LOCAL)) { sb.append(" LOCAL"); } sb.append(" INDEX IF NOT EXISTS "); sb.append(SchemaUtil.getEscapedArgument(indexName)) .append(" ON ").append(SchemaUtil.getEscapedFullTableName(tableName)).append(" ( "); for (Iterator<String> iterator = columns.iterator(); iterator.hasNext();) { String column = iterator.next(); sb.append(SchemaUtil.getEscapedFullColumnName(column)); if (iterator.hasNext()) { sb.append(", "); } } sb.append(" )"); if (include != null && !include.isEmpty()) { sb.append(" INCLUDE("); for (Iterator<String> iterator = include.iterator(); iterator.hasNext();) { String column = iterator.next(); sb.append(SchemaUtil.getEscapedFullColumnName(column)); if (iterator.hasNext()) { sb.append(", "); } } sb.append(" )"); } if (async) { sb.append(" ASYNC"); } String sql = sb.toString(); logger.info("Creating index: {}", sql); return sql; } public boolean tableExists(Connection con, String table) throws SQLException { try { PhoenixRuntime.getTable(con, table); return true; } catch (TableNotFoundException ignore) { return false; } } public List<Column> getColumns(Connection con, String tableName) throws SQLException { String sql = "SELECT * FROM " + SchemaUtil.getEscapedFullTableName(tableName) + " LIMIT 0"; logger.debug(sql); try (Statement statement = con.createStatement(); // Clean up Statement and RS ResultSet resultSet = statement.executeQuery(sql)) { ResultSetMetaData metaData = resultSet.getMetaData(); List<Column> columns = new ArrayList<>(metaData.getColumnCount()); // 1-based for (int i = 1; i <= metaData.getColumnCount(); i++) { columns.add(Column.build(metaData.getColumnName(i), PDataType.fromSqlTypeName(metaData.getColumnTypeName(i)))); } return columns; } } public interface Column { String column(); byte[] bytes(); PDataType getPDataType(); String sqlType(); boolean nullable(); static Column build(String column, PDataType pDataType) { return new ColumnImpl(column, pDataType, false); } static Column build(String column, PDataType pDataType, boolean nullable) { return new ColumnImpl(column, pDataType, nullable); } default ColumnInfo toColumnInfo() { return new ColumnInfo(column(), getPDataType().getSqlType()); } } private static class ColumnImpl implements Column { private final String column; private final PDataType pDataType; private boolean nullable; ColumnImpl(String column, PDataType pDataType, boolean nullable) { this.bytes = Bytes.toBytes(column); this.column = column; this.pDataType = pDataType; this.nullable = nullable; } private byte[] bytes; @Override public String column() { return column; } @Override public byte[] bytes() { return bytes; } @Override public PDataType getPDataType() { return pDataType; } @Override public String sqlType() { return pDataType.getSqlTypeName(); } @Override public boolean nullable() { return nullable; } @Override public String toString() { return column; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof ColumnImpl)) { return false; } ColumnImpl column1 = (ColumnImpl) o; if (column != null ? !column.equals(column1.column) : column1.column != null) { return false; } return pDataType != null ? pDataType.equals(column1.pDataType) : column1.pDataType == null; } @Override public int hashCode() { int result = column != null ? column.hashCode() : 0; result = 31 * result + (pDataType != null ? pDataType.hashCode() : 0); return result; } } public static class Index { private final String indexName; private final PTable.IndexType indexType; private final List<String> columns; private final List<String> include; public Index(String indexName, PTable.IndexType indexType, Column ... columns) { this.indexName = indexName; this.indexType = indexType; this.columns = Arrays.stream(columns).map(Column::column).collect(Collectors.toList()); this.include = null; } public Index(TableName table, PTable.IndexType indexType, List<?> columns, List<?> include) { this(table.getNameAsString().replaceAll("[:\\\\.]", "_").toUpperCase() + "_" + columns .stream() .map(Object::toString) .collect(Collectors.joining("_")) .replaceAll("[\"\\[\\]]", "") + "_IDX", indexType, columns, include); } public Index(String indexName, PTable.IndexType indexType, List<?> columns, List<?> include) { this.indexName = indexName; this.indexType = indexType; this.columns = columns.stream() .map(o -> o instanceof Column ? ((Column) o).column() : o.toString()) .collect(Collectors.toList()); this.include = include.stream() .map(o -> o instanceof Column ? ((Column) o).column() : o.toString()) .collect(Collectors.toList()); } } }