diff --git a/plugin/trino-base-jdbc/src/test/java/io/trino/plugin/jdbc/BaseJdbcTableStatisticsTest.java b/plugin/trino-base-jdbc/src/test/java/io/trino/plugin/jdbc/BaseJdbcTableStatisticsTest.java
index eeee51c6f31c..7b2158e24dc0 100644
--- a/plugin/trino-base-jdbc/src/test/java/io/trino/plugin/jdbc/BaseJdbcTableStatisticsTest.java
+++ b/plugin/trino-base-jdbc/src/test/java/io/trino/plugin/jdbc/BaseJdbcTableStatisticsTest.java
@@ -74,7 +74,7 @@ private void setUpTableFromTpch(String tableName)
@Test
public void testEmptyTable()
{
- String tableName = "test_stats_table_empty_" + randomTableSuffix();
+ String tableName = "test_empty_" + randomTableSuffix();
computeActual(format("CREATE TABLE %s AS SELECT orderkey, custkey, orderpriority, comment FROM tpch.tiny.orders WHERE false", tableName));
try {
gatherStats(tableName);
diff --git a/plugin/trino-oracle/pom.xml b/plugin/trino-oracle/pom.xml
index 344590a8cd29..cc22c391e4e1 100644
--- a/plugin/trino-oracle/pom.xml
+++ b/plugin/trino-oracle/pom.xml
@@ -77,6 +77,11 @@
runtime
+
+ org.jdbi
+ jdbi3-core
+
+
io.airlift
log-manager
diff --git a/plugin/trino-oracle/src/main/java/io/trino/plugin/oracle/OracleClient.java b/plugin/trino-oracle/src/main/java/io/trino/plugin/oracle/OracleClient.java
index 53c7103872a4..8ec6eee1d8d4 100644
--- a/plugin/trino-oracle/src/main/java/io/trino/plugin/oracle/OracleClient.java
+++ b/plugin/trino-oracle/src/main/java/io/trino/plugin/oracle/OracleClient.java
@@ -28,6 +28,8 @@
import io.trino.plugin.jdbc.JdbcColumnHandle;
import io.trino.plugin.jdbc.JdbcExpression;
import io.trino.plugin.jdbc.JdbcJoinCondition;
+import io.trino.plugin.jdbc.JdbcSortItem;
+import io.trino.plugin.jdbc.JdbcStatisticsConfig;
import io.trino.plugin.jdbc.JdbcTableHandle;
import io.trino.plugin.jdbc.JdbcTypeHandle;
import io.trino.plugin.jdbc.LongReadFunction;
@@ -56,6 +58,10 @@
import io.trino.spi.connector.ConnectorSession;
import io.trino.spi.connector.JoinCondition;
import io.trino.spi.connector.SchemaTableName;
+import io.trino.spi.predicate.TupleDomain;
+import io.trino.spi.statistics.ColumnStatistics;
+import io.trino.spi.statistics.Estimate;
+import io.trino.spi.statistics.TableStatistics;
import io.trino.spi.type.CharType;
import io.trino.spi.type.DecimalType;
import io.trino.spi.type.Decimals;
@@ -63,6 +69,8 @@
import io.trino.spi.type.VarcharType;
import oracle.jdbc.OraclePreparedStatement;
import oracle.jdbc.OracleTypes;
+import org.jdbi.v3.core.Handle;
+import org.jdbi.v3.core.Jdbi;
import javax.inject.Inject;
@@ -85,7 +93,10 @@
import java.util.Set;
import java.util.function.BiFunction;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Throwables.throwIfInstanceOf;
import static com.google.common.base.Verify.verify;
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static io.airlift.slice.Slices.utf8Slice;
import static io.airlift.slice.Slices.wrappedBuffer;
import static io.trino.plugin.jdbc.JdbcErrorCode.JDBC_ERROR;
@@ -142,6 +153,7 @@
import static java.lang.String.format;
import static java.util.Locale.ENGLISH;
import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.function.Function.identity;
public class OracleClient
extends BaseJdbcClient
@@ -198,6 +210,7 @@ public class OracleClient
.buildOrThrow();
private final boolean synonymsEnabled;
+ private final boolean statisticsEnabled;
private final ConnectorExpressionRewriter connectorExpressionRewriter;
private final AggregateFunctionRewriter aggregateFunctionRewriter;
@@ -205,6 +218,7 @@ public class OracleClient
public OracleClient(
BaseJdbcConfig config,
OracleConfig oracleConfig,
+ JdbcStatisticsConfig statisticsConfig,
ConnectionFactory connectionFactory,
QueryBuilder queryBuilder,
IdentifierMapping identifierMapping)
@@ -212,6 +226,7 @@ public OracleClient(
super(config, "\"", connectionFactory, queryBuilder, identifierMapping);
this.synonymsEnabled = oracleConfig.isSynonymsEnabled();
+ this.statisticsEnabled = requireNonNull(statisticsConfig, "statisticsConfig is null").isEnabled();
this.connectorExpressionRewriter = JdbcConnectorExpressionRewriterBuilder.newBuilder()
.addStandardRules(this::quoted)
@@ -470,12 +485,109 @@ public boolean isLimitGuaranteed(ConnectorSession session)
return true;
}
+ @Override
+ public boolean supportsTopN(ConnectorSession session, JdbcTableHandle handle, List sortOrder)
+ {
+ return true;
+ }
+
+ @Override
+ protected Optional topNFunction()
+ {
+ // NOTE: The syntax used here is supported since Oracle 12c (older releases are not supported by Oracle)
+ return Optional.of(TopNFunction.sqlStandard(this::quoted));
+ }
+
+ @Override
+ public boolean isTopNGuaranteed(ConnectorSession session)
+ {
+ return true;
+ }
+
@Override
protected boolean isSupportedJoinCondition(ConnectorSession session, JdbcJoinCondition joinCondition)
{
return joinCondition.getOperator() != JoinCondition.Operator.IS_DISTINCT_FROM;
}
+ @Override
+ public TableStatistics getTableStatistics(ConnectorSession session, JdbcTableHandle handle, TupleDomain tupleDomain)
+ {
+ if (!statisticsEnabled) {
+ return TableStatistics.empty();
+ }
+ if (!handle.isNamedRelation()) {
+ return TableStatistics.empty();
+ }
+ try {
+ return readTableStatistics(session, handle);
+ }
+ catch (SQLException | RuntimeException e) {
+ throwIfInstanceOf(e, TrinoException.class);
+ throw new TrinoException(JDBC_ERROR, "Failed fetching statistics for table: " + handle, e);
+ }
+ }
+
+ private TableStatistics readTableStatistics(ConnectorSession session, JdbcTableHandle table)
+ throws SQLException
+ {
+ checkArgument(table.isNamedRelation(), "Relation is not a table: %s", table);
+
+ try (Connection connection = connectionFactory.openConnection(session);
+ Handle handle = Jdbi.open(connection)) {
+ StatisticsDao statisticsDao = new StatisticsDao(handle);
+
+ Long rowCount = statisticsDao.getRowCount(table.getSchemaName(), table.getTableName());
+ if (rowCount == null) {
+ return TableStatistics.empty();
+ }
+
+ TableStatistics.Builder tableStatistics = TableStatistics.builder();
+ tableStatistics.setRowCount(Estimate.of(rowCount));
+
+ if (rowCount == 0) {
+ return tableStatistics.build();
+ }
+
+ Map columnStatistics = statisticsDao.getColumnStatistics(table.getSchemaName(), table.getTableName()).stream()
+ .collect(toImmutableMap(ColumnStatisticsResult::getColumnName, identity()));
+
+ for (JdbcColumnHandle column : this.getColumns(session, table)) {
+ ColumnStatisticsResult result = columnStatistics.get(column.getColumnName());
+ if (result == null) {
+ continue;
+ }
+
+ ColumnStatistics statistics = ColumnStatistics.builder()
+ .setNullsFraction(result.getNullsCount()
+ .map(nullsCount -> Estimate.of(1.0 * nullsCount / rowCount))
+ .orElseGet(Estimate::unknown))
+ .setDistinctValuesCount(result.getDistinctValuesCount()
+ .map(Estimate::of)
+ .orElseGet(Estimate::unknown))
+ .setDataSize(result.getAverageColumnLength()
+ /*
+ * ALL_TAB_COLUMNS.AVG_COL_LEN is hard to interpret precisely:
+ * - it can be `0` for all-null column
+ * - it can be `len+1` for varchar column filled with constant of length `len`, as if each row contained a is-null byte or length
+ * - it can be `len/2+1` for varchar column half-filled with constant (or random) of length `len`, as if each row contained a is-null byte or length
+ * - it can be `2` for varchar column with single non-null value of length 10, as if ... (?)
+ * - it looks storage size does not directly depend on `IS NULL` column attribute
+ *
+ * Since the interpretation of the value is not obvious, we do not deduce is-null bytes. They will be accounted for second time in
+ * `PlanNodeStatsEstimate.getOutputSizeForSymbol`, but this is the safer thing to do.
+ */
+ .map(averageColumnLength -> Estimate.of(1.0 * averageColumnLength * rowCount))
+ .orElseGet(Estimate::unknown))
+ .build();
+
+ tableStatistics.setColumnStatistics(column, statistics);
+ }
+
+ return tableStatistics.build();
+ }
+ }
+
public static LongWriteFunction trinoDateToOracleDateWriteFunction()
{
return new LongWriteFunction()
@@ -683,4 +795,76 @@ public void setColumnComment(ConnectorSession session, JdbcTableHandle handle, J
varcharLiteral(comment.orElse("")));
execute(session, sql);
}
+
+ private static class StatisticsDao
+ {
+ private final Handle handle;
+
+ public StatisticsDao(Handle handle)
+ {
+ this.handle = requireNonNull(handle, "handle is null");
+ }
+
+ Long getRowCount(String schema, String tableName)
+ {
+ return handle.createQuery("SELECT NUM_ROWS FROM ALL_TAB_STATISTICS WHERE OWNER = :schema AND TABLE_NAME = :table_name and PARTITION_NAME IS NULL")
+ .bind("schema", schema)
+ .bind("table_name", tableName)
+ .mapTo(Long.class)
+ .findFirst()
+ .orElse(null);
+ }
+
+ List getColumnStatistics(String schema, String tableName)
+ {
+ // [SEP-3425] we are not using ALL_TAB_COL_STATISTICS, here because we observed queries which took multiple minutes when obtaining statistics for partitioned tables.
+ // It adds slight risk, because the statistics-related columns in ALL_TAB_COLUMNS are marked as deprecated and present only for backward
+ // compatibility with Oracle 7 (see: https://docs.oracle.com/cd/B14117_01/server.101/b10755/statviews_1180.htm)
+ return handle.createQuery("SELECT COLUMN_NAME, NUM_NULLS, NUM_DISTINCT, AVG_COL_LEN FROM ALL_TAB_COLUMNS WHERE OWNER = :schema AND TABLE_NAME = :table_name")
+ .bind("schema", schema)
+ .bind("table_name", tableName)
+ .map((rs, ctx) -> new ColumnStatisticsResult(
+ requireNonNull(rs.getString("COLUMN_NAME"), "COLUMN_NAME is null"),
+ Optional.ofNullable(rs.getObject("NUM_NULLS", Long.class)),
+ Optional.ofNullable(rs.getObject("NUM_DISTINCT", Long.class)),
+ Optional.ofNullable(rs.getObject("AVG_COL_LEN", Long.class))))
+ .list();
+ }
+ }
+
+ private static class ColumnStatisticsResult
+ {
+ private final String columnName;
+ private final Optional nullsCount;
+ private final Optional distinctValuesCount;
+ private final Optional averageColumnLength;
+
+ ColumnStatisticsResult(String columnName, Optional nullsCount, Optional distinctValuesCount, Optional averageColumnLength)
+ {
+ this.columnName = columnName;
+ this.nullsCount = nullsCount;
+ this.distinctValuesCount = distinctValuesCount;
+ this.averageColumnLength = averageColumnLength;
+ }
+
+ String getColumnName()
+ {
+ return columnName;
+ }
+
+ Optional getNullsCount()
+ {
+ return nullsCount;
+ }
+
+ Optional getDistinctValuesCount()
+ {
+ return distinctValuesCount;
+ }
+
+ Optional getAverageColumnLength()
+ {
+ return averageColumnLength;
+ }
+ }
}
diff --git a/plugin/trino-oracle/src/main/java/io/trino/plugin/oracle/OracleClientModule.java b/plugin/trino-oracle/src/main/java/io/trino/plugin/oracle/OracleClientModule.java
index befc4338b224..d43897143e07 100644
--- a/plugin/trino-oracle/src/main/java/io/trino/plugin/oracle/OracleClientModule.java
+++ b/plugin/trino-oracle/src/main/java/io/trino/plugin/oracle/OracleClientModule.java
@@ -15,15 +15,17 @@
import com.google.inject.Binder;
import com.google.inject.Key;
-import com.google.inject.Module;
import com.google.inject.Provides;
import com.google.inject.Scopes;
import com.google.inject.Singleton;
+import io.airlift.configuration.AbstractConfigurationAwareModule;
import io.trino.plugin.jdbc.BaseJdbcConfig;
import io.trino.plugin.jdbc.ConnectionFactory;
import io.trino.plugin.jdbc.DriverConnectionFactory;
import io.trino.plugin.jdbc.ForBaseJdbc;
import io.trino.plugin.jdbc.JdbcClient;
+import io.trino.plugin.jdbc.JdbcJoinPushdownSupportModule;
+import io.trino.plugin.jdbc.JdbcStatisticsConfig;
import io.trino.plugin.jdbc.MaxDomainCompactionThreshold;
import io.trino.plugin.jdbc.RetryingConnectionFactory;
import io.trino.plugin.jdbc.credential.CredentialProvider;
@@ -42,16 +44,18 @@
import static io.trino.plugin.oracle.OracleClient.ORACLE_MAX_LIST_EXPRESSIONS;
public class OracleClientModule
- implements Module
+ extends AbstractConfigurationAwareModule
{
@Override
- public void configure(Binder binder)
+ protected void setup(Binder binder)
{
binder.bind(JdbcClient.class).annotatedWith(ForBaseJdbc.class).to(OracleClient.class).in(Scopes.SINGLETON);
bindSessionPropertiesProvider(binder, OracleSessionProperties.class);
configBinder(binder).bindConfig(OracleConfig.class);
+ configBinder(binder).bindConfig(JdbcStatisticsConfig.class);
newOptionalBinder(binder, Key.get(int.class, MaxDomainCompactionThreshold.class)).setBinding().toInstance(ORACLE_MAX_LIST_EXPRESSIONS);
newSetBinder(binder, ConnectorTableFunction.class).addBinding().toProvider(Query.class).in(Scopes.SINGLETON);
+ install(new JdbcJoinPushdownSupportModule());
}
@Provides
diff --git a/plugin/trino-oracle/src/test/java/io/trino/plugin/oracle/BaseOracleConnectorTest.java b/plugin/trino-oracle/src/test/java/io/trino/plugin/oracle/BaseOracleConnectorTest.java
index ce415a83c1c9..33f6a91a0cc2 100644
--- a/plugin/trino-oracle/src/test/java/io/trino/plugin/oracle/BaseOracleConnectorTest.java
+++ b/plugin/trino-oracle/src/test/java/io/trino/plugin/oracle/BaseOracleConnectorTest.java
@@ -48,9 +48,6 @@ public abstract class BaseOracleConnectorTest
protected boolean hasBehavior(TestingConnectorBehavior connectorBehavior)
{
switch (connectorBehavior) {
- case SUPPORTS_TOPN_PUSHDOWN:
- return false;
-
case SUPPORTS_AGGREGATION_PUSHDOWN:
case SUPPORTS_AGGREGATION_PUSHDOWN_STDDEV:
case SUPPORTS_AGGREGATION_PUSHDOWN_VARIANCE:
diff --git a/plugin/trino-oracle/src/test/java/io/trino/plugin/oracle/TestOracleTableStatistics.java b/plugin/trino-oracle/src/test/java/io/trino/plugin/oracle/TestOracleTableStatistics.java
new file mode 100644
index 000000000000..516e7993e364
--- /dev/null
+++ b/plugin/trino-oracle/src/test/java/io/trino/plugin/oracle/TestOracleTableStatistics.java
@@ -0,0 +1,396 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.trino.plugin.oracle;
+
+import com.google.common.collect.ImmutableMap;
+import io.trino.plugin.jdbc.BaseJdbcTableStatisticsTest;
+import io.trino.testing.QueryRunner;
+import io.trino.testing.sql.TestTable;
+import io.trino.tpch.TpchTable;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.sql.CallableStatement;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import static io.trino.plugin.oracle.OracleQueryRunner.createOracleQueryRunner;
+import static io.trino.plugin.oracle.TestingOracleServer.TEST_PASS;
+import static io.trino.plugin.oracle.TestingOracleServer.TEST_USER;
+import static io.trino.testing.sql.TestTable.fromColumns;
+import static java.lang.String.format;
+
+@Test(singleThreaded = true)
+public class TestOracleTableStatistics
+ extends BaseJdbcTableStatisticsTest
+{
+ private TestingOracleServer oracleServer;
+
+ @Override
+ protected QueryRunner createQueryRunner()
+ throws Exception
+ {
+ oracleServer = closeAfterClass(new TestingOracleServer());
+ return createOracleQueryRunner(
+ oracleServer,
+ Map.of(),
+ Map.of(
+ "connection-url", oracleServer.getJdbcUrl(),
+ "connection-user", TEST_USER,
+ "connection-password", TEST_PASS,
+ "case-insensitive-name-matching", "true"),
+ List.of(TpchTable.ORDERS));
+ }
+
+ @Override
+ @Test
+ public void testNotAnalyzed()
+ {
+ String tableName = "test_stats_not_analyzed";
+ assertUpdate("DROP TABLE IF EXISTS " + tableName);
+ computeActual(format("CREATE TABLE %s AS SELECT * FROM tpch.tiny.orders", tableName));
+ try {
+ assertQuery(
+ "SHOW STATS FOR " + tableName,
+ "VALUES " +
+ "('orderkey', null, null, null, null, null, null)," +
+ "('custkey', null, null, null, null, null, null)," +
+ "('orderstatus', null, null, null, null, null, null)," +
+ "('totalprice', null, null, null, null, null, null)," +
+ "('orderdate', null, null, null, null, null, null)," +
+ "('orderpriority', null, null, null, null, null, null)," +
+ "('clerk', null, null, null, null, null, null)," +
+ "('shippriority', null, null, null, null, null, null)," +
+ "('comment', null, null, null, null, null, null)," +
+ "(null, null, null, null, null, null, null)");
+ }
+ finally {
+ assertUpdate("DROP TABLE " + tableName);
+ }
+ }
+
+ @Override
+ @Test
+ public void testBasic()
+ {
+ String tableName = "test_stats_orders";
+ assertUpdate("DROP TABLE IF EXISTS " + tableName);
+ computeActual(format("CREATE TABLE %s AS SELECT * FROM tpch.tiny.orders", tableName));
+ try {
+ gatherStats(tableName);
+ assertQuery(
+ "SHOW STATS FOR " + tableName,
+ "VALUES " +
+ "('orderkey', null, 15000, 0, null, null, null)," +
+ "('custkey', null, 1000, 0, null, null, null)," +
+ "('orderstatus', 30000, 3, 0, null, null, null)," +
+ "('totalprice', null, 14996, 0, null, null, null)," +
+ "('orderdate', null, 2401, 0, null, null, null)," +
+ "('orderpriority', 150000, 5, 0, null, null, null)," +
+ "('clerk', 240000, 1000, 0, null, null, null)," +
+ "('shippriority', null, 1, 0, null, null, null)," +
+ "('comment', 750000, 14995, 0, null, null, null)," +
+ "(null, null, null, null, 15000, null, null)");
+ }
+ finally {
+ assertUpdate("DROP TABLE " + tableName);
+ }
+ }
+
+ @Override
+ @Test
+ public void testAllNulls()
+ {
+ String tableName = "test_stats_table_all_nulls";
+ assertUpdate("DROP TABLE IF EXISTS " + tableName);
+ computeActual(format("CREATE TABLE %s AS SELECT orderkey, custkey, orderpriority, comment FROM tpch.tiny.orders WHERE false", tableName));
+ try {
+ computeActual(format("INSERT INTO %s (orderkey) VALUES NULL, NULL, NULL", tableName));
+ gatherStats(tableName);
+ assertQuery(
+ "SHOW STATS FOR " + tableName,
+ "VALUES " +
+ "('orderkey', 0, 0, 1, null, null, null)," +
+ "('custkey', 0, 0, 1, null, null, null)," +
+ "('orderpriority', 0, 0, 1, null, null, null)," +
+ "('comment', 0, 0, 1, null, null, null)," +
+ "(null, null, null, null, 3, null, null)");
+ }
+ finally {
+ assertUpdate("DROP TABLE " + tableName);
+ }
+ }
+
+ @Override
+ @Test
+ public void testNullsFraction()
+ {
+ String tableName = "test_stats_table_with_nulls";
+ assertUpdate("DROP TABLE IF EXISTS " + tableName);
+ assertUpdate("" +
+ "CREATE TABLE " + tableName + " AS " +
+ "SELECT " +
+ " orderkey, " +
+ " if(orderkey % 3 = 0, NULL, custkey) custkey, " +
+ " if(orderkey % 5 = 0, NULL, orderpriority) orderpriority " +
+ "FROM tpch.tiny.orders",
+ 15000);
+ try {
+ gatherStats(tableName);
+ assertQuery(
+ "SHOW STATS FOR " + tableName,
+ "VALUES " +
+ "('orderkey', null, 15000, 0, null, null, null)," +
+ "('custkey', null, 1000, 0.3333333333333333, null, null, null)," +
+ "('orderpriority', 120000, 5, 0.2, null, null, null)," +
+ "(null, null, null, null, 15000, null, null)");
+ }
+ finally {
+ assertUpdate("DROP TABLE " + tableName);
+ }
+ }
+
+ @Override
+ @Test
+ public void testAverageColumnLength()
+ {
+ String tableName = "test_stats_table_avg_col_len";
+ assertUpdate("DROP TABLE IF EXISTS " + tableName);
+ computeActual("" +
+ "CREATE TABLE " + tableName + " AS SELECT " +
+ " orderkey, " +
+ " 'abc' v3_in_3, " +
+ " CAST('abc' AS varchar(42)) v3_in_42, " +
+ " if(orderkey = 1, '0123456789', NULL) single_10v_value, " +
+ " if(orderkey % 2 = 0, '0123456789', NULL) half_10v_value, " +
+ " if(orderkey % 2 = 0, CAST((1000000 - orderkey) * (1000000 - orderkey) AS varchar(20)), NULL) half_distinct_20v_value, " + // 12 chars each
+ " CAST(NULL AS varchar(10)) all_nulls " +
+ "FROM tpch.tiny.orders " +
+ "ORDER BY orderkey LIMIT 100");
+ try {
+ gatherStats(tableName);
+ assertQuery(
+ "SHOW STATS FOR " + tableName,
+ "VALUES " +
+ "('orderkey', null, 100, 0, null, null, null)," +
+ "('v3_in_3', 400, 1, 0, null, null, null)," +
+ "('v3_in_42', 400, 1, 0, null, null, null)," +
+ "('single_10v_value', 200, 1, 0.99, null, null, null)," +
+ "('half_10v_value', 600, 1, 0.5, null, null, null)," +
+ "('half_distinct_20v_value', 700, 50, 0.5, null, null, null)," +
+ "('all_nulls', 0, 0, 1, null, null, null)," +
+ "(null, null, null, null, 100, null, null)");
+ }
+ finally {
+ assertUpdate("DROP TABLE " + tableName);
+ }
+ }
+
+ @Override
+ @Test
+ public void testPartitionedTable()
+ {
+ String tableName = "test_stats_orders_part";
+ assertUpdate("DROP TABLE IF EXISTS " + tableName);
+ executeInOracle("CREATE TABLE " + tableName + " PARTITION BY HASH(ORDERKEY) PARTITIONS 4 AS SELECT * FROM ORDERS");
+ try {
+ gatherStats(tableName);
+ assertQuery(
+ "SHOW STATS FOR " + tableName,
+ "VALUES " +
+ "('orderkey', null, 15000, 0, null, null, null)," +
+ "('custkey', null, 1000, 0, null, null, null)," +
+ "('orderstatus', 30000, 3, 0, null, null, null)," +
+ "('totalprice', null, 14996, 0, null, null, null)," +
+ "('orderdate', null, 2401, 0, null, null, null)," +
+ "('orderpriority', 150000, 5, 0, null, null, null)," +
+ "('clerk', 240000, 1000, 0, null, null, null)," +
+ "('shippriority', null, 1, 0, null, null, null)," +
+ "('comment', 750000, 14995, 0, null, null, null)," +
+ "(null, null, null, null, 15000, null, null)");
+ }
+ finally {
+ assertUpdate("DROP TABLE " + tableName);
+ }
+ }
+
+ @Override
+ @Test
+ public void testView()
+ {
+ String tableName = "test_stats_view";
+ executeInOracle("CREATE OR REPLACE VIEW " + tableName + " AS SELECT orderkey, custkey, orderpriority, \"COMMENT\" FROM orders");
+ try {
+ assertQuery(
+ "SHOW STATS FOR " + tableName,
+ "VALUES " +
+ "('orderkey', null, null, null, null, null, null)," +
+ "('custkey', null, null, null, null, null, null)," +
+ "('orderpriority', null, null, null, null, null, null)," +
+ "('comment', null, null, null, null, null, null)," +
+ "(null, null, null, null, null, null, null)");
+ // It's not possible to DBMS_STATS.GATHER_TABLE_STATS on a VIEW in Oracle
+ }
+ finally {
+ executeInOracle("DROP VIEW " + tableName);
+ }
+ }
+
+ @Override
+ @Test
+ public void testMaterializedView()
+ {
+ String tableName = "test_stats_materialized_view";
+ executeInOracle("" +
+ "CREATE MATERIALIZED VIEW " + tableName + " " +
+ "BUILD IMMEDIATE REFRESH ON DEMAND " +
+ "AS SELECT orderkey, custkey, orderpriority, \"COMMENT\" FROM orders");
+ try {
+ // MATERIALIZED VIEW has stats gathered implicitly
+ assertQuery(
+ "SHOW STATS FOR " + tableName,
+ "VALUES " +
+ "('orderkey', null, 15000, 0, null, null, null)," +
+ "('custkey', null, 1000, 0, null, null, null)," +
+ "('orderpriority', 150000, 5, 0, null, null, null)," +
+ "('comment', 750000, 14995, 0, null, null, null)," +
+ "(null, null, null, null, 15000, null, null)");
+ }
+ finally {
+ executeInOracle("DROP MATERIALIZED VIEW " + tableName);
+ }
+ }
+
+ @Override
+ @Test(dataProvider = "testCaseColumnNamesDataProvider")
+ public void testCaseColumnNames(String tableName)
+ {
+ executeInOracle("" +
+ "CREATE TABLE " + tableName + " " +
+ "AS SELECT " +
+ " orderkey AS CASE_UNQUOTED_UPPER, " +
+ " custkey AS case_unquoted_lower, " +
+ " orderstatus AS cASe_uNQuoTeD_miXED, " +
+ " totalprice AS \"CASE_QUOTED_UPPER\", " +
+ " orderdate AS \"case_quoted_lower\"," +
+ " orderpriority AS \"CasE_QuoTeD_miXED\" " +
+ "FROM orders");
+ try {
+ gatherStats(tableName);
+ assertQuery(
+ "SHOW STATS FOR " + tableName,
+ "VALUES " +
+ "('case_unquoted_upper', null, 15000, 0, null, null, null)," +
+ "('case_unquoted_lower', null, 1000, 0, null, null, null)," +
+ "('case_unquoted_mixed', 30000, 3, 0, null, null, null)," +
+ "('case_quoted_upper', null, 14996, 0, null, null, null)," +
+ "('case_quoted_lower', null, 2401, 0, null, null, null)," +
+ "('case_quoted_mixed', 150000, 5, 0, null, null, null)," +
+ "(null, null, null, null, 15000, null, null)");
+ }
+ finally {
+ executeInOracle("DROP TABLE " + tableName);
+ }
+ }
+
+ @Override
+ @DataProvider
+ public Object[][] testCaseColumnNamesDataProvider()
+ {
+ return new Object[][] {
+ {"MIXED_UNQUOTED_UPPER"},
+ {"mixed_unquoted_lower"},
+ {"mixed_uNQuoTeD_miXED"},
+ {"\"MIXED_QUOTED_UPPER\""},
+ {"\"mixed_quoted_lower\""},
+ {"\"mixed_QuoTeD_miXED\""},
+ };
+ }
+
+ @Override
+ @Test
+ public void testNumericCornerCases()
+ {
+ try (TestTable table = fromColumns(
+ getQueryRunner()::execute,
+ "numeric_corner_cases_",
+ ImmutableMap.>builder()
+ .put("only_negative_infinity double", List.of("-infinity()", "-infinity()", "-infinity()", "-infinity()"))
+ .put("only_positive_infinity double", List.of("infinity()", "infinity()", "infinity()", "infinity()"))
+ .put("mixed_infinities double", List.of("-infinity()", "infinity()", "-infinity()", "infinity()"))
+ .put("mixed_infinities_and_numbers double", List.of("-infinity()", "infinity()", "-5.0", "7.0"))
+ .put("nans_only double", List.of("nan()", "nan()"))
+ .put("nans_and_numbers double", List.of("nan()", "nan()", "-5.0", "7.0"))
+ .put("large_doubles double", List.of("CAST(-50371909150609548946090.0 AS DOUBLE)", "CAST(50371909150609548946090.0 AS DOUBLE)")) // 2^77 DIV 3
+ .put("short_decimals_big_fraction decimal(16,15)", List.of("-1.234567890123456", "1.234567890123456"))
+ .put("short_decimals_big_integral decimal(16,1)", List.of("-123456789012345.6", "123456789012345.6"))
+ .put("long_decimals_big_fraction decimal(38,37)", List.of("-1.2345678901234567890123456789012345678", "1.2345678901234567890123456789012345678"))
+ .put("long_decimals_middle decimal(38,16)", List.of("-1234567890123456.7890123456789012345678", "1234567890123456.7890123456789012345678"))
+ .put("long_decimals_big_integral decimal(38,1)", List.of("-1234567890123456789012345678901234567.8", "1234567890123456789012345678901234567.8"))
+ .buildOrThrow(),
+ "null")) {
+ gatherStats(table.getName());
+ assertQuery(
+ "SHOW STATS FOR " + table.getName(),
+ "VALUES " +
+ "('only_negative_infinity', null, 1, 0, null, null, null)," +
+ "('only_positive_infinity', null, 1, 0, null, null, null)," +
+ "('mixed_infinities', null, 2, 0, null, null, null)," +
+ "('mixed_infinities_and_numbers', null, 4.0, 0.0, null, null, null)," +
+ "('nans_only', null, 1.0, 0.5, null, null, null)," +
+ "('nans_and_numbers', null, 3.0, 0.0, null, null, null)," +
+ "('large_doubles', null, 2.0, 0.5, null, null, null)," +
+ "('short_decimals_big_fraction', null, 2.0, 0.5, null, null, null)," +
+ "('short_decimals_big_integral', null, 2.0, 0.5, null, null, null)," +
+ "('long_decimals_big_fraction', null, 2.0, 0.5, null, null, null)," +
+ "('long_decimals_middle', null, 2.0, 0.5, null, null, null)," +
+ "('long_decimals_big_integral', null, 2.0, 0.5, null, null, null)," +
+ "(null, null, null, null, 4, null, null)");
+ }
+ }
+
+ @Override
+ protected void gatherStats(String tableName)
+ {
+ executeInOracle(connection -> {
+ try (CallableStatement statement = connection.prepareCall("{CALL DBMS_STATS.GATHER_TABLE_STATS(?, ?)}")) {
+ statement.setString(1, TestingOracleServer.TEST_SCHEMA);
+ statement.setString(2, tableName);
+ statement.execute();
+ }
+ catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ private void executeInOracle(String sql)
+ {
+ oracleServer.execute(sql);
+ }
+
+ private void executeInOracle(Consumer connectionCallback)
+ {
+ try (Connection connection = DriverManager.getConnection(oracleServer.getJdbcUrl(), TestingOracleServer.TEST_USER, TestingOracleServer.TEST_PASS)) {
+ connectionCallback.accept(connection);
+ }
+ catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}