Skip to content

Commit d1aec45

Browse files
committed
Implement getTableStatistics for Oracle connector.
1 parent 003cda6 commit d1aec45

File tree

1 file changed

+110
-0
lines changed

1 file changed

+110
-0
lines changed

presto-oracle/src/main/java/com/facebook/presto/plugin/oracle/OracleClient.java

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,40 @@
1313
*/
1414
package com.facebook.presto.plugin.oracle;
1515

16+
import com.facebook.airlift.log.Logger;
17+
import com.facebook.presto.common.predicate.TupleDomain;
1618
import com.facebook.presto.common.type.Decimals;
1719
import com.facebook.presto.common.type.VarcharType;
1820
import com.facebook.presto.plugin.jdbc.BaseJdbcClient;
1921
import com.facebook.presto.plugin.jdbc.BaseJdbcConfig;
2022
import com.facebook.presto.plugin.jdbc.ConnectionFactory;
23+
import com.facebook.presto.plugin.jdbc.JdbcColumnHandle;
2124
import com.facebook.presto.plugin.jdbc.JdbcConnectorId;
2225
import com.facebook.presto.plugin.jdbc.JdbcIdentity;
26+
import com.facebook.presto.plugin.jdbc.JdbcTableHandle;
2327
import com.facebook.presto.plugin.jdbc.JdbcTypeHandle;
2428
import com.facebook.presto.plugin.jdbc.mapping.ReadMapping;
29+
import com.facebook.presto.spi.ColumnHandle;
2530
import com.facebook.presto.spi.ConnectorSession;
2631
import com.facebook.presto.spi.PrestoException;
2732
import com.facebook.presto.spi.SchemaTableName;
33+
import com.facebook.presto.spi.statistics.ColumnStatistics;
34+
import com.facebook.presto.spi.statistics.DoubleRange;
35+
import com.facebook.presto.spi.statistics.Estimate;
36+
import com.facebook.presto.spi.statistics.TableStatistics;
37+
import com.google.common.collect.Maps;
2838
import jakarta.inject.Inject;
2939

3040
import java.sql.Connection;
3141
import java.sql.DatabaseMetaData;
42+
import java.sql.Date;
3243
import java.sql.PreparedStatement;
3344
import java.sql.ResultSet;
3445
import java.sql.SQLException;
3546
import java.sql.Types;
47+
import java.util.HashMap;
48+
import java.util.List;
49+
import java.util.Map;
3650
import java.util.Optional;
3751

3852
import static com.facebook.presto.common.type.DecimalType.createDecimalType;
@@ -46,13 +60,15 @@
4660
import static com.facebook.presto.plugin.jdbc.mapping.StandardColumnMappings.smallintReadMapping;
4761
import static com.facebook.presto.plugin.jdbc.mapping.StandardColumnMappings.varcharReadMapping;
4862
import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED;
63+
import static java.lang.Double.NaN;
4964
import static java.lang.String.format;
5065
import static java.util.Locale.ENGLISH;
5166
import static java.util.Objects.requireNonNull;
5267

5368
public class OracleClient
5469
extends BaseJdbcClient
5570
{
71+
private static final Logger LOG = Logger.get(OracleClient.class);
5672
private static final int FETCH_SIZE = 1000;
5773

5874
private final boolean synonymsEnabled;
@@ -92,6 +108,7 @@ protected ResultSet getTables(Connection connection, Optional<String> schemaName
92108
escapeNamePattern(tableName, Optional.of(escape)).orElse(null),
93109
getTableTypes());
94110
}
111+
95112
@Override
96113
public PreparedStatement getPreparedStatement(ConnectorSession session, Connection connection, String sql)
97114
throws SQLException
@@ -129,6 +146,99 @@ protected void renameTable(JdbcIdentity identity, String catalogName, SchemaTabl
129146
}
130147
}
131148

