blob: 78a9d509767d1d9bee38ca4c2f92dd09f54cd41d [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.processors.query;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.binary.BinaryObject;
import org.apache.ignite.cache.QueryIndexType;
import org.apache.ignite.internal.binary.BinaryArray;
import org.apache.ignite.internal.binary.BinaryUtils;
import org.apache.ignite.internal.processors.cache.CacheObject;
import org.apache.ignite.internal.processors.cache.CacheObjectContext;
import org.apache.ignite.internal.processors.cache.KeyCacheObject;
import org.apache.ignite.internal.util.tostring.GridToStringExclude;
import org.apache.ignite.internal.util.tostring.GridToStringInclude;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.internal.A;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.jetbrains.annotations.Nullable;
import static org.apache.ignite.internal.binary.BinaryUtils.typeName;
import static org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode.KEY_SCALE_OUT_OF_RANGE;
import static org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode.NULL_KEY;
import static org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode.NULL_VALUE;
import static org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode.TOO_LONG_KEY;
import static org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode.TOO_LONG_VALUE;
import static org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode.VALUE_SCALE_OUT_OF_RANGE;
import static org.apache.ignite.internal.processors.query.QueryUtils.KEY_FIELD_NAME;
import static org.apache.ignite.internal.processors.query.QueryUtils.VAL_FIELD_NAME;
/**
* Descriptor of type.
*/
public class QueryTypeDescriptorImpl implements GridQueryTypeDescriptor {
/** Cache name. */
private final String cacheName;
/** */
private String name;
/** Schema name. */
private String schemaName;
/** */
private String tblName;
/** Value field names and types with preserved order. */
@GridToStringInclude
private final LinkedHashMap<String, Class<?>> fields = new LinkedHashMap<>();
/** */
@GridToStringExclude
private final Map<String, GridQueryProperty> props = new HashMap<>();
/** Map with upper cased property names to help find properties based on SQL INSERT and MERGE queries. */
private final Map<String, GridQueryProperty> uppercaseProps = new HashMap<>();
/** Mutex for operations on indexes. */
private final Object idxMux = new Object();
/** */
@GridToStringInclude
private final Map<String, QueryIndexDescriptorImpl> idxs = new HashMap<>();
/** Aliases. */
private Map<String, String> aliases;
/** */
private QueryIndexDescriptorImpl fullTextIdx;
/** */
private Class<?> keyCls;
/** */
private Class<?> valCls;
/** */
private String keyTypeName;
/** */
private String valTypeName;
/** */
private boolean valTextIdx;
/** */
private int typeId;
/** */
private String affKey;
/** */
private boolean customAffKeyMapper;
/** */
private String keyFieldName;
/** */
private String valFieldName;
/** Obsolete. */
private volatile boolean obsolete;
/** */
private List<GridQueryProperty> validateProps;
/** */
private List<GridQueryProperty> propsWithDefaultValue;
/** */
private final CacheObjectContext coCtx;
/** Primary key fields. */
private Set<String> pkFields;
/** */
private boolean implicitPk;
/** Whether absent PK parts should be filled with defaults or not. */
private boolean fillAbsentPKsWithDefaults;
/** */
private int pkInlineSize;
/** */
private int affFieldInlineSize;
/** Logger. */
private final IgniteLogger log;
/**
* Constructor.
*
* @param cacheName Cache name.
* @param coCtx Cache object context.
*/
public QueryTypeDescriptorImpl(String cacheName, CacheObjectContext coCtx) {
this.cacheName = cacheName;
this.coCtx = coCtx;
this.log = coCtx.kernalContext().log(getClass());
}
/** {@inheritDoc} */
@Override public String cacheName() {
return cacheName;
}
/** {@inheritDoc} */
@Override public String name() {
return name;
}
/** {@inheritDoc} */
@Override public String schemaName() {
return schemaName;
}
/**
* Sets type name.
*
* @param name Name.
*/
public void name(String name) {
this.name = name;
}
/**
* Gets table name for type.
* @return Table name.
*/
@Override public String tableName() {
return tblName != null ? tblName : name;
}
/**
* Sets table name for type.
*
* @param tblName Table name.
*/
public void tableName(String tblName) {
this.tblName = tblName;
}
/** {@inheritDoc} */
@Override public LinkedHashMap<String, Class<?>> fields() {
return fields;
}
/** {@inheritDoc} */
@Override public GridQueryProperty property(String name) {
GridQueryProperty res = props.get(name);
if (res == null)
res = uppercaseProps.get(name.toUpperCase());
return res;
}
/** {@inheritDoc} */
@Override public Map<String, GridQueryProperty> properties() {
return props;
}
/** {@inheritDoc} */
@Override public <T> T value(String field, Object key, Object val) throws IgniteCheckedException {
assert field != null;
GridQueryProperty prop = property(field);
if (prop == null)
throw new IgniteCheckedException("Failed to find field '" + field + "' in type '" + name + "'.");
return (T)prop.value(key, val);
}
/** {@inheritDoc} */
@Override public void setValue(String field, Object key, Object val, Object propVal)
throws IgniteCheckedException {
assert field != null;
GridQueryProperty prop = property(field);
if (prop == null)
throw new IgniteCheckedException("Failed to find field '" + field + "' in type '" + name + "'.");
prop.setValue(key, val, propVal);
}
/** {@inheritDoc} */
@Override public Map<String, GridQueryIndexDescriptor> indexes() {
synchronized (idxMux) {
return Collections.<String, GridQueryIndexDescriptor>unmodifiableMap(idxs);
}
}
/** {@inheritDoc} */
@Override public int typeId() {
return typeId;
}
/** {@inheritDoc} */
@Override public boolean matchType(CacheObject val) {
if (val instanceof BinaryObject)
return ((BinaryObject)val).type().typeId() == typeId;
// Value type name can be manually set in QueryEntity to any random value,
// also for some reason our conversion from setIndexedTypes sets a full class name
// instead of a simple name there, thus we can have a typeId mismatch.
// Also, if the type is not in binary format, we always must have it's class available.
return val.value(coCtx, false).getClass() == valCls;
}
/**
* @param typeId Type ID.
*/
public void typeId(int typeId) {
this.typeId = typeId;
}
/**
* Get index by name.
*
* @param name Name.
* @return Index.
*/
@Nullable public QueryIndexDescriptorImpl index(String name) {
synchronized (idxMux) {
return idxs.get(name);
}
}
/**
* @return Raw index descriptors.
*/
public Collection<QueryIndexDescriptorImpl> indexes0() {
synchronized (idxMux) {
return new ArrayList<>(idxs.values());
}
}
/** {@inheritDoc} */
@Override public GridQueryIndexDescriptor textIndex() {
return fullTextIdx;
}
/**
* Add index.
*
* @param idx Index.
* @throws IgniteCheckedException If failed.
*/
public void addIndex(QueryIndexDescriptorImpl idx) throws IgniteCheckedException {
synchronized (idxMux) {
if (idxs.put(idx.name(), idx) != null)
throw new IgniteCheckedException("Index with name '" + idx.name() + "' already exists.");
}
}
/**
* Drop index.
*
* @param idxName Index name.
*/
public void dropIndex(String idxName) {
synchronized (idxMux) {
idxs.remove(idxName);
}
}
/**
* Chedk if particular field exists.
*
* @param field Field.
* @return {@code True} if exists.
*/
public boolean hasField(String field) {
return props.containsKey(field) || QueryUtils.VAL_FIELD_NAME.equalsIgnoreCase(field);
}
/**
* Adds field to text index.
*
* @param field Field name.
* @throws IgniteCheckedException If failed.
*/
public void addFieldToTextIndex(String field) throws IgniteCheckedException {
if (fullTextIdx == null)
fullTextIdx = new QueryIndexDescriptorImpl(this, null, QueryIndexType.FULLTEXT, 0);
fullTextIdx.addField(field, 0, false);
}
/** {@inheritDoc} */
@Override public Class<?> valueClass() {
return valCls;
}
/**
* Sets value class.
*
* @param valCls Value class.
*/
public void valueClass(Class<?> valCls) {
A.notNull(valCls, "Value class must not be null");
this.valCls = valCls;
}
/** {@inheritDoc} */
@Override public Class<?> keyClass() {
return keyCls;
}
/**
* Set key class.
*
* @param keyCls Key class.
*/
public void keyClass(Class<?> keyCls) {
this.keyCls = keyCls;
}
/** {@inheritDoc} */
@Override public String keyTypeName() {
return keyTypeName;
}
/**
* Set key type name.
*
* @param keyTypeName Key type name.
*/
public void keyTypeName(String keyTypeName) {
this.keyTypeName = keyTypeName;
}
/** {@inheritDoc} */
@Override public String valueTypeName() {
return valTypeName;
}
/**
* Set value type name.
*
* @param valTypeName Value type name.
*/
public void valueTypeName(String valTypeName) {
this.valTypeName = valTypeName;
}
/**
* Adds property to the type descriptor.
*
* @param prop Property.
* @param failOnDuplicate Fail on duplicate flag.
* @throws IgniteCheckedException In case of error.
*/
public void addProperty(GridQueryProperty prop, boolean failOnDuplicate) throws IgniteCheckedException {
addProperty(prop, failOnDuplicate, true);
}
/**
* Adds property to the type descriptor.
*
* @param prop Property.
* @param failOnDuplicate Fail on duplicate flag.
* @param isField {@code True} if {@code prop} if field, {@code False} if prop is "_KEY" or "_VAL".
* @throws IgniteCheckedException In case of error.
*/
public void addProperty(GridQueryProperty prop, boolean failOnDuplicate, boolean isField)
throws IgniteCheckedException {
String name = prop.name();
if (props.put(name, prop) != null && failOnDuplicate)
throw new IgniteCheckedException("Property with name '" + name + "' already exists.");
if (uppercaseProps.put(name.toUpperCase(), prop) != null && failOnDuplicate)
throw new IgniteCheckedException("Property with upper cased name '" + name + "' already exists.");
if ((prop.notNull() && !prop.name().equals(KEY_FIELD_NAME) && !prop.name().equals(VAL_FIELD_NAME))
|| prop.precision() != -1 || coCtx.kernalContext().config().getSqlConfiguration().isValidationEnabled()) {
if (validateProps == null)
validateProps = new ArrayList<>();
validateProps.add(prop);
}
if (prop.defaultValue() != null) {
if (propsWithDefaultValue == null)
propsWithDefaultValue = new ArrayList<>();
propsWithDefaultValue.add(prop);
}
if (isField)
fields.put(name, prop.type());
}
/**
* Removes a property with specified name.
*
* @param name Name of a property to remove.
*/
public void removeProperty(String name) throws IgniteCheckedException {
GridQueryProperty prop = props.remove(name);
if (prop == null)
throw new IgniteCheckedException("Property with name '" + name + "' does not exist.");
if (validateProps != null)
validateProps.remove(prop);
uppercaseProps.remove(name.toUpperCase());
fields.remove(name);
}
/**
* @param schemaName Schema name.
*/
public void schemaName(String schemaName) {
this.schemaName = schemaName;
}
/** {@inheritDoc} */
@Override public boolean valueTextIndex() {
return valTextIdx;
}
/**
* Sets if this value should be text indexed.
*
* @param valTextIdx Flag value.
*/
public void valueTextIndex(boolean valTextIdx) {
this.valTextIdx = valTextIdx;
}
/** {@inheritDoc} */
@Override public String affinityKey() {
return affKey;
}
/**
* @param affKey Affinity key field.
*/
public void affinityKey(String affKey) {
this.affKey = affKey;
}
/** {@inheritDoc} */
@Override public boolean customAffinityKeyMapper() {
return customAffKeyMapper;
}
/**
* @param customAffKeyMapper Whether custom affinity key mapper is set.
*/
public void customAffinityKeyMapper(boolean customAffKeyMapper) {
this.customAffKeyMapper = customAffKeyMapper;
}
/**
* @return Aliases.
*/
public Map<String, String> aliases() {
return aliases != null ? aliases : Collections.<String, String>emptyMap();
}
/**
* @param aliases Aliases.
*/
public void aliases(Map<String, String> aliases) {
this.aliases = aliases;
}
/**
* @return {@code True} if obsolete.
*/
public boolean obsolete() {
return obsolete;
}
/**
* Mark index as obsolete.
*/
public void markObsolete() {
obsolete = true;
}
/** {@inheritDoc} */
@Override public String toString() {
return S.toString(QueryTypeDescriptorImpl.class, this);
}
/**
* Sets key field name.
* @param keyFieldName Key field name.
*/
void keyFieldName(String keyFieldName) {
this.keyFieldName = keyFieldName;
}
/** {@inheritDoc} */
@Override public String keyFieldName() {
return keyFieldName;
}
/**
* Sets value field name.
* @param valFieldName value field name.
*/
void valueFieldName(String valFieldName) {
this.valFieldName = valFieldName;
}
/** {@inheritDoc} */
@Override public String valueFieldName() {
return valFieldName;
}
/** {@inheritDoc} */
@Nullable @Override public String keyFieldAlias() {
return keyFieldName != null ? aliases.get(keyFieldName) : null;
}
/** {@inheritDoc} */
@Nullable @Override public String valueFieldAlias() {
return valFieldName != null ? aliases.get(valFieldName) : null;
}
/** {@inheritDoc} */
@SuppressWarnings("ForLoopReplaceableByForEach")
@Override public void validateKeyAndValue(Object key, Object val) throws IgniteCheckedException {
if (F.isEmpty(validateProps) && F.isEmpty(idxs))
return;
validateProps(key, val);
validateIndexes(key, val);
}
/** Validate properties. */
private void validateProps(Object key, Object val) throws IgniteCheckedException {
if (F.isEmpty(validateProps))
return;
final boolean validateTypes = coCtx.kernalContext().config().getSqlConfiguration().isValidationEnabled();
for (int i = 0; i < validateProps.size(); ++i) {
GridQueryProperty prop = validateProps.get(i);
Object propVal;
boolean isKey = false;
if (F.eq(prop.name(), keyFieldAlias()) || (keyFieldName == null && F.eq(prop.name(), KEY_FIELD_NAME))) {
propVal = key instanceof KeyCacheObject ? ((CacheObject)key).value(coCtx, true) : key;
isKey = true;
}
else if (F.eq(prop.name(), valueFieldAlias()) ||
(valFieldName == null && F.eq(prop.name(), VAL_FIELD_NAME)))
propVal = val instanceof CacheObject ? ((CacheObject)val).value(coCtx, true) : val;
else
propVal = prop.value(key, val);
if (propVal == null && prop.notNull()) {
throw new IgniteSQLException("Null value is not allowed for column '" + prop.name() + "'",
isKey ? NULL_KEY : NULL_VALUE);
}
if (validateTypes && propVal != null && !isCompatibleWithPropertyType(propVal, prop.type())) {
throw new IgniteSQLException("Type for a column '" + prop.name() + "' is not compatible with table definition." +
" Expected '" + prop.type().getSimpleName() + "', actual type '" + typeName(propVal) + "'");
}
if (propVal == null || prop.precision() == -1)
continue;
if (String.class == propVal.getClass() || byte[].class == propVal.getClass()) {
int propValLen = String.class == propVal.getClass() ? ((String)propVal).length()
: ((byte[])propVal).length;
if (propValLen > prop.precision()) {
throw new IgniteSQLException("Value for a column '" + prop.name() + "' is too long. " +
"Maximum length: " + prop.precision() + ", actual length: " + propValLen,
isKey ? TOO_LONG_KEY : TOO_LONG_VALUE);
}
}
else if (BigDecimal.class == propVal.getClass()) {
BigDecimal dec = (BigDecimal)propVal;
if (dec.precision() > prop.precision()) {
throw new IgniteSQLException("Value for a column '" + prop.name() + "' is out of range. " +
"Maximum precision: " + prop.precision() + ", actual precision: " + dec.precision(),
isKey ? TOO_LONG_KEY : TOO_LONG_VALUE);
}
else if (prop.scale() != -1 &&
dec.scale() > prop.scale()) {
throw new IgniteSQLException("Value for a column '" + prop.name() + "' is out of range. " +
"Maximum scale : " + prop.scale() + ", actual scale: " + dec.scale(),
isKey ? KEY_SCALE_OUT_OF_RANGE : VALUE_SCALE_OUT_OF_RANGE);
}
}
}
}
/** Validate indexed values. */
private void validateIndexes(Object key, Object val) throws IgniteCheckedException {
if (F.isEmpty(idxs))
return;
for (QueryIndexDescriptorImpl idx : idxs.values()) {
for (String idxField : idx.fields()) {
GridQueryProperty prop = props.get(idxField);
Object propVal;
Class<?> propType;
if (F.eq(idxField, keyFieldAlias()) || F.eq(idxField, KEY_FIELD_NAME)) {
propVal = key instanceof KeyCacheObject ? ((CacheObject)key).value(coCtx, true) : key;
propType = propVal == null ? null : propVal.getClass();
}
else if (F.eq(idxField, valueFieldAlias()) || F.eq(idxField, VAL_FIELD_NAME)) {
propVal = val instanceof CacheObject ? ((CacheObject)val).value(coCtx, true) : val;
propType = propVal == null ? null : propVal.getClass();
}
else {
propVal = prop.value(key, val);
propType = prop.type();
}
if (propVal == null)
continue;
if (!isCompatibleWithPropertyType(propVal, propType)) {
throw new IgniteSQLException("Type for a column '" + idxField + "' is not compatible with index definition." +
" Expected '" + prop.type().getSimpleName() + "', actual type '" + typeName(propVal) + "'");
}
}
}
}
/**
* Checks if the specified object is compatible with the type of the column through which this object will be accessed.
*
* @param val Object to check.
* @param expColType Type of the column based on Query Property info.
*/
private boolean isCompatibleWithPropertyType(Object val, Class<?> expColType) {
if (!(val instanceof BinaryObject) || val instanceof BinaryArray) {
if (U.box(expColType).isAssignableFrom(U.box(val.getClass())))
return true;
if (QueryUtils.isConvertibleTypes(val, expColType))
return true;
return expColType.isArray()
&& BinaryUtils.isObjectArray(val.getClass())
&& Arrays.stream(BinaryUtils.rawArrayFromBinary(val))
.allMatch(x -> x == null || U.box(expColType.getComponentType()).isAssignableFrom(U.box(x.getClass())));
}
else if (coCtx.kernalContext().cacheObjects().typeId(expColType.getName()) != ((BinaryObject)val).type().typeId()) {
final Class<?> cls = U.classForName(((BinaryObject)val).type().typeName(), null, true);
return (cls == null && expColType == Object.class) || (cls != null && expColType.isAssignableFrom(cls));
}
return true;
}
/** {@inheritDoc} */
@SuppressWarnings("ForLoopReplaceableByForEach")
@Override public void setDefaults(Object key, Object val) throws IgniteCheckedException {
if (F.isEmpty(propsWithDefaultValue))
return;
for (int i = 0; i < propsWithDefaultValue.size(); ++i) {
GridQueryProperty prop = propsWithDefaultValue.get(i);
prop.setValue(key, val, prop.defaultValue());
}
}
/** {@inheritDoc} */
@Override public Set<String> primaryKeyFields() {
return pkFields == null ? Collections.emptySet() : pkFields;
}
/** {@inheritDoc} */
@Override public void primaryKeyFields(Set<String> keys) {
pkFields = keys;
}
/** {@inheritDoc} */
@Override public boolean implicitPk() {
return implicitPk;
}
/** {@inheritDoc} */
@Override public void implicitPk(boolean implicitPk) {
this.implicitPk = implicitPk;
}
/** {@inheritDoc} */
@Override public boolean fillAbsentPKsWithDefaults() {
return fillAbsentPKsWithDefaults;
}
/** {@inheritDoc} */
@Override public void setFillAbsentPKsWithDefaults(boolean fillAbsentPKsWithDefaults) {
this.fillAbsentPKsWithDefaults = fillAbsentPKsWithDefaults;
}
/** {@inheritDoc} */
@Override public int primaryKeyInlineSize() {
return pkInlineSize;
}
/** {@inheritDoc} */
@Override public void primaryKeyInlineSize(int pkInlineSize) {
this.pkInlineSize = pkInlineSize;
}
/** {@inheritDoc} */
@Override public int affinityFieldInlineSize() {
return affFieldInlineSize;
}
/** {@inheritDoc} */
@Override public void affinityFieldInlineSize(int affFieldInlineSize) {
this.affFieldInlineSize = affFieldInlineSize;
}
}