diff --git a/presto-oracle/src/main/java/com/facebook/presto/plugin/oracle/OracleClient.java b/presto-oracle/src/main/java/com/facebook/presto/plugin/oracle/OracleClient.java index 69d3e216f043e..36f3357f0ed04 100644 --- a/presto-oracle/src/main/java/com/facebook/presto/plugin/oracle/OracleClient.java +++ b/presto-oracle/src/main/java/com/facebook/presto/plugin/oracle/OracleClient.java @@ -13,26 +13,40 @@ */ package com.facebook.presto.plugin.oracle; +import com.facebook.airlift.log.Logger; +import com.facebook.presto.common.predicate.TupleDomain; import com.facebook.presto.common.type.Decimals; import com.facebook.presto.common.type.VarcharType; import com.facebook.presto.plugin.jdbc.BaseJdbcClient; import com.facebook.presto.plugin.jdbc.BaseJdbcConfig; import com.facebook.presto.plugin.jdbc.ConnectionFactory; +import com.facebook.presto.plugin.jdbc.JdbcColumnHandle; import com.facebook.presto.plugin.jdbc.JdbcConnectorId; import com.facebook.presto.plugin.jdbc.JdbcIdentity; +import com.facebook.presto.plugin.jdbc.JdbcTableHandle; import com.facebook.presto.plugin.jdbc.JdbcTypeHandle; import com.facebook.presto.plugin.jdbc.mapping.ReadMapping; +import com.facebook.presto.spi.ColumnHandle; import com.facebook.presto.spi.ConnectorSession; import com.facebook.presto.spi.PrestoException; import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.statistics.ColumnStatistics; +import com.facebook.presto.spi.statistics.DoubleRange; +import com.facebook.presto.spi.statistics.Estimate; +import com.facebook.presto.spi.statistics.TableStatistics; +import com.google.common.collect.Maps; import jakarta.inject.Inject; import java.sql.Connection; import java.sql.DatabaseMetaData; +import java.sql.Date; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Optional; import static com.facebook.presto.common.type.DecimalType.createDecimalType; @@ -47,6 +61,7 @@ import static com.facebook.presto.plugin.jdbc.mapping.StandardColumnMappings.varbinaryReadMapping; import static com.facebook.presto.plugin.jdbc.mapping.StandardColumnMappings.varcharReadMapping; import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static java.lang.Double.NaN; import static java.lang.String.format; import static java.util.Locale.ENGLISH; import static java.util.Objects.requireNonNull; @@ -54,6 +69,7 @@ public class OracleClient extends BaseJdbcClient { + private static final Logger LOG = Logger.get(OracleClient.class); private static final int FETCH_SIZE = 1000; private final boolean synonymsEnabled; @@ -93,6 +109,7 @@ protected ResultSet getTables(Connection connection, Optional schemaName escapeNamePattern(tableName, Optional.of(escape)).orElse(null), getTableTypes()); } + @Override public PreparedStatement getPreparedStatement(ConnectorSession session, Connection connection, String sql) throws SQLException @@ -137,6 +154,99 @@ protected void renameTable(JdbcIdentity identity, String catalogName, SchemaTabl } } + @Override + public TableStatistics getTableStatistics(ConnectorSession session, JdbcTableHandle handle, List columnHandles, TupleDomain tupleDomain) + { + try { + requireNonNull(handle.getSchemaName(), "schema name is null"); + requireNonNull(handle.getTableName(), "table name is null"); + String sql = format( + "SELECT NUM_ROWS, AVG_ROW_LEN, LAST_ANALYZED\n" + + "FROM DBA_TAB_STATISTICS\n" + + "WHERE OWNER='%s'\n" + + "AND TABLE_NAME='%s'", + handle.getSchemaName().toUpperCase(), handle.getTableName().toUpperCase()); + try (Connection connection = connectionFactory.openConnection(JdbcIdentity.from(session)); + PreparedStatement preparedStatement = getPreparedStatement(session, connection, sql); + PreparedStatement preparedStatementCol = getPreparedStatement(session, connection, getColumnStaticsSql(handle)); + ResultSet resultSet = preparedStatement.executeQuery(); + ResultSet resultSetColumnStats = preparedStatementCol.executeQuery()) { + if (!resultSet.next()) { + LOG.debug("Stats not found for table : %s.%s", handle.getSchemaName(), handle.getTableName()); + return TableStatistics.empty(); + } + double numRows = resultSet.getDouble("NUM_ROWS"); + // double avgRowLen = resultSet.getDouble("AVG_ROW_LEN"); + Date lastAnalyzed = resultSet.getDate("LAST_ANALYZED"); + + Map columnStatisticsMap = new HashMap<>(); + Map columnHandleMap = Maps.uniqueIndex(columnHandles, JdbcColumnHandle::getColumnName); + while (resultSetColumnStats.next() && numRows > 0) { + String columnName = resultSetColumnStats.getString("COLUMN_NAME"); + double nullsCount = resultSetColumnStats.getDouble("NUM_NULLS"); + double ndv = resultSetColumnStats.getDouble("NUM_DISTINCT"); + // Oracle stores low and high values as RAW(1000) i.e. a byte array. No way to unwrap it, without a clue about the underlying type + // So we use column type as a clue and parse to double by converting as string first. + double lowValue = toDouble(resultSetColumnStats.getString("LOW_VALUE")); + double highValue = toDouble(resultSetColumnStats.getString("HIGH_VALUE")); + ColumnStatistics.Builder columnStatisticsBuilder = ColumnStatistics.builder() + .setDataSize(Estimate.estimateFromDouble(resultSet.getDouble("DATA_LENGTH"))) + .setNullsFraction(Estimate.estimateFromDouble(nullsCount / numRows)) + .setDistinctValuesCount(Estimate.estimateFromDouble(ndv)); + ColumnStatistics columnStatistics = columnStatisticsBuilder.build(); + if (Double.isFinite(lowValue) && Double.isFinite(highValue)) { + columnStatistics = columnStatisticsBuilder.setRange(new DoubleRange(lowValue, highValue)).build(); + } + columnStatisticsMap.put(columnHandleMap.get(columnName), columnStatistics); + } + LOG.info("getTableStatics for table: %s.%s.%s with last analyzed: %s", + handle.getCatalogName(), handle.getSchemaName(), handle.getTableName(), lastAnalyzed); + return TableStatistics.builder() + .setColumnStatistics(columnStatisticsMap) + .setRowCount(Estimate.estimateFromDouble(numRows)).build(); + } + } + catch (SQLException | RuntimeException e) { + throw new PrestoException(JDBC_ERROR, "Failed fetching statistics for table: " + handle, e); + } + } + + private String getColumnStaticsSql(JdbcTableHandle handle) + { + // UTL_RAW.CAST_TO_BINARY_X does not render correctly so those types are not supported. + return format( + "SELECT COLUMN_NAME,\n" + + "DATA_TYPE,\n" + + "DATA_LENGTH,\n" + + "NUM_NULLS,\n" + + "NUM_DISTINCT,\n" + + "DENSITY,\n" + + "CASE DATA_TYPE\n" + + " WHEN 'NUMBER' THEN TO_CHAR(UTL_RAW.CAST_TO_NUMBER(LOW_VALUE))\n" + + " ELSE NULL\n" + + "END AS LOW_VALUE,\n" + + "CASE DATA_TYPE\n" + + " WHEN 'NUMBER' THEN TO_CHAR(UTL_RAW.CAST_TO_NUMBER(HIGH_VALUE))\n" + + " ELSE NULL\n" + + "END AS HIGH_VALUE\n" + + "FROM ALL_TAB_COLUMNS\n" + + "WHERE OWNER = '%s'\n" + + " AND TABLE_NAME = '%s'", handle.getSchemaName().toUpperCase(), handle.getTableName().toUpperCase()); + } + + private double toDouble(String number) + { + try { + return Double.parseDouble(number); + } + catch (Exception e) { + // a string represented by number, may not even be a parseable number this is expected. e.g. if column type is + // varchar. + LOG.debug(e, "error while decoding : %s", number); + } + return NaN; + } + @Override public Optional toPrestoType(ConnectorSession session, JdbcTypeHandle typeHandle) {