/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.presto.hive.metastore;
import com.facebook.presto.hive.SchemaAlreadyExistsException;
import com.facebook.presto.hive.TableAlreadyExistsException;
import com.facebook.presto.spi.PrestoException;
import com.facebook.presto.spi.SchemaNotFoundException;
import com.facebook.presto.spi.SchemaTableName;
import com.facebook.presto.spi.TableNotFoundException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hive.common.FileUtils;
import org.apache.hadoop.hive.metastore.TableType;
import org.apache.hadoop.hive.metastore.api.Database;
import org.apache.hadoop.hive.metastore.api.FieldSchema;
import org.apache.hadoop.hive.metastore.api.Partition;
import org.apache.hadoop.hive.metastore.api.PrincipalPrivilegeSet;
import org.apache.hadoop.hive.metastore.api.PrincipalType;
import org.apache.hadoop.hive.metastore.api.PrivilegeGrantInfo;
import org.apache.hadoop.hive.metastore.api.Table;
import javax.annotation.concurrent.GuardedBy;
import java.io.File;
import java.net.URI;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import static com.facebook.presto.hive.HiveUtil.toPartitionValues;
import static com.facebook.presto.hive.metastore.HivePrivilegeInfo.HivePrivilege.OWNERSHIP;
import static com.facebook.presto.spi.StandardErrorCode.SCHEMA_NOT_EMPTY;
import static com.google.common.base.MoreObjects.toStringHelper;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static io.airlift.testing.FileUtils.deleteRecursively;
import static java.util.Locale.US;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static org.apache.hadoop.hive.metastore.TableType.EXTERNAL_TABLE;
import static org.apache.hadoop.hive.metastore.TableType.MANAGED_TABLE;
import static org.apache.hadoop.hive.metastore.TableType.VIRTUAL_VIEW;
import static org.apache.hadoop.hive.metastore.api.PrincipalType.ROLE;
import static org.apache.hadoop.hive.metastore.api.PrincipalType.USER;
public class InMemoryHiveMetastore
implements HiveMetastore
{
private static final String PUBLIC_ROLE_NAME = "public";
@GuardedBy("this")
private final Map<String, Database> databases = new HashMap<>();
@GuardedBy("this")
private final Map<SchemaTableName, Table> relations = new HashMap<>();
@GuardedBy("this")
private final Map<SchemaTableName, Table> views = new HashMap<>();
@GuardedBy("this")
private final Map<PartitionName, Partition> partitions = new HashMap<>();
@GuardedBy("this")
private final Map<String, Set<String>> roleGrants = new HashMap<>();
@GuardedBy("this")
private final Map<PrincipalTableKey, Set<HivePrivilegeInfo>> tablePrivileges = new HashMap<>();
private final File baseDirectory;
public InMemoryHiveMetastore(File baseDirectory)
{
this.baseDirectory = requireNonNull(baseDirectory, "baseDirectory is null");
checkArgument(!baseDirectory.exists(), "Base directory already exists");
checkArgument(baseDirectory.mkdirs(), "Could not create base directory");
}
@Override
public synchronized void createDatabase(Database database)
{
requireNonNull(database, "database is null");
File directory;
if (database.getLocationUri() != null) {
directory = new File(URI.create(database.getLocationUri()));
}
else {
// use Hive default naming convention
directory = new File(baseDirectory, database.getName() + ".db");
database = database.deepCopy();
database.setLocationUri(directory.toURI().toString());
}
checkArgument(!directory.exists(), "Database directory already exists");
checkArgument(isParentDir(directory, baseDirectory), "Database directory must be inside of the metastore base directory");
checkArgument(directory.mkdirs(), "Could not create database directory");
if (databases.putIfAbsent(database.getName(), database) != null) {
throw new SchemaAlreadyExistsException(database.getName());
}
}
@Override
public synchronized void dropDatabase(String databaseName)
{
if (!databases.containsKey(databaseName)) {
throw new SchemaNotFoundException(databaseName);
}
if (!getAllTables(databaseName).orElse(ImmutableList.of()).isEmpty()) {
throw new PrestoException(SCHEMA_NOT_EMPTY, "Schema not empty: " + databaseName);
}
databases.remove(databaseName);
}
@Override
public synchronized void alterDatabase(String databaseName, Database newDatabase)
{
String newDatabaseName = newDatabase.getName();
if (databaseName.equals(newDatabaseName)) {
if (databases.replace(databaseName, newDatabase) == null) {
throw new SchemaNotFoundException(databaseName);
}
return;
}
Database database = databases.get(databaseName);
if (database == null) {
throw new SchemaNotFoundException(databaseName);
}
if (databases.putIfAbsent(newDatabaseName, database) != null) {
throw new SchemaAlreadyExistsException(newDatabaseName);
}
databases.remove(databaseName);
rewriteKeys(relations, name -> new SchemaTableName(newDatabaseName, name.getTableName()));
rewriteKeys(views, name -> new SchemaTableName(newDatabaseName, name.getTableName()));
rewriteKeys(partitions, name -> name.withSchemaName(newDatabaseName));
rewriteKeys(tablePrivileges, name -> name.withDatabase(newDatabaseName));
}
@Override
public synchronized List<String> getAllDatabases()
{
return ImmutableList.copyOf(databases.keySet());
}
@Override
public synchronized void createTable(Table table)
{
TableType tableType = TableType.valueOf(table.getTableType());
checkArgument(EnumSet.of(MANAGED_TABLE, EXTERNAL_TABLE, VIRTUAL_VIEW).contains(tableType), "Invalid table type: %s", tableType);
if (tableType == VIRTUAL_VIEW) {
checkArgument(table.getSd().getLocation() == null, "Storage location for view must be null");
}
else {
File directory = new File(new Path(table.getSd().getLocation()).toUri());
checkArgument(directory.exists(), "Table directory does not exist");
if (tableType == MANAGED_TABLE) {
checkArgument(isParentDir(directory, baseDirectory), "Table directory must be inside of the metastore base directory");
}
}
SchemaTableName schemaTableName = new SchemaTableName(table.getDbName(), table.getTableName());
Table tableCopy = table.deepCopy();
if (relations.putIfAbsent(schemaTableName, tableCopy) != null) {
throw new TableAlreadyExistsException(schemaTableName);
}
if (tableType == VIRTUAL_VIEW) {
views.put(schemaTableName, tableCopy);
}
PrincipalPrivilegeSet privileges = table.getPrivileges();
if (privileges != null) {
for (Entry<String, List<PrivilegeGrantInfo>> entry : privileges.getUserPrivileges().entrySet()) {
String user = entry.getKey();
Set<HivePrivilegeInfo> userPrivileges = entry.getValue().stream()
.map(HivePrivilegeInfo::parsePrivilege)
.flatMap(Collection::stream)
.collect(toImmutableSet());
setTablePrivileges(user, USER, table.getDbName(), table.getTableName(), userPrivileges);
}
for (Entry<String, List<PrivilegeGrantInfo>> entry : privileges.getRolePrivileges().entrySet()) {
String role = entry.getKey();
Set<HivePrivilegeInfo> rolePrivileges = entry.getValue().stream()
.map(HivePrivilegeInfo::parsePrivilege)
.flatMap(Collection::stream)
.collect(toImmutableSet());
setTablePrivileges(role, ROLE, table.getDbName(), table.getTableName(), rolePrivileges);
}
}
}
@Override
public synchronized void dropTable(String databaseName, String tableName, boolean deleteData)
{
List<String> locations = listAllDataPaths(this, databaseName, tableName);
SchemaTableName schemaTableName = new SchemaTableName(databaseName, tableName);
Table table = relations.remove(schemaTableName);
if (table == null) {
throw new TableNotFoundException(schemaTableName);
}
views.remove(schemaTableName);
partitions.keySet().removeIf(partitionName -> partitionName.matches(databaseName, tableName));
// remove data
if (deleteData && table.getTableType().equals(MANAGED_TABLE.name())) {
for (String location : locations) {
if (location != null) {
File directory = new File(new Path(location).toUri());
checkArgument(isParentDir(directory, baseDirectory), "Table directory must be inside of the metastore base directory");
deleteRecursively(directory);
}
}
}
}
private static List<String> listAllDataPaths(HiveMetastore metastore, String schemaName, String tableName)
{
ImmutableList.Builder<String> locations = ImmutableList.builder();
Table table = metastore.getTable(schemaName, tableName).get();
if (table.getSd().getLocation() != null) {
// For unpartitioned table, there should be nothing directly under this directory.
// But including this location in the set makes the directory content assert more
// extensive, which is desirable.
locations.add(table.getSd().getLocation());
}
Optional<List<String>> partitionNames = metastore.getPartitionNames(schemaName, tableName);
if (partitionNames.isPresent()) {
metastore.getPartitionsByNames(schemaName, tableName, partitionNames.get()).stream()
.map(partition -> partition.getSd().getLocation())
.filter(location -> !location.startsWith(table.getSd().getLocation()))
.forEach(locations::add);
}
return locations.build();
}
@Override
public synchronized void alterTable(String databaseName, String tableName, Table newTable)
{
SchemaTableName oldName = new SchemaTableName(databaseName, tableName);
SchemaTableName newName = new SchemaTableName(newTable.getDbName(), newTable.getTableName());
// if the name did not change, this is a simple schema change
if (oldName.equals(newName)) {
if (relations.replace(oldName, newTable) == null) {
throw new TableNotFoundException(oldName);
}
return;
}
// remove old table definition and add the new one
Table table = relations.get(oldName);
if (table == null) {
throw new TableNotFoundException(oldName);
}
if (relations.putIfAbsent(newName, newTable) != null) {
throw new TableAlreadyExistsException(newName);
}
relations.remove(oldName);
}
@Override
public synchronized Optional<List<String>> getAllTables(String databaseName)
{
ImmutableList.Builder<String> tables = ImmutableList.builder();
for (SchemaTableName schemaTableName : this.relations.keySet()) {
if (schemaTableName.getSchemaName().equals(databaseName)) {
tables.add(schemaTableName.getTableName());
}
}
return Optional.of(tables.build());
}
@Override
public synchronized Optional<List<String>> getAllViews(String databaseName)
{
ImmutableList.Builder<String> tables = ImmutableList.builder();
for (SchemaTableName schemaTableName : this.views.keySet()) {
if (schemaTableName.getSchemaName().equals(databaseName)) {
tables.add(schemaTableName.getTableName());
}
}
return Optional.of(tables.build());
}
@Override
public synchronized Optional<Database> getDatabase(String databaseName)
{
return Optional.ofNullable(databases.get(databaseName));
}
@Override
public synchronized void addPartitions(String databaseName, String tableName, List<Partition> partitions)
{
Optional<Table> table = getTable(databaseName, tableName);
if (!table.isPresent()) {
throw new TableNotFoundException(new SchemaTableName(databaseName, tableName));
}
for (Partition partition : partitions) {
String partitionName = createPartitionName(partition, table.get());
partition = partition.deepCopy();
if (partition.getParameters() == null) {
partition.setParameters(ImmutableMap.of());
}
this.partitions.put(PartitionName.partition(databaseName, tableName, partitionName), partition);
}
}
private static String createPartitionName(Partition partition, Table table)
{
return makePartName(table.getPartitionKeys(), partition.getValues());
}
private static String makePartName(List<FieldSchema> partitionColumns, List<String> values)
{
checkArgument(partitionColumns.size() == values.size());
List<String> partitionColumnNames = partitionColumns.stream().map(FieldSchema::getName).collect(toList());
return FileUtils.makePartName(partitionColumnNames, values);
}
@Override
public synchronized void dropPartition(String databaseName, String tableName, List<String> parts, boolean deleteData)
{
partitions.entrySet().removeIf(entry ->
entry.getKey().matches(databaseName, tableName) && entry.getValue().getValues().equals(parts));
}
@Override
public synchronized void alterPartition(String databaseName, String tableName, Partition partition)
{
Optional<Table> table = getTable(databaseName, tableName);
if (!table.isPresent()) {
throw new TableNotFoundException(new SchemaTableName(databaseName, tableName));
}
String partitionName = createPartitionName(partition, table.get());
this.partitions.put(PartitionName.partition(databaseName, tableName, partitionName), partition);
}
@Override
public synchronized Optional<List<String>> getPartitionNames(String databaseName, String tableName)
{
return Optional.of(ImmutableList.copyOf(partitions.entrySet().stream()
.filter(entry -> entry.getKey().matches(databaseName, tableName))
.map(entry -> entry.getKey().getPartitionName())
.collect(toList())));
}
@Override
public synchronized Optional<Partition> getPartition(String databaseName, String tableName, List<String> partitionValues)
{
PartitionName name = PartitionName.partition(databaseName, tableName, partitionValues);
Partition partition = partitions.get(name);
if (partition == null) {
return Optional.empty();
}
return Optional.of(partition.deepCopy());
}
@Override
public synchronized Optional<List<String>> getPartitionNamesByParts(String databaseName, String tableName, List<String> parts)
{
return Optional.of(partitions.entrySet().stream()
.filter(entry -> partitionMatches(entry.getValue(), databaseName, tableName, parts))
.map(entry -> entry.getKey().getPartitionName())
.collect(toList()));
}
private static boolean partitionMatches(Partition partition, String databaseName, String tableName, List<String> parts)
{
if (!partition.getDbName().equals(databaseName) ||
!partition.getTableName().equals(tableName)) {
return false;
}
List<String> values = partition.getValues();
if (values.size() != parts.size()) {
return false;
}
for (int i = 0; i < values.size(); i++) {
String part = parts.get(i);
if (!part.isEmpty() && !values.get(i).equals(part)) {
return false;
}
}
return true;
}
@Override
public synchronized List<Partition> getPartitionsByNames(String databaseName, String tableName, List<String> partitionNames)
{
ImmutableList.Builder<Partition> builder = ImmutableList.builder();
for (String name : partitionNames) {
PartitionName partitionName = PartitionName.partition(databaseName, tableName, name);
Partition partition = partitions.get(partitionName);
if (partition == null) {
return ImmutableList.of();
}
builder.add(partition.deepCopy());
}
return builder.build();
}
@Override
public synchronized Optional<Table> getTable(String databaseName, String tableName)
{
SchemaTableName schemaTableName = new SchemaTableName(databaseName, tableName);
return Optional.ofNullable(relations.get(schemaTableName));
}
@Override
public synchronized Set<String> getRoles(String user)
{
return roleGrants.getOrDefault(user, ImmutableSet.of(PUBLIC_ROLE_NAME));
}
public synchronized void setUserRoles(String user, Set<String> roles)
{
if (!roles.contains(PUBLIC_ROLE_NAME)) {
roles = ImmutableSet.<String>builder()
.addAll(roles)
.add(PUBLIC_ROLE_NAME)
.build();
}
roleGrants.put(user, ImmutableSet.copyOf(roles));
}
@Override
public synchronized Set<HivePrivilegeInfo> getDatabasePrivileges(String user, String databaseName)
{
Set<HivePrivilegeInfo> privileges = new HashSet<>();
if (isDatabaseOwner(user, databaseName)) {
privileges.add(new HivePrivilegeInfo(OWNERSHIP, true));
}
return privileges;
}
@Override
public synchronized Set<HivePrivilegeInfo> getTablePrivileges(String user, String databaseName, String tableName)
{
Set<HivePrivilegeInfo> privileges = new HashSet<>();
if (isTableOwner(user, databaseName, tableName)) {
privileges.add(new HivePrivilegeInfo(OWNERSHIP, true));
}
privileges.addAll(tablePrivileges.getOrDefault(new PrincipalTableKey(user, USER, tableName, databaseName), ImmutableSet.of()));
for (String role : getRoles(user)) {
privileges.addAll(tablePrivileges.getOrDefault(new PrincipalTableKey(role, ROLE, tableName, databaseName), ImmutableSet.of()));
}
return privileges;
}
public synchronized void setTablePrivileges(String principalName,
PrincipalType principalType,
String databaseName,
String tableName,
Set<HivePrivilegeInfo> privileges)
{
tablePrivileges.put(new PrincipalTableKey(principalName, principalType, tableName, databaseName), ImmutableSet.copyOf(privileges));
}
@Override
public synchronized void grantTablePrivileges(String databaseName, String tableName, String grantee, Set<PrivilegeGrantInfo> privilegeGrantInfoSet)
{
Set<HivePrivilegeInfo> hivePrivileges = privilegeGrantInfoSet.stream()
.map(HivePrivilegeInfo::parsePrivilege)
.flatMap(Collection::stream)
.collect(toImmutableSet());
setTablePrivileges(grantee, USER, databaseName, tableName, hivePrivileges);
}
@Override
public synchronized void revokeTablePrivileges(String databaseName, String tableName, String grantee, Set<PrivilegeGrantInfo> privilegeGrantInfoSet)
{
Set<HivePrivilegeInfo> currentPrivileges = getTablePrivileges(grantee, databaseName, tableName);
currentPrivileges.removeAll(privilegeGrantInfoSet.stream()
.map(HivePrivilegeInfo::parsePrivilege)
.flatMap(Collection::stream)
.collect(toImmutableSet()));
setTablePrivileges(grantee, USER, databaseName, tableName, currentPrivileges);
}
private static boolean isParentDir(File directory, File baseDirectory)
{
for (File parent = directory.getParentFile(); parent != null; parent = parent.getParentFile()) {
if (parent.equals(baseDirectory)) {
return true;
}
}
return false;
}
private static class PartitionName
{
private final String schemaName;
private final String tableName;
private final List<String> partitionValues;
private final String partitionName; // does not participate in equals and hashValue
private PartitionName(String schemaName, String tableName, List<String> partitionValues, String partitionName)
{
this.schemaName = requireNonNull(schemaName, "schemaName is null").toLowerCase(US);
this.tableName = requireNonNull(tableName, "tableName is null").toLowerCase(US);
this.partitionValues = requireNonNull(partitionValues, "partitionValues is null");
this.partitionName = partitionName;
}
public static PartitionName partition(String schemaName, String tableName, String partitionName)
{
return new PartitionName(schemaName.toLowerCase(US), tableName.toLowerCase(US), toPartitionValues(partitionName), partitionName);
}
public static PartitionName partition(String schemaName, String tableName, List<String> partitionValues)
{
return new PartitionName(schemaName.toLowerCase(US), tableName.toLowerCase(US), partitionValues, null);
}
public String getPartitionName()
{
return requireNonNull(partitionName, "partitionName is null");
}
public boolean matches(String schemaName, String tableName)
{
return this.schemaName.equals(schemaName) &&
this.tableName.equals(tableName);
}
public boolean matches(String schemaName, String tableName, String partitionName)
{
return this.schemaName.equals(schemaName) &&
this.tableName.equals(tableName) &&
this.partitionName.equals(partitionName);
}
public PartitionName withSchemaName(String schemaName)
{
return new PartitionName(schemaName, tableName, partitionValues, partitionName);
}
@Override
public int hashCode()
{
return Objects.hash(schemaName, tableName, partitionValues);
}
@Override
public boolean equals(Object obj)
{
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
PartitionName other = (PartitionName) obj;
return Objects.equals(this.schemaName, other.schemaName)
&& Objects.equals(this.tableName, other.tableName)
&& Objects.equals(this.partitionValues, other.partitionValues);
}
@Override
public String toString()
{
return schemaName + "/" + tableName + "/" + partitionName;
}
}
private static class PrincipalTableKey
{
private final String principalName;
private final PrincipalType principalType;
private final String database;
private final String table;
public PrincipalTableKey(String principalName, PrincipalType principalType, String table, String database)
{
this.principalName = requireNonNull(principalName, "principalName is null");
this.principalType = requireNonNull(principalType, "principalType is null");
this.table = requireNonNull(table, "table is null");
this.database = requireNonNull(database, "database is null");
}
public PrincipalTableKey withDatabase(String database)
{
return new PrincipalTableKey(principalName, principalType, table, database);
}
@Override
public boolean equals(Object o)
{
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PrincipalTableKey that = (PrincipalTableKey) o;
return Objects.equals(principalName, that.principalName) &&
Objects.equals(principalType, that.principalType) &&
Objects.equals(table, that.table) &&
Objects.equals(database, that.database);
}
@Override
public int hashCode()
{
return Objects.hash(principalName, principalType, table, database);
}
@Override
public String toString()
{
return toStringHelper(this)
.add("principalName", principalName)
.add("principalType", principalType)
.add("table", table)
.add("database", database)
.toString();
}
}
private static <K, V> void rewriteKeys(Map<K, V> map, Function<K, K> keyRewriter)
{
for (K key : ImmutableSet.copyOf(map.keySet())) {
K newKey = keyRewriter.apply(key);
if (!newKey.equals(key)) {
map.put(newKey, map.remove(key));
}
}
}
}