diff --git a/docs/content/flink/sql-alter.md b/docs/content/flink/sql-alter.md index 877995cc631b..6c3186b4af75 100644 --- a/docs/content/flink/sql-alter.md +++ b/docs/content/flink/sql-alter.md @@ -227,3 +227,19 @@ The following SQL modifies the watermark strategy to `ts - INTERVAL '2' HOUR`. ```sql ALTER TABLE my_table MODIFY WATERMARK FOR ts AS ts - INTERVAL '2' HOUR ``` + +# ALTER DATABASE + +The following SQL sets one or more properties in the specified database. If a particular property is already set in the database, override the old value with the new one. + +```sql +ALTER DATABASE [catalog_name.]db_name SET (key1=val1, key2=val2, ...) +``` + +## Altering Database Location + +The following SQL changes location of database `my_database` to `file:/temp/my_database`. + +```sql +ALTER DATABASE my_database SET ('location' = 'file:/temp/my_database') +``` diff --git a/docs/content/program-api/catalog-api.md b/docs/content/program-api/catalog-api.md index 570577437d86..7e716aad15bb 100644 --- a/docs/content/program-api/catalog-api.md +++ b/docs/content/program-api/catalog-api.md @@ -82,7 +82,7 @@ public class ListDatabases { ## Drop Database -You can use the catalog to drop databases. +You can use the catalog to drop database. ```java import org.apache.paimon.catalog.Catalog; @@ -102,6 +102,30 @@ public class DropDatabase { } ``` +## Alter Database + +You can use the catalog to alter database's properties.(ps: only support hive and jdbc catalog) + +```java +import java.util.ArrayList; +import org.apache.paimon.catalog.Catalog; + +public class AlterDatabase { + + public static void main(String[] args) { + try { + Catalog catalog = CreateCatalog.createHiveCatalog(); + List changes = new ArrayList<>(); + changes.add(DatabaseChange.setProperty("k1", "v1")); + changes.add(DatabaseChange.removeProperty("k2")); + catalog.alterDatabase("my_db", changes, true); + } catch (Catalog.DatabaseNotExistException e) { + // do something + } + } +} +``` + ## Determine Whether Table Exists You can use the catalog to determine whether the table exists diff --git a/docs/content/spark/sql-alter.md b/docs/content/spark/sql-alter.md index 3ad72048029b..359b1187292d 100644 --- a/docs/content/spark/sql-alter.md +++ b/docs/content/spark/sql-alter.md @@ -240,3 +240,21 @@ The following SQL changes the type of a nested column `f2` to `BIGINT` in a stru -- column v previously has type MAP> ALTER TABLE my_table ALTER COLUMN v.value.f2 TYPE BIGINT; ``` + + +# ALTER DATABASE + +The following SQL sets one or more properties in the specified database. If a particular property is already set in the database, override the old value with the new one. + +```sql +ALTER { DATABASE | SCHEMA | NAMESPACE } my_database + SET { DBPROPERTIES | PROPERTIES } ( property_name = property_value [ , ... ] ) +``` + +## Altering Database Location + +The following SQL sets the location of the specified database to `file:/temp/my_database.db`. + +```sql +ALTER DATABASE my_database SET LOCATION 'file:/temp/my_database.db' +``` \ No newline at end of file diff --git a/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java b/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java index db6909295556..892e77735b4b 100644 --- a/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java +++ b/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java @@ -223,6 +223,26 @@ public void dropDatabase(String name, boolean ignoreIfNotExists, boolean cascade protected abstract void dropDatabaseImpl(String name); + @Override + public void alterDatabase(String name, List changes, boolean ignoreIfNotExists) + throws DatabaseNotExistException { + checkNotSystemDatabase(name); + try { + if (changes == null || changes.isEmpty()) { + return; + } + alterDatabaseImpl(name, changes); + } catch (DatabaseNotExistException e) { + if (ignoreIfNotExists) { + return; + } + throw new DatabaseNotExistException(name); + } + } + + protected abstract void alterDatabaseImpl(String name, List changes) + throws DatabaseNotExistException; + @Override public List listTables(String databaseName) throws DatabaseNotExistException { if (isSystemDatabase(databaseName)) { diff --git a/paimon-core/src/main/java/org/apache/paimon/catalog/CachingCatalog.java b/paimon-core/src/main/java/org/apache/paimon/catalog/CachingCatalog.java index 82d503b7a272..99540cf0cea5 100644 --- a/paimon-core/src/main/java/org/apache/paimon/catalog/CachingCatalog.java +++ b/paimon-core/src/main/java/org/apache/paimon/catalog/CachingCatalog.java @@ -187,6 +187,13 @@ public void dropDatabase(String name, boolean ignoreIfNotExists, boolean cascade } } + @Override + public void alterDatabase(String name, List changes, boolean ignoreIfNotExists) + throws DatabaseNotExistException { + super.alterDatabase(name, changes, ignoreIfNotExists); + databaseCache.invalidate(name); + } + @Override public void dropTable(Identifier identifier, boolean ignoreIfNotExists) throws TableNotExistException { diff --git a/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java b/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java index 7b1fe0ea072e..37ea6fa5e203 100644 --- a/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java +++ b/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java @@ -126,6 +126,19 @@ void createDatabase(String name, boolean ignoreIfExists, Map pro void dropDatabase(String name, boolean ignoreIfNotExists, boolean cascade) throws DatabaseNotExistException, DatabaseNotEmptyException; + /** + * Alter a database. + * + * @param name Name of the database to alter. + * @param changes the property changes + * @param ignoreIfNotExists Flag to specify behavior when the database does not exist: if set to + * false, throw an exception, if set to true, do nothing. + * @throws DatabaseNotExistException if the given database is not exist and ignoreIfNotExists is + * false + */ + void alterDatabase(String name, List changes, boolean ignoreIfNotExists) + throws DatabaseNotExistException; + /** * Return a {@link Table} identified by the given {@link Identifier}. * diff --git a/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java b/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java index 93e8ce2581ad..968f00cfcae5 100644 --- a/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java +++ b/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java @@ -83,6 +83,12 @@ public void dropDatabase(String name, boolean ignoreIfNotExists, boolean cascade wrapped.dropDatabase(name, ignoreIfNotExists, cascade); } + @Override + public void alterDatabase(String name, List changes, boolean ignoreIfNotExists) + throws DatabaseNotExistException { + wrapped.alterDatabase(name, changes, ignoreIfNotExists); + } + @Override public List listTables(String databaseName) throws DatabaseNotExistException { return wrapped.listTables(databaseName); diff --git a/paimon-core/src/main/java/org/apache/paimon/catalog/FileSystemCatalog.java b/paimon-core/src/main/java/org/apache/paimon/catalog/FileSystemCatalog.java index 279ddb26ee53..cb0c358259f8 100644 --- a/paimon-core/src/main/java/org/apache/paimon/catalog/FileSystemCatalog.java +++ b/paimon-core/src/main/java/org/apache/paimon/catalog/FileSystemCatalog.java @@ -92,6 +92,11 @@ protected void dropDatabaseImpl(String name) { uncheck(() -> fileIO.delete(newDatabasePath(name), true)); } + @Override + protected void alterDatabaseImpl(String name, List changes) { + throw new UnsupportedOperationException("Alter database is not supported."); + } + @Override protected List listTablesImpl(String databaseName) { return uncheck(() -> listTablesInFileSystem(newDatabasePath(databaseName))); diff --git a/paimon-core/src/main/java/org/apache/paimon/catalog/PropertyChange.java b/paimon-core/src/main/java/org/apache/paimon/catalog/PropertyChange.java new file mode 100644 index 000000000000..c3423efd081e --- /dev/null +++ b/paimon-core/src/main/java/org/apache/paimon/catalog/PropertyChange.java @@ -0,0 +1,91 @@ +/* + * 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.paimon.catalog; + +import org.apache.paimon.utils.Pair; + +import org.apache.paimon.shade.guava30.com.google.common.collect.Maps; +import org.apache.paimon.shade.guava30.com.google.common.collect.Sets; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** define change to the database property. */ +public interface PropertyChange { + + static PropertyChange setProperty(String property, String value) { + return new SetProperty(property, value); + } + + static PropertyChange removeProperty(String property) { + return new RemoveProperty(property); + } + + static Pair, Set> getSetPropertiesToRemoveKeys( + List changes) { + Map setProperties = Maps.newHashMap(); + Set removeKeys = Sets.newHashSet(); + changes.forEach( + change -> { + if (change instanceof PropertyChange.SetProperty) { + PropertyChange.SetProperty setProperty = + (PropertyChange.SetProperty) change; + setProperties.put(setProperty.property(), setProperty.value()); + } else { + removeKeys.add(((PropertyChange.RemoveProperty) change).property()); + } + }); + return Pair.of(setProperties, removeKeys); + } + + /** Set property for database change. */ + final class SetProperty implements PropertyChange { + + private final String property; + private final String value; + + private SetProperty(String property, String value) { + this.property = property; + this.value = value; + } + + public String property() { + return this.property; + } + + public String value() { + return this.value; + } + } + + /** Remove property for database change. */ + final class RemoveProperty implements PropertyChange { + + private final String property; + + private RemoveProperty(String property) { + this.property = property; + } + + public String property() { + return this.property; + } + } +} diff --git a/paimon-core/src/main/java/org/apache/paimon/jdbc/JdbcCatalog.java b/paimon-core/src/main/java/org/apache/paimon/jdbc/JdbcCatalog.java index 551b2d8fc910..63cb54c180f5 100644 --- a/paimon-core/src/main/java/org/apache/paimon/jdbc/JdbcCatalog.java +++ b/paimon-core/src/main/java/org/apache/paimon/jdbc/JdbcCatalog.java @@ -24,6 +24,7 @@ import org.apache.paimon.catalog.CatalogLockFactory; import org.apache.paimon.catalog.Database; import org.apache.paimon.catalog.Identifier; +import org.apache.paimon.catalog.PropertyChange; import org.apache.paimon.fs.FileIO; import org.apache.paimon.fs.Path; import org.apache.paimon.operation.Lock; @@ -33,11 +34,13 @@ import org.apache.paimon.schema.SchemaChange; import org.apache.paimon.schema.SchemaManager; import org.apache.paimon.schema.TableSchema; +import org.apache.paimon.utils.Pair; import org.apache.paimon.utils.Preconditions; import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableMap; import org.apache.paimon.shade.guava30.com.google.common.collect.Lists; import org.apache.paimon.shade.guava30.com.google.common.collect.Maps; +import org.apache.paimon.shade.guava30.com.google.common.collect.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,12 +55,15 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import static org.apache.paimon.jdbc.JdbcCatalogLock.acquireTimeout; import static org.apache.paimon.jdbc.JdbcCatalogLock.checkMaxSleep; +import static org.apache.paimon.jdbc.JdbcUtils.deleteProperties; import static org.apache.paimon.jdbc.JdbcUtils.execute; import static org.apache.paimon.jdbc.JdbcUtils.insertProperties; +import static org.apache.paimon.jdbc.JdbcUtils.updateProperties; import static org.apache.paimon.jdbc.JdbcUtils.updateTable; /* This file is based on source code from the Iceberg Project (http://iceberg.apache.org/), licensed by the Apache @@ -197,6 +203,45 @@ protected void dropDatabaseImpl(String name) { execute(connections, JdbcUtils.DELETE_ALL_DATABASE_PROPERTIES_SQL, catalogKey, name); } + @Override + protected void alterDatabaseImpl(String name, List changes) { + Pair, Set> setPropertiesToRemoveKeys = + PropertyChange.getSetPropertiesToRemoveKeys(changes); + Map setProperties = setPropertiesToRemoveKeys.getLeft(); + Set removeKeys = setPropertiesToRemoveKeys.getRight(); + Map startingProperties = fetchProperties(name); + Map inserts = Maps.newHashMap(); + Map updates = Maps.newHashMap(); + Set removes = Sets.newHashSet(); + if (!setProperties.isEmpty()) { + setProperties.forEach( + (k, v) -> { + if (!startingProperties.containsKey(k)) { + inserts.put(k, v); + } else { + updates.put(k, v); + } + }); + } + if (!removeKeys.isEmpty()) { + removeKeys.forEach( + k -> { + if (startingProperties.containsKey(k)) { + removes.add(k); + } + }); + } + if (!inserts.isEmpty()) { + insertProperties(connections, catalogKey, name, inserts); + } + if (!updates.isEmpty()) { + updateProperties(connections, catalogKey, name, updates); + } + if (!removes.isEmpty()) { + deleteProperties(connections, catalogKey, name, removes); + } + } + @Override protected List listTablesImpl(String databaseName) { return fetch( diff --git a/paimon-core/src/main/java/org/apache/paimon/jdbc/JdbcUtils.java b/paimon-core/src/main/java/org/apache/paimon/jdbc/JdbcUtils.java index 4acb0f25aa91..1b9e599d72bc 100644 --- a/paimon-core/src/main/java/org/apache/paimon/jdbc/JdbcUtils.java +++ b/paimon-core/src/main/java/org/apache/paimon/jdbc/JdbcUtils.java @@ -30,8 +30,10 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLIntegrityConstraintViolationException; +import java.util.Collections; import java.util.Map; import java.util.Properties; +import java.util.Set; import java.util.function.Consumer; import java.util.stream.Stream; @@ -202,6 +204,16 @@ public class JdbcUtils { + " = ? AND " + DATABASE_NAME + " = ? "; + static final String DELETE_DATABASE_PROPERTIES_SQL = + "DELETE FROM " + + DATABASE_PROPERTIES_TABLE_NAME + + " WHERE " + + CATALOG_KEY + + " = ? AND " + + DATABASE_NAME + + " = ? AND " + + DATABASE_PROPERTY_KEY + + " IN "; static final String DELETE_ALL_DATABASE_PROPERTIES_SQL = "DELETE FROM " + DATABASE_PROPERTIES_TABLE_NAME @@ -403,6 +415,75 @@ private static String insertPropertiesStatement(int size) { return sqlStatement.toString(); } + public static boolean updateProperties( + JdbcClientPool connections, + String storeKey, + String databaseName, + Map properties) { + Stream caseArgs = + properties.entrySet().stream() + .flatMap(entry -> Stream.of(entry.getKey(), entry.getValue())); + Stream whereArgs = + Stream.concat(Stream.of(storeKey, databaseName), properties.keySet().stream()); + + String[] args = Stream.concat(caseArgs, whereArgs).toArray(String[]::new); + + int updatedRecords = + execute(connections, JdbcUtils.updatePropertiesStatement(properties.size()), args); + if (updatedRecords == properties.size()) { + return true; + } + throw new IllegalStateException( + String.format( + "Failed to update: %d of %d succeeded", updatedRecords, properties.size())); + } + + private static String updatePropertiesStatement(int size) { + StringBuilder sqlStatement = + new StringBuilder( + "UPDATE " + + DATABASE_PROPERTIES_TABLE_NAME + + " SET " + + DATABASE_PROPERTY_VALUE + + " = CASE"); + for (int i = 0; i < size; i += 1) { + sqlStatement.append(" WHEN " + DATABASE_PROPERTY_KEY + " = ? THEN ?"); + } + + sqlStatement.append( + " END WHERE " + + CATALOG_KEY + + " = ? AND " + + DATABASE_NAME + + " = ? AND " + + DATABASE_PROPERTY_KEY + + " IN "); + + String values = String.join(",", Collections.nCopies(size, String.valueOf('?'))); + sqlStatement.append("(").append(values).append(")"); + + return sqlStatement.toString(); + } + + public static boolean deleteProperties( + JdbcClientPool connections, + String storeKey, + String databaseName, + Set removeKeys) { + String[] args = + Stream.concat(Stream.of(storeKey, databaseName), removeKeys.stream()) + .toArray(String[]::new); + + int deleteRecords = + execute(connections, JdbcUtils.deletePropertiesStatement(removeKeys), args); + if (deleteRecords > 0) { + return true; + } + throw new IllegalStateException( + String.format( + "Failed to delete: %d of %d succeeded", deleteRecords, removeKeys.size())); + } + public static void createDistributedLockTable(JdbcClientPool connections, Options options) throws SQLException, InterruptedException { DistributedLockDialectFactory.create(connections.getProtocol()) @@ -427,4 +508,13 @@ public static void release(JdbcClientPool connections, String lockId) DistributedLockDialectFactory.create(connections.getProtocol()) .releaseLock(connections, lockId); } + + private static String deletePropertiesStatement(Set properties) { + StringBuilder sqlStatement = new StringBuilder(JdbcUtils.DELETE_DATABASE_PROPERTIES_SQL); + String values = + String.join(",", Collections.nCopies(properties.size(), String.valueOf('?'))); + sqlStatement.append("(").append(values).append(")"); + + return sqlStatement.toString(); + } } diff --git a/paimon-core/src/main/java/org/apache/paimon/privilege/AllGrantedPrivilegeChecker.java b/paimon-core/src/main/java/org/apache/paimon/privilege/AllGrantedPrivilegeChecker.java index 09944681a2e7..8e8e4cd53d04 100644 --- a/paimon-core/src/main/java/org/apache/paimon/privilege/AllGrantedPrivilegeChecker.java +++ b/paimon-core/src/main/java/org/apache/paimon/privilege/AllGrantedPrivilegeChecker.java @@ -41,6 +41,9 @@ public void assertCanCreateTable(String databaseName) {} @Override public void assertCanDropDatabase(String databaseName) {} + @Override + public void assertCanAlterDatabase(String databaseName) {} + @Override public void assertCanCreateDatabase() {} diff --git a/paimon-core/src/main/java/org/apache/paimon/privilege/PrivilegeChecker.java b/paimon-core/src/main/java/org/apache/paimon/privilege/PrivilegeChecker.java index 1771d40f4028..14cbbc6f36a3 100644 --- a/paimon-core/src/main/java/org/apache/paimon/privilege/PrivilegeChecker.java +++ b/paimon-core/src/main/java/org/apache/paimon/privilege/PrivilegeChecker.java @@ -53,6 +53,8 @@ default void assertCanSelectOrInsert(Identifier identifier) { void assertCanDropDatabase(String databaseName); + void assertCanAlterDatabase(String databaseName); + void assertCanCreateDatabase(); void assertCanCreateUser(); diff --git a/paimon-core/src/main/java/org/apache/paimon/privilege/PrivilegeCheckerImpl.java b/paimon-core/src/main/java/org/apache/paimon/privilege/PrivilegeCheckerImpl.java index 19c1813ee852..7e7876fa4e44 100644 --- a/paimon-core/src/main/java/org/apache/paimon/privilege/PrivilegeCheckerImpl.java +++ b/paimon-core/src/main/java/org/apache/paimon/privilege/PrivilegeCheckerImpl.java @@ -85,6 +85,14 @@ public void assertCanDropDatabase(String databaseName) { } } + @Override + public void assertCanAlterDatabase(String databaseName) { + if (!check(databaseName, PrivilegeType.ALTER_DATABASE)) { + throw new NoPrivilegeException( + user, "database", databaseName, PrivilegeType.ALTER_DATABASE); + } + } + @Override public void assertCanCreateDatabase() { if (!check( diff --git a/paimon-core/src/main/java/org/apache/paimon/privilege/PrivilegeType.java b/paimon-core/src/main/java/org/apache/paimon/privilege/PrivilegeType.java index 375f5030d557..00b3a50596cb 100644 --- a/paimon-core/src/main/java/org/apache/paimon/privilege/PrivilegeType.java +++ b/paimon-core/src/main/java/org/apache/paimon/privilege/PrivilegeType.java @@ -34,6 +34,7 @@ public enum PrivilegeType { CREATE_TABLE(PrivilegeTarget.DATABASE), DROP_DATABASE(PrivilegeTarget.DATABASE), + ALTER_DATABASE(PrivilegeTarget.DATABASE), CREATE_DATABASE(PrivilegeTarget.CATALOG), // you can create and drop users, grant and revoke any privileges to or from others diff --git a/paimon-core/src/main/java/org/apache/paimon/privilege/PrivilegedCatalog.java b/paimon-core/src/main/java/org/apache/paimon/privilege/PrivilegedCatalog.java index 2e88213a24b9..35822471a2d6 100644 --- a/paimon-core/src/main/java/org/apache/paimon/privilege/PrivilegedCatalog.java +++ b/paimon-core/src/main/java/org/apache/paimon/privilege/PrivilegedCatalog.java @@ -21,6 +21,7 @@ import org.apache.paimon.catalog.Catalog; import org.apache.paimon.catalog.DelegateCatalog; import org.apache.paimon.catalog.Identifier; +import org.apache.paimon.catalog.PropertyChange; import org.apache.paimon.options.ConfigOption; import org.apache.paimon.options.ConfigOptions; import org.apache.paimon.options.Options; @@ -82,6 +83,13 @@ public void dropDatabase(String name, boolean ignoreIfNotExists, boolean cascade privilegeManager.objectDropped(name); } + @Override + public void alterDatabase(String name, List changes, boolean ignoreIfNotExists) + throws DatabaseNotExistException { + privilegeManager.getPrivilegeChecker().assertCanAlterDatabase(name); + super.alterDatabase(name, changes, ignoreIfNotExists); + } + @Override public void dropTable(Identifier identifier, boolean ignoreIfNotExists) throws TableNotExistException { diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java index c30e1109e2ec..8b53bef8486b 100644 --- a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java +++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java @@ -21,6 +21,7 @@ import org.apache.paimon.catalog.Catalog; import org.apache.paimon.catalog.Database; import org.apache.paimon.catalog.Identifier; +import org.apache.paimon.catalog.PropertyChange; import org.apache.paimon.fs.FileIO; import org.apache.paimon.manifest.PartitionEntry; import org.apache.paimon.options.CatalogOptions; @@ -30,7 +31,9 @@ import org.apache.paimon.rest.auth.CredentialsProviderFactory; import org.apache.paimon.rest.exceptions.AlreadyExistsException; import org.apache.paimon.rest.exceptions.NoSuchResourceException; +import org.apache.paimon.rest.requests.AlterDatabaseRequest; import org.apache.paimon.rest.requests.CreateDatabaseRequest; +import org.apache.paimon.rest.responses.AlterDatabaseResponse; import org.apache.paimon.rest.responses.ConfigResponse; import org.apache.paimon.rest.responses.CreateDatabaseResponse; import org.apache.paimon.rest.responses.DatabaseName; @@ -39,6 +42,7 @@ import org.apache.paimon.schema.Schema; import org.apache.paimon.schema.SchemaChange; import org.apache.paimon.table.Table; +import org.apache.paimon.utils.Pair; import org.apache.paimon.shade.guava30.com.google.common.annotations.VisibleForTesting; import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableList; @@ -49,6 +53,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.stream.Collectors; @@ -136,12 +141,14 @@ public List listDatabases() { @Override public void createDatabase(String name, boolean ignoreIfExists, Map properties) throws DatabaseAlreadyExistException { - CreateDatabaseRequest request = new CreateDatabaseRequest(name, ignoreIfExists, properties); + CreateDatabaseRequest request = new CreateDatabaseRequest(name, properties); try { client.post( resourcePaths.databases(), request, CreateDatabaseResponse.class, headers()); } catch (AlreadyExistsException e) { - throw new DatabaseAlreadyExistException(name); + if (!ignoreIfExists) { + throw new DatabaseAlreadyExistException(name); + } } } @@ -172,6 +179,32 @@ public void dropDatabase(String name, boolean ignoreIfNotExists, boolean cascade } } + @Override + public void alterDatabase(String name, List changes, boolean ignoreIfNotExists) + throws DatabaseNotExistException { + try { + Pair, Set> setPropertiesToRemoveKeys = + PropertyChange.getSetPropertiesToRemoveKeys(changes); + Map updateProperties = setPropertiesToRemoveKeys.getLeft(); + Set removeKeys = setPropertiesToRemoveKeys.getRight(); + AlterDatabaseRequest request = + new AlterDatabaseRequest(new ArrayList<>(removeKeys), updateProperties); + AlterDatabaseResponse response = + client.post( + resourcePaths.databaseProperties(name), + request, + AlterDatabaseResponse.class, + headers()); + if (response.getUpdated().isEmpty()) { + throw new IllegalStateException("Failed to update properties"); + } + } catch (NoSuchResourceException e) { + if (!ignoreIfNotExists) { + throw new DatabaseNotExistException(name); + } + } + } + @Override public Table getTable(Identifier identifier) throws TableNotExistException { throw new UnsupportedOperationException(); diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java b/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java index b58053374daa..51277454ffb0 100644 --- a/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java +++ b/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java @@ -43,4 +43,13 @@ public String databases() { public String database(String databaseName) { return SLASH.add("v1").add(prefix).add("databases").add(databaseName).toString(); } + + public String databaseProperties(String databaseName) { + return SLASH.add("v1") + .add(prefix) + .add("databases") + .add(databaseName) + .add("properties") + .toString(); + } } diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/requests/AlterDatabaseRequest.java b/paimon-core/src/main/java/org/apache/paimon/rest/requests/AlterDatabaseRequest.java new file mode 100644 index 000000000000..c1330142bb7e --- /dev/null +++ b/paimon-core/src/main/java/org/apache/paimon/rest/requests/AlterDatabaseRequest.java @@ -0,0 +1,59 @@ +/* + * 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.paimon.rest.requests; + +import org.apache.paimon.rest.RESTRequest; + +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator; +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter; +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Map; + +/** Request for altering database. */ +public class AlterDatabaseRequest implements RESTRequest { + + private static final String FIELD_REMOVALS = "removals"; + private static final String FIELD_UPDATES = "updates"; + + @JsonProperty(FIELD_REMOVALS) + private List removals; + + @JsonProperty(FIELD_UPDATES) + private Map updates; + + @JsonCreator + public AlterDatabaseRequest( + @JsonProperty(FIELD_REMOVALS) List removals, + @JsonProperty(FIELD_UPDATES) Map updates) { + this.removals = removals; + this.updates = updates; + } + + @JsonGetter(FIELD_REMOVALS) + public List getRemovals() { + return removals; + } + + @JsonGetter(FIELD_UPDATES) + public Map getUpdates() { + return updates; + } +} diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/requests/CreateDatabaseRequest.java b/paimon-core/src/main/java/org/apache/paimon/rest/requests/CreateDatabaseRequest.java index 6067bf544b87..07e5cf2462f2 100644 --- a/paimon-core/src/main/java/org/apache/paimon/rest/requests/CreateDatabaseRequest.java +++ b/paimon-core/src/main/java/org/apache/paimon/rest/requests/CreateDatabaseRequest.java @@ -30,25 +30,19 @@ public class CreateDatabaseRequest implements RESTRequest { private static final String FIELD_NAME = "name"; - private static final String FIELD_IGNORE_IF_EXISTS = "ignoreIfExists"; private static final String FIELD_OPTIONS = "options"; @JsonProperty(FIELD_NAME) private String name; - @JsonProperty(FIELD_IGNORE_IF_EXISTS) - private boolean ignoreIfExists; - @JsonProperty(FIELD_OPTIONS) private Map options; @JsonCreator public CreateDatabaseRequest( @JsonProperty(FIELD_NAME) String name, - @JsonProperty(FIELD_IGNORE_IF_EXISTS) boolean ignoreIfExists, @JsonProperty(FIELD_OPTIONS) Map options) { this.name = name; - this.ignoreIfExists = ignoreIfExists; this.options = options; } @@ -57,11 +51,6 @@ public String getName() { return name; } - @JsonGetter(FIELD_IGNORE_IF_EXISTS) - public boolean getIgnoreIfExists() { - return ignoreIfExists; - } - @JsonGetter(FIELD_OPTIONS) public Map getOptions() { return options; diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/responses/AlterDatabaseResponse.java b/paimon-core/src/main/java/org/apache/paimon/rest/responses/AlterDatabaseResponse.java new file mode 100644 index 000000000000..08d751dc595c --- /dev/null +++ b/paimon-core/src/main/java/org/apache/paimon/rest/responses/AlterDatabaseResponse.java @@ -0,0 +1,69 @@ +/* + * 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.paimon.rest.responses; + +import org.apache.paimon.rest.RESTResponse; + +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator; +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter; +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** Response for altering database. */ +public class AlterDatabaseResponse implements RESTResponse { + + private static final String FIELD_REMOVED = "removed"; + private static final String FIELD_UPDATED = "updated"; + private static final String FIELD_MISSING = "missing"; + + @JsonProperty(FIELD_REMOVED) + private List removed; + + @JsonProperty(FIELD_UPDATED) + private List updated; + + @JsonProperty(FIELD_MISSING) + private List missing; + + @JsonCreator + public AlterDatabaseResponse( + @JsonProperty(FIELD_REMOVED) List removed, + @JsonProperty(FIELD_UPDATED) List updated, + @JsonProperty(FIELD_MISSING) List missing) { + this.removed = removed; + this.updated = updated; + this.missing = missing; + } + + @JsonGetter(FIELD_REMOVED) + public List getRemoved() { + return removed; + } + + @JsonGetter(FIELD_UPDATED) + public List getUpdated() { + return updated; + } + + @JsonGetter(FIELD_MISSING) + public List getMissing() { + return missing; + } +} diff --git a/paimon-core/src/test/java/org/apache/paimon/catalog/CachingCatalogTest.java b/paimon-core/src/test/java/org/apache/paimon/catalog/CachingCatalogTest.java index 4792e33c932b..fee6d1433143 100644 --- a/paimon-core/src/test/java/org/apache/paimon/catalog/CachingCatalogTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/catalog/CachingCatalogTest.java @@ -44,6 +44,7 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import java.io.FileNotFoundException; import java.time.Duration; @@ -63,6 +64,8 @@ import static org.apache.paimon.options.CatalogOptions.CACHE_MANIFEST_SMALL_FILE_THRESHOLD; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; class CachingCatalogTest extends CatalogTestBase { @@ -86,6 +89,26 @@ public void testListDatabasesWhenNoDatabases() { assertThat(databases).contains("db"); } + @Test + public void testInvalidateWhenDatabaseIsAltered() throws Exception { + Catalog mockcatalog = Mockito.mock(Catalog.class); + Catalog catalog = new CachingCatalog(mockcatalog); + String databaseName = "db"; + boolean ignoreIfExists = false; + Database database = Database.of(databaseName); + Database secondDatabase = Database.of(databaseName); + when(mockcatalog.getDatabase(databaseName)).thenReturn(database, secondDatabase); + doNothing().when(mockcatalog).alterDatabase(databaseName, emptyList(), ignoreIfExists); + Database cachingDatabase = catalog.getDatabase(databaseName); + assertThat(cachingDatabase.name()).isEqualTo(databaseName); + catalog.alterDatabase(databaseName, emptyList(), ignoreIfExists); + Database newCachingDatabase = catalog.getDatabase(databaseName); + // same as secondDatabase means cache is invalidated, so call getDatabase again then return + // secondDatabase + assertThat(newCachingDatabase).isNotSameAs(database); + assertThat(newCachingDatabase).isSameAs(secondDatabase); + } + @Test public void testInvalidateSystemTablesIfBaseTableIsModified() throws Exception { Catalog catalog = new CachingCatalog(this.catalog); diff --git a/paimon-core/src/test/java/org/apache/paimon/catalog/CatalogTestBase.java b/paimon-core/src/test/java/org/apache/paimon/catalog/CatalogTestBase.java index 98a9b92c5c38..31c4c8e682b8 100644 --- a/paimon-core/src/test/java/org/apache/paimon/catalog/CatalogTestBase.java +++ b/paimon-core/src/test/java/org/apache/paimon/catalog/CatalogTestBase.java @@ -54,6 +54,7 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertEquals; /** Base test class of paimon catalog in {@link Catalog}. */ public abstract class CatalogTestBase { @@ -960,4 +961,50 @@ public void testTableUUID() throws Exception { assertThat(Long.parseLong(uuid.substring((identifier.getFullName() + ".").length()))) .isGreaterThan(0); } + + protected void alterDatabaseWhenSupportAlter() throws Exception { + // Alter database + String databaseName = "db_to_alter"; + catalog.createDatabase(databaseName, false); + String key = "key1"; + String key2 = "key2"; + // Add property + catalog.alterDatabase( + databaseName, + Lists.newArrayList( + PropertyChange.setProperty(key, "value"), + PropertyChange.setProperty(key2, "value")), + false); + Database db = catalog.getDatabase(databaseName); + assertEquals("value", db.options().get(key)); + assertEquals("value", db.options().get(key2)); + // Update property + catalog.alterDatabase( + databaseName, + Lists.newArrayList( + PropertyChange.setProperty(key, "value1"), + PropertyChange.setProperty(key2, "value1")), + false); + db = catalog.getDatabase(databaseName); + assertEquals("value1", db.options().get(key)); + assertEquals("value1", db.options().get(key2)); + // remove property + catalog.alterDatabase( + databaseName, + Lists.newArrayList( + PropertyChange.removeProperty(key), PropertyChange.removeProperty(key2)), + false); + db = catalog.getDatabase(databaseName); + assertEquals(false, db.options().containsKey(key)); + assertEquals(false, db.options().containsKey(key2)); + // Remove non-existent property + catalog.alterDatabase( + databaseName, + Lists.newArrayList( + PropertyChange.removeProperty(key), PropertyChange.removeProperty(key2)), + false); + db = catalog.getDatabase(databaseName); + assertEquals(false, db.options().containsKey(key)); + assertEquals(false, db.options().containsKey(key2)); + } } diff --git a/paimon-core/src/test/java/org/apache/paimon/catalog/FileSystemCatalogTest.java b/paimon-core/src/test/java/org/apache/paimon/catalog/FileSystemCatalogTest.java index 65ea6721c220..7045daca8e86 100644 --- a/paimon-core/src/test/java/org/apache/paimon/catalog/FileSystemCatalogTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/catalog/FileSystemCatalogTest.java @@ -24,10 +24,13 @@ import org.apache.paimon.schema.Schema; import org.apache.paimon.types.DataTypes; +import org.apache.paimon.shade.guava30.com.google.common.collect.Lists; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** Tests for {@link FileSystemCatalog}. */ public class FileSystemCatalogTest extends CatalogTestBase { @@ -67,4 +70,17 @@ public void testCreateTableCaseSensitive() throws Exception { .isThrownBy(() -> catalog.createTable(identifier, schema, false)) .withMessage("Field name [Pk1, Col1] cannot contain upper case in the catalog."); } + + @Test + public void testAlterDatabase() throws Exception { + String databaseName = "test_alter_db"; + catalog.createDatabase(databaseName, false); + assertThatThrownBy( + () -> + catalog.alterDatabase( + databaseName, + Lists.newArrayList(PropertyChange.removeProperty("a")), + false)) + .isInstanceOf(UnsupportedOperationException.class); + } } diff --git a/paimon-core/src/test/java/org/apache/paimon/jdbc/JdbcCatalogTest.java b/paimon-core/src/test/java/org/apache/paimon/jdbc/JdbcCatalogTest.java index f5befc724f8b..f01a46fd6bb4 100644 --- a/paimon-core/src/test/java/org/apache/paimon/jdbc/JdbcCatalogTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/jdbc/JdbcCatalogTest.java @@ -122,4 +122,9 @@ public void testSerializeTable() throws Exception { } }); } + + @Test + public void testAlterDatabase() throws Exception { + this.alterDatabaseWhenSupportAlter(); + } } diff --git a/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java index a605e5e77c2a..821257a0e10e 100644 --- a/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java +++ b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java @@ -18,13 +18,17 @@ package org.apache.paimon.rest; +import org.apache.paimon.rest.requests.AlterDatabaseRequest; import org.apache.paimon.rest.requests.CreateDatabaseRequest; +import org.apache.paimon.rest.responses.AlterDatabaseResponse; import org.apache.paimon.rest.responses.CreateDatabaseResponse; import org.apache.paimon.rest.responses.DatabaseName; import org.apache.paimon.rest.responses.ErrorResponse; import org.apache.paimon.rest.responses.GetDatabaseResponse; import org.apache.paimon.rest.responses.ListDatabasesResponse; +import org.apache.paimon.shade.guava30.com.google.common.collect.Lists; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -40,10 +44,9 @@ public static String databaseName() { } public static CreateDatabaseRequest createDatabaseRequest(String name) { - boolean ignoreIfExists = true; Map options = new HashMap<>(); options.put("a", "b"); - return new CreateDatabaseRequest(name, ignoreIfExists, options); + return new CreateDatabaseRequest(name, options); } public static CreateDatabaseResponse createDatabaseResponse(String name) { @@ -69,4 +72,15 @@ public static ListDatabasesResponse listDatabasesResponse(String name) { public static ErrorResponse noSuchResourceExceptionErrorResponse() { return new ErrorResponse("message", 404, new ArrayList<>()); } + + public static AlterDatabaseRequest alterDatabaseRequest() { + Map add = new HashMap<>(); + add.put("add", "value"); + return new AlterDatabaseRequest(Lists.newArrayList("remove"), add); + } + + public static AlterDatabaseResponse alterDatabaseResponse() { + return new AlterDatabaseResponse( + Lists.newArrayList("remove"), Lists.newArrayList("add"), new ArrayList<>()); + } } diff --git a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java index 0fff81afdcde..9b1582929560 100644 --- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java @@ -22,6 +22,7 @@ import org.apache.paimon.catalog.Database; import org.apache.paimon.options.CatalogOptions; import org.apache.paimon.options.Options; +import org.apache.paimon.rest.responses.AlterDatabaseResponse; import org.apache.paimon.rest.responses.CreateDatabaseResponse; import org.apache.paimon.rest.responses.ErrorResponse; import org.apache.paimon.rest.responses.GetDatabaseResponse; @@ -185,6 +186,33 @@ public void testDropDatabaseWhenCascadeIsFalseAndTablesExist() throws Exception verify(mockRestCatalog, times(1)).listTables(eq(name)); } + @Test + public void testAlterDatabase() throws Exception { + String name = MockRESTMessage.databaseName(); + AlterDatabaseResponse response = MockRESTMessage.alterDatabaseResponse(); + mockResponse(mapper.writeValueAsString(response), 200); + assertDoesNotThrow(() -> mockRestCatalog.alterDatabase(name, new ArrayList<>(), true)); + } + + @Test + public void testAlterDatabaseWhenDatabaseNotExistAndIgnoreIfNotExistsIsFalse() + throws Exception { + String name = MockRESTMessage.databaseName(); + ErrorResponse response = MockRESTMessage.noSuchResourceExceptionErrorResponse(); + mockResponse(mapper.writeValueAsString(response), 404); + assertThrows( + Catalog.DatabaseNotExistException.class, + () -> mockRestCatalog.alterDatabase(name, new ArrayList<>(), false)); + } + + @Test + public void testAlterDatabaseWhenDatabaseNotExistAndIgnoreIfNotExistsIsTrue() throws Exception { + String name = MockRESTMessage.databaseName(); + ErrorResponse response = MockRESTMessage.noSuchResourceExceptionErrorResponse(); + mockResponse(mapper.writeValueAsString(response), 404); + assertDoesNotThrow(() -> mockRestCatalog.alterDatabase(name, new ArrayList<>(), true)); + } + private void mockResponse(String mockResponse, int httpCode) { MockResponse mockResponseObj = new MockResponse() diff --git a/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java b/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java index 7fee81ef1024..0e5a71be39c0 100644 --- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java @@ -18,7 +18,9 @@ package org.apache.paimon.rest; +import org.apache.paimon.rest.requests.AlterDatabaseRequest; import org.apache.paimon.rest.requests.CreateDatabaseRequest; +import org.apache.paimon.rest.responses.AlterDatabaseResponse; import org.apache.paimon.rest.responses.ConfigResponse; import org.apache.paimon.rest.responses.CreateDatabaseResponse; import org.apache.paimon.rest.responses.ErrorResponse; @@ -68,7 +70,6 @@ public void createDatabaseRequestParseTest() throws Exception { String requestStr = mapper.writeValueAsString(request); CreateDatabaseRequest parseData = mapper.readValue(requestStr, CreateDatabaseRequest.class); assertEquals(request.getName(), parseData.getName()); - assertEquals(request.getIgnoreIfExists(), parseData.getIgnoreIfExists()); assertEquals(request.getOptions().size(), parseData.getOptions().size()); } @@ -104,4 +105,24 @@ public void listDatabaseResponseParseTest() throws Exception { assertEquals(response.getDatabases().size(), parseData.getDatabases().size()); assertEquals(name, parseData.getDatabases().get(0).getName()); } + + @Test + public void alterDatabaseRequestParseTest() throws Exception { + AlterDatabaseRequest request = MockRESTMessage.alterDatabaseRequest(); + String requestStr = mapper.writeValueAsString(request); + AlterDatabaseRequest parseData = mapper.readValue(requestStr, AlterDatabaseRequest.class); + assertEquals(request.getRemovals().size(), parseData.getRemovals().size()); + assertEquals(request.getUpdates().size(), parseData.getUpdates().size()); + } + + @Test + public void alterDatabaseResponseParseTest() throws Exception { + AlterDatabaseResponse response = MockRESTMessage.alterDatabaseResponse(); + String responseStr = mapper.writeValueAsString(response); + AlterDatabaseResponse parseData = + mapper.readValue(responseStr, AlterDatabaseResponse.class); + assertEquals(response.getRemoved().size(), parseData.getRemoved().size()); + assertEquals(response.getUpdated().size(), parseData.getUpdated().size()); + assertEquals(response.getMissing().size(), parseData.getMissing().size()); + } } diff --git a/paimon-flink/paimon-flink-common/pom.xml b/paimon-flink/paimon-flink-common/pom.xml index 91222983bf6b..7388f8944109 100644 --- a/paimon-flink/paimon-flink-common/pom.xml +++ b/paimon-flink/paimon-flink-common/pom.xml @@ -169,8 +169,17 @@ under the License. + + + org.mockito + mockito-inline + ${mockito.version} + jar + test + + diff --git a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkCatalog.java b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkCatalog.java index 3407735b4b79..ec3c4a47a69d 100644 --- a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkCatalog.java +++ b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkCatalog.java @@ -20,9 +20,12 @@ import org.apache.paimon.CoreOptions; import org.apache.paimon.TableType; +import org.apache.paimon.annotation.VisibleForTesting; import org.apache.paimon.catalog.Catalog; import org.apache.paimon.catalog.CatalogUtils; +import org.apache.paimon.catalog.Database; import org.apache.paimon.catalog.Identifier; +import org.apache.paimon.catalog.PropertyChange; import org.apache.paimon.flink.procedure.ProcedureUtil; import org.apache.paimon.flink.utils.FlinkCatalogPropertiesUtil; import org.apache.paimon.flink.utils.FlinkDescriptorProperties; @@ -133,6 +136,7 @@ import static org.apache.paimon.CoreOptions.MATERIALIZED_TABLE_REFRESH_MODE; import static org.apache.paimon.CoreOptions.MATERIALIZED_TABLE_REFRESH_STATUS; import static org.apache.paimon.CoreOptions.PATH; +import static org.apache.paimon.catalog.Catalog.COMMENT_PROP; import static org.apache.paimon.catalog.Catalog.LAST_UPDATE_TIME_PROP; import static org.apache.paimon.catalog.Catalog.NUM_FILES_PROP; import static org.apache.paimon.catalog.Catalog.NUM_ROWS_PROP; @@ -236,19 +240,19 @@ public CatalogDatabase getDatabase(String databaseName) @Override public void createDatabase(String name, CatalogDatabase database, boolean ignoreIfExists) throws DatabaseAlreadyExistException, CatalogException { + Map properties; if (database != null) { + properties = new HashMap<>(database.getProperties()); if (database.getDescription().isPresent() && !database.getDescription().get().equals("")) { - throw new UnsupportedOperationException( - "Create database with description is unsupported."); + properties.put(COMMENT_PROP, database.getDescription().get()); } + } else { + properties = Collections.emptyMap(); } try { - catalog.createDatabase( - name, - ignoreIfExists, - database == null ? Collections.emptyMap() : database.getProperties()); + catalog.createDatabase(name, ignoreIfExists, properties); } catch (Catalog.DatabaseAlreadyExistException e) { throw new DatabaseAlreadyExistException(getName(), e.database()); } @@ -620,7 +624,7 @@ private List toSchemaChange( SchemaManager.checkAlterTablePath(key); - if (Catalog.COMMENT_PROP.equals(key)) { + if (COMMENT_PROP.equals(key)) { schemaChanges.add(SchemaChange.updateComment(value)); } else { schemaChanges.add(SchemaChange.setOption(key, value)); @@ -629,7 +633,7 @@ private List toSchemaChange( } else if (change instanceof ResetOption) { ResetOption resetOption = (ResetOption) change; String key = resetOption.getKey(); - if (Catalog.COMMENT_PROP.equals(key)) { + if (COMMENT_PROP.equals(key)) { schemaChanges.add(SchemaChange.updateComment(null)); } else { schemaChanges.add(SchemaChange.removeOption(resetOption.getKey())); @@ -1209,13 +1213,22 @@ public static Identifier toIdentifier(ObjectPath path) { return new Identifier(path.getDatabaseName(), path.getObjectName()); } - // --------------------- unsupported methods ---------------------------- - @Override public final void alterDatabase( String name, CatalogDatabase newDatabase, boolean ignoreIfNotExists) - throws CatalogException { - throw new UnsupportedOperationException(); + throws CatalogException, DatabaseNotExistException { + try { + Database oldDatabase = catalog.getDatabase(name); + List changes = + getPropertyChanges(oldDatabase.options(), newDatabase.getProperties()); + getPropertyChangeFromComment(oldDatabase.comment(), newDatabase.getDescription()) + .ifPresent(changes::add); + catalog.alterDatabase(name, changes, ignoreIfNotExists); + } catch (Catalog.DatabaseNotExistException e) { + if (!ignoreIfNotExists) { + throw new DatabaseNotExistException(getName(), e.database()); + } + } } @Override @@ -1264,6 +1277,36 @@ public final List listPartitions(ObjectPath tablePath) return getPartitionSpecs(tablePath, null); } + @VisibleForTesting + static List getPropertyChanges( + Map oldOptions, Map newOptions) { + List changes = new ArrayList<>(); + newOptions.forEach( + (k, v) -> { + if (!oldOptions.containsKey(k) || !oldOptions.get(k).equals(v)) { + changes.add(PropertyChange.setProperty(k, v)); + } + }); + oldOptions + .keySet() + .forEach( + (k) -> { + if (!newOptions.containsKey(k)) { + changes.add(PropertyChange.removeProperty(k)); + } + }); + return changes; + } + + @VisibleForTesting + static Optional getPropertyChangeFromComment( + Optional oldComment, Optional newComment) { + if (newComment.isPresent() && !oldComment.equals(newComment)) { + return Optional.of(PropertyChange.setProperty(COMMENT_PROP, newComment.get())); + } + return Optional.empty(); + } + private Table getPaimonTable(ObjectPath tablePath) throws TableNotExistException { try { Identifier identifier = toIdentifier(tablePath); diff --git a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/FlinkCatalogTest.java b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/FlinkCatalogTest.java index 734a47dead06..4b8cf7912192 100644 --- a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/FlinkCatalogTest.java +++ b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/FlinkCatalogTest.java @@ -22,6 +22,7 @@ import org.apache.paimon.TableType; import org.apache.paimon.catalog.CatalogContext; import org.apache.paimon.catalog.Identifier; +import org.apache.paimon.catalog.PropertyChange; import org.apache.paimon.flink.log.LogSinkProvider; import org.apache.paimon.flink.log.LogSourceProvider; import org.apache.paimon.flink.log.LogStoreRegister; @@ -83,6 +84,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.stream.Stream; @@ -98,7 +100,13 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatCollection; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** Test for {@link FlinkCatalog}. */ public class FlinkCatalogTest { @@ -571,11 +579,9 @@ public void testCreateDb_DatabaseWithProperties() throws Exception { } @Test - public void testCreateDb_DatabaseWithCommentException() { + public void testCreateDb_DatabaseWithCommentSuccessful() throws DatabaseAlreadyExistException { CatalogDatabaseImpl database = new CatalogDatabaseImpl(Collections.emptyMap(), "haha"); - assertThatThrownBy(() -> catalog.createDatabase(path1.getDatabaseName(), database, false)) - .isInstanceOf(UnsupportedOperationException.class) - .hasMessage("Create database with description is unsupported."); + assertDoesNotThrow(() -> catalog.createDatabase(path1.getDatabaseName(), database, false)); } @ParameterizedTest @@ -596,6 +602,75 @@ public void testDropDb_DatabaseNotExistException() { .hasMessage("Database db1 does not exist in Catalog test-catalog."); } + @Test + public void testAlterDb() throws DatabaseAlreadyExistException, DatabaseNotExistException { + CatalogDatabaseImpl database = new CatalogDatabaseImpl(Collections.emptyMap(), null); + catalog.createDatabase(path1.getDatabaseName(), database, false); + Map properties = Collections.singletonMap("haa", "ccc"); + CatalogDatabaseImpl newDatabase = new CatalogDatabaseImpl(properties, "haha"); + // as file system catalog don't support alter database, so we have to use mock to overview + // this method to test + Catalog mockCatalog = spy(catalog); + doNothing().when(mockCatalog).alterDatabase(path1.getDatabaseName(), newDatabase, false); + when(mockCatalog.getDatabase(path1.getDatabaseName())).thenReturn(database); + mockCatalog.alterDatabase(path1.getDatabaseName(), newDatabase, false); + verify(mockCatalog, times(1)).alterDatabase(path1.getDatabaseName(), newDatabase, false); + verify(mockCatalog, times(1)).getDatabase(path1.getDatabaseName()); + } + + @Test + public void testAlterDbComment() + throws DatabaseAlreadyExistException, DatabaseNotExistException { + CatalogDatabaseImpl database = new CatalogDatabaseImpl(Collections.emptyMap(), null); + catalog.createDatabase(path1.getDatabaseName(), database, false); + Catalog mockCatalog = spy(catalog); + when(mockCatalog.getDatabase(path1.getDatabaseName())).thenReturn(database); + CatalogDatabaseImpl newDatabase = new CatalogDatabaseImpl(Collections.emptyMap(), "aa"); + doNothing().when(mockCatalog).alterDatabase(path1.getDatabaseName(), newDatabase, false); + mockCatalog.alterDatabase(path1.getDatabaseName(), newDatabase, false); + verify(mockCatalog, times(1)).alterDatabase(path1.getDatabaseName(), newDatabase, false); + verify(mockCatalog, times(1)).getDatabase(path1.getDatabaseName()); + } + + @Test + public void testAlterDb_DatabaseNotExistException() { + CatalogDatabaseImpl database = new CatalogDatabaseImpl(Collections.emptyMap(), null); + assertThatThrownBy(() -> catalog.alterDatabase(path1.getDatabaseName(), database, false)) + .isInstanceOf(DatabaseNotExistException.class) + .hasMessage("Database db1 does not exist in Catalog test-catalog."); + } + + @Test + public void testGetProperties() throws Exception { + Map oldProperties = Collections.emptyMap(); + Map newProperties = Collections.singletonMap("haa", "ccc"); + List propertyChanges = + FlinkCatalog.getPropertyChanges(oldProperties, newProperties); + assertThat(propertyChanges.size()).isEqualTo(1); + oldProperties = newProperties; + propertyChanges = FlinkCatalog.getPropertyChanges(oldProperties, newProperties); + assertThat(propertyChanges.size()).isEqualTo(0); + oldProperties = Collections.singletonMap("aa", "ccc"); + propertyChanges = FlinkCatalog.getPropertyChanges(oldProperties, newProperties); + assertThat(propertyChanges.size()).isEqualTo(2); + } + + @Test + public void testGetPropertyChangeFromComment() { + Optional commentChange = + FlinkCatalog.getPropertyChangeFromComment(Optional.empty(), Optional.empty()); + assertThat(commentChange.isPresent()).isFalse(); + commentChange = + FlinkCatalog.getPropertyChangeFromComment(Optional.of("aa"), Optional.of("bb")); + assertThat(commentChange.isPresent()).isTrue(); + commentChange = + FlinkCatalog.getPropertyChangeFromComment(Optional.of("aa"), Optional.empty()); + assertThat(commentChange.isPresent()).isFalse(); + commentChange = + FlinkCatalog.getPropertyChangeFromComment(Optional.empty(), Optional.of("bb")); + assertThat(commentChange.isPresent()).isTrue(); + } + @Test public void testCreateTableWithColumnOptions() throws Exception { ResolvedExpression expression = diff --git a/paimon-hive/paimon-hive-catalog/src/main/java/org/apache/paimon/hive/HiveCatalog.java b/paimon-hive/paimon-hive-catalog/src/main/java/org/apache/paimon/hive/HiveCatalog.java index 5744ac894d12..237b59e43c4d 100644 --- a/paimon-hive/paimon-hive-catalog/src/main/java/org/apache/paimon/hive/HiveCatalog.java +++ b/paimon-hive/paimon-hive-catalog/src/main/java/org/apache/paimon/hive/HiveCatalog.java @@ -26,6 +26,7 @@ import org.apache.paimon.catalog.CatalogLockContext; import org.apache.paimon.catalog.CatalogLockFactory; import org.apache.paimon.catalog.Identifier; +import org.apache.paimon.catalog.PropertyChange; import org.apache.paimon.client.ClientPool; import org.apache.paimon.data.BinaryRow; import org.apache.paimon.fs.FileIO; @@ -53,6 +54,8 @@ import org.apache.paimon.view.View; import org.apache.paimon.view.ViewImpl; +import org.apache.paimon.shade.guava30.com.google.common.collect.Maps; + import org.apache.flink.table.hive.LegacyHiveClasses; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; @@ -405,6 +408,33 @@ protected void dropDatabaseImpl(String name) { } } + @Override + protected void alterDatabaseImpl(String name, List changes) { + try { + Database database = clients.run(client -> client.getDatabase(name)); + Map parameter = Maps.newHashMap(); + parameter.putAll(database.getParameters()); + Pair, Set> setPropertiesToRemoveKeys = + PropertyChange.getSetPropertiesToRemoveKeys(changes); + Map setProperties = setPropertiesToRemoveKeys.getLeft(); + Set removeKeys = setPropertiesToRemoveKeys.getRight(); + if (setProperties.size() > 0) { + parameter.putAll(setProperties); + } + if (removeKeys.size() > 0) { + parameter.keySet().removeAll(removeKeys); + } + Map newProperties = Collections.unmodifiableMap(parameter); + Database alterDatabase = convertToHiveDatabase(name, newProperties); + clients.execute(client -> client.alterDatabase(name, alterDatabase)); + } catch (TException e) { + throw new RuntimeException("Failed to alter database " + name, e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted in call to alterDatabase " + name, e); + } + } + @Override protected List listTablesImpl(String databaseName) { try { diff --git a/paimon-hive/paimon-hive-catalog/src/test/java/org/apache/paimon/hive/HiveCatalogTest.java b/paimon-hive/paimon-hive-catalog/src/test/java/org/apache/paimon/hive/HiveCatalogTest.java index 267bdf0c7100..e3b48f02a696 100644 --- a/paimon-hive/paimon-hive-catalog/src/test/java/org/apache/paimon/hive/HiveCatalogTest.java +++ b/paimon-hive/paimon-hive-catalog/src/test/java/org/apache/paimon/hive/HiveCatalogTest.java @@ -173,6 +173,11 @@ private void testHiveConfDirFromEnvImpl() { assertThat(hiveConf.get("hive.metastore.uris")).isEqualTo("dummy-hms"); } + @Test + public void testAlterDatabase() throws Exception { + this.alterDatabaseWhenSupportAlter(); + } + @Test public void testAddHiveTableParameters() { try { diff --git a/paimon-open-api/rest-catalog-open-api.yaml b/paimon-open-api/rest-catalog-open-api.yaml index 9b69b3de2776..f7f9529f53dd 100644 --- a/paimon-open-api/rest-catalog-open-api.yaml +++ b/paimon-open-api/rest-catalog-open-api.yaml @@ -80,6 +80,43 @@ paths: $ref: '#/components/schemas/ErrorResponse' "500": description: Internal Server Error + /v1/{prefix}/databases/{database}/properties: + post: + tags: + - database + summary: Alter Database + operationId: alterDatabase + parameters: + - name: prefix + in: path + required: true + schema: + type: string + - name: database + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AlterDatabaseRequest' + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/AlterDatabaseResponse' + "404": + description: Resource not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + "500": + description: Internal Server Error /v1/{prefix}/databases/{database}: get: tags: @@ -116,7 +153,7 @@ paths: tags: - database summary: Drop Database - operationId: dropDatabases + operationId: dropDatabase parameters: - name: prefix in: path @@ -159,12 +196,22 @@ components: properties: name: type: string - ignoreIfExists: - type: boolean options: type: object additionalProperties: type: string + ErrorResponse: + type: object + properties: + message: + type: string + code: + type: integer + format: int32 + stack: + type: array + items: + type: string CreateDatabaseResponse: type: object properties: @@ -174,15 +221,29 @@ components: type: object additionalProperties: type: string - ErrorResponse: + AlterDatabaseRequest: type: object properties: - message: - type: string - code: - type: integer - format: int32 - stack: + removals: + type: array + items: + type: string + updates: + type: object + additionalProperties: + type: string + AlterDatabaseResponse: + type: object + properties: + removed: + type: array + items: + type: string + updated: + type: array + items: + type: string + missing: type: array items: type: string diff --git a/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.java b/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.java index 19f6f8cdf673..5331b65d71b6 100644 --- a/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.java +++ b/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.java @@ -19,7 +19,9 @@ package org.apache.paimon.open.api; import org.apache.paimon.rest.ResourcePaths; +import org.apache.paimon.rest.requests.AlterDatabaseRequest; import org.apache.paimon.rest.requests.CreateDatabaseRequest; +import org.apache.paimon.rest.responses.AlterDatabaseResponse; import org.apache.paimon.rest.responses.ConfigResponse; import org.apache.paimon.rest.responses.CreateDatabaseResponse; import org.apache.paimon.rest.responses.DatabaseName; @@ -28,6 +30,7 @@ import org.apache.paimon.rest.responses.ListDatabasesResponse; import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableList; +import org.apache.paimon.shade.guava30.com.google.common.collect.Lists; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -149,5 +152,33 @@ public GetDatabaseResponse getDatabases( content = {@Content(schema = @Schema())}) }) @DeleteMapping("/v1/{prefix}/databases/{database}") - public void dropDatabases(@PathVariable String prefix, @PathVariable String database) {} + public void dropDatabase(@PathVariable String prefix, @PathVariable String database) {} + + @Operation( + summary = "Alter Database", + tags = {"database"}) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + content = { + @Content(schema = @Schema(implementation = AlterDatabaseResponse.class)) + }), + @ApiResponse( + responseCode = "404", + description = "Resource not found", + content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}), + @ApiResponse( + responseCode = "500", + content = {@Content(schema = @Schema())}) + }) + @PostMapping("/v1/{prefix}/databases/{database}/properties") + public AlterDatabaseResponse alterDatabase( + @PathVariable String prefix, + @PathVariable String database, + @RequestBody AlterDatabaseRequest request) { + return new AlterDatabaseResponse( + Lists.newArrayList("remove"), + Lists.newArrayList("add"), + Lists.newArrayList("missing")); + } } diff --git a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkCatalog.java b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkCatalog.java index de6e2414fc8f..12023cb84779 100644 --- a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkCatalog.java +++ b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkCatalog.java @@ -22,6 +22,7 @@ import org.apache.paimon.catalog.Catalog; import org.apache.paimon.catalog.CatalogContext; import org.apache.paimon.catalog.CatalogFactory; +import org.apache.paimon.catalog.PropertyChange; import org.apache.paimon.options.Options; import org.apache.paimon.schema.Schema; import org.apache.paimon.schema.SchemaChange; @@ -130,7 +131,8 @@ public void createNamespace(String[] namespace, Map metadata) throws NamespaceAlreadyExistsException { checkNamespace(namespace); try { - catalog.createDatabase(namespace[0], false, metadata); + String databaseName = getDatabaseNameFromNamespace(namespace); + catalog.createDatabase(databaseName, false, metadata); } catch (Catalog.DatabaseAlreadyExistException e) { throw new NamespaceAlreadyExistsException(namespace); } @@ -153,7 +155,8 @@ public String[][] listNamespaces(String[] namespace) throws NoSuchNamespaceExcep } checkNamespace(namespace); try { - catalog.getDatabase(namespace[0]); + String databaseName = getDatabaseNameFromNamespace(namespace); + catalog.getDatabase(databaseName); return new String[0][]; } catch (Catalog.DatabaseNotExistException e) { throw new NoSuchNamespaceException(namespace); @@ -164,9 +167,9 @@ public String[][] listNamespaces(String[] namespace) throws NoSuchNamespaceExcep public Map loadNamespaceMetadata(String[] namespace) throws NoSuchNamespaceException { checkNamespace(namespace); - String dataBaseName = namespace[0]; try { - return catalog.getDatabase(dataBaseName).options(); + String databaseName = getDatabaseNameFromNamespace(namespace); + return catalog.getDatabase(databaseName).options(); } catch (Catalog.DatabaseNotExistException e) { throw new NoSuchNamespaceException(namespace); } @@ -203,7 +206,8 @@ public boolean dropNamespace(String[] namespace, boolean cascade) throws NoSuchNamespaceException { checkNamespace(namespace); try { - catalog.dropDatabase(namespace[0], false, cascade); + String databaseName = getDatabaseNameFromNamespace(namespace); + catalog.dropDatabase(databaseName, false, cascade); return true; } catch (Catalog.DatabaseNotExistException e) { throw new NoSuchNamespaceException(namespace); @@ -217,7 +221,8 @@ public boolean dropNamespace(String[] namespace, boolean cascade) public Identifier[] listTables(String[] namespace) throws NoSuchNamespaceException { checkNamespace(namespace); try { - return catalog.listTables(namespace[0]).stream() + String databaseName = getDatabaseNameFromNamespace(namespace); + return catalog.listTables(databaseName).stream() .map(table -> Identifier.of(namespace, table)) .toArray(Identifier[]::new); } catch (Catalog.DatabaseNotExistException e) { @@ -516,10 +521,35 @@ protected List convertPartitionTransforms(Transform[] transforms) { return partitionColNames; } - // --------------------- unsupported methods ---------------------------- - @Override - public void alterNamespace(String[] namespace, NamespaceChange... changes) { - throw new UnsupportedOperationException("Alter namespace in Spark is not supported yet."); + public void alterNamespace(String[] namespace, NamespaceChange... changes) + throws NoSuchNamespaceException { + checkNamespace(namespace); + try { + String databaseName = getDatabaseNameFromNamespace(namespace); + List propertyChanges = + Arrays.stream(changes).map(this::toPropertyChange).collect(Collectors.toList()); + catalog.alterDatabase(databaseName, propertyChanges, false); + } catch (Catalog.DatabaseNotExistException e) { + throw new NoSuchNamespaceException(namespace); + } + } + + private PropertyChange toPropertyChange(NamespaceChange change) { + if (change instanceof NamespaceChange.SetProperty) { + NamespaceChange.SetProperty set = (NamespaceChange.SetProperty) change; + return PropertyChange.setProperty(set.property(), set.value()); + } else if (change instanceof NamespaceChange.RemoveProperty) { + NamespaceChange.RemoveProperty remove = (NamespaceChange.RemoveProperty) change; + return PropertyChange.removeProperty(remove.property()); + + } else { + throw new UnsupportedOperationException( + "Change is not supported: " + change.getClass()); + } + } + + private String getDatabaseNameFromNamespace(String[] namespace) { + return namespace[0]; } } diff --git a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/DDLWithHiveCatalogTestBase.scala b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/DDLWithHiveCatalogTestBase.scala index 4ba079ea0bb2..526e24250751 100644 --- a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/DDLWithHiveCatalogTestBase.scala +++ b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/DDLWithHiveCatalogTestBase.scala @@ -22,7 +22,7 @@ import org.apache.paimon.hive.HiveMetastoreClient import org.apache.paimon.spark.PaimonHiveTestBase import org.apache.paimon.table.FileStoreTable -import org.apache.spark.sql.{Row, SparkSession} +import org.apache.spark.sql.{AnalysisException, Row, SparkSession} import org.junit.jupiter.api.Assertions abstract class DDLWithHiveCatalogTestBase extends PaimonHiveTestBase { @@ -194,6 +194,46 @@ abstract class DDLWithHiveCatalogTestBase extends PaimonHiveTestBase { } } + test("Paimon DDL with hive catalog: alter database's properties") { + Seq(sparkCatalogName, paimonHiveCatalogName).foreach { + catalogName => + spark.sql(s"USE $catalogName") + val databaseName = "paimon_db" + withDatabase(databaseName) { + spark.sql(s"CREATE DATABASE $databaseName WITH DBPROPERTIES ('k1' = 'v1', 'k2' = 'v2')") + var props = getDatabaseProps(databaseName) + Assertions.assertEquals(props("k1"), "v1") + Assertions.assertEquals(props("k2"), "v2") + spark.sql(s"ALTER DATABASE $databaseName SET DBPROPERTIES ('k1' = 'v11', 'k2' = 'v22')") + props = getDatabaseProps(databaseName) + Assertions.assertEquals(props("k1"), "v11") + Assertions.assertEquals(props("k2"), "v22") + } + } + } + + test("Paimon DDL with hive catalog: alter database location") { + Seq(sparkCatalogName, paimonHiveCatalogName).foreach { + catalogName => + spark.sql(s"USE $catalogName") + val databaseName = "paimon_db" + withDatabase(databaseName) { + spark.sql(s"CREATE DATABASE $databaseName WITH DBPROPERTIES ('k1' = 'v1', 'k2' = 'v2')") + withTempDir { + dBLocation => + try { + spark.sql( + s"ALTER DATABASE $databaseName SET LOCATION '${dBLocation.getCanonicalPath}'") + } catch { + case e: AnalysisException => + Assertions.assertTrue( + e.getMessage.contains("does not support altering database location")) + } + } + } + } + } + test("Paimon DDL with hive catalog: set default database") { var reusedSpark = spark