blob: 7ad10e54bdfba2edda8e0ea64eb7f862b6d94d56 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ignite.internal.index;
import static org.apache.ignite.lang.IgniteStringFormatter.format;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import org.apache.ignite.configuration.ConfigurationValue;
import org.apache.ignite.configuration.notifications.ConfigurationNamedListListener;
import org.apache.ignite.configuration.notifications.ConfigurationNotificationEvent;
import org.apache.ignite.configuration.schemas.store.DataStorageConfigurationSchema;
import org.apache.ignite.configuration.schemas.store.UnknownDataStorageConfigurationSchema;
import org.apache.ignite.configuration.schemas.table.ColumnDefaultConfigurationSchema;
import org.apache.ignite.configuration.schemas.table.ConstantValueDefaultConfigurationSchema;
import org.apache.ignite.configuration.schemas.table.FunctionCallDefaultConfigurationSchema;
import org.apache.ignite.configuration.schemas.table.HashIndexChange;
import org.apache.ignite.configuration.schemas.table.HashIndexConfigurationSchema;
import org.apache.ignite.configuration.schemas.table.HashIndexView;
import org.apache.ignite.configuration.schemas.table.NullValueDefaultConfigurationSchema;
import org.apache.ignite.configuration.schemas.table.SortedIndexChange;
import org.apache.ignite.configuration.schemas.table.SortedIndexConfigurationSchema;
import org.apache.ignite.configuration.schemas.table.TableChange;
import org.apache.ignite.configuration.schemas.table.TableConfigurationSchema;
import org.apache.ignite.configuration.schemas.table.TableIndexConfigurationSchema;
import org.apache.ignite.configuration.schemas.table.TableIndexView;
import org.apache.ignite.configuration.schemas.table.TablesConfiguration;
import org.apache.ignite.internal.configuration.asm.ConfigurationAsmGenerator;
import org.apache.ignite.internal.configuration.schema.ExtendedTableConfiguration;
import org.apache.ignite.internal.configuration.tree.ConverterToMapVisitor;
import org.apache.ignite.internal.configuration.tree.TraversableTreeNode;
import org.apache.ignite.internal.index.event.IndexEvent;
import org.apache.ignite.internal.index.event.IndexEventParameters;
import org.apache.ignite.internal.table.TableImpl;
import org.apache.ignite.internal.table.distributed.TableManager;
import org.apache.ignite.lang.ErrorGroups;
import org.apache.ignite.lang.ErrorGroups.Table;
import org.apache.ignite.lang.IgniteInternalException;
import org.apache.ignite.lang.IndexNotFoundException;
import org.apache.ignite.lang.TableNotFoundException;
import org.junit.jupiter.api.Test;
/**
* Test class to verify {@link IndexManager}.
*/
public class IndexManagerTest {
@Test
void indexManagerSubscribedToIdxConfigurationChangesOnStart() {
var indicesConfigurationChangeSubscription = new AtomicReference<>();
var indexManager = new IndexManager(mock(TableManager.class), indicesConfigurationChangeSubscription::set);
indexManager.start();
assertThat(indicesConfigurationChangeSubscription.get(), notNullValue());
}
@Test
void configurationChangedWhenCreateIsInvoked() {
var tableManagerMock = mock(TableManager.class);
var canonicalName = "sName.tName";
TableChange tableChange = createNode(TableConfigurationSchema.class);
tableChange.changeColumns(columnListChange -> {
columnListChange.create("c1", columnChange -> {});
columnListChange.create("c2", columnChange -> {});
});
var successfulCompletion = new RuntimeException("This is expected");
when(tableManagerMock.tableAsyncInternal(eq(canonicalName))).thenReturn(CompletableFuture.completedFuture(mock(TableImpl.class)));
when(tableManagerMock.alterTableAsync(any(), any())).thenAnswer(answer -> {
Consumer<TableChange> changeConsumer = answer.getArgument(1);
try {
changeConsumer.accept(tableChange);
} catch (Throwable th) {
return CompletableFuture.failedFuture(th);
}
// return failed future here to prevent index manager to further process the
// create request, because after returned future is competed, index manager expects
// to all configuration listeners to be executed, but no one have configured them
// for this test
return CompletableFuture.failedFuture(successfulCompletion);
});
var indexManager = new IndexManager(tableManagerMock, listener -> {});
try {
indexManager.createIndexAsync("sName", "idx", "tName", indexChange -> {
var sortedIndexChange = indexChange.convert(SortedIndexChange.class);
sortedIndexChange.changeColumns(columns -> {
columns.create("c1", columnChange -> columnChange.changeAsc(true));
columns.create("c2", columnChange -> columnChange.changeAsc(false));
});
}).join();
} catch (CompletionException ex) {
if (ex.getCause() != successfulCompletion) {
throw ex;
}
}
var expected = List.of(
Map.of(
"columns", List.of(
Map.of(
"asc", true,
"name", "c1"
),
Map.of(
"asc", false,
"name", "c2"
)
),
"name", "idx",
"type", "SORTED",
"uniq", false
)
);
assertSameObjects(expected, toMap(tableChange.indices()));
}
@Test
public void createIndexForNonExistingTable() {
var tableManagerMock = mock(TableManager.class);
var canonicalName = "sName.tName";
when(tableManagerMock.tableAsyncInternal(eq(canonicalName))).thenReturn(CompletableFuture.completedFuture(null));
var indexManager = new IndexManager(tableManagerMock, listener -> {});
CompletionException completionException = assertThrows(
CompletionException.class,
() -> indexManager.createIndexAsync("sName", "idx", "tName", indexChange -> {/* doesn't matter */}).join()
);
assertThat(completionException.getCause(), instanceOf(TableNotFoundException.class));
assertThat(completionException.getCause().getMessage(), containsString(canonicalName));
}
@Test
public void createIndexWithEmptyName() {
var tableManagerMock = mock(TableManager.class);
var indexManager = new IndexManager(tableManagerMock, listener -> {});
CompletionException completionException = assertThrows(
CompletionException.class,
() -> indexManager.createIndexAsync("sName", "", "tName", indexChange -> {/* doesn't matter */}).join()
);
assertThat(completionException.getCause(), instanceOf(IgniteInternalException.class));
assertThat(
((IgniteInternalException) completionException.getCause()).code(),
equalTo(ErrorGroups.Index.INVALID_INDEX_DEFINITION_ERR)
);
}
@Test
public void createIndexWithEmptyColumnList() {
var tableManagerMock = mock(TableManager.class);
var tableMock = mock(TableImpl.class);
TableChange tableChange = createNode(TableConfigurationSchema.class);
when(tableManagerMock.tableAsyncInternal(any())).thenReturn(CompletableFuture.completedFuture(tableMock));
when(tableManagerMock.alterTableAsync(any(), any())).thenAnswer(answer -> {
Consumer<TableChange> changeConsumer = answer.getArgument(1);
Throwable exception = null;
try {
changeConsumer.accept(tableChange);
} catch (Exception ex) {
exception = ex;
}
if (exception == null) {
// consumer expected to fail on validation
exception = new AssertionError();
}
return CompletableFuture.failedFuture(exception);
});
var indexManager = new IndexManager(tableManagerMock, listener -> {});
CompletionException completionException = assertThrows(
CompletionException.class,
() -> indexManager.createIndexAsync("sName", "idx", "tName",
indexChange -> indexChange.convert(HashIndexChange.class).changeColumnNames()).join()
);
assertThat(completionException.getCause(), instanceOf(IgniteInternalException.class));
assertThat(
((IgniteInternalException) completionException.getCause()).code(),
equalTo(ErrorGroups.Index.INVALID_INDEX_DEFINITION_ERR)
);
}
@Test
public void createIndexForNonExistingColumn() {
var tableManagerMock = mock(TableManager.class);
var canonicalName = "sName.tName";
var tableMock = mock(TableImpl.class);
TableChange tableChange = createNode(TableConfigurationSchema.class);
when(tableManagerMock.tableAsyncInternal(eq(canonicalName))).thenReturn(CompletableFuture.completedFuture(tableMock));
when(tableManagerMock.alterTableAsync(any(), any())).thenAnswer(answer -> {
Consumer<TableChange> changeConsumer = answer.getArgument(1);
Throwable exception = null;
try {
changeConsumer.accept(tableChange);
} catch (Exception ex) {
exception = ex;
}
if (exception == null) {
// consumer expected to fail on validation
exception = new AssertionError();
}
return CompletableFuture.failedFuture(exception);
});
var indexManager = new IndexManager(tableManagerMock, listener -> {});
CompletionException completionException = assertThrows(
CompletionException.class,
() -> indexManager.createIndexAsync("sName", "idx", "tName",
indexChange -> indexChange.convert(HashIndexChange.class).changeColumnNames("nonExistingColumn")).join()
);
assertThat(completionException.getCause(), instanceOf(IgniteInternalException.class));
assertThat(
((IgniteInternalException) completionException.getCause()).code(),
equalTo(Table.COLUMN_NOT_FOUND_ERR)
);
}
@Test
public void dropNonExistingIndex() {
var tableManagerMock = mock(TableManager.class);
var indexManager = new IndexManager(tableManagerMock, listener -> {});
CompletionException completionException = assertThrows(
CompletionException.class,
() -> indexManager.dropIndexAsync("sName", "nonExisting").join()
);
assertThat(completionException.getCause(), instanceOf(IndexNotFoundException.class));
assertThat(
((IndexNotFoundException) completionException.getCause()).code(),
equalTo(ErrorGroups.Index.INDEX_NOT_FOUND_ERR)
);
}
@Test
@SuppressWarnings("ConstantConditions")
public void eventIsFiredWhenIndexCreated() {
var tableManagerMock = mock(TableManager.class);
var indexId = UUID.randomUUID();
var tableId = UUID.randomUUID();
var indexName = "idxName";
Consumer<ConfigurationNamedListListener<TableIndexView>> listenerConsumer = listener -> {
listener.onCreate(createConfigurationEventIndexAddedMock(indexId, tableId, indexName));
};
var indexManager = new IndexManager(tableManagerMock, listenerConsumer);
AtomicReference<IndexEventParameters> holder = new AtomicReference<>();
indexManager.listen(IndexEvent.CREATE, (param, th) -> {
holder.set(param);
return CompletableFuture.completedFuture(true);
});
indexManager.start();
assertThat(holder.get(), notNullValue());
assertThat(holder.get().index().id(), equalTo(indexId));
assertThat(holder.get().index().tableId(), equalTo(tableId));
assertThat(holder.get().index().name(), equalTo("PUBLIC." + indexName));
}
@SuppressWarnings("unchecked")
private ConfigurationNotificationEvent<TableIndexView> createConfigurationEventIndexAddedMock(
UUID indexId,
UUID tableId,
String canonicalIndexName
) {
ConfigurationValue<UUID> confValueMock = mock(ConfigurationValue.class);
when(confValueMock.value()).thenReturn(tableId).getMock();
ExtendedTableConfiguration extendedConfMock = mock(ExtendedTableConfiguration.class);
when(extendedConfMock.id()).thenReturn(confValueMock);
HashIndexView indexViewMock = mock(HashIndexView.class);
when(indexViewMock.id()).thenReturn(indexId);
when(indexViewMock.name()).thenReturn(canonicalIndexName);
when(indexViewMock.columnNames()).thenReturn(new String[] {"c1", "c2"});
ConfigurationNotificationEvent<TableIndexView> configurationEventMock = mock(ConfigurationNotificationEvent.class);
when(configurationEventMock.config(any())).thenReturn(extendedConfMock);
when(configurationEventMock.newValue()).thenReturn(indexViewMock);
return configurationEventMock;
}
/** Creates configuration node for given *SchemaConfiguration class. */
@SuppressWarnings({"unchecked", "SameParameterValue"})
private <T> T createNode(Class<?> cls) {
var cgen = new ConfigurationAsmGenerator();
Map<Class<?>, Set<Class<?>>> polymorphicExtensions = Map.of(
DataStorageConfigurationSchema.class,
Set.of(
UnknownDataStorageConfigurationSchema.class
),
TableIndexConfigurationSchema.class,
Set.of(
HashIndexConfigurationSchema.class,
SortedIndexConfigurationSchema.class
),
ColumnDefaultConfigurationSchema.class,
Set.of(
ConstantValueDefaultConfigurationSchema.class,
NullValueDefaultConfigurationSchema.class,
FunctionCallDefaultConfigurationSchema.class
)
);
cgen.compileRootSchema(TablesConfiguration.KEY.schemaClass(), Map.of(), polymorphicExtensions);
return (T) cgen.instantiateNode(cls);
}
private static Object toMap(Object obj) {
assert obj instanceof TraversableTreeNode;
return ((TraversableTreeNode) obj).accept(null, new ConverterToMapVisitor(false));
}
private static void assertSameObjects(Object expected, Object actual) {
try {
contentEquals(expected, actual);
} catch (ObjectsNotEqualException ex) {
fail(
format(
"Objects are not equal at position {}:\n\texpected={}\n\tactual={}",
String.join(".", ex.path), ex.o1, ex.o2)
);
}
}
private static void contentEquals(Object o1, Object o2) {
if (o1 instanceof Map && o2 instanceof Map) {
var m1 = (Map<?, ?>) o1;
var m2 = (Map<?, ?>) o2;
if (m1.size() != m2.size()) {
throw new ObjectsNotEqualException(m1, m2);
}
for (Map.Entry<?, ?> entry : m1.entrySet()) {
var v1 = entry.getValue();
var v2 = m2.get(entry.getKey());
try {
contentEquals(v1, v2);
} catch (ObjectsNotEqualException ex) {
ex.path.add(0, entry.getKey().toString());
throw ex;
}
}
} else if (o1 instanceof List && o2 instanceof List) {
var l1 = (List<?>) o1;
var l2 = (List<?>) o2;
if (l1.size() != l2.size()) {
throw new ObjectsNotEqualException(l1, l2);
}
var it1 = l1.iterator();
var it2 = l2.iterator();
int idx = 0;
while (it1.hasNext()) {
var v1 = it1.next();
var v2 = it2.next();
try {
contentEquals(v1, v2);
} catch (ObjectsNotEqualException ex) {
ex.path.add(0, "[" + idx + ']');
throw ex;
}
}
} else if (!Objects.equals(o1, o2)) {
throw new ObjectsNotEqualException(o1, o2);
}
}
static class ObjectsNotEqualException extends RuntimeException {
private final Object o1;
private final Object o2;
private final List<String> path = new ArrayList<>();
public ObjectsNotEqualException(Object o1, Object o2) {
super(null, null, false, false);
this.o1 = o1;
this.o2 = o2;
}
}
}