149+
@Override
150+
public TableStatistics getTableStatistics(ConnectorSession session, JdbcTableHandle handle, List<JdbcColumnHandle> columnHandles, TupleDomain<ColumnHandle> tupleDomain)
151+
{
152+
try {
153+
requireNonNull(handle.getSchemaName(), "schema name is null");
154+
requireNonNull(handle.getTableName(), "table name is null");
155+
String sql = format(
156+
"SELECT NUM_ROWS, AVG_ROW_LEN, LAST_ANALYZED\n" +
157+
"FROM DBA_TAB_STATISTICS\n" +
158+
"WHERE OWNER='%s'\n" +
159+
"AND TABLE_NAME='%s'",
160+
handle.getSchemaName().toUpperCase(), handle.getTableName().toUpperCase());
161+
try (Connection connection = connectionFactory.openConnection(JdbcIdentity.from(session));
162+
PreparedStatement preparedStatement = getPreparedStatement(session, connection, sql);
163+
PreparedStatement preparedStatementCol = getPreparedStatement(session, connection, getColumnStaticsSql(handle));
164+
ResultSet resultSet = preparedStatement.executeQuery();
165+
ResultSet resultSetColumnStats = preparedStatementCol.executeQuery()) {
166+
if (!resultSet.next()) {
167+
LOG.debug("Stats not found for table : %s.%s", handle.getSchemaName(), handle.getTableName());
168+
return TableStatistics.empty();
169+
}
170+
double numRows = resultSet.getDouble("NUM_ROWS");
171+
// double avgRowLen = resultSet.getDouble("AVG_ROW_LEN");
172+
Date lastAnalyzed = resultSet.getDate("LAST_ANALYZED");
173+
174+
Map<ColumnHandle, ColumnStatistics> columnStatisticsMap = new HashMap<>();
175+
Map<String, JdbcColumnHandle> columnHandleMap = Maps.uniqueIndex(columnHandles, JdbcColumnHandle::getColumnName);
176+
while (resultSetColumnStats.next() && numRows > 0) {
177+
String columnName = resultSetColumnStats.getString("COLUMN_NAME");
178+
double nullsCount = resultSetColumnStats.getDouble("NUM_NULLS");
179+
double ndv = resultSetColumnStats.getDouble("NUM_DISTINCT");
180+
// 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
181+
// So we use column type as a clue and parse to double by converting as string first.
182+
double lowValue = toDouble(resultSetColumnStats.getString("LOW_VALUE"));
183+
double highValue = toDouble(resultSetColumnStats.getString("HIGH_VALUE"));
184+
ColumnStatistics.Builder columnStatisticsBuilder = ColumnStatistics.builder()
185+
.setDataSize(Estimate.estimateFromDouble(resultSet.getDouble("DATA_LENGTH")))
186+
.setNullsFraction(Estimate.estimateFromDouble(nullsCount / numRows))
187+
.setDistinctValuesCount(Estimate.estimateFromDouble(ndv));
188+
ColumnStatistics columnStatistics = columnStatisticsBuilder.build();
189+
if (Double.isFinite(lowValue) && Double.isFinite(highValue)) {
190+
columnStatistics = columnStatisticsBuilder.setRange(new DoubleRange(lowValue, highValue)).build();
191+
}
192+
columnStatisticsMap.put(columnHandleMap.get(columnName), columnStatistics);
193+
}
194+
LOG.info("getTableStatics for table: %s.%s.%s with last analyzed: %s",
195+
handle.getCatalogName(), handle.getSchemaName(), handle.getTableName(), lastAnalyzed);
196+
return TableStatistics.builder()
197+
.setColumnStatistics(columnStatisticsMap)
198+
.setRowCount(Estimate.estimateFromDouble(numRows)).build();
199+
}
200+
}
201+
catch (SQLException | RuntimeException e) {
202+
throw new PrestoException(JDBC_ERROR, "Failed fetching statistics for table: " + handle, e);
203+
}
204+
}
205+
206+
private String getColumnStaticsSql(JdbcTableHandle handle)
207+
{
208+
// UTL_RAW.CAST_TO_BINARY_X does not render correctly so those types are not supported.
209+
return format(
210+
"SELECT COLUMN_NAME,\n" +
211+
"DATA_TYPE,\n" +
212+
"DATA_LENGTH,\n" +
213+
"NUM_NULLS,\n" +
214+
"NUM_DISTINCT,\n" +
215+
"DENSITY,\n" +
216+
"CASE DATA_TYPE\n" +
217+
" WHEN 'NUMBER' THEN TO_CHAR(UTL_RAW.CAST_TO_NUMBER(LOW_VALUE))\n" +
218+
" ELSE NULL\n" +
219+
"END AS LOW_VALUE,\n" +
220+
"CASE DATA_TYPE\n" +
221+
" WHEN 'NUMBER' THEN TO_CHAR(UTL_RAW.CAST_TO_NUMBER(HIGH_VALUE))\n" +
222+
" ELSE NULL\n" +
223+
"END AS HIGH_VALUE\n" +
224+
"FROM ALL_TAB_COLUMNS\n" +
225+
"WHERE OWNER = '%s'\n" +
226+
" AND TABLE_NAME = '%s'", handle.getSchemaName().toUpperCase(), handle.getTableName().toUpperCase());
227+
}
228+
229+
private double toDouble(String number)
230+
{
231+
try {
232+
return Double.parseDouble(number);
233+
}
234+
catch (Exception e) {
235+
// a string represented by number, may not even be a parseable number this is expected. e.g. if column type is
236+
// varchar.
237+
LOG.debug(e, "error while decoding : %s", number);
238+
}
239+
return NaN;
240+
}
241+
132242
@Override
133243
public Optional<ReadMapping> toPrestoType(ConnectorSession session, JdbcTypeHandle typeHandle)
134244
{

0 commit comments

Comments
 (0)