diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab3d071e8de1..b6ffc39a6853 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,6 +136,7 @@ jobs: !presto-mongodb,!presto-kafka,!presto-elasticsearch, !presto-redis, !presto-sqlserver,!presto-postgresql,!presto-mysql, + !presto-oracle, !presto-phoenix,!presto-iceberg, !presto-docs,!presto-server,!presto-server-rpm, !presto-kudu' @@ -159,6 +160,7 @@ jobs: - "presto-sqlserver,presto-postgresql,presto-mysql" - "presto-phoenix,presto-iceberg" - "presto-kudu" + - "presto-oracle" steps: - uses: actions/checkout@v2 - uses: actions/setup-java@v1 diff --git a/pom.xml b/pom.xml index 2412bb661480..dcdf1b64fa2d 100644 --- a/pom.xml +++ b/pom.xml @@ -141,6 +141,7 @@ presto-google-sheets presto-bigquery presto-pinot + presto-oracle diff --git a/presto-docs/src/main/sphinx/connector.rst b/presto-docs/src/main/sphinx/connector.rst index 54426da2300a..430a9078669e 100644 --- a/presto-docs/src/main/sphinx/connector.rst +++ b/presto-docs/src/main/sphinx/connector.rst @@ -27,6 +27,7 @@ from different data sources. connector/memsql connector/mongodb connector/mysql + connector/oracle connector/phoenix connector/pinot connector/postgresql diff --git a/presto-docs/src/main/sphinx/connector/oracle.rst b/presto-docs/src/main/sphinx/connector/oracle.rst new file mode 100644 index 000000000000..fdba3c8f6a04 --- /dev/null +++ b/presto-docs/src/main/sphinx/connector/oracle.rst @@ -0,0 +1,71 @@ +================ +Oracle Connector +================ + +The Oracle connector allows querying and creating tables in an +external Oracle database. This can be used to join data between +different systems like Oracle and Hive, or between two different +Oracle instances. + +Configuration +------------- + +To configure the Oracle connector, create a catalog properties file +in ``etc/catalog`` named, for example, ``oracle.properties``, to +mount the Oracle connector as the ``oracle`` catalog. +Create the file with the following contents, replacing the +connection properties as appropriate for your setup: + +.. code-block:: none + + connector.name=oracle + connection-url=jdbc:oracle:thin:@example.net:1521/ORCLCDB + connection-user=root + connection-password=secret + +Multiple Oracle Servers +^^^^^^^^^^^^^^^^^^^^^^^ + +You can have as many catalogs as you need, so if you have additional +Oracle servers, simply add another properties file to ``etc/catalog`` +with a different name (making sure it ends in ``.properties``). For +example, if you name the property file ``sales.properties``, Presto +will create a catalog named ``sales`` using the configured connector. + +Querying Oracle +--------------- + +The Oracle connector provides a schema for every Oracle *database*. +You can see the available Oracle databases by running ``SHOW SCHEMAS``:: + + SHOW SCHEMAS FROM oracle; + +If you have a Oracle database named ``web``, you can view the tables +in this database by running ``SHOW TABLES``:: + + SHOW TABLES FROM oracle.web; + +You can see a list of the columns in the ``clicks`` table in the ``web`` database +using either of the following:: + + DESCRIBE oracle.web.clicks; + SHOW COLUMNS FROM oracle.web.clicks; + +Finally, you can access the ``clicks`` table in the ``web`` database:: + + SELECT * FROM oracle.web.clicks; + +If you used a different name for your catalog properties file, use +that catalog name instead of ``oracle`` in the above examples. + +Oracle Connector Limitations +---------------------------- + +The following SQL statements are not yet supported: + +* :doc:`/sql/delete` +* :doc:`/sql/grant` +* :doc:`/sql/revoke` +* :doc:`/sql/show-grants` +* :doc:`/sql/show-roles` +* :doc:`/sql/show-role-grants` diff --git a/presto-oracle/pom.xml b/presto-oracle/pom.xml new file mode 100644 index 000000000000..2ac9147e047c --- /dev/null +++ b/presto-oracle/pom.xml @@ -0,0 +1,154 @@ + + + 4.0.0 + + + io.prestosql + presto-root + 334-SNAPSHOT + + + presto-oracle + Presto - Oracle Connector + presto-plugin + + + ${project.parent.basedir} + + + + + com.oracle.ojdbc + ojdbc8 + 19.3.0.0 + + + + io.prestosql + presto-base-jdbc + + + + io.airlift + configuration + + + + io.airlift + log + + + + io.airlift + log-manager + runtime + + + + com.google.guava + guava + + + + com.google.inject + guice + + + + javax.inject + javax.inject + + + + javax.validation + validation-api + + + + io.airlift + units + + + + + io.prestosql + presto-spi + provided + + + + io.airlift + slice + provided + + + + com.fasterxml.jackson.core + jackson-annotations + provided + + + + org.openjdk.jol + jol-core + provided + + + + + org.testng + testng + test + + + + io.prestosql + presto-main + test + + + + io.prestosql + presto-testing + test + + + + io.prestosql.tpch + tpch + test + + + + io.prestosql + presto-tpch + test + + + + org.testcontainers + testcontainers + test + + + + org.testcontainers + oracle-xe + test + + + + io.airlift + testing + test + + + + org.assertj + assertj-core + test + + + diff --git a/presto-oracle/src/main/java/io/prestosql/plugin/oracle/OracleClient.java b/presto-oracle/src/main/java/io/prestosql/plugin/oracle/OracleClient.java new file mode 100644 index 000000000000..eeef6b3b555a --- /dev/null +++ b/presto-oracle/src/main/java/io/prestosql/plugin/oracle/OracleClient.java @@ -0,0 +1,233 @@ +/* + * 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.prestosql.plugin.oracle; + +import io.prestosql.plugin.jdbc.BaseJdbcClient; +import io.prestosql.plugin.jdbc.BaseJdbcConfig; +import io.prestosql.plugin.jdbc.ColumnMapping; +import io.prestosql.plugin.jdbc.ConnectionFactory; +import io.prestosql.plugin.jdbc.JdbcIdentity; +import io.prestosql.plugin.jdbc.JdbcTypeHandle; +import io.prestosql.plugin.jdbc.WriteMapping; +import io.prestosql.spi.PrestoException; +import io.prestosql.spi.connector.ConnectorSession; +import io.prestosql.spi.connector.SchemaTableName; +import io.prestosql.spi.type.BigintType; +import io.prestosql.spi.type.BooleanType; +import io.prestosql.spi.type.Decimals; +import io.prestosql.spi.type.IntegerType; +import io.prestosql.spi.type.SmallintType; +import io.prestosql.spi.type.TimestampType; +import io.prestosql.spi.type.TimestampWithTimeZoneType; +import io.prestosql.spi.type.TinyintType; +import io.prestosql.spi.type.Type; +import io.prestosql.spi.type.VarcharType; + +import javax.inject.Inject; + +import java.math.RoundingMode; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Optional; + +import static io.prestosql.plugin.jdbc.JdbcErrorCode.JDBC_ERROR; +import static io.prestosql.plugin.jdbc.StandardColumnMappings.bigintColumnMapping; +import static io.prestosql.plugin.jdbc.StandardColumnMappings.bigintWriteFunction; +import static io.prestosql.plugin.jdbc.StandardColumnMappings.booleanWriteFunction; +import static io.prestosql.plugin.jdbc.StandardColumnMappings.decimalColumnMapping; +import static io.prestosql.plugin.jdbc.StandardColumnMappings.doubleColumnMapping; +import static io.prestosql.plugin.jdbc.StandardColumnMappings.integerWriteFunction; +import static io.prestosql.plugin.jdbc.StandardColumnMappings.realColumnMapping; +import static io.prestosql.plugin.jdbc.StandardColumnMappings.smallintColumnMapping; +import static io.prestosql.plugin.jdbc.StandardColumnMappings.smallintWriteFunction; +import static io.prestosql.plugin.jdbc.StandardColumnMappings.timestampWriteFunction; +import static io.prestosql.plugin.jdbc.StandardColumnMappings.tinyintWriteFunction; +import static io.prestosql.plugin.jdbc.StandardColumnMappings.varcharColumnMapping; +import static io.prestosql.plugin.jdbc.StandardColumnMappings.varcharWriteFunction; +import static io.prestosql.spi.StandardErrorCode.NOT_SUPPORTED; +import static io.prestosql.spi.type.DecimalType.createDecimalType; +import static io.prestosql.spi.type.VarcharType.createUnboundedVarcharType; +import static io.prestosql.spi.type.VarcharType.createVarcharType; +import static io.prestosql.spi.type.Varchars.isVarcharType; +import static java.lang.String.format; +import static java.util.Locale.ENGLISH; +import static java.util.Objects.requireNonNull; + +public class OracleClient + extends BaseJdbcClient +{ + private final boolean synonymsEnabled; + private final int fetchSize = 1000; + private final int varcharMaxSize; + private final int timestampDefaultPrecision; + private final int numberDefaultScale; + private final RoundingMode numberRoundingMode; + + @Inject + public OracleClient( + BaseJdbcConfig config, + OracleConfig oracleConfig, + ConnectionFactory connectionFactory) + { + super(config, "\"", connectionFactory); + + requireNonNull(oracleConfig, "oracle config is null"); + this.synonymsEnabled = oracleConfig.isSynonymsEnabled(); + this.varcharMaxSize = oracleConfig.getVarcharMaxSize(); + this.timestampDefaultPrecision = oracleConfig.getTimestampDefaultPrecision(); + this.numberDefaultScale = oracleConfig.getNumberDefaultScale(); + this.numberRoundingMode = oracleConfig.getNumberRoundingMode(); + } + + private String[] getTableTypes() + { + if (synonymsEnabled) { + return new String[] {"TABLE", "VIEW", "SYNONYM"}; + } + return new String[] {"TABLE", "VIEW"}; + } + + @Override + protected ResultSet getTables(Connection connection, Optional schemaName, Optional tableName) + throws SQLException + { + DatabaseMetaData metadata = connection.getMetaData(); + String escape = metadata.getSearchStringEscape(); + return metadata.getTables( + connection.getCatalog(), + escapeNamePattern(schemaName, escape).orElse(null), + escapeNamePattern(tableName, escape).orElse(null), + getTableTypes()); + } + + @Override + public PreparedStatement getPreparedStatement(Connection connection, String sql) + throws SQLException + { + PreparedStatement statement = connection.prepareStatement(sql); + statement.setFetchSize(fetchSize); + return statement; + } + + @Override + protected String generateTemporaryTableName() + { + return "presto_tmp_" + System.nanoTime(); + } + + @Override + protected void renameTable(JdbcIdentity identity, String catalogName, String schemaName, String tableName, SchemaTableName newTable) + { + if (!schemaName.equalsIgnoreCase(newTable.getSchemaName())) { + throw new PrestoException(NOT_SUPPORTED, "Table rename across schemas is not supported in Oracle"); + } + + String newTableName = newTable.getTableName().toUpperCase(ENGLISH); + String sql = format( + "ALTER TABLE %s RENAME TO %s", + quoted(catalogName, schemaName, tableName), + quoted(newTableName)); + + try (Connection connection = connectionFactory.openConnection(identity)) { + execute(connection, sql); + } + catch (SQLException e) { + throw new PrestoException(JDBC_ERROR, e); + } + } + + @Override + public void createSchema(JdbcIdentity identity, String schemaName) + { + // ORA-02420: missing schema authorization clause + throw new PrestoException(NOT_SUPPORTED, "This connector does not support creating schemas"); + } + + @Override + public Optional toPrestoType(ConnectorSession session, Connection connection, JdbcTypeHandle typeHandle) + { + int columnSize = typeHandle.getColumnSize(); + + switch (typeHandle.getJdbcType()) { + case Types.CLOB: + return Optional.of(varcharColumnMapping(createUnboundedVarcharType())); + case Types.SMALLINT: + return Optional.of(smallintColumnMapping()); + case Types.FLOAT: + if (columnSize == 63) { + return Optional.of(realColumnMapping()); + } + return Optional.of(doubleColumnMapping()); + case Types.NUMERIC: + int precision = columnSize == 0 ? Decimals.MAX_PRECISION : columnSize; + int scale = typeHandle.getDecimalDigits(); + + if (scale == 0) { + return Optional.of(bigintColumnMapping()); + } + if (scale < 0 || scale > precision) { + return Optional.of(decimalColumnMapping(createDecimalType(precision, numberDefaultScale), numberRoundingMode)); + } + + return Optional.of(decimalColumnMapping(createDecimalType(precision, scale), numberRoundingMode)); + case Types.LONGVARCHAR: + if (columnSize > VarcharType.MAX_LENGTH || columnSize == 0) { + return Optional.of(varcharColumnMapping(createUnboundedVarcharType())); + } + return Optional.of(varcharColumnMapping(createVarcharType(columnSize))); + case Types.VARCHAR: + return Optional.of(varcharColumnMapping(createVarcharType(columnSize))); + } + return super.toPrestoType(session, connection, typeHandle); + } + + @Override + public WriteMapping toWriteMapping(ConnectorSession session, Type type) + { + if (type instanceof BooleanType) { + return WriteMapping.booleanMapping("number(1,0)", booleanWriteFunction()); + } + if (type instanceof TinyintType) { + return WriteMapping.longMapping("number(3,0)", tinyintWriteFunction()); + } + if (type instanceof SmallintType) { + return WriteMapping.longMapping("number(5,0)", smallintWriteFunction()); + } + if (type instanceof IntegerType) { + return WriteMapping.longMapping("number(10,0)", integerWriteFunction()); + } + if (type instanceof BigintType) { + return WriteMapping.longMapping("number(19,0)", bigintWriteFunction()); + } + if (type instanceof TimestampType) { + return WriteMapping.longMapping(format("timestamp(%s)", timestampDefaultPrecision), timestampWriteFunction(session)); + } + if (type instanceof TimestampWithTimeZoneType) { + return WriteMapping.longMapping(format("timestamp(%s) with time zone", timestampDefaultPrecision), timestampWriteFunction(session)); + } + if (isVarcharType(type)) { + if (((VarcharType) type).isUnbounded()) { + return super.toWriteMapping(session, createVarcharType(varcharMaxSize)); + } + if (((VarcharType) type).getBoundedLength() > varcharMaxSize) { + return WriteMapping.sliceMapping("clob", varcharWriteFunction()); + } + } + return super.toWriteMapping(session, type); + } +} diff --git a/presto-oracle/src/main/java/io/prestosql/plugin/oracle/OracleClientModule.java b/presto-oracle/src/main/java/io/prestosql/plugin/oracle/OracleClientModule.java new file mode 100644 index 000000000000..0d9e6fcd90aa --- /dev/null +++ b/presto-oracle/src/main/java/io/prestosql/plugin/oracle/OracleClientModule.java @@ -0,0 +1,60 @@ +/* + * 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.prestosql.plugin.oracle; + +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Provides; +import com.google.inject.Scopes; +import com.google.inject.Singleton; +import io.prestosql.plugin.jdbc.BaseJdbcConfig; +import io.prestosql.plugin.jdbc.ConnectionFactory; +import io.prestosql.plugin.jdbc.DriverConnectionFactory; +import io.prestosql.plugin.jdbc.ForBaseJdbc; +import io.prestosql.plugin.jdbc.JdbcClient; +import io.prestosql.plugin.jdbc.credential.CredentialProvider; +import oracle.jdbc.OracleConnection; +import oracle.jdbc.OracleDriver; + +import java.sql.SQLException; +import java.util.Properties; + +import static io.airlift.configuration.ConfigBinder.configBinder; + +public class OracleClientModule + implements Module +{ + @Override + public void configure(Binder binder) + { + binder.bind(JdbcClient.class).annotatedWith(ForBaseJdbc.class).to(OracleClient.class).in(Scopes.SINGLETON); + configBinder(binder).bindConfig(OracleConfig.class); + } + + @Provides + @Singleton + @ForBaseJdbc + public static ConnectionFactory connectionFactory(BaseJdbcConfig config, CredentialProvider credentialProvider, OracleConfig oracleConfig) + throws SQLException + { + Properties connectionProperties = new Properties(); + connectionProperties.setProperty(OracleConnection.CONNECTION_PROPERTY_INCLUDE_SYNONYMS, String.valueOf(oracleConfig.isSynonymsEnabled())); + + return new DriverConnectionFactory( + new OracleDriver(), + config.getConnectionUrl(), + connectionProperties, + credentialProvider); + } +} diff --git a/presto-oracle/src/main/java/io/prestosql/plugin/oracle/OracleConfig.java b/presto-oracle/src/main/java/io/prestosql/plugin/oracle/OracleConfig.java new file mode 100644 index 000000000000..ca1e97c03333 --- /dev/null +++ b/presto-oracle/src/main/java/io/prestosql/plugin/oracle/OracleConfig.java @@ -0,0 +1,98 @@ +/* + * 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.prestosql.plugin.oracle; + +import io.airlift.configuration.Config; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +import java.math.RoundingMode; + +public class OracleConfig +{ + private boolean synonymsEnabled; + private int varcharMaxSize = 4000; + private int timestampDefaultPrecision = 6; + private int numberDefaultScale = 10; + private RoundingMode numberRoundingMode = RoundingMode.HALF_UP; + + @NotNull + public boolean isSynonymsEnabled() + { + return synonymsEnabled; + } + + @Config("oracle.synonyms.enabled") + public OracleConfig setSynonymsEnabled(boolean enabled) + { + this.synonymsEnabled = enabled; + return this; + } + + @Min(0) + @Max(38) + public int getNumberDefaultScale() + { + return numberDefaultScale; + } + + @Config("oracle.number.default-scale") + public OracleConfig setNumberDefaultScale(Integer numberDefaultScale) + { + this.numberDefaultScale = numberDefaultScale; + return this; + } + + @NotNull + public RoundingMode getNumberRoundingMode() + { + return numberRoundingMode; + } + + @Config("oracle.number.rounding-mode") + public OracleConfig setNumberRoundingMode(RoundingMode numberRoundingMode) + { + this.numberRoundingMode = numberRoundingMode; + return this; + } + + @Min(4000) + public int getVarcharMaxSize() + { + return varcharMaxSize; + } + + @Config("oracle.varchar.max-size") + public OracleConfig setVarcharMaxSize(int varcharMaxSize) + { + this.varcharMaxSize = varcharMaxSize; + return this; + } + + @Min(0) + @Max(9) + public int getTimestampDefaultPrecision() + { + return timestampDefaultPrecision; + } + + @Config("oracle.timestamp.precision") + public OracleConfig setTimestampDefaultPrecision(int timestampDefaultPrecision) + { + this.timestampDefaultPrecision = timestampDefaultPrecision; + return this; + } +} diff --git a/presto-oracle/src/main/java/io/prestosql/plugin/oracle/OraclePlugin.java b/presto-oracle/src/main/java/io/prestosql/plugin/oracle/OraclePlugin.java new file mode 100644 index 000000000000..ef0cb4d843c3 --- /dev/null +++ b/presto-oracle/src/main/java/io/prestosql/plugin/oracle/OraclePlugin.java @@ -0,0 +1,25 @@ +/* + * 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.prestosql.plugin.oracle; + +import io.prestosql.plugin.jdbc.JdbcPlugin; + +public class OraclePlugin + extends JdbcPlugin +{ + public OraclePlugin() + { + super("oracle", new OracleClientModule()); + } +} diff --git a/presto-oracle/src/test/java/io/prestosql/plugin/oracle/OracleQueryRunner.java b/presto-oracle/src/test/java/io/prestosql/plugin/oracle/OracleQueryRunner.java new file mode 100644 index 000000000000..40eb6f8dbacb --- /dev/null +++ b/presto-oracle/src/test/java/io/prestosql/plugin/oracle/OracleQueryRunner.java @@ -0,0 +1,101 @@ +/* + * 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.prestosql.plugin.oracle; + +import com.google.common.collect.ImmutableList; +import io.airlift.log.Logger; +import io.airlift.log.Logging; +import io.prestosql.Session; +import io.prestosql.plugin.tpch.TpchPlugin; +import io.prestosql.testing.DistributedQueryRunner; +import io.prestosql.tpch.TpchTable; + +import java.util.HashMap; +import java.util.Map; + +import static io.airlift.testing.Closeables.closeAllSuppress; +import static io.prestosql.plugin.oracle.TestingOracleServer.TEST_PASS; +import static io.prestosql.plugin.oracle.TestingOracleServer.TEST_SCHEMA; +import static io.prestosql.plugin.oracle.TestingOracleServer.TEST_USER; +import static io.prestosql.plugin.tpch.TpchMetadata.TINY_SCHEMA_NAME; +import static io.prestosql.testing.QueryAssertions.copyTpchTables; +import static io.prestosql.testing.TestingSession.testSessionBuilder; + +public final class OracleQueryRunner +{ + private OracleQueryRunner() {} + + public static DistributedQueryRunner createOracleQueryRunner(TestingOracleServer server) + throws Exception + { + return createOracleQueryRunner(server, ImmutableList.of()); + } + + public static DistributedQueryRunner createOracleQueryRunner(TestingOracleServer server, TpchTable... tables) + throws Exception + { + return createOracleQueryRunner(server, ImmutableList.copyOf(tables)); + } + + public static DistributedQueryRunner createOracleQueryRunner(TestingOracleServer server, Iterable> tables) + throws Exception + { + DistributedQueryRunner queryRunner = null; + try { + queryRunner = DistributedQueryRunner.builder(createSession()).build(); + + queryRunner.installPlugin(new TpchPlugin()); + queryRunner.createCatalog("tpch", "tpch"); + + Map connectorProperties = new HashMap<>(); + connectorProperties.putIfAbsent("connection-url", server.getJdbcUrl()); + connectorProperties.putIfAbsent("connection-user", TEST_USER); + connectorProperties.putIfAbsent("connection-password", TEST_PASS); + connectorProperties.putIfAbsent("allow-drop-table", "true"); + + queryRunner.installPlugin(new OraclePlugin()); + queryRunner.createCatalog("oracle", "oracle", connectorProperties); + + copyTpchTables(queryRunner, "tpch", TINY_SCHEMA_NAME, createSession(), tables); + + return queryRunner; + } + catch (Throwable e) { + closeAllSuppress(e, queryRunner, server); + throw e; + } + } + + public static Session createSession() + { + return testSessionBuilder() + .setCatalog("oracle") + .setSchema(TEST_SCHEMA) + .build(); + } + + public static void main(String[] args) + throws Exception + { + Logging.initialize(); + + DistributedQueryRunner queryRunner = createOracleQueryRunner( + new TestingOracleServer(), + TpchTable.getTables()); + + Logger log = Logger.get(OracleQueryRunner.class); + log.info("======== SERVER STARTED ========"); + log.info("\n====\n%s\n====", queryRunner.getCoordinator().getBaseUrl()); + } +} diff --git a/presto-oracle/src/test/java/io/prestosql/plugin/oracle/TestOracleConfig.java b/presto-oracle/src/test/java/io/prestosql/plugin/oracle/TestOracleConfig.java new file mode 100644 index 000000000000..ace39f09109e --- /dev/null +++ b/presto-oracle/src/test/java/io/prestosql/plugin/oracle/TestOracleConfig.java @@ -0,0 +1,59 @@ +/* + * 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.prestosql.plugin.oracle; + +import com.google.common.collect.ImmutableMap; +import org.testng.annotations.Test; + +import java.math.RoundingMode; +import java.util.Map; + +import static io.airlift.configuration.testing.ConfigAssertions.assertFullMapping; +import static io.airlift.configuration.testing.ConfigAssertions.assertRecordedDefaults; +import static io.airlift.configuration.testing.ConfigAssertions.recordDefaults; + +public class TestOracleConfig +{ + @Test + public void testDefaults() + { + assertRecordedDefaults(recordDefaults(OracleConfig.class) + .setSynonymsEnabled(false) + .setVarcharMaxSize(4000) + .setTimestampDefaultPrecision(6) + .setNumberDefaultScale(10) + .setNumberRoundingMode(RoundingMode.HALF_UP)); + } + + @Test + public void testExplicitPropertyMappings() + { + Map properties = new ImmutableMap.Builder() + .put("oracle.synonyms.enabled", "true") + .put("oracle.varchar.max-size", "10000") + .put("oracle.timestamp.precision", "3") + .put("oracle.number.default-scale", "2") + .put("oracle.number.rounding-mode", "CEILING") + .build(); + + OracleConfig expected = new OracleConfig() + .setSynonymsEnabled(true) + .setVarcharMaxSize(10000) + .setTimestampDefaultPrecision(3) + .setNumberDefaultScale(2) + .setNumberRoundingMode(RoundingMode.CEILING); + + assertFullMapping(properties, expected); + } +} diff --git a/presto-oracle/src/test/java/io/prestosql/plugin/oracle/TestOracleDistributedQueries.java b/presto-oracle/src/test/java/io/prestosql/plugin/oracle/TestOracleDistributedQueries.java new file mode 100644 index 000000000000..f0aa6130e683 --- /dev/null +++ b/presto-oracle/src/test/java/io/prestosql/plugin/oracle/TestOracleDistributedQueries.java @@ -0,0 +1,404 @@ +/* + * 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.prestosql.plugin.oracle; + +import io.prestosql.Session; +import io.prestosql.execution.QueryInfo; +import io.prestosql.testing.AbstractTestDistributedQueries; +import io.prestosql.testing.DistributedQueryRunner; +import io.prestosql.testing.MaterializedResult; +import io.prestosql.testing.QueryRunner; +import io.prestosql.testing.ResultWithQueryId; +import io.prestosql.testing.sql.TestTable; +import io.prestosql.tpch.TpchTable; +import org.testng.annotations.AfterClass; +import org.testng.annotations.Test; + +import java.util.Optional; + +import static io.prestosql.spi.type.VarcharType.VARCHAR; +import static io.prestosql.testing.MaterializedResult.resultBuilder; +import static io.prestosql.testing.sql.TestTable.randomTableSuffix; +import static java.lang.String.format; +import static java.util.stream.Collectors.joining; +import static java.util.stream.IntStream.range; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class TestOracleDistributedQueries + extends AbstractTestDistributedQueries +{ + private TestingOracleServer oracleServer; + + @Override + protected QueryRunner createQueryRunner() + throws Exception + { + this.oracleServer = new TestingOracleServer(); + return OracleQueryRunner.createOracleQueryRunner( + oracleServer, + TpchTable.getTables()); + } + + @AfterClass(alwaysRun = true) + public final void destroy() + { + if (oracleServer != null) { + oracleServer.close(); + } + } + + @Override + protected boolean supportsViews() + { + return false; + } + + @Override + protected boolean supportsArrays() + { + return false; + } + + @Override + public void testCommentTable() + { + // table comment not supported + } + + @Override + public void testDelete() + { + // delete is not supported + } + + @Override + public void testCreateSchema() + { + // schema creation is not supported + } + + @Override + protected String dataMappingTableName(String prestoTypeName) + { + return "presto_tmp_" + System.nanoTime(); + } + + @Override + protected Optional filterDataMappingSmokeTestData(DataMappingTestSetup dataMappingTestSetup) + { + String typeName = dataMappingTestSetup.getPrestoTypeName(); + if (typeName.equals("timestamp with time zone") + || typeName.equals("time") + || typeName.equals("varbinary") + || typeName.equals("char(3)")) { + return Optional.empty(); + } + + return Optional.of(dataMappingTestSetup); + } + + @Override + protected TestTable createTableWithDefaultColumns() + { + return new TestTable( + oracleServer::execute, + "tpch.table", + "(col_required decimal(20,0) NOT NULL," + + "col_nullable decimal(20,0)," + + "col_default decimal(20,0) DEFAULT 43," + + "col_nonnull_default decimal(20,0) DEFAULT 42 NOT NULL ," + + "col_required2 decimal(20,0) NOT NULL)"); + } + + @Test + @Override + public void testLargeIn() + { + int numberOfElements = 1000; + String longValues = range(0, numberOfElements) + .mapToObj(Integer::toString) + .collect(joining(", ")); + assertQuery("SELECT orderkey FROM orders WHERE orderkey IN (" + longValues + ")"); + assertQuery("SELECT orderkey FROM orders WHERE orderkey NOT IN (" + longValues + ")"); + + assertQuery("SELECT orderkey FROM orders WHERE orderkey IN (mod(1000, orderkey), " + longValues + ")"); + assertQuery("SELECT orderkey FROM orders WHERE orderkey NOT IN (mod(1000, orderkey), " + longValues + ")"); + + String arrayValues = range(0, numberOfElements) + .mapToObj(i -> format("ARRAY[%s, %s, %s]", i, i + 1, i + 2)) + .collect(joining(", ")); + assertQuery("SELECT ARRAY[0, 0, 0] in (ARRAY[0, 0, 0], " + arrayValues + ")", "values true"); + assertQuery("SELECT ARRAY[0, 0, 0] in (" + arrayValues + ")", "values false"); + } + + @Test + @Override + public void testCreateTableAsSelect() + { + String tableName = "test_ctas" + randomTableSuffix(); + assertUpdate("CREATE TABLE IF NOT EXISTS " + tableName + " AS SELECT name, regionkey FROM nation", "SELECT count(*) FROM nation"); + assertTableColumnNames(tableName, "name", "regionkey"); + assertUpdate("DROP TABLE " + tableName); + + // Some connectors support CREATE TABLE AS but not the ordinary CREATE TABLE. Let's test CTAS IF NOT EXISTS with a table that is guaranteed to exist. + assertUpdate("CREATE TABLE IF NOT EXISTS nation AS SELECT orderkey, discount FROM lineitem", 0); + assertTableColumnNames("nation", "nationkey", "name", "regionkey", "comment"); + + assertCreateTableAsSelect( + "SELECT orderdate, orderkey, totalprice FROM orders", + "SELECT count(*) FROM orders"); + + assertCreateTableAsSelect( + "SELECT orderstatus, sum(totalprice) x FROM orders GROUP BY orderstatus", + "SELECT count(DISTINCT orderstatus) FROM orders"); + + assertCreateTableAsSelect( + "SELECT count(*) x FROM lineitem JOIN orders ON lineitem.orderkey = orders.orderkey", + "SELECT 1"); + + assertCreateTableAsSelect( + "SELECT orderkey FROM orders ORDER BY orderkey LIMIT 10", + "SELECT 10"); + + // this is comment because presto creates a table of varchar(1) and in oracle this unicode occupy 3 char + // we should try to get bytes instead of size ?? + /* + assertCreateTableAsSelect( + "SELECT '\u2603' unicode", + "SELECT 1"); + */ + assertCreateTableAsSelect( + "SELECT * FROM orders WITH DATA", + "SELECT * FROM orders", + "SELECT count(*) FROM orders"); + + assertCreateTableAsSelect( + "SELECT * FROM orders WITH NO DATA", + "SELECT * FROM orders LIMIT 0", + "SELECT 0"); + + // Tests for CREATE TABLE with UNION ALL: exercises PushTableWriteThroughUnion optimizer + + assertCreateTableAsSelect( + "SELECT orderdate, orderkey, totalprice FROM orders WHERE orderkey % 2 = 0 UNION ALL " + + "SELECT orderdate, orderkey, totalprice FROM orders WHERE orderkey % 2 = 1", + "SELECT orderdate, orderkey, totalprice FROM orders", + "SELECT count(*) FROM orders"); + + assertCreateTableAsSelect( + Session.builder(getSession()).setSystemProperty("redistribute_writes", "true").build(), + "SELECT CAST(orderdate AS DATE) orderdate, orderkey, totalprice FROM orders UNION ALL " + + "SELECT DATE '2000-01-01', 1234567890, 1.23", + "SELECT orderdate, orderkey, totalprice FROM orders UNION ALL " + + "SELECT DATE '2000-01-01', 1234567890, 1.23", + "SELECT count(*) + 1 FROM orders"); + + assertCreateTableAsSelect( + Session.builder(getSession()).setSystemProperty("redistribute_writes", "false").build(), + "SELECT CAST(orderdate AS DATE) orderdate, orderkey, totalprice FROM orders UNION ALL " + + "SELECT DATE '2000-01-01', 1234567890, 1.23", + "SELECT orderdate, orderkey, totalprice FROM orders UNION ALL " + + "SELECT DATE '2000-01-01', 1234567890, 1.23", + "SELECT count(*) + 1 FROM orders"); + + assertExplainAnalyze("EXPLAIN ANALYZE CREATE TABLE " + tableName + " AS SELECT orderstatus FROM orders"); + assertQuery("SELECT * from " + tableName, "SELECT orderstatus FROM orders"); + assertUpdate("DROP TABLE " + tableName); + } + + @Test + @Override + public void testCreateTable() + { + assertUpdate("CREATE TABLE test_create (a bigint, b double, c varchar)"); + assertTrue(getQueryRunner().tableExists(getSession(), "test_create")); + assertTableColumnNames("test_create", "a", "b", "c"); + + assertUpdate("DROP TABLE test_create"); + assertFalse(getQueryRunner().tableExists(getSession(), "test_create")); + + assertQueryFails("CREATE TABLE test_create (a bad_type)", ".* Unknown type 'bad_type' for column 'a'"); + assertFalse(getQueryRunner().tableExists(getSession(), "test_create")); + + // Replace test_create_table_if_not_exists with test_create_table_if_not_exist to fetch max size naming on oracle + assertUpdate("CREATE TABLE test_create_table_if_not_exist (a bigint, b varchar, c double)"); + assertTrue(getQueryRunner().tableExists(getSession(), "test_create_table_if_not_exist")); + assertTableColumnNames("test_create_table_if_not_exist", "a", "b", "c"); + + assertUpdate("CREATE TABLE IF NOT EXISTS test_create_table_if_not_exist (d bigint, e varchar)"); + assertTrue(getQueryRunner().tableExists(getSession(), "test_create_table_if_not_exist")); + assertTableColumnNames("test_create_table_if_not_exist", "a", "b", "c"); + + assertUpdate("DROP TABLE test_create_table_if_not_exist"); + assertFalse(getQueryRunner().tableExists(getSession(), "test_create_table_if_not_exist")); + + // Test CREATE TABLE LIKE + assertUpdate("CREATE TABLE test_create_original (a bigint, b double, c varchar)"); + assertTrue(getQueryRunner().tableExists(getSession(), "test_create_original")); + assertTableColumnNames("test_create_original", "a", "b", "c"); + + assertUpdate("CREATE TABLE test_create_like (LIKE test_create_original, d boolean, e varchar)"); + assertTrue(getQueryRunner().tableExists(getSession(), "test_create_like")); + assertTableColumnNames("test_create_like", "a", "b", "c", "d", "e"); + + assertUpdate("DROP TABLE test_create_original"); + assertFalse(getQueryRunner().tableExists(getSession(), "test_create_original")); + + assertUpdate("DROP TABLE test_create_like"); + assertFalse(getQueryRunner().tableExists(getSession(), "test_create_like")); + } + + @Test + @Override + public void testSymbolAliasing() + { + // Replace tablename to less than 30chars, max size naming on oracle + String tableName = "symbol_aliasing" + System.currentTimeMillis(); + assertUpdate("CREATE TABLE " + tableName + " AS SELECT 1 foo_1, 2 foo_2_4", 1); + assertQuery("SELECT foo_1, foo_2_4 FROM " + tableName, "SELECT 1, 2"); + assertUpdate("DROP TABLE " + tableName); + } + + @Test + @Override + public void testRenameColumn() + { + // Replace tablename to less than 30chars, max size naming on oracle + String tableName = "test_renamecol_" + System.currentTimeMillis(); + assertUpdate("CREATE TABLE " + tableName + " AS SELECT 'some value' x", 1); + + assertUpdate("ALTER TABLE " + tableName + " RENAME COLUMN x TO y"); + assertQuery("SELECT y FROM " + tableName, "VALUES 'some value'"); + + assertUpdate("ALTER TABLE " + tableName + " RENAME COLUMN y TO Z"); // 'Z' is upper-case, not delimited + assertQuery( + "SELECT z FROM " + tableName, // 'z' is lower-case, not delimited + "VALUES 'some value'"); + + // There should be exactly one column + assertQuery("SELECT * FROM " + tableName, "VALUES 'some value'"); + + assertUpdate("DROP TABLE " + tableName); + } + + @Test + @Override + public void testQueryLoggingCount() + { + // table name has more than 30chars,max size naming on oracle. + // but as this methods call on private methods we disable it + } + + @Test + @Override + public void testWrittenStats() + { + // Replace tablename to fetch max size naming on oracle + String tableName = "written_stats_" + System.currentTimeMillis(); + String sql = "CREATE TABLE " + tableName + " AS SELECT * FROM nation"; + DistributedQueryRunner distributedQueryRunner = (DistributedQueryRunner) getQueryRunner(); + ResultWithQueryId resultResultWithQueryId = distributedQueryRunner.executeWithQueryId(getSession(), sql); + QueryInfo queryInfo = distributedQueryRunner.getCoordinator().getQueryManager().getFullQueryInfo(resultResultWithQueryId.getQueryId()); + + assertEquals(queryInfo.getQueryStats().getOutputPositions(), 1L); + assertEquals(queryInfo.getQueryStats().getWrittenPositions(), 25L); + assertTrue(queryInfo.getQueryStats().getLogicalWrittenDataSize().toBytes() > 0L); + + sql = "INSERT INTO " + tableName + " SELECT * FROM nation LIMIT 10"; + resultResultWithQueryId = distributedQueryRunner.executeWithQueryId(getSession(), sql); + queryInfo = distributedQueryRunner.getCoordinator().getQueryManager().getFullQueryInfo(resultResultWithQueryId.getQueryId()); + + assertEquals(queryInfo.getQueryStats().getOutputPositions(), 1L); + assertEquals(queryInfo.getQueryStats().getWrittenPositions(), 10L); + assertTrue(queryInfo.getQueryStats().getLogicalWrittenDataSize().toBytes() > 0L); + + assertUpdate("DROP TABLE " + tableName); + } + + @Test + @Override + public void testShowColumns() + { + MaterializedResult actual = computeActual("SHOW COLUMNS FROM orders"); + + MaterializedResult expectedParametrizedVarchar = resultBuilder(getSession(), VARCHAR, VARCHAR, VARCHAR, VARCHAR) + .row("orderkey", "bigint", "", "") + .row("custkey", "bigint", "", "") + .row("orderstatus", "varchar(1)", "", "") + .row("totalprice", "double", "", "") + .row("orderdate", "timestamp", "", "") + .row("orderpriority", "varchar(15)", "", "") + .row("clerk", "varchar(15)", "", "") + .row("shippriority", "bigint", "", "") + .row("comment", "varchar(79)", "", "") + .build(); + + // Until we migrate all connectors to parametrized varchar we check two options + assertTrue(actual.equals(expectedParametrizedVarchar), + format("%s does not matches %s", actual, expectedParametrizedVarchar)); + } + + @Test + @Override + public void testInsertUnicode() + { + // unicode not working correctly as one unicode char takes more than one byte + } + + @Test + @Override + public void testInsertWithCoercion() + { + assertUpdate("CREATE TABLE test_insert_with_coercion (" + + "tinyint_column TINYINT, " + + "integer_column INTEGER, " + + "decimal_column DECIMAL(5, 3), " + + "real_column REAL, " + + "char_column CHAR(3), " + + "bounded_varchar_column VARCHAR(3), " + + "unbounded_varchar_column VARCHAR, " + + "date_column DATE)"); + + assertUpdate("INSERT INTO test_insert_with_coercion (tinyint_column, integer_column, decimal_column, real_column) VALUES (1e0, 2e0, 3e0, 4e0)", 1); + assertUpdate("INSERT INTO test_insert_with_coercion (char_column, bounded_varchar_column, unbounded_varchar_column) VALUES (CAST('aa ' AS varchar), CAST('aa ' AS varchar), CAST('aa ' AS varchar))", 1); + assertUpdate("INSERT INTO test_insert_with_coercion (char_column, bounded_varchar_column, unbounded_varchar_column) VALUES (NULL, NULL, NULL)", 1); + assertUpdate("INSERT INTO test_insert_with_coercion (char_column, bounded_varchar_column, unbounded_varchar_column) VALUES (CAST(NULL AS varchar), CAST(NULL AS varchar), CAST(NULL AS varchar))", 1); + assertUpdate("INSERT INTO test_insert_with_coercion (date_column) VALUES (TIMESTAMP '2019-11-18 22:13:40')", 1); + + // at oracle date field has time part to + assertQuery( + "SELECT * FROM test_insert_with_coercion", + "VALUES " + + "(1, 2, 3, 4, NULL, NULL, NULL, NULL), " + + "(NULL, NULL, NULL, NULL, 'aa ', 'aa ', 'aa ', NULL), " + + "(NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL), " + + "(NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL), " + + "(NULL, NULL, NULL, NULL, NULL, NULL, NULL, TIMESTAMP '2019-11-18 22:13:40')"); + + // this wont fail + //assertQueryFails("INSERT INTO test_insert_with_coercion (integer_column) VALUES (3e9)", "Out of range for integer: 3.0E9"); + assertQueryFails("INSERT INTO test_insert_with_coercion (char_column) VALUES ('abcd')", "Cannot truncate non-space characters on INSERT"); + assertQueryFails("INSERT INTO test_insert_with_coercion (bounded_varchar_column) VALUES ('abcd')", "Cannot truncate non-space characters on INSERT"); + + assertUpdate("DROP TABLE test_insert_with_coercion"); + } + + @Override + public void testColumnName(String columnName) + { + // table names generated has more than 30chars, max size naming on oracle. + } +} diff --git a/presto-oracle/src/test/java/io/prestosql/plugin/oracle/TestOracleIntegrationSmokeTest.java b/presto-oracle/src/test/java/io/prestosql/plugin/oracle/TestOracleIntegrationSmokeTest.java new file mode 100644 index 000000000000..e5aebffa4304 --- /dev/null +++ b/presto-oracle/src/test/java/io/prestosql/plugin/oracle/TestOracleIntegrationSmokeTest.java @@ -0,0 +1,83 @@ +/* + * 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.prestosql.plugin.oracle; + +import io.prestosql.testing.AbstractTestIntegrationSmokeTest; +import io.prestosql.testing.MaterializedResult; +import io.prestosql.testing.QueryRunner; +import org.testng.annotations.AfterClass; +import org.testng.annotations.Test; + +import static io.prestosql.spi.type.VarcharType.VARCHAR; +import static io.prestosql.testing.assertions.Assert.assertEquals; +import static io.prestosql.tpch.TpchTable.ORDERS; +import static org.assertj.core.api.Assertions.assertThat; + +public class TestOracleIntegrationSmokeTest + extends AbstractTestIntegrationSmokeTest +{ + private TestingOracleServer oracleServer; + + @Override + protected QueryRunner createQueryRunner() + throws Exception + { + oracleServer = new TestingOracleServer(); + return OracleQueryRunner.createOracleQueryRunner(oracleServer, ORDERS); + } + + @AfterClass(alwaysRun = true) + public final void destroy() + { + oracleServer.close(); + } + + @Test + @Override + public void testDescribeTable() + { + MaterializedResult expectedColumns = MaterializedResult.resultBuilder(getQueryRunner().getDefaultSession(), VARCHAR, VARCHAR, VARCHAR, VARCHAR) + .row("orderkey", "bigint", "", "") + .row("custkey", "bigint", "", "") + .row("orderstatus", "varchar(1)", "", "") + .row("totalprice", "double", "", "") + .row("orderdate", "timestamp", "", "") + .row("orderpriority", "varchar(15)", "", "") + .row("clerk", "varchar(15)", "", "") + .row("shippriority", "bigint", "", "") + .row("comment", "varchar(79)", "", "") + .build(); + MaterializedResult actualColumns = computeActual("DESCRIBE orders"); + assertEquals(actualColumns, expectedColumns); + } + + @Test + @Override + public void testShowCreateTable() + { + assertThat((String) computeActual("SHOW CREATE TABLE orders").getOnlyValue()) + // If the connector reports additional column properties, the expected value needs to be adjusted in the test subclass + .matches("CREATE TABLE \\w+\\.\\w+\\.orders \\Q(\n" + + " orderkey bigint,\n" + + " custkey bigint,\n" + + " orderstatus varchar(1),\n" + + " totalprice double,\n" + + " orderdate timestamp,\n" + + " orderpriority varchar(15),\n" + + " clerk varchar(15),\n" + + " shippriority bigint,\n" + + " comment varchar(79)\n" + + ")"); + } +} diff --git a/presto-oracle/src/test/java/io/prestosql/plugin/oracle/TestOraclePlugin.java b/presto-oracle/src/test/java/io/prestosql/plugin/oracle/TestOraclePlugin.java new file mode 100644 index 000000000000..3cc47166bbf9 --- /dev/null +++ b/presto-oracle/src/test/java/io/prestosql/plugin/oracle/TestOraclePlugin.java @@ -0,0 +1,33 @@ +/* + * 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.prestosql.plugin.oracle; + +import com.google.common.collect.ImmutableMap; +import io.prestosql.spi.Plugin; +import io.prestosql.spi.connector.ConnectorFactory; +import io.prestosql.testing.TestingConnectorContext; +import org.testng.annotations.Test; + +import static com.google.common.collect.Iterables.getOnlyElement; + +public class TestOraclePlugin +{ + @Test + public void testCreateConnector() + { + Plugin plugin = new OraclePlugin(); + ConnectorFactory factory = getOnlyElement(plugin.getConnectorFactories()); + factory.create("test", ImmutableMap.of("connection-url", "jdbc:oracle:thin//test"), new TestingConnectorContext()); + } +} diff --git a/presto-oracle/src/test/java/io/prestosql/plugin/oracle/TestOracleTypes.java b/presto-oracle/src/test/java/io/prestosql/plugin/oracle/TestOracleTypes.java new file mode 100644 index 000000000000..b264e09462b2 --- /dev/null +++ b/presto-oracle/src/test/java/io/prestosql/plugin/oracle/TestOracleTypes.java @@ -0,0 +1,164 @@ +/* + * 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.prestosql.plugin.oracle; + +import io.prestosql.spi.type.BigintType; +import io.prestosql.spi.type.DecimalType; +import io.prestosql.spi.type.TimestampType; +import io.prestosql.spi.type.Type; +import io.prestosql.testing.AbstractTestQueryFramework; +import io.prestosql.testing.QueryRunner; +import io.prestosql.testing.datatype.CreateAsSelectDataSetup; +import io.prestosql.testing.datatype.DataSetup; +import io.prestosql.testing.datatype.DataType; +import io.prestosql.testing.datatype.DataTypeTest; +import io.prestosql.testing.sql.PrestoSqlExecutor; +import org.testng.annotations.AfterClass; +import org.testng.annotations.Test; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.function.Function; + +import static io.prestosql.plugin.oracle.OracleQueryRunner.createOracleQueryRunner; +import static io.prestosql.spi.type.DecimalType.createDecimalType; +import static io.prestosql.spi.type.VarcharType.createUnboundedVarcharType; +import static io.prestosql.spi.type.VarcharType.createVarcharType; +import static io.prestosql.testing.datatype.DataType.stringDataType; +import static io.prestosql.testing.datatype.DataType.timestampDataType; +import static io.prestosql.testing.datatype.DataType.varcharDataType; +import static java.lang.String.format; +import static java.math.RoundingMode.HALF_UP; + +public class TestOracleTypes + extends AbstractTestQueryFramework +{ + private TestingOracleServer oracleServer; + + @Override + protected QueryRunner createQueryRunner() + throws Exception + { + this.oracleServer = new TestingOracleServer(); + return createOracleQueryRunner(oracleServer); + } + + @AfterClass(alwaysRun = true) + public final void destroy() + { + if (oracleServer != null) { + oracleServer.close(); + } + } + + private DataSetup prestoCreateAsSelect(String tableNamePrefix) + { + return new CreateAsSelectDataSetup(new PrestoSqlExecutor(getQueryRunner()), tableNamePrefix); + } + + @Test + public void testBooleanType() + { + DataTypeTest.create() + .addRoundTrip(booleanOracleType(), true) + .addRoundTrip(booleanOracleType(), false) + .execute(getQueryRunner(), prestoCreateAsSelect("boolean_types")); + } + + @Test + public void testSpecialNumberFormats() + { + oracleServer.execute("CREATE TABLE test (num1 number, num2 number(*,-2))"); + oracleServer.execute("INSERT INTO test VALUES (12345678901234567890.12345678901234567890123456789012345678, 1234567890.123)"); + assertQuery("SELECT * FROM test", "VALUES (12345678901234567890.1234567890, 1234567900.0000000000)"); + } + + @Test + public void testDateTimeType() + { + DataTypeTest.create() + .addRoundTrip(dateOracleType(), LocalDate.of(2020, 1, 1)) + .addRoundTrip(timestampDataType(), LocalDateTime.of(2020, 1, 1, 13, 10, 1)) + .execute(getQueryRunner(), prestoCreateAsSelect("datetime_types")); + } + + @Test + public void testVarcharType() + { + DataTypeTest.create() + .addRoundTrip(varcharDataType(10), "test") + .addRoundTrip(stringDataType("varchar", createVarcharType(4000)), "test") + .addRoundTrip(stringDataType("varchar(5000)", createUnboundedVarcharType()), "test") + .addRoundTrip(varcharDataType(3), String.valueOf('\u2603')) + .execute(getQueryRunner(), prestoCreateAsSelect("varchar_types")); + } + + @Test + public void testNumericTypes() + { + DataTypeTest.create() + .addRoundTrip(numberOracleType("tinyint", BigintType.BIGINT), 123L) + .addRoundTrip(numberOracleType("tinyint", BigintType.BIGINT), null) + .addRoundTrip(numberOracleType("smallint", BigintType.BIGINT), 123L) + .addRoundTrip(numberOracleType("integer", BigintType.BIGINT), 123L) + .addRoundTrip(numberOracleType("bigint", BigintType.BIGINT), 123L) + .addRoundTrip(numberOracleType("decimal", BigintType.BIGINT), 123L) + .addRoundTrip(numberOracleType("decimal(20)", BigintType.BIGINT), 123L) + .addRoundTrip(numberOracleType("decimal(20,0)", BigintType.BIGINT), 123L) + .addRoundTrip(numberOracleType(createDecimalType(5, 1)), BigDecimal.valueOf(123)) + .addRoundTrip(numberOracleType(createDecimalType(5, 2)), BigDecimal.valueOf(123)) + .addRoundTrip(numberOracleType(createDecimalType(5, 2)), BigDecimal.valueOf(123.046)) + .execute(getQueryRunner(), prestoCreateAsSelect("numeric_types")); + } + + private static DataType booleanOracleType() + { + return DataType.dataType( + "boolean", + BigintType.BIGINT, + val -> val ? "1" : "0", + val -> val ? 1L : 0L); + } + + private static DataType numberOracleType(DecimalType type) + { + String databaseType = format("decimal(%s, %s)", type.getPrecision(), type.getScale()); + return numberOracleType(databaseType, type); + } + + private static DataType numberOracleType(String inputType, Type resultType) + { + Function queryResult = (Function) value -> + (value instanceof BigDecimal && resultType instanceof DecimalType) + ? ((BigDecimal) value).setScale(((DecimalType) resultType).getScale(), HALF_UP) + : value; + + return DataType.dataType( + inputType, + resultType, + value -> format("CAST('%s' AS %s)", value, resultType), + queryResult); + } + + public static DataType dateOracleType() + { + return DataType.dataType( + "date", + TimestampType.TIMESTAMP, + DateTimeFormatter.ofPattern("'DATE '''yyyy-MM-dd''")::format, + LocalDate::atStartOfDay); + } +} diff --git a/presto-oracle/src/test/java/io/prestosql/plugin/oracle/TestingOracleServer.java b/presto-oracle/src/test/java/io/prestosql/plugin/oracle/TestingOracleServer.java new file mode 100644 index 000000000000..94536a3c3ca1 --- /dev/null +++ b/presto-oracle/src/test/java/io/prestosql/plugin/oracle/TestingOracleServer.java @@ -0,0 +1,83 @@ +/* + * 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.prestosql.plugin.oracle; + +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.OracleContainer; + +import java.io.Closeable; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +import static java.lang.String.format; + +public class TestingOracleServer + extends OracleContainer + implements Closeable +{ + public static final String TEST_SCHEMA = "tpch"; + public static final String TEST_USER = "tpch"; + public static final String TEST_PASS = "oracle"; + + public TestingOracleServer() + { + super("wnameless/oracle-xe-11g-r2"); + + // this is added to allow more processes on database, otherwise the tests end up giving a ORA-12519 + // to fix this we have to change the number of processes of SPFILE + // but this command needs a database restart and if we restart the docker the configuration is lost. + // configuration added: + // ALTER SYSTEM SET processes=500 SCOPE=SPFILE + // ALTER SYSTEM SET disk_asynch_io = FALSE SCOPE = SPFILE + this.addFileSystemBind("src/test/resources/spfileXE.ora", + "/u01/app/oracle/product/11.2.0/xe/dbs/spfileXE.ora", + BindMode.READ_ONLY); + + start(); + try (Connection connection = DriverManager.getConnection(getJdbcUrl(), super.getUsername(), super.getPassword()); + Statement statement = connection.createStatement()) { + statement.execute(format("CREATE TABLESPACE %s DATAFILE 'test_db.dat' SIZE 100M ONLINE", TEST_SCHEMA)); + statement.execute(format("CREATE USER %s IDENTIFIED BY %s DEFAULT TABLESPACE %s", TEST_USER, TEST_PASS, TEST_SCHEMA)); + statement.execute(format("GRANT UNLIMITED TABLESPACE TO %s", TEST_USER)); + statement.execute(format("GRANT ALL PRIVILEGES TO %s", TEST_USER)); + } + catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public void execute(String sql) + { + execute(sql, TEST_USER, TEST_PASS); + } + + public void execute(String sql, String user, String password) + { + try (Connection connection = DriverManager.getConnection(getJdbcUrl(), user, password); + Statement statement = connection.createStatement()) { + statement.execute(sql); + } + catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public void close() + { + stop(); + } +} diff --git a/presto-oracle/src/test/resources/spfileXE.ora b/presto-oracle/src/test/resources/spfileXE.ora new file mode 100644 index 000000000000..92a8ea486856 Binary files /dev/null and b/presto-oracle/src/test/resources/spfileXE.ora differ diff --git a/presto-server/src/main/provisio/presto.xml b/presto-server/src/main/provisio/presto.xml index 84f71f3c52fa..e87c168e2a6f 100644 --- a/presto-server/src/main/provisio/presto.xml +++ b/presto-server/src/main/provisio/presto.xml @@ -134,6 +134,12 @@ + + + + + + diff --git a/presto-testing/src/main/java/io/prestosql/testing/AbstractTestDistributedQueries.java b/presto-testing/src/main/java/io/prestosql/testing/AbstractTestDistributedQueries.java index f89752934ef3..339d1e748687 100644 --- a/presto-testing/src/main/java/io/prestosql/testing/AbstractTestDistributedQueries.java +++ b/presto-testing/src/main/java/io/prestosql/testing/AbstractTestDistributedQueries.java @@ -1241,6 +1241,11 @@ public Object[][] testColumnNameDataProvider() }; } + protected String dataMappingTableName(String prestoTypeName) + { + return "test_data_mapping_smoke_" + prestoTypeName.replaceAll("[^a-zA-Z0-9]", "_") + "_" + randomTableSuffix(); + } + @Test(dataProvider = "testDataMappingSmokeTestDataProvider") public void testDataMappingSmokeTest(DataMappingTestSetup dataMappingTestSetup) { @@ -1248,7 +1253,7 @@ public void testDataMappingSmokeTest(DataMappingTestSetup dataMappingTestSetup) String sampleValueLiteral = dataMappingTestSetup.getSampleValueLiteral(); String highValueLiteral = dataMappingTestSetup.getHighValueLiteral(); - String tableName = "test_data_mapping_smoke_" + prestoTypeName.replaceAll("[^a-zA-Z0-9]", "_") + "_" + randomTableSuffix(); + String tableName = dataMappingTableName(prestoTypeName); Runnable setup = () -> { // TODO test with both CTAS *and* CREATE TABLE + INSERT, since they use different connector API methods.