From 046f643f6812cfe33d0dcb37bc4185dc353590dd Mon Sep 17 00:00:00 2001 From: Max Zhuravkov Date: Mon, 20 Jan 2025 17:00:29 +0200 Subject: [PATCH] IGNITE-24030: Schema commands DDL handler. (#5021) --- .../catalog/SchemaExistsException.java | 37 +++ .../catalog/SchemaNotFoundException.java | 36 +++ .../catalog/commands/CreateSchemaCommand.java | 34 ++- .../commands/CreateSchemaCommandBuilder.java | 3 + .../catalog/commands/DropSchemaCommand.java | 48 +++- .../commands/DropSchemaCommandBuilder.java | 3 + .../internal/catalog/CatalogSchemaTest.java | 58 +++++ .../catalog/CatalogSchemaValidationTest.java | 21 ++ .../handler/JdbcQueryEventHandlerImpl.java | 23 +- .../requests/sql/ClientSqlProperties.java | 5 +- .../ignite/jdbc/AbstractJdbcSelfTest.java | 5 +- .../apache/ignite/jdbc/ItJdbcSchemaTest.java | 234 ++++++++++++++++++ .../table/ItColumnNameMappingTest.java | 4 +- .../table/ItPublicApiColocationTest.java | 5 +- .../table/ItTablePutGetEmbeddedTest.java | 5 +- .../ClusterPerClassIntegrationTest.java | 61 ++++- .../internal/sql/api/ItSqlApiBaseTest.java | 99 +++++++- .../internal/sql/engine/ItDataTypesTest.java | 6 +- .../internal/sql/engine/ItSchemaTest.java | 184 ++++++++++++++ .../datatypes/ItDivisionDecimalTest.java | 6 +- .../internal/sql/api/IgniteSqlImpl.java | 6 + .../engine/exec/ddl/DdlCommandHandler.java | 22 +- .../prepare/ddl/DdlSqlToCommandConverter.java | 28 +++ 23 files changed, 883 insertions(+), 50 deletions(-) create mode 100644 modules/catalog/src/main/java/org/apache/ignite/internal/catalog/SchemaExistsException.java create mode 100644 modules/catalog/src/main/java/org/apache/ignite/internal/catalog/SchemaNotFoundException.java create mode 100644 modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcSchemaTest.java create mode 100644 modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItSchemaTest.java diff --git a/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/SchemaExistsException.java b/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/SchemaExistsException.java new file mode 100644 index 00000000000..d5fe0853d66 --- /dev/null +++ b/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/SchemaExistsException.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.ignite.internal.catalog; + +/** + * This exception is thrown when a schema cannot be created because another schema with + * the same name already exists in a catalog. + * + *

This exception is used to properly handle IF NOT EXISTS flag in ddl command handler. + */ +public class SchemaExistsException extends CatalogValidationException { + private static final long serialVersionUID = 6017288060655861875L; + + /** + * Constructor. + * + * @param message Error message. + */ + public SchemaExistsException(String message) { + super(message); + } +} diff --git a/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/SchemaNotFoundException.java b/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/SchemaNotFoundException.java new file mode 100644 index 00000000000..ae828b686d1 --- /dev/null +++ b/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/SchemaNotFoundException.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.ignite.internal.catalog; + +/** + * This exception is thrown when a schema is not found in a catalog. + * + *

This exception is used to properly handle IF EXISTS flag in ddl command handler. + */ +public class SchemaNotFoundException extends CatalogValidationException { + private static final long serialVersionUID = 6017288060655861875L; + + /** + * Constructor. + * + * @param message Error message. + */ + public SchemaNotFoundException(String message) { + super(message); + } +} diff --git a/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/commands/CreateSchemaCommand.java b/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/commands/CreateSchemaCommand.java index 38f15e0bdf8..590a2d41dce 100644 --- a/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/commands/CreateSchemaCommand.java +++ b/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/commands/CreateSchemaCommand.java @@ -24,7 +24,7 @@ import java.util.List; import org.apache.ignite.internal.catalog.Catalog; import org.apache.ignite.internal.catalog.CatalogCommand; -import org.apache.ignite.internal.catalog.CatalogValidationException; +import org.apache.ignite.internal.catalog.SchemaExistsException; import org.apache.ignite.internal.catalog.descriptors.CatalogIndexDescriptor; import org.apache.ignite.internal.catalog.descriptors.CatalogSchemaDescriptor; import org.apache.ignite.internal.catalog.descriptors.CatalogSystemViewDescriptor; @@ -40,10 +40,17 @@ public class CreateSchemaCommand implements CatalogCommand { private final String schemaName; - private CreateSchemaCommand(String schemaName) { + private final boolean ifNotExists; + + private CreateSchemaCommand(String schemaName, boolean ifNotExists) { validateIdentifier(schemaName, "Name of the schema"); this.schemaName = schemaName; + this.ifNotExists = ifNotExists; + } + + public boolean ifNotExists() { + return ifNotExists; } /** {@inheritDoc} */ @@ -51,11 +58,15 @@ private CreateSchemaCommand(String schemaName) { public List get(Catalog catalog) { int id = catalog.objectIdGenState(); - if (catalog.schema(schemaName) != null) { - throw new CatalogValidationException(format("Schema with name '{}' already exists", schemaName)); + CatalogSchemaDescriptor schema = catalog.schema(schemaName); + + if (ifNotExists && schema != null) { + return List.of(); + } else if (schema != null) { + throw new SchemaExistsException(format("Schema with name '{}' already exists.", schemaName)); } - CatalogSchemaDescriptor schema = new CatalogSchemaDescriptor( + CatalogSchemaDescriptor newSchema = new CatalogSchemaDescriptor( id, schemaName, new CatalogTableDescriptor[0], @@ -65,7 +76,7 @@ public List get(Catalog catalog) { ); return List.of( - new NewSchemaEntry(schema), + new NewSchemaEntry(newSchema), new ObjectIdGenUpdateEntry(1) ); } @@ -80,6 +91,8 @@ public static class Builder implements CreateSchemaCommandBuilder { private String name; + private boolean ifNotExists; + /** {@inheritDoc} */ @Override public CreateSchemaCommandBuilder name(String name) { @@ -87,10 +100,17 @@ public CreateSchemaCommandBuilder name(String name) { return this; } + /** {@inheritDoc} */ + @Override + public CreateSchemaCommandBuilder ifNotExists(boolean value) { + this.ifNotExists = value; + return this; + } + /** {@inheritDoc} */ @Override public CatalogCommand build() { - return new CreateSchemaCommand(name); + return new CreateSchemaCommand(name, ifNotExists); } } } diff --git a/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/commands/CreateSchemaCommandBuilder.java b/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/commands/CreateSchemaCommandBuilder.java index b3e690f6b86..7d06f5cbf4a 100644 --- a/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/commands/CreateSchemaCommandBuilder.java +++ b/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/commands/CreateSchemaCommandBuilder.java @@ -27,6 +27,9 @@ public interface CreateSchemaCommandBuilder { /** Sets schema name. Should not be null or blank. */ CreateSchemaCommandBuilder name(String name); + /** Sets a flag indicating whether {@code IF NOT EXISTS} option was specified. */ + CreateSchemaCommandBuilder ifNotExists(boolean value); + /** Creates new schema command. */ CatalogCommand build(); } diff --git a/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/commands/DropSchemaCommand.java b/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/commands/DropSchemaCommand.java index d166d5db796..bf638fdfb30 100644 --- a/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/commands/DropSchemaCommand.java +++ b/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/commands/DropSchemaCommand.java @@ -19,18 +19,19 @@ import static org.apache.ignite.internal.catalog.CatalogParamsValidationUtils.validateIdentifier; import static org.apache.ignite.internal.catalog.commands.CatalogUtils.schemaOrThrow; +import static org.apache.ignite.internal.lang.IgniteStringFormatter.format; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.apache.ignite.internal.catalog.Catalog; import org.apache.ignite.internal.catalog.CatalogCommand; import org.apache.ignite.internal.catalog.CatalogValidationException; -import org.apache.ignite.internal.catalog.descriptors.CatalogIndexDescriptor; import org.apache.ignite.internal.catalog.descriptors.CatalogSchemaDescriptor; import org.apache.ignite.internal.catalog.descriptors.CatalogTableDescriptor; -import org.apache.ignite.internal.catalog.storage.DropIndexEntry; import org.apache.ignite.internal.catalog.storage.DropSchemaEntry; import org.apache.ignite.internal.catalog.storage.DropTableEntry; +import org.apache.ignite.internal.catalog.storage.RemoveIndexEntry; import org.apache.ignite.internal.catalog.storage.UpdateEntry; /** @@ -46,18 +47,26 @@ public static DropSchemaCommandBuilder builder() { private final boolean cascade; + private final boolean ifExists; + /** * Constructor. * * @param schemaName Name of the schema. * @param cascade Flag indicating forced deletion of a non-empty schema. + * @param ifExists Flag indicating * @throws CatalogValidationException if any of restrictions above is violated. */ - private DropSchemaCommand(String schemaName, boolean cascade) throws CatalogValidationException { + private DropSchemaCommand(String schemaName, boolean cascade, boolean ifExists) throws CatalogValidationException { validateIdentifier(schemaName, "Name of the schema"); this.schemaName = schemaName; this.cascade = cascade; + this.ifExists = ifExists; + } + + public boolean ifExists() { + return ifExists; } @Override @@ -66,17 +75,30 @@ public List get(Catalog catalog) { throw new CatalogValidationException("System schema can't be dropped [name={}].", schemaName); } - CatalogSchemaDescriptor schema = schemaOrThrow(catalog, schemaName); + CatalogSchemaDescriptor schema; + + if (ifExists) { + schema = catalog.schema(schemaName); + if (schema == null) { + return List.of(); + } + } else { + schema = schemaOrThrow(catalog, schemaName); + } if (!cascade && !schema.isEmpty()) { - throw new CatalogValidationException("Schema '{}' is not empty. Use CASCADE to drop it anyway.", schemaName); + throw new CatalogValidationException(format("Schema '{}' is not empty. Use CASCADE to drop it anyway.", schemaName)); } List updateEntries = new ArrayList<>(); - for (CatalogIndexDescriptor idx : schema.indexes()) { - updateEntries.add(new DropIndexEntry(idx.id())); - } + Arrays.stream(schema.indexes()) + .forEach(index -> { + // We can remove AVAILABLE/STOPPED index right away as the only reason to have an index in the STOPPING state is to + // allow RW transactions started before the index drop to write to it, but as the table is already dropped, + // the writes are not possible in any case. + updateEntries.add(new RemoveIndexEntry(index.id())); + }); for (CatalogTableDescriptor tbl : schema.tables()) { updateEntries.add(new DropTableEntry(tbl.id())); @@ -93,6 +115,7 @@ public List get(Catalog catalog) { private static class Builder implements DropSchemaCommandBuilder { private String schemaName; private boolean cascade; + private boolean ifExists; @Override public DropSchemaCommandBuilder name(String schemaName) { @@ -108,9 +131,16 @@ public DropSchemaCommandBuilder cascade(boolean cascade) { return this; } + @Override + public DropSchemaCommandBuilder ifExists(boolean ifExists) { + this.ifExists = ifExists; + + return this; + } + @Override public CatalogCommand build() { - return new DropSchemaCommand(schemaName, cascade); + return new DropSchemaCommand(schemaName, cascade, ifExists); } } } diff --git a/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/commands/DropSchemaCommandBuilder.java b/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/commands/DropSchemaCommandBuilder.java index fa5d5d7ad4f..5dda74a9fd8 100644 --- a/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/commands/DropSchemaCommandBuilder.java +++ b/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/commands/DropSchemaCommandBuilder.java @@ -29,6 +29,9 @@ public interface DropSchemaCommandBuilder { /** Sets flag indicating forced deletion of a non-empty schema. */ DropSchemaCommandBuilder cascade(boolean cascade); + /** Sets a flag indicating whether {@code IF EXISTS} option was specified. */ + DropSchemaCommandBuilder ifExists(boolean ifExists); + /** Returns a command with specified parameters. */ CatalogCommand build(); } diff --git a/modules/catalog/src/test/java/org/apache/ignite/internal/catalog/CatalogSchemaTest.java b/modules/catalog/src/test/java/org/apache/ignite/internal/catalog/CatalogSchemaTest.java index 3a00f619c7c..86cfeb674a5 100644 --- a/modules/catalog/src/test/java/org/apache/ignite/internal/catalog/CatalogSchemaTest.java +++ b/modules/catalog/src/test/java/org/apache/ignite/internal/catalog/CatalogSchemaTest.java @@ -21,6 +21,7 @@ import static org.apache.ignite.internal.catalog.descriptors.CatalogColumnCollation.ASC_NULLS_LAST; import static org.apache.ignite.internal.testframework.matchers.CompletableFutureExceptionMatcher.willThrowFast; import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully; +import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willSucceedFast; import static org.apache.ignite.sql.ColumnType.INT32; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; @@ -53,6 +54,48 @@ public void testCreateSchema() { manager.execute(CreateSchemaCommand.builder().name(TEST_SCHEMA).build()), willThrowFast(CatalogValidationException.class, "Schema with name 'S1' already exists") ); + + assertThat( + manager.execute(CreateSchemaCommand.builder().name(TEST_SCHEMA).ifNotExists(true).build()), + willSucceedFast() + ); + } + + @Test + public void testCreateSchemaIfNotExists() { + { + assertThat( + manager.execute(CreateSchemaCommand.builder().name(TEST_SCHEMA).ifNotExists(false).build()), + willCompleteSuccessfully() + ); + + Catalog latestCatalog = latestCatalog(); + + assertNotNull(latestCatalog.schema(TEST_SCHEMA)); + assertNotNull(latestCatalog.schema(SqlCommon.DEFAULT_SCHEMA_NAME)); + + assertThat( + manager.execute(CreateSchemaCommand.builder().name(TEST_SCHEMA).build()), + willThrowFast(CatalogValidationException.class, "Schema with name 'S1' already exists") + ); + } + + { + assertThat( + manager.execute(CreateSchemaCommand.builder().name(TEST_SCHEMA + "_1").ifNotExists(false).build()), + willCompleteSuccessfully() + ); + + Catalog latestCatalog = latestCatalog(); + + assertNotNull(latestCatalog.schema(TEST_SCHEMA + "_1")); + assertNotNull(latestCatalog.schema(TEST_SCHEMA)); + + assertThat( + manager.execute(CreateSchemaCommand.builder().name(TEST_SCHEMA + "_1").ifNotExists(true).build()), + willSucceedFast() + ); + } } @Test @@ -75,6 +118,21 @@ public void testDropEmpty() { ); } + @Test + public void testDropIfExists() { + assertThat(manager.execute(DropSchemaCommand.builder().name(TEST_SCHEMA).ifExists(true).build()), willCompleteSuccessfully()); + + assertThat( + manager.execute(DropSchemaCommand.builder().name(TEST_SCHEMA).ifExists(false).build()), + willThrowFast(CatalogValidationException.class, "Schema with name 'S1' not found") + ); + + assertThat(manager.execute(CreateSchemaCommand.builder().name(TEST_SCHEMA).build()), willCompleteSuccessfully()); + + assertThat(manager.execute(DropSchemaCommand.builder().name(TEST_SCHEMA).ifExists(true).build()), willCompleteSuccessfully()); + assertThat(latestCatalog().schema(TEST_SCHEMA), nullValue()); + } + @Test public void testDropDefaultSchemaIsAllowed() { CatalogCommand cmd = DropSchemaCommand.builder().name(SqlCommon.DEFAULT_SCHEMA_NAME).build(); diff --git a/modules/catalog/src/test/java/org/apache/ignite/internal/catalog/CatalogSchemaValidationTest.java b/modules/catalog/src/test/java/org/apache/ignite/internal/catalog/CatalogSchemaValidationTest.java index 134495cfe3d..08989d8204f 100644 --- a/modules/catalog/src/test/java/org/apache/ignite/internal/catalog/CatalogSchemaValidationTest.java +++ b/modules/catalog/src/test/java/org/apache/ignite/internal/catalog/CatalogSchemaValidationTest.java @@ -19,6 +19,7 @@ import static org.apache.ignite.internal.lang.IgniteStringFormatter.format; import static org.apache.ignite.internal.testframework.matchers.CompletableFutureExceptionMatcher.willThrowFast; +import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willSucceedFast; import static org.hamcrest.MatcherAssert.assertThat; import org.apache.ignite.internal.catalog.commands.CreateSchemaCommand; @@ -37,6 +38,16 @@ public void testCreateSchemaWithExistingName() { manager.execute(CreateSchemaCommand.builder().name(SqlCommon.DEFAULT_SCHEMA_NAME).build()), willThrowFast(CatalogValidationException.class, "Schema with name 'PUBLIC' already exists") ); + + assertThat( + manager.execute(CreateSchemaCommand.builder().name(SqlCommon.DEFAULT_SCHEMA_NAME).ifNotExists(false).build()), + willThrowFast(CatalogValidationException.class, "Schema with name 'PUBLIC' already exists") + ); + + assertThat( + manager.execute(CreateSchemaCommand.builder().name(SqlCommon.DEFAULT_SCHEMA_NAME).ifNotExists(true).build()), + willSucceedFast() + ); } @Test @@ -45,6 +56,16 @@ public void testDropNonExistingSchema() { manager.execute(DropSchemaCommand.builder().name("NON_EXISTING").build()), willThrowFast(CatalogValidationException.class, "Schema with name 'NON_EXISTING' not found") ); + + assertThat( + manager.execute(DropSchemaCommand.builder().name("NON_EXISTING").ifExists(false).build()), + willThrowFast(CatalogValidationException.class, "Schema with name 'NON_EXISTING' not found") + ); + + assertThat( + manager.execute(DropSchemaCommand.builder().name("NON_EXISTING").ifExists(true).build()), + willSucceedFast() + ); } @Test diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImpl.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImpl.java index 4614a044236..0cc8bf99427 100644 --- a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImpl.java +++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImpl.java @@ -72,6 +72,7 @@ import org.apache.ignite.internal.util.AsyncCursor.BatchedResult; import org.apache.ignite.lang.CancelHandle; import org.apache.ignite.lang.CancellationToken; +import org.apache.ignite.table.QualifiedName; import org.jetbrains.annotations.Nullable; /** @@ -161,11 +162,12 @@ public CompletableFuture queryAsync(long connectionId, JdbcQ InternalTransaction tx = req.autoCommit() ? null : connectionContext.getOrStartTransaction(timestampTracker); JdbcStatementType reqStmtType = req.getStmtType(); + String defaultSchemaName = req.schemaName(); boolean multiStatement = req.multiStatement(); ZoneId timeZoneId = connectionContext.timeZoneId(); long timeoutMillis = req.queryTimeoutMillis(); - SqlProperties properties = createProperties(reqStmtType, multiStatement, timeZoneId, timeoutMillis); + SqlProperties properties = createProperties(reqStmtType, defaultSchemaName, multiStatement, timeZoneId, timeoutMillis); CompletableFuture> result = processor.queryAsync( properties, @@ -188,6 +190,7 @@ public HybridTimestampTracker getTimestampTracker() { private static SqlProperties createProperties( JdbcStatementType stmtType, + String defaultSchemaName, boolean multiStatement, ZoneId timeZoneId, long queryTimeoutMillis @@ -208,9 +211,14 @@ private static SqlProperties createProperties( throw new AssertionError("Unexpected jdbc statement type: " + stmtType); } + // TODO: https://issues.apache.org/jira/browse/IGNITE-24021 + // Replace this with using correct implementation of `IgniteNameUtils.parseSimpleName`, when it will be fixed. + String schemaNameInCanonicalForm = QualifiedName.fromSimple(defaultSchemaName).objectName(); + return SqlPropertiesHelper.newBuilder() .set(QueryProperty.ALLOWED_QUERY_TYPES, allowedTypes) .set(QueryProperty.TIME_ZONE_ID, timeZoneId) + .set(QueryProperty.DEFAULT_SCHEMA, schemaNameInCanonicalForm) .set(QueryProperty.QUERY_TIMEOUT, queryTimeoutMillis) .build(); } @@ -228,6 +236,7 @@ public CompletableFuture batchAsync(long connectionId, J InternalTransaction tx = req.autoCommit() ? null : connectionContext.getOrStartTransaction(timestampTracker); long correlationToken = req.correlationToken(); CancellationToken token = connectionContext.registerExecution(correlationToken); + String defaultSchemaName = req.schemaName(); var queries = req.queries(); var counters = new IntArrayList(req.queries().size()); var tail = CompletableFuture.completedFuture(counters); @@ -236,6 +245,7 @@ public CompletableFuture batchAsync(long connectionId, J for (String query : queries) { tail = tail.thenCompose(list -> executeAndCollectUpdateCount( connectionContext, + defaultSchemaName, tx, token, query, @@ -270,13 +280,14 @@ public CompletableFuture batchPrepStatementAsync(long co long correlationToken = req.correlationToken(); CancellationToken token = connectionContext.registerExecution(correlationToken); var argList = req.getArgs(); + String defaultSchemaName = req.schemaName(); var counters = new IntArrayList(req.getArgs().size()); var tail = CompletableFuture.completedFuture(counters); long timeoutMillis = req.queryTimeoutMillis(); for (Object[] args : argList) { tail = tail.thenCompose(list -> executeAndCollectUpdateCount( - connectionContext, tx, token, req.getQuery(), args, timeoutMillis, list + connectionContext, defaultSchemaName, tx, token, req.getQuery(), args, timeoutMillis, list )); } @@ -293,6 +304,7 @@ public CompletableFuture batchPrepStatementAsync(long co private CompletableFuture executeAndCollectUpdateCount( JdbcConnectionContext context, + String defaultSchemaName, @Nullable InternalTransaction tx, CancellationToken token, String sql, @@ -304,7 +316,12 @@ private CompletableFuture executeAndCollectUpdateCount( return CompletableFuture.failedFuture(new IgniteInternalException(CONNECTION_ERR, "Connection is closed")); } - SqlProperties properties = createProperties(JdbcStatementType.UPDATE_STATEMENT_TYPE, false, context.timeZoneId(), timeoutMillis); + SqlProperties properties = createProperties(JdbcStatementType.UPDATE_STATEMENT_TYPE, + defaultSchemaName, + false, + context.timeZoneId(), + timeoutMillis + ); CompletableFuture> result = processor.queryAsync( properties, diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlProperties.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlProperties.java index ccd74bb0883..44037b35812 100644 --- a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlProperties.java +++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlProperties.java @@ -23,6 +23,7 @@ import org.apache.ignite.internal.sql.engine.QueryProperty; import org.apache.ignite.internal.sql.engine.property.SqlProperties; import org.apache.ignite.internal.sql.engine.property.SqlPropertiesHelper; +import org.apache.ignite.table.QualifiedName; import org.jetbrains.annotations.Nullable; class ClientSqlProperties { @@ -37,7 +38,9 @@ class ClientSqlProperties { private final @Nullable String timeZoneId; ClientSqlProperties(ClientMessageUnpacker in) { - schema = in.tryUnpackNil() ? null : in.unpackString(); + // TODO: https://issues.apache.org/jira/browse/IGNITE-24021 + // Do parse simple name correctly. + schema = in.tryUnpackNil() ? null : QualifiedName.fromSimple(in.unpackString()).objectName(); pageSize = in.tryUnpackNil() ? SqlCommon.DEFAULT_PAGE_SIZE : in.unpackInt(); queryTimeout = in.tryUnpackNil() ? 0 : in.unpackLong(); idleTimeout = in.tryUnpackNil() ? 0 : in.unpackLong(); diff --git a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/AbstractJdbcSelfTest.java b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/AbstractJdbcSelfTest.java index f3ac3dafffd..87237f7fb18 100644 --- a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/AbstractJdbcSelfTest.java +++ b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/AbstractJdbcSelfTest.java @@ -44,6 +44,8 @@ public class AbstractJdbcSelfTest extends ClusterPerClassIntegrationTest { /** URL. */ protected static final String URL = "jdbc:ignite:thin://127.0.0.1:10800"; + /** Default schema. */ + protected static final String DEFAULT_SCHEMA = "PUBLIC"; /** Connection. */ protected static Connection conn; @@ -65,7 +67,7 @@ protected int initialNodes() { public static void beforeAllBase(TestInfo testInfo) throws Exception { conn = DriverManager.getConnection(URL); - conn.setSchema("PUBLIC"); + conn.setSchema(DEFAULT_SCHEMA); } /** @@ -82,6 +84,7 @@ public static void afterAllBase(TestInfo testInfo) throws Exception { @BeforeEach protected void setUpBase() throws Exception { conn.setAutoCommit(true); + conn.setSchema(DEFAULT_SCHEMA); stmt = conn.createStatement(); diff --git a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcSchemaTest.java b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcSchemaTest.java new file mode 100644 index 00000000000..71ea66e1bb4 --- /dev/null +++ b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcSchemaTest.java @@ -0,0 +1,234 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.ignite.jdbc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * Tests that involve non-default schema and DDL schema commands. + */ +public class ItJdbcSchemaTest extends AbstractJdbcSelfTest { + + @AfterEach + public void dropSchemas() { + dropAllSchemas(); + } + + @Test + public void createSchema() throws SQLException { + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("CREATE SCHEMA schema1"); + + stmt.executeUpdate("CREATE TABLE schema1.t1(ID INT PRIMARY KEY, val INT)"); + stmt.executeUpdate("INSERT INTO schema1.t1 VALUES (1, 1)"); + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM schema1.T1")) { + assertTrue(rs.next()); + assertEquals(1, rs.getLong(1)); + + ResultSetMetaData metaData = rs.getMetaData(); + assertEquals("SCHEMA1", metaData.getSchemaName(1)); + } + } + } + + @Test + public void dropSchema() throws SQLException { + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("CREATE SCHEMA schema1"); + + stmt.executeUpdate("CREATE TABLE schema1.t1(ID INT PRIMARY KEY, val INT)"); + try (ResultSet rs = stmt.executeQuery("SELECT * FROM schema1.T1")) { + assertFalse(rs.next()); + } + + stmt.executeUpdate("DROP SCHEMA schema1 CASCADE"); + } + } + + @Test + public void useSchemaWithQuery() throws SQLException { + try (Statement stmt = conn.createStatement()) { + // Schema 1 + stmt.executeUpdate("CREATE SCHEMA schema1"); + stmt.executeUpdate("CREATE TABLE schema1.t1(ID INT PRIMARY KEY, val INT)"); + stmt.executeUpdate("INSERT INTO schema1.t1 VALUES (1, 1)"); + + // Schema 2 + stmt.executeUpdate("CREATE SCHEMA schema2"); + stmt.executeUpdate("CREATE TABLE schema2.t1(ID INT PRIMARY KEY, val INT)"); + stmt.executeUpdate("INSERT INTO schema2.t1 VALUES (2, 2)"); + } + + // Check schema1 + conn.setSchema("schema1"); + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM T1")) { + assertTrue(rs.next()); + assertEquals(1, rs.getLong(1)); + + ResultSetMetaData metaData = rs.getMetaData(); + assertEquals("SCHEMA1", metaData.getSchemaName(1)); + } + } + + // Check schema2 + conn.setSchema("schema2"); + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM T1")) { + assertTrue(rs.next()); + assertEquals(2, rs.getLong(1)); + + ResultSetMetaData metaData = rs.getMetaData(); + assertEquals("SCHEMA2", metaData.getSchemaName(1)); + } + } + } + + @Test + public void useSchemaWithUpdate() throws SQLException { + try (Statement stmt = conn.createStatement()) { + // Schema 1 + stmt.executeUpdate("CREATE SCHEMA schema1"); + stmt.executeUpdate("CREATE TABLE schema1.t1(ID INT PRIMARY KEY, val INT)"); + + // Schema 2 + stmt.executeUpdate("CREATE SCHEMA schema2"); + stmt.executeUpdate("CREATE TABLE schema2.t1(ID INT PRIMARY KEY, val INT)"); + } + + // Update schema1 + conn.setSchema("schema1"); + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("INSERT INTO t1 VALUES (1, 1), (2, 2)"); + } + + // Update schema2 + conn.setSchema("schema2"); + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("INSERT INTO t1 VALUES (1, 1), (2, 2), (3, 3)"); + } + + // Check schema 1 + try (ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM schema1.T1")) { + assertTrue(rs.next()); + assertEquals(2, rs.getLong(1)); + } + + // Check schema 2 + try (ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM schema2.T1")) { + assertTrue(rs.next()); + assertEquals(3, rs.getLong(1)); + } + } + + @Test + public void useSchemaWithBatch() throws SQLException { + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("CREATE SCHEMA schema1"); + stmt.executeUpdate("CREATE TABLE schema1.t1(ID INT PRIMARY KEY, val INT)"); + + stmt.executeUpdate("CREATE SCHEMA schema2"); + stmt.executeUpdate("CREATE TABLE schema2.t1(ID INT PRIMARY KEY, val INT)"); + } + + // Insert into schema 1 + conn.setSchema("schema1"); + try (Statement stmt = conn.createStatement()) { + stmt.addBatch("INSERT INTO t1 VALUES (1, 1), (2, 1)"); + stmt.addBatch("INSERT INTO t1 VALUES (3, 1), (4, 1)"); + stmt.executeBatch(); + } + + // Insert into schema 2 + conn.setSchema("schema2"); + try (Statement stmt = conn.createStatement()) { + stmt.addBatch("INSERT INTO t1 VALUES (1, 2)"); + stmt.addBatch("INSERT INTO t1 VALUES (2, 2), (3, 2)"); + stmt.executeBatch(); + } + + // Check schema 1 + try (ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM schema1.T1")) { + assertTrue(rs.next()); + assertEquals(4, rs.getLong(1)); + } + + // Check schema 2 + try (ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM schema2.T1")) { + assertTrue(rs.next()); + assertEquals(3, rs.getLong(1)); + } + } + + @Test + public void quotedSchemaTest() throws SQLException { + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("CREATE SCHEMA \"ScheMa1\""); + + stmt.executeUpdate("CREATE TABLE \"ScheMa1\".t1(ID INT PRIMARY KEY, val INT)"); + stmt.executeUpdate("INSERT INTO \"ScheMa1\".t1 VALUES (1, 1)"); + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM \"ScheMa1\".T1")) { + assertTrue(rs.next()); + assertEquals(1, rs.getLong(1)); + + ResultSetMetaData metaData = rs.getMetaData(); + assertEquals("ScheMa1", metaData.getSchemaName(1)); + } + + stmt.executeUpdate("DROP TABLE \"ScheMa1\".t1"); + stmt.executeUpdate("DROP SCHEMA \"ScheMa1\""); + + stmt.executeUpdate("CREATE SCHEMA \"ScheMa2\""); + stmt.executeUpdate("CREATE TABLE \"ScheMa2\".t1(ID INT PRIMARY KEY, val INT)"); + stmt.executeUpdate("DROP SCHEMA \"ScheMa2\" CASCADE"); + } + } + + @Test + public void quotedSchemaAndTableTest() throws SQLException { + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("CREATE SCHEMA \"Sche Ma1\""); + + stmt.executeUpdate("CREATE TABLE \"Sche Ma1\".\"Ta ble1\"(ID INT PRIMARY KEY, val INT)"); + stmt.executeUpdate("INSERT INTO \"Sche Ma1\".\"Ta ble1\" VALUES (1, 1)"); + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM \"Sche Ma1\".\"Ta ble1\"")) { + assertTrue(rs.next()); + assertEquals(1, rs.getLong(1)); + + ResultSetMetaData metaData = rs.getMetaData(); + assertEquals("Ta ble1", metaData.getTableName(1)); + assertEquals("Sche Ma1", metaData.getSchemaName(1)); + } + + stmt.executeUpdate("DROP TABLE \"Sche Ma1\".\"Ta ble1\""); + stmt.executeUpdate("DROP SCHEMA \"Sche Ma1\""); + } + } +} diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItColumnNameMappingTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItColumnNameMappingTest.java index 8d09c3ad031..89a5f46799d 100644 --- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItColumnNameMappingTest.java +++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItColumnNameMappingTest.java @@ -54,9 +54,7 @@ public void clearTables() { @AfterAll public void dropTables() { - for (Table t : CLUSTER.aliveNode().tables().tables()) { - sql("DROP TABLE " + t.name()); - } + dropAllTables(); } @Test diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItPublicApiColocationTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItPublicApiColocationTest.java index cf5d298a776..dcbb4cf52c0 100644 --- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItPublicApiColocationTest.java +++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItPublicApiColocationTest.java @@ -39,7 +39,6 @@ import org.apache.ignite.internal.testframework.IgniteTestUtils; import org.apache.ignite.internal.testframework.WorkDirectoryExtension; import org.apache.ignite.internal.type.NativeTypeSpec; -import org.apache.ignite.table.Table; import org.apache.ignite.table.Tuple; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Disabled; @@ -58,9 +57,7 @@ public class ItPublicApiColocationTest extends ClusterPerClassIntegrationTest { @AfterEach public void dropTables() { - for (Table t : CLUSTER.aliveNode().tables().tables()) { - sql("DROP TABLE " + t.name()); - } + dropAllTables(); } /** diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItTablePutGetEmbeddedTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItTablePutGetEmbeddedTest.java index a3b11d47a9f..5a071d85c7e 100644 --- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItTablePutGetEmbeddedTest.java +++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItTablePutGetEmbeddedTest.java @@ -26,7 +26,6 @@ import org.apache.ignite.table.IgniteTables; import org.apache.ignite.table.KeyValueView; import org.apache.ignite.table.RecordView; -import org.apache.ignite.table.Table; import org.apache.ignite.table.Tuple; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -40,9 +39,7 @@ public class ItTablePutGetEmbeddedTest extends ClusterPerClassIntegrationTest { @AfterEach void dropTables() { - for (Table table : tables().tables()) { - sql("DROP TABLE " + table.name()); - } + dropAllTables(); } @ParameterizedTest diff --git a/modules/runner/src/testFixtures/java/org/apache/ignite/internal/ClusterPerClassIntegrationTest.java b/modules/runner/src/testFixtures/java/org/apache/ignite/internal/ClusterPerClassIntegrationTest.java index 92f21b8df0a..242c928b2eb 100644 --- a/modules/runner/src/testFixtures/java/org/apache/ignite/internal/ClusterPerClassIntegrationTest.java +++ b/modules/runner/src/testFixtures/java/org/apache/ignite/internal/ClusterPerClassIntegrationTest.java @@ -22,6 +22,7 @@ import static org.apache.ignite.internal.TestDefaultProfilesNames.DEFAULT_ROCKSDB_PROFILE_NAME; import static org.apache.ignite.internal.TestDefaultProfilesNames.DEFAULT_TEST_PROFILE_NAME; import static org.apache.ignite.internal.TestWrappers.unwrapIgniteImpl; +import static org.apache.ignite.internal.TestWrappers.unwrapTableImpl; import static org.apache.ignite.internal.catalog.CatalogService.DEFAULT_STORAGE_PROFILE; import static org.apache.ignite.internal.catalog.descriptors.CatalogIndexStatus.AVAILABLE; import static org.apache.ignite.internal.lang.IgniteStringFormatter.format; @@ -34,27 +35,35 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.ignite.Ignite; import org.apache.ignite.InitParametersBuilder; import org.apache.ignite.internal.app.IgniteImpl; import org.apache.ignite.internal.catalog.Catalog; import org.apache.ignite.internal.catalog.CatalogManager; +import org.apache.ignite.internal.catalog.commands.CatalogUtils; import org.apache.ignite.internal.catalog.descriptors.CatalogIndexDescriptor; +import org.apache.ignite.internal.catalog.descriptors.CatalogSchemaDescriptor; +import org.apache.ignite.internal.catalog.descriptors.CatalogTableDescriptor; import org.apache.ignite.internal.catalog.descriptors.CatalogZoneDescriptor; import org.apache.ignite.internal.hlc.HybridClock; import org.apache.ignite.internal.lang.IgniteBiTuple; +import org.apache.ignite.internal.table.TableImpl; import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest; import org.apache.ignite.internal.testframework.TestIgnitionManager; import org.apache.ignite.internal.testframework.WorkDirectory; import org.apache.ignite.internal.testframework.WorkDirectoryExtension; +import org.apache.ignite.lang.util.IgniteNameUtils; import org.apache.ignite.network.ClusterNode; import org.apache.ignite.sql.IgniteSql; import org.apache.ignite.sql.ResultSet; import org.apache.ignite.sql.SqlRow; import org.apache.ignite.sql.Statement; import org.apache.ignite.sql.Statement.StatementBuilder; +import org.apache.ignite.table.QualifiedName; import org.apache.ignite.table.Table; import org.apache.ignite.tx.Transaction; import org.jetbrains.annotations.Nullable; @@ -168,8 +177,56 @@ void stopCluster() { /** Drops all visible tables. */ protected static void dropAllTables() { - for (Table t : CLUSTER.aliveNode().tables().tables()) { - sql("DROP TABLE " + t.name()); + Ignite aliveNode = CLUSTER.aliveNode(); + + for (Table t : aliveNode.tables().tables()) { + IgniteImpl ignite = unwrapIgniteImpl(aliveNode); + CatalogManager catalogManager = ignite.catalogManager(); + + int latestCatalogVersion = catalogManager.latestCatalogVersion(); + Catalog latestCatalog = catalogManager.catalog(latestCatalogVersion); + assert latestCatalog != null; + + TableImpl tableImpl = unwrapTableImpl(t); + int tableId = tableImpl.tableId(); + + for (CatalogSchemaDescriptor schema : latestCatalog.schemas()) { + for (CatalogTableDescriptor table : schema.tables()) { + if (table.id() != tableId) { + continue; + } + String schemaName = IgniteNameUtils.quote(schema.name()); + String tableName = IgniteNameUtils.quote(table.name()); + + sql("DROP TABLE " + QualifiedName.of(schemaName, tableName).toCanonicalForm()); + } + } + } + } + + /** Drops all non-system schemas. */ + protected static void dropAllSchemas() { + Ignite aliveNode = CLUSTER.aliveNode(); + IgniteImpl ignite = unwrapIgniteImpl(aliveNode); + CatalogManager catalogManager = ignite.catalogManager(); + + int latestCatalogVersion = catalogManager.latestCatalogVersion(); + Catalog latestCatalog = catalogManager.catalog(latestCatalogVersion); + assert latestCatalog != null; + + Set quotedSystemSchemas = CatalogUtils.SYSTEM_SCHEMAS.stream() + .map(IgniteNameUtils::quote) + .collect(Collectors.toSet()); + + quotedSystemSchemas.add(IgniteNameUtils.quote(QualifiedName.DEFAULT_SCHEMA_NAME)); + + for (CatalogSchemaDescriptor schema : latestCatalog.schemas()) { + String schemaName = IgniteNameUtils.quote(schema.name()); + + if (quotedSystemSchemas.contains(schemaName)) { + continue; + } + sql("DROP SCHEMA " + schemaName + " CASCADE"); } } diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItSqlApiBaseTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItSqlApiBaseTest.java index 3bab2b9e3ff..f9957ed9b53 100644 --- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItSqlApiBaseTest.java +++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItSqlApiBaseTest.java @@ -19,6 +19,7 @@ import static java.util.stream.Collectors.toList; import static org.apache.ignite.internal.TestWrappers.unwrapIgniteImpl; +import static org.apache.ignite.internal.lang.IgniteStringFormatter.format; import static org.apache.ignite.internal.sql.engine.util.QueryChecker.containsIndexScan; import static org.apache.ignite.internal.sql.engine.util.QueryChecker.containsTableScan; import static org.apache.ignite.internal.sql.engine.util.SqlTestUtils.asStream; @@ -75,7 +76,6 @@ import org.apache.ignite.sql.SqlRow; import org.apache.ignite.sql.Statement; import org.apache.ignite.sql.Statement.StatementBuilder; -import org.apache.ignite.table.Table; import org.apache.ignite.tx.Transaction; import org.apache.ignite.tx.TransactionOptions; import org.hamcrest.Matcher; @@ -96,10 +96,9 @@ public abstract class ItSqlApiBaseTest extends BaseSqlIntegrationTest { protected static final int ROW_COUNT = 16; @AfterEach - public void dropTables() { - for (Table t : CLUSTER.aliveNode().tables().tables()) { - sql("DROP TABLE " + t.name()); - } + public void dropTablesAndSchemas() { + dropAllTables(); + dropAllSchemas(); } @Test @@ -1112,6 +1111,96 @@ public void testKillCommand() { } } + @Test + public void useNonDefaultSchema() { + IgniteSql sql = igniteSql(); + + sql("CREATE SCHEMA schema1"); + sql("CREATE TABLE schema1.t1 (id INT PRIMARY KEY, val INT)"); + sql("INSERT INTO schema1.t1 VALUES (1, 1), (2, 2)"); + + // Schema 2 has t1 as well + + sql("CREATE SCHEMA schema2"); + sql("CREATE TABLE schema2.t1 (id INT PRIMARY KEY, val INT)"); + sql("INSERT INTO schema2.t1 VALUES (1, 1), (2, 2), (3, 3)"); + + { + Statement stmt = sql.statementBuilder() + .query("SELECT COUNT(*) FROM schema1.t1") + .build(); + + try (ResultSet rs = executeForRead(sql, stmt)) { + assertEquals(2, rs.next().longValue(0)); + } + } + + { + Statement stmt = sql.statementBuilder() + .defaultSchema("schema1") + .query("SELECT COUNT(*) FROM t1") + .build(); + + try (ResultSet rs = executeForRead(sql, stmt)) { + assertEquals(2, rs.next().longValue(0)); + } + } + + // Check schema 2 + + { + Statement stmt = sql.statementBuilder() + .defaultSchema("schema2") + .query(format("SELECT COUNT(*) FROM t1")) + .build(); + + try (ResultSet rs = executeForRead(sql, stmt)) { + assertEquals(3, rs.next().longValue(0)); + } + } + } + + @Test + public void useNonDefaultSchemaWithQuotedName() { + IgniteSql sql = igniteSql(); + + sql("CREATE SCHEMA schema1"); + sql("CREATE TABLE schema1.\"T 1\" (id INT PRIMARY KEY, val INT)"); + sql("INSERT INTO schema1.\"T 1\" VALUES (1, 1), (2, 2)"); + + // Schema 2 has T1 as well + + sql("CREATE SCHEMA \"ScheMa1\""); + sql("CREATE TABLE \"ScheMa1\".\"T 1\" (id INT PRIMARY KEY, val INT)"); + sql("INSERT INTO \"ScheMa1\".\"T 1\" VALUES (1, 1), (2, 2), (3, 3)"); + + // Check schema 1 + + { + Statement stmt = sql.statementBuilder() + .defaultSchema("schema1") + .query("SELECT COUNT(*) FROM \"T 1\"") + .build(); + + try (ResultSet rs = executeForRead(sql, stmt)) { + assertEquals(2, rs.next().longValue(0)); + } + } + + // Check schema 2 + + { + Statement stmt = sql.statementBuilder() + .defaultSchema("\"ScheMa1\"") + .query(format("SELECT COUNT(*) FROM \"T 1\"")) + .build(); + + try (ResultSet rs = executeForRead(sql, stmt)) { + assertEquals(3, rs.next().longValue(0)); + } + } + } + protected ResultSet executeForRead(IgniteSql sql, String query, Object... args) { return executeForRead(sql, null, query, args); } diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java index 1dfc604713c..e94cb5c58dc 100644 --- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java +++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java @@ -66,11 +66,7 @@ public class ItDataTypesTest extends BaseSqlIntegrationTest { */ @AfterEach public void dropTables() { - var igniteTables = CLUSTER.aliveNode().tables(); - - for (var table : igniteTables.tables()) { - sql("DROP TABLE " + table.name()); - } + dropAllTables(); } /** Tests correctness with unicode. */ diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItSchemaTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItSchemaTest.java new file mode 100644 index 00000000000..3413bcf2f50 --- /dev/null +++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItSchemaTest.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.ignite.internal.sql.engine; + +import static org.apache.ignite.internal.sql.engine.util.SqlTestUtils.assertThrowsSqlException; + +import org.apache.ignite.internal.sql.BaseSqlIntegrationTest; +import org.apache.ignite.lang.ErrorGroups.Sql; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * SQL tests for schema DDL commands. + */ +public class ItSchemaTest extends BaseSqlIntegrationTest { + + @AfterEach + public void dropSchemas() { + dropAllSchemas(); + } + + @Test + public void createSchema() { + sql("CREATE SCHEMA IF NOT EXISTS schema1"); + + sql("CREATE TABLE schema1.test1 (id INT PRIMARY KEY, val INT)"); + sql("INSERT INTO schema1.test1 VALUES (1, 1), (2, 2)"); + + assertQuery("SELECT * FROM schema1.test1") + .returnRowCount(2) + .check(); + + assertThrowsSqlException( + Sql.STMT_VALIDATION_ERR, + "Schema with name 'SCHEMA1' already exists.", + () -> sql("CREATE SCHEMA schema1") + ); + + // Table is still accessible + assertQuery("SELECT * FROM schema1.test1") + .returnRowCount(2) + .check(); + } + + @Test + public void createSchemaIfExists() { + sql("CREATE SCHEMA IF NOT EXISTS schema1"); + + sql("CREATE TABLE schema1.test1 (id INT PRIMARY KEY, val INT)"); + sql("INSERT INTO schema1.test1 VALUES (1, 1), (2, 2)"); + + assertQuery("SELECT * FROM schema1.test1") + .returnRowCount(2) + .check(); + + sql("CREATE SCHEMA IF NOT EXISTS schema1"); + + assertQuery("SELECT * FROM schema1.test1") + .returnRowCount(2) + .check(); + } + + @Test + public void dropSchemaDefaultBehaviour() { + sql("CREATE SCHEMA schema1"); + sql("CREATE TABLE schema1.test1 (id INT PRIMARY KEY, val INT)"); + + assertThrowsSqlException( + Sql.STMT_VALIDATION_ERR, + "Schema 'SCHEMA1' is not empty. Use CASCADE to drop it anyway.", + () -> sql("DROP SCHEMA schema1") + ); + + // Succeeds + sql("DROP TABLE schema1.test1"); + sql("DROP SCHEMA schema1"); + } + + @Test + public void dropSchemaRestrict() { + sql("CREATE SCHEMA schema1"); + sql("CREATE TABLE schema1.test1 (id INT PRIMARY KEY, val INT)"); + + assertThrowsSqlException( + Sql.STMT_VALIDATION_ERR, + "Schema 'SCHEMA1' is not empty. Use CASCADE to drop it anyway.", + () -> sql("DROP SCHEMA schema1 RESTRICT") + ); + + // Succeeds + sql("DROP TABLE schema1.test1"); + sql("DROP SCHEMA schema1 RESTRICT"); + } + + @Test + public void dropSchemaCascade() { + { + sql("CREATE SCHEMA schema1"); + sql("CREATE TABLE schema1.test1 (id INT PRIMARY KEY, val INT)"); + + sql("DROP SCHEMA schema1 CASCADE"); + + assertThrowsSqlException( + Sql.STMT_VALIDATION_ERR, + "Object 'SCHEMA1' not found", + () -> sql("SELECT * FROM schema1.test1") + ); + + assertThrowsSqlException( + Sql.SCHEMA_NOT_FOUND_ERR, + "Schema not found [schemaName=SCHEMA1]", + () -> sql("CREATE TABLE schema1.test1 (id INT PRIMARY KEY, val INT)") + ); + } + + // IF EXISTS + + { + sql("CREATE SCHEMA schema2"); + sql("CREATE TABLE schema2.test1 (id INT PRIMARY KEY, val INT)"); + + sql("DROP SCHEMA IF EXISTS schema2 CASCADE"); + + assertThrowsSqlException( + Sql.STMT_VALIDATION_ERR, + "Object 'SCHEMA2' not found", + () -> sql("SELECT * FROM schema2.test1") + ); + + assertThrowsSqlException( + Sql.SCHEMA_NOT_FOUND_ERR, + "Schema not found [schemaName=SCHEMA2]", + () -> sql("CREATE TABLE schema2.test1 (id INT PRIMARY KEY, val INT)") + ); + } + } + + @Test + public void schemaQuoted() { + { + sql("CREATE SCHEMA IF NOT EXISTS \"Sche ma1\""); + sql("CREATE TABLE \"Sche ma1\".test1 (id INT PRIMARY KEY, val INT)"); + sql("INSERT INTO \"Sche ma1\".test1 VALUES (1, 1), (2, 2)"); + + assertThrowsSqlException( + Sql.STMT_VALIDATION_ERR, + "Schema with name 'Sche ma1' already exists.", + () -> sql("CREATE SCHEMA \"Sche ma1\"") + ); + + assertQuery("SELECT * FROM \"Sche ma1\".test1") + .returnRowCount(2) + .check(); + + sql("DROP SCHEMA \"Sche ma1\" CASCADE"); + } + + { + sql("CREATE SCHEMA \"Sche ma2\""); + sql("DROP SCHEMA IF EXISTS \"Sche ma2\""); + + assertThrowsSqlException( + Sql.SCHEMA_NOT_FOUND_ERR, + "Schema not found [schemaName=Sche ma2]", + () -> sql("CREATE TABLE \"Sche ma2\".test1 (id INT PRIMARY KEY, val INT)") + ); + } + } +} diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/ItDivisionDecimalTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/ItDivisionDecimalTest.java index ae35cbdb606..563e0cd7c47 100644 --- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/ItDivisionDecimalTest.java +++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/ItDivisionDecimalTest.java @@ -54,11 +54,7 @@ public static void createTable() { */ @AfterAll public void dropTables() { - var igniteTables = CLUSTER.aliveNode().tables(); - - for (var table : igniteTables.tables()) { - sql("DROP TABLE " + table.name()); - } + dropAllTables(); } diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/IgniteSqlImpl.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/IgniteSqlImpl.java index 352820a676b..09015bbac61 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/IgniteSqlImpl.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/IgniteSqlImpl.java @@ -73,6 +73,7 @@ import org.apache.ignite.sql.Statement; import org.apache.ignite.sql.Statement.StatementBuilder; import org.apache.ignite.sql.async.AsyncResultSet; +import org.apache.ignite.table.QualifiedName; import org.apache.ignite.table.mapper.Mapper; import org.apache.ignite.tx.Transaction; import org.jetbrains.annotations.Nullable; @@ -370,8 +371,13 @@ private CompletableFuture> executeAsyncInternal( CompletableFuture> result; try { + // TODO: https://issues.apache.org/jira/browse/IGNITE-24021 + // Use correct implementation to parse identifier. + String schemaName = QualifiedName.fromSimple(statement.defaultSchema()).objectName(); + SqlProperties properties = toPropertiesBuilder(statement) .set(QueryProperty.ALLOWED_QUERY_TYPES, SqlQueryType.SINGLE_STMT_TYPES) + .set(QueryProperty.DEFAULT_SCHEMA, schemaName) .build(); result = queryProcessor.queryAsync( diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ddl/DdlCommandHandler.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ddl/DdlCommandHandler.java index e788d59bb44..62f8900831a 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ddl/DdlCommandHandler.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ddl/DdlCommandHandler.java @@ -34,6 +34,8 @@ import org.apache.ignite.internal.catalog.DistributionZoneNotFoundValidationException; import org.apache.ignite.internal.catalog.IndexExistsValidationException; import org.apache.ignite.internal.catalog.IndexNotFoundValidationException; +import org.apache.ignite.internal.catalog.SchemaExistsException; +import org.apache.ignite.internal.catalog.SchemaNotFoundException; import org.apache.ignite.internal.catalog.TableExistsValidationException; import org.apache.ignite.internal.catalog.TableNotFoundValidationException; import org.apache.ignite.internal.catalog.commands.AbstractCreateIndexCommand; @@ -42,9 +44,11 @@ import org.apache.ignite.internal.catalog.commands.AlterTableDropColumnCommand; import org.apache.ignite.internal.catalog.commands.AlterZoneCommand; import org.apache.ignite.internal.catalog.commands.AlterZoneSetDefaultCommand; +import org.apache.ignite.internal.catalog.commands.CreateSchemaCommand; import org.apache.ignite.internal.catalog.commands.CreateTableCommand; import org.apache.ignite.internal.catalog.commands.CreateZoneCommand; import org.apache.ignite.internal.catalog.commands.DropIndexCommand; +import org.apache.ignite.internal.catalog.commands.DropSchemaCommand; import org.apache.ignite.internal.catalog.commands.DropTableCommand; import org.apache.ignite.internal.catalog.commands.DropZoneCommand; import org.apache.ignite.internal.catalog.commands.RenameZoneCommand; @@ -90,7 +94,11 @@ public DdlCommandHandler( /** Handles ddl commands. */ public CompletableFuture handle(CatalogCommand cmd) { - if (cmd instanceof CreateTableCommand) { + if (cmd instanceof CreateSchemaCommand) { + return handleCreateSchema((CreateSchemaCommand) cmd); + } else if (cmd instanceof DropSchemaCommand) { + return handleDropSchema((DropSchemaCommand) cmd); + } else if (cmd instanceof CreateTableCommand) { return handleCreateTable((CreateTableCommand) cmd); } else if (cmd instanceof DropTableCommand) { return handleDropTable((DropTableCommand) cmd); @@ -181,6 +189,18 @@ private CompletableFuture handleAlterColumn(AlterTableAlterColumnComman .handle(handleModificationResult(cmd.ifTableExists(), TableNotFoundValidationException.class)); } + /** Handles create schema command. */ + private CompletableFuture handleCreateSchema(CreateSchemaCommand cmd) { + return catalogManager.execute(cmd) + .handle(handleModificationResult(cmd.ifNotExists(), SchemaExistsException.class)); + } + + /** Handles drop schema command. */ + private CompletableFuture handleDropSchema(DropSchemaCommand cmd) { + return catalogManager.execute(cmd) + .handle(handleModificationResult(cmd.ifExists(), SchemaNotFoundException.class)); + } + private static BiFunction handleModificationResult(boolean ignoreExpectedError, Class expErrCls) { return (val, err) -> { if (err == null) { diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DdlSqlToCommandConverter.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DdlSqlToCommandConverter.java index f6db68d67ba..8199cb9aecf 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DdlSqlToCommandConverter.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DdlSqlToCommandConverter.java @@ -94,6 +94,7 @@ import org.apache.ignite.internal.catalog.commands.AlterZoneSetDefaultCommand; import org.apache.ignite.internal.catalog.commands.ColumnParams; import org.apache.ignite.internal.catalog.commands.CreateHashIndexCommand; +import org.apache.ignite.internal.catalog.commands.CreateSchemaCommand; import org.apache.ignite.internal.catalog.commands.CreateSortedIndexCommand; import org.apache.ignite.internal.catalog.commands.CreateTableCommand; import org.apache.ignite.internal.catalog.commands.CreateTableCommandBuilder; @@ -102,6 +103,7 @@ import org.apache.ignite.internal.catalog.commands.DefaultValue; import org.apache.ignite.internal.catalog.commands.DeferredDefaultValue; import org.apache.ignite.internal.catalog.commands.DropIndexCommand; +import org.apache.ignite.internal.catalog.commands.DropSchemaCommand; import org.apache.ignite.internal.catalog.commands.DropTableCommand; import org.apache.ignite.internal.catalog.commands.DropTableCommandBuilder; import org.apache.ignite.internal.catalog.commands.DropZoneCommand; @@ -121,9 +123,12 @@ import org.apache.ignite.internal.sql.engine.sql.IgniteSqlAlterZoneSet; import org.apache.ignite.internal.sql.engine.sql.IgniteSqlAlterZoneSetDefault; import org.apache.ignite.internal.sql.engine.sql.IgniteSqlCreateIndex; +import org.apache.ignite.internal.sql.engine.sql.IgniteSqlCreateSchema; import org.apache.ignite.internal.sql.engine.sql.IgniteSqlCreateTable; import org.apache.ignite.internal.sql.engine.sql.IgniteSqlCreateZone; import org.apache.ignite.internal.sql.engine.sql.IgniteSqlDropIndex; +import org.apache.ignite.internal.sql.engine.sql.IgniteSqlDropSchema; +import org.apache.ignite.internal.sql.engine.sql.IgniteSqlDropSchemaBehavior; import org.apache.ignite.internal.sql.engine.sql.IgniteSqlDropTable; import org.apache.ignite.internal.sql.engine.sql.IgniteSqlDropZone; import org.apache.ignite.internal.sql.engine.sql.IgniteSqlIndexType; @@ -265,11 +270,34 @@ public CatalogCommand convert(SqlDdl ddlNode, PlanningContext ctx) { return convertDropZone((IgniteSqlDropZone) ddlNode, ctx); } + if (ddlNode instanceof IgniteSqlCreateSchema) { + return convertCreateSchema((IgniteSqlCreateSchema) ddlNode, ctx); + } + + if (ddlNode instanceof IgniteSqlDropSchema) { + return convertDropSchema((IgniteSqlDropSchema) ddlNode, ctx); + } + throw new SqlException(STMT_VALIDATION_ERR, "Unsupported operation [" + "sqlNodeKind=" + ddlNode.getKind() + "; " + "querySql=\"" + ctx.query() + "\"]"); } + private CatalogCommand convertCreateSchema(IgniteSqlCreateSchema ddlNode, PlanningContext ctx) { + return CreateSchemaCommand.builder() + .name(deriveObjectName(ddlNode.name(), ctx, "schemaName")) + .ifNotExists(ddlNode.ifNotExists()) + .build(); + } + + private CatalogCommand convertDropSchema(IgniteSqlDropSchema ddlNode, PlanningContext ctx) { + return DropSchemaCommand.builder() + .name(deriveObjectName(ddlNode.name(), ctx, "schemaName")) + .ifExists(ddlNode.ifExists()) + .cascade(ddlNode.behavior() == IgniteSqlDropSchemaBehavior.CASCADE) + .build(); + } + /** * Converts the given '{@code CREATE TABLE}' AST to the {@link CreateTableCommand} catalog command. */