/**
* Copyright (C) 2009-2013 FoundationDB, LLC
*
* 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.foundationdb.server.types.service;
import com.foundationdb.server.error.AkibanInternalException;
import com.foundationdb.server.types.TCast;
import com.foundationdb.server.types.TCastIdentifier;
import com.foundationdb.server.types.TCastPath;
import com.foundationdb.server.types.TClass;
import com.foundationdb.server.types.TExecutionContext;
import com.foundationdb.server.types.TInstance;
import com.foundationdb.server.types.TPreptimeValue;
import com.foundationdb.server.types.TStrongCasts;
import com.foundationdb.server.types.mcompat.mtypes.MString;
import com.foundationdb.server.types.value.Value;
import com.foundationdb.server.types.value.ValueSource;
import com.foundationdb.server.types.value.ValueTarget;
import com.foundationdb.server.types.texpressions.Constantness;
import com.foundationdb.util.DagChecker;
import com.google.common.base.Predicate;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
public final class TCastsRegistry {
public TCast cast(TClass source, TClass target) {
return cast(castsBySource, source, target);
}
public Set<TClass> stronglyCastableFrom(TClass tClass) {
Map<TClass, TCast> castsFrom = strongCastsBySource.get(tClass);
return castsFrom.keySet();
}
public boolean isStrong(TCast cast) {
TClass source = cast.sourceClass();
TClass target = cast.targetClass();
return stronglyCastableFrom(source).contains(target);
}
Collection<Map<TClass, TCast>> castsBySource() {
return castsBySource.values();
}
TCastsRegistry(Collection<? extends TClass> tClasses, InstanceFinder finder)
{
castsBySource = createCasts(tClasses, finder);
createDerivedCasts(castsBySource, finder);
deriveCastsFromVarchar();
strongCastsBySource = createStrongCastsMap(castsBySource, finder);
checkDag(strongCastsBySource);
}
private final Map<TClass,Map<TClass,TCast>> castsBySource;
private final Map<TClass,Map<TClass,TCast>> strongCastsBySource;
private static final Logger logger = LoggerFactory.getLogger(TCastsRegistry.class);
private static final Comparator<TCastIdentifier> tcastIdentifierComparator = new Comparator<TCastIdentifier>() {
@Override
public int compare(TCastIdentifier o1, TCastIdentifier o2) {
String o1Str = o1.toString();
String o2Str = o2.toString();
return o1Str.compareTo(o2Str);
}
};
// package-local; also used in testing
static Map<TClass, Map<TClass, TCast>> createCasts(Collection<? extends TClass> tClasses,
InstanceFinder finder) {
Map<TClass, Map<TClass, TCast>> localCastsMap = new HashMap<>(tClasses.size());
// First, define the self casts
for (TClass tClass : tClasses) {
Map<TClass, TCast> map = new HashMap<>();
map.put(tClass, new SelfCast(tClass));
localCastsMap.put(tClass, map);
}
Set<TCastIdentifier> duplicates = new TreeSet<>(tcastIdentifierComparator);
// Next, to/from varchar
for (TClass tClass : tClasses) {
putCast(localCastsMap, tClass.castToVarchar(), duplicates);
putCast(localCastsMap, tClass.castFromVarchar(), duplicates);
}
// Now the registered casts
for (TCast cast : finder.find(TCast.class)) {
putCast(localCastsMap, cast, duplicates);
}
if (!duplicates.isEmpty())
throw new AkibanInternalException("duplicate casts found for: " + duplicates);
return localCastsMap;
}
static Map<TClass, Map<TClass, TCast>> createStrongCastsMap(Map<TClass, Map<TClass, TCast>> castsBySource,
final Set<TCastIdentifier> strongCasts) {
Map<TClass,Map<TClass,TCast>> result = new HashMap<>();
for (Map.Entry<TClass, Map<TClass,TCast>> origEntry : castsBySource.entrySet()) {
final TClass source = origEntry.getKey();
Map<TClass, TCast> filteredView = Maps.filterKeys(origEntry.getValue(), new Predicate<TClass>() {
@Override
public boolean apply(TClass target) {
return (source == target) || strongCasts.contains(new TCastIdentifier(source, target));
}
});
assert ! filteredView.isEmpty() : "no strong casts (including self casts) found for " + source;
result.put(source, new HashMap<>(filteredView));
}
return result;
}
// private
private static TCast cast(Map<TClass, Map<TClass, TCast>> castsBySource, TClass source, TClass target) {
TCast result = null;
Map<TClass,TCast> castsByTarget = castsBySource.get(source);
if (castsByTarget != null)
result = castsByTarget.get(target);
return result;
}
private static void checkDag(final Map<TClass, Map<TClass, TCast>> castsBySource) {
DagChecker<TClass> checker = new DagChecker<TClass>() {
@Override
protected Set<? extends TClass> initialNodes() {
return castsBySource.keySet();
}
@Override
protected Set<? extends TClass> nodesFrom(TClass starting) {
Set<TClass> result = new HashSet<>(castsBySource.get(starting).keySet());
result.remove(starting);
return result;
}
};
if (!checker.isDag()) {
List<TClass> badPath = checker.getBadNodePath();
// create a List<String> where everything is lowercase except for the first and last instances
// of the offending node
List<String> names = new ArrayList<>(badPath.size());
for (TClass tClass : badPath)
names.add(tClass.toString().toLowerCase());
String lastName = names.get(names.size() - 1);
String lastNameUpper = lastName.toUpperCase();
names.set(names.size() - 1, lastNameUpper);
names.set(names.indexOf(lastName), lastNameUpper);
throw new AkibanInternalException("non-DAG detected involving " + names);
}
}
static void createDerivedCasts(Map<TClass,Map<TClass,TCast>> castsBySource, InstanceFinder finder) {
for (TCastPath castPath : finder.find(TCastPath.class)) {
List<? extends TClass> path = castPath.getPath();
// We need this loop to protect against "jumps." For instance, let's say the cast path is
// [ a, b, c, d, e ] and we have the following casts:
// "single step" casts: (a -> b), (b -> c), (c -> d), (d -> e)
// one "jump" cast: (a -> d),
// The first pass of this loop will create a derived cast (a -> d -> e), but we wouldn't have created
// (a -> c). This loop ensures that we will.
// We work from both ends, shrinking iteratively from the beginning and recursively (within deriveCast)
// from the end. A derived cast has to have at least three participants, so we can stop when we get
// to a path whose size is less than 3.
while (path.size() >= 3) {
for (int i = path.size() - 1; i > 0; --i) {
deriveCast(castsBySource, path, i);
}
path = path.subList(1, path.size());
}
}
}
private static Map<TClass,Map<TClass,TCast>> createStrongCastsMap(Map<TClass, Map<TClass, TCast>> castsBySource,
InstanceFinder finder)
{
Collection<? extends TStrongCasts> strongCastIds = finder.find(TStrongCasts.class);
Set<TCastIdentifier> strongCasts = new HashSet<>(strongCastIds.size()); // rough guess
for (TStrongCasts strongCastGenerator : strongCastIds) {
for (TCastIdentifier castId : strongCastGenerator.get()) {
TCast cast = cast(castsBySource, castId.getSource(), castId.getTarget());
if (cast == null)
throw new AkibanInternalException("no cast defined for " + castId +", which is marked as strong");
if (!strongCasts.add(castId)) {
logger.warn("multiple sources have listed cast {} as strong", castId);
}
}
}
return createStrongCastsMap(castsBySource, strongCasts);
}
/**
* Add derived casts for any pair of TClasses (A, B) s.t. there is not a cast from A to B, but there are casts
* from A to VARCHAR and from VARCHAR to B. This essentially uses VARCHAR as a base type. Not pretty, but effective.
* Uses the instance variable #castsBySource for its input and output; it must be initialized with at least
* the self-casts and declared casts.
*/
private void deriveCastsFromVarchar() {
final TClass COMMON = MString.VARCHAR;
Set<TClass> tClasses = castsBySource.keySet();
for (Map.Entry<TClass, Map<TClass, TCast>> entry : castsBySource.entrySet()) {
TClass source = entry.getKey();
Map<TClass, TCast> castsByTarget = entry.getValue();
for (TClass target : tClasses) {
if (target == source || castsByTarget.containsKey(target))
continue;
TCast sourceToVarchar = cast(source, COMMON);
if (sourceToVarchar == null)
continue;
TCast varcharToTarget = cast(COMMON, target);
if (varcharToTarget == null)
continue;
TCast derived = new ChainedCast(sourceToVarchar, varcharToTarget);
castsByTarget.put(target, derived);
}
}
}
private static TCast deriveCast(Map<TClass,Map<TClass,TCast>> castsBySource,
List<? extends TClass> path, int targetIndex) {
TClass source = path.get(0);
TClass target = path.get(targetIndex);
TCast alreadyThere = cast(castsBySource, source, target);
if (alreadyThere != null)
return alreadyThere;
int intermediateIndex = targetIndex - 1;
TClass intermediateClass = path.get(intermediateIndex);
TCast second = cast(castsBySource, intermediateClass, target);
if (second == null)
throw new AkibanInternalException("no explicit cast between " + intermediateClass + " and " + target
+ " while creating cast path: " + path);
TCast first = deriveCast(castsBySource, path, intermediateIndex);
if (first == null)
throw new AkibanInternalException("couldn't derive cast between " + source + " and " + intermediateClass
+ " while creating cast path: " + path);
TCast result = new ChainedCast(first, second);
putCast(castsBySource, result, null);
return result;
}
private static void putCast(Map<TClass, Map<TClass, TCast>> toMap, TCast cast, Set<TCastIdentifier> duplicates) {
if (cast == null)
return;
TClass source = cast.sourceClass();
TClass target = cast.targetClass();
Map<TClass,TCast> castsByTarget = toMap.get(source);
TCast old = castsByTarget.put(target, cast);
if (old != null) {
logger.error("CAST({} AS {}): {} replaced by {} ", new Object[]{
source, target, old.getClass(), cast.getClass()
});
if (duplicates == null)
throw new AkibanInternalException("multiple casts defined from " + source + " to " + target);
duplicates.add(new TCastIdentifier(source, target));
}
}
// nested classes
private static class SelfCast implements TCast {
@Override
public Constantness constness() {
return Constantness.UNKNOWN;
}
@Override
public TClass sourceClass() {
return tClass;
}
@Override
public TClass targetClass() {
return tClass;
}
@Override
public TInstance preferredTarget(TPreptimeValue source) {
return source.type();
}
@Override
public void evaluate(TExecutionContext context, ValueSource source, ValueTarget target) {
if (source.isNull()) {
target.putNull();
return;
}
TInstance srcInst = context.inputTypeAt(0);
TInstance dstInst = context.outputType();
tClass.selfCast(context, srcInst, source, dstInst, target);
}
SelfCast(TClass tClass) {
this.tClass = tClass;
}
private final TClass tClass;
}
static class ChainedCast implements TCast {
@Override
public Constantness constness() {
Constantness firstConst = first.constness();
return (firstConst == second.constness()) ? firstConst : Constantness.UNKNOWN;
}
@Override
public TClass sourceClass() {
return first.sourceClass();
}
@Override
public TClass targetClass() {
return second.targetClass();
}
@Override
public TInstance preferredTarget(TPreptimeValue source) {
TInstance intermediateTInstance = first.preferredTarget(source);
ValueSource firstValue = source.value();
TInstance result;
if (firstValue != null) {
Value intermediateValue = new Value(first.targetClass().instance(true));
TExecutionContext context = new TExecutionContext(
Collections.singletonList(source.type()),
intermediateTInstance,
null // TODO is this null a problem?
);
try {
first.evaluate(context, firstValue, intermediateValue);
result = second.preferredTarget(new TPreptimeValue(intermediateTInstance, intermediateValue));
} catch (Exception e) {
logger.error("while evaluating intermediate value for " + source + " in " + this, e);
result = second.preferredTarget(new TPreptimeValue(intermediateTInstance));
}
}
else {
result = second.preferredTarget(new TPreptimeValue(intermediateTInstance));
}
return result;
}
@Override
public void evaluate(TExecutionContext context, ValueSource source, ValueTarget target) {
if (source.isNull()) {
target.putNull();
return;
}
Value tmp = (Value) context.exectimeObjectAt(TMP_PVALUE);
if (tmp == null) {
tmp = new Value(first.targetClass().instance(true));
context.putExectimeObject(TMP_PVALUE, tmp);
}
// TODO cache
TExecutionContext firstContext = context.deriveContext(
Collections.singletonList(context.inputTypeAt(0)),
intermediateType
);
TExecutionContext secondContext = context.deriveContext(
Collections.singletonList(intermediateType),
context.outputType()
);
first.evaluate(firstContext, source, tmp);
second.evaluate(secondContext, tmp, target);
}
private ChainedCast(TCast first, TCast second) {
if (first.targetClass() != second.sourceClass()) {
throw new IllegalArgumentException("can't chain casts: " + first + " and " + second);
}
this.first = first;
this.second = second;
this.intermediateType = first.targetClass().instance(true);
}
private final TCast first;
private final TCast second;
private final TInstance intermediateType;
private static final int TMP_PVALUE = 0;
}
}