Skip to content

Commit 314094b

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

File tree

1 file changed

+111
-0
lines changed

1 file changed

+111
-0
lines changed

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

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,41 @@
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.function.table.Preconditions;
34+
import com.facebook.presto.spi.statistics.ColumnStatistics;
35+
import com.facebook.presto.spi.statistics.DoubleRange;
36+
import com.facebook.presto.spi.statistics.Estimate;
37+
import com.facebook.presto.spi.statistics.TableStatistics;
38+
import com.google.common.collect.Maps;
2839
import jakarta.inject.Inject;
2940

3041
import java.sql.Connection;
3142
import java.sql.DatabaseMetaData;
43+
import java.sql.Date;
3244
import java.sql.PreparedStatement;
3345
import java.sql.ResultSet;
3446
import java.sql.SQLException;
3547
import java.sql.Types;
48+
import java.util.HashMap;
49+
import java.util.List;
50+
import java.util.Map;
3651
import java.util.Optional;
3752

3853
import static com.facebook.presto.common.type.DecimalType.createDecimalType;
@@ -46,13 +61,15 @@
4661
import static com.facebook.presto.plugin.jdbc.mapping.StandardColumnMappings.smallintReadMapping;
4762
import static com.facebook.presto.plugin.jdbc.mapping.StandardColumnMappings.varcharReadMapping;
4863
import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED;
64+
import static java.lang.Double.NaN;
4965
import static java.lang.String.format;
5066
import static java.util.Locale.ENGLISH;
5167
import static java.util.Objects.requireNonNull;
5268

5369
public class OracleClient
5470
extends BaseJdbcClient
5571
{
72+
private static final Logger LOG = Logger.get(OracleClient.class);
5673
private static final int FETCH_SIZE = 1000;
5774

5875
private final boolean synonymsEnabled;
@@ -92,6 +109,7 @@ protected ResultSet getTables(Connection connection, Optional<String> schemaName
92109
escapeNamePattern(tableName, Optional.of(escape)).orElse(null),
93110
getTableTypes());
94111
}
112+
95113
@Override
96114
public PreparedStatement getPreparedStatement(ConnectorSession session, Connection connection, String sql)
97115
throws SQLException
@@ -129,6 +147,99 @@ protected void renameTable(JdbcIdentity identity, String catalogName, SchemaTabl
129147
}
130148
}
131149

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

0 commit comments

Comments
 (0)