/* * ToroDB * Copyright © 2014 8Kdata Technology (www.8kdata.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.torodb.core.transaction.metainf; import com.google.common.collect.HashBasedTable; import com.google.common.collect.Maps; import com.google.common.collect.Table; import com.torodb.core.TableRef; import com.torodb.core.annotations.DoNotChange; import com.torodb.core.transaction.metainf.ImmutableMetaDocPart.Builder; import org.jooq.lambda.Seq; import org.jooq.lambda.tuple.Tuple2; import java.util.ArrayList; import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Consumer; import java.util.stream.IntStream; import java.util.stream.Stream; /** * */ public class WrapperMutableMetaDocPart implements MutableMetaDocPart { private final ImmutableMetaDocPart wrapped; /** * This table contains all fields contained by wrapper and all new fields */ private final Table<String, FieldType, ImmutableMetaField> newFields; /** * This list just contains the fields that have been added on this wrapper but not on the wrapped * object. */ private final List<ImmutableMetaField> addedFields; private final Map<String, ImmutableMetaField> addedFieldsByIndetifiers; private final Consumer<WrapperMutableMetaDocPart> changeConsumer; private final EnumMap<FieldType, ImmutableMetaScalar> newScalars; @SuppressWarnings("checkstyle:LineLength") private final HashMap<String, Tuple2<ImmutableMetaIdentifiedDocPartIndex, MetaElementState>> indexesByIdentifier; @SuppressWarnings("checkstyle:LineLength") private final Map<String, Tuple2<ImmutableMetaIdentifiedDocPartIndex, MetaElementState>> aliveIndexesMap; private final List<MutableMetaDocPartIndex> addedMutableIndexes; public WrapperMutableMetaDocPart(ImmutableMetaDocPart wrapped, Consumer<WrapperMutableMetaDocPart> changeConsumer) { this.wrapped = wrapped; newFields = HashBasedTable.create(); wrapped.streamFields().forEach((field) -> newFields.put(field.getName(), field.getType(), field) ); addedFields = new ArrayList<>(); addedFieldsByIndetifiers = new HashMap<>(); this.changeConsumer = changeConsumer; this.newScalars = new EnumMap<>(FieldType.class); indexesByIdentifier = new HashMap<>(); wrapped.streamIndexes().forEach((docPartIndexindex) -> { indexesByIdentifier.put(docPartIndexindex.getIdentifier(), new Tuple2<>(docPartIndexindex, MetaElementState.NOT_CHANGED)); }); aliveIndexesMap = Maps.filterValues(indexesByIdentifier, tuple -> tuple.v2().isAlive()); addedMutableIndexes = new ArrayList<>(); } @Override public ImmutableMetaField addMetaField(String name, String identifier, FieldType type) throws IllegalArgumentException { if (getMetaFieldByNameAndType(name, type) != null) { throw new IllegalArgumentException("There is another field with the name " + name + " whose type is " + type); } assert getMetaFieldByIdentifier(identifier) == null : "There is another field with the identifier " + identifier; ImmutableMetaField newField = new ImmutableMetaField(name, identifier, type); newFields.put(name, type, newField); addedFields.add(newField); addedFieldsByIndetifiers.put(newField.getIdentifier(), newField); changeConsumer.accept(this); return newField; } @Override public ImmutableMetaScalar addMetaScalar(String identifier, FieldType type) throws IllegalArgumentException { if (getScalar(type) != null) { throw new IllegalArgumentException("There is another scalar with type " + type + ", " + "whose identifier is " + identifier); } ImmutableMetaScalar scalar = new ImmutableMetaScalar(identifier, type); newScalars.put(type, scalar); changeConsumer.accept(this); return scalar; } @Override @DoNotChange public Iterable<ImmutableMetaField> getAddedMetaFields() { return addedFields; } @Override public ImmutableMetaField getAddedFieldByIdentifier(String identifier) { return addedFieldsByIndetifiers.get(identifier); } @Override public Iterable<? extends ImmutableMetaScalar> getAddedMetaScalars() { return newScalars.values(); } @Override public MutableMetaDocPartIndex addMetaDocPartIndex(boolean unique) { MutableMetaDocPartIndex newIndex = new WrapperMutableMetaDocPartIndex(unique, this::onDocPartIndexChange); addedMutableIndexes.add(newIndex); return newIndex; } @Override public boolean removeMetaDocPartIndexByIdentifier(String indexId) { ImmutableMetaIdentifiedDocPartIndex metaDocPartIndex = getMetaDocPartIndexByIdentifier(indexId); if (metaDocPartIndex == null) { return false; } indexesByIdentifier.put(metaDocPartIndex.getIdentifier(), new Tuple2<>(metaDocPartIndex, MetaElementState.REMOVED)); changeConsumer.accept(this); return true; } @Override @DoNotChange @SuppressWarnings("checkstyle:LineLength") public Iterable<Tuple2<ImmutableMetaIdentifiedDocPartIndex, MetaElementState>> getModifiedMetaDocPartIndexes() { return Maps.filterValues(indexesByIdentifier, tuple -> tuple.v2().hasChanged()) .values(); } @Override @DoNotChange public Iterable<MutableMetaDocPartIndex> getAddedMutableMetaDocPartIndexes() { return addedMutableIndexes; } @Override public ImmutableMetaDocPart immutableCopy() { if (addedFields.isEmpty() && newScalars.isEmpty() && indexesByIdentifier.values().stream() .noneMatch(tuple -> tuple.v2().hasChanged())) { return wrapped; } else { ImmutableMetaDocPart.Builder builder = new Builder(wrapped); for (ImmutableMetaField addedField : addedFields) { builder.put(addedField); } for (ImmutableMetaScalar value : newScalars.values()) { builder.put(value); } indexesByIdentifier.values() .forEach(tuple -> { switch (tuple.v2()) { case ADDED: case MODIFIED: case NOT_CHANGED: builder.put(tuple.v1()); break; case REMOVED: builder.remove(tuple.v1()); break; case NOT_EXISTENT: default: throw new AssertionError("Unexpected case " + tuple.v2()); } }); return builder.build(); } } @Override public TableRef getTableRef() { return wrapped.getTableRef(); } @Override public String getIdentifier() { return wrapped.getIdentifier(); } @Override public Stream<? extends ImmutableMetaField> streamFields() { return newFields.values().stream(); } @Override public Stream<? extends ImmutableMetaField> streamMetaFieldByName(String columnName) { return newFields.row(columnName).values().stream(); } @Override public ImmutableMetaField getMetaFieldByNameAndType(String fieldName, FieldType type) { return newFields.get(fieldName, type); } @Override public ImmutableMetaField getMetaFieldByIdentifier(String fieldId) { return newFields.values().stream() .filter((field) -> field.getIdentifier().equals(fieldId)) .findAny() .orElse(null); } @Override public Stream<? extends MetaScalar> streamScalars() { return Stream.concat(newScalars.values().stream(), wrapped.streamScalars()); } @Override public MetaScalar getScalar(FieldType type) { ImmutableMetaScalar scalar = newScalars.get(type); if (scalar != null) { return scalar; } return wrapped.getScalar(type); } @Override public Stream<? extends ImmutableMetaIdentifiedDocPartIndex> streamIndexes() { return aliveIndexesMap.values().stream().map(tuple -> tuple.v1()); } @Override public ImmutableMetaIdentifiedDocPartIndex getMetaDocPartIndexByIdentifier(String indexId) { Tuple2<ImmutableMetaIdentifiedDocPartIndex, MetaElementState> tuple = aliveIndexesMap.get( indexId); if (tuple == null) { return null; } return tuple.v1(); } @Override public MutableMetaDocPartIndex getOrCreatePartialMutableDocPartIndexForMissingIndexAndNewField( MetaIndex missingIndex, List<String> identifiers, MetaField newField) { int position = identifiers.indexOf(newField.getIdentifier()); Optional<MutableMetaDocPartIndex> matchingMutableDocPartIndex = Seq.seq( getAddedMutableMetaDocPartIndexes()) .filter(docPartIndex -> docPartIndex.getMetaDocPartIndexColumnByPosition(position) == null && identifiers.size() >= docPartIndex.size() && missingIndex.isSubMatch(this, identifiers, docPartIndex) // We ensure we do not pick a doc part index that fit a isSubMatch for our index but // was the only chance for another combination. For example: // 1. a_i, b_i, c_i are old fields // 2. a_s, b_s, c_s are new fields // 3. we have index a asc, b asc, c asc // 4. we added doc part index a_s asc, null, null and a_s asc, b_i asc, null // 5. we search for a sub match for a_s, b_i, c_s and found a_s asc, null, null && noneNonCurrentAndNullIndexColumnIsNew(position, docPartIndex, identifiers)) .findAny(); MutableMetaDocPartIndex docPartIndex; if (matchingMutableDocPartIndex.isPresent()) { docPartIndex = matchingMutableDocPartIndex.get(); } else { docPartIndex = addMetaDocPartIndex(missingIndex.isUnique()); int index = 0; for (String identifier : identifiers) { if (getAddedFieldByIdentifier(identifier) == null) { MetaIndexField indexField = missingIndex.getMetaIndexFieldByTableRefAndPosition( getTableRef(), index); docPartIndex.putMetaDocPartIndexColumn(index, identifier, indexField.getOrdering()); } index++; } } MetaIndexField indexField = missingIndex.getMetaIndexFieldByTableRefAndPosition(getTableRef(), position); docPartIndex.putMetaDocPartIndexColumn(position, newField.getIdentifier(), indexField.getOrdering()); return docPartIndex; } private boolean noneNonCurrentAndNullIndexColumnIsNew(int position, MutableMetaDocPartIndex docPartIndex, List<String> identifiers) { return IntStream.range(0, identifiers.size()) .noneMatch(index -> index != position && docPartIndex.getMetaDocPartIndexColumnByPosition( index) == null && getAddedFieldByIdentifier(identifiers.get(index)) == null); } private boolean isTransitionAllowed(MetaIdentifiedDocPartIndex metaDocPartIndexIndex, MetaElementState newState) { MetaElementState oldState; Tuple2<ImmutableMetaIdentifiedDocPartIndex, MetaElementState> tuple = indexesByIdentifier.get( metaDocPartIndexIndex.getIdentifier()); if (tuple == null) { oldState = MetaElementState.NOT_EXISTENT; } else { oldState = tuple.v2(); } oldState.assertLegalTransition(newState); return true; } protected void onDocPartIndexChange(WrapperMutableMetaDocPartIndex changedIndex, ImmutableMetaIdentifiedDocPartIndex immutableIndex) { assert isTransitionAllowed(immutableIndex, MetaElementState.ADDED); if (getMetaDocPartIndexByIdentifier(immutableIndex.getIdentifier()) != null) { throw new IllegalArgumentException("There is another index with the identifier " + immutableIndex.getIdentifier()); } addedMutableIndexes.remove(changedIndex); indexesByIdentifier.put(immutableIndex.getIdentifier(), new Tuple2<>(immutableIndex, MetaElementState.ADDED)); changeConsumer.accept(this); } @Override public String toString() { return defautToString(); } }