blob: d977a49006648abd0e697415a90d638eb7dc3f8e [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.lang.reflect.Method;
import java.math.BigDecimal;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteException;
import org.apache.ignite.binary.BinaryField;
import org.apache.ignite.cache.QueryEntity;
import org.apache.ignite.cache.QueryIndex;
import org.apache.ignite.cache.QueryIndexType;
import org.apache.ignite.cache.affinity.AffinityKeyMapper;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.internal.GridKernalContext;
import org.apache.ignite.internal.binary.BinaryMarshaller;
import org.apache.ignite.internal.processors.cache.CacheDefaultBinaryAffinityKeyMapper;
import org.apache.ignite.internal.processors.cache.CacheObjectContext;
import org.apache.ignite.internal.processors.cache.DynamicCacheDescriptor;
import org.apache.ignite.internal.processors.cache.GridCacheContext;
import org.apache.ignite.internal.processors.cache.GridCacheDefaultAffinityKeyMapper;
import org.apache.ignite.internal.processors.cache.binary.CacheObjectBinaryProcessorImpl;
import org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode;
import org.apache.ignite.internal.processors.query.property.QueryBinaryProperty;
import org.apache.ignite.internal.processors.query.property.QueryClassProperty;
import org.apache.ignite.internal.processors.query.property.QueryFieldAccessor;
import org.apache.ignite.internal.processors.query.property.QueryMethodsAccessor;
import org.apache.ignite.internal.processors.query.property.QueryPropertyAccessor;
import org.apache.ignite.internal.processors.query.property.QueryReadOnlyMethodsAccessor;
import org.apache.ignite.internal.processors.query.schema.SchemaOperationException;
import org.apache.ignite.internal.util.Jsr310Java8DateTimeApiUtils;
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.U;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import static org.apache.ignite.IgniteSystemProperties.IGNITE_INDEXING_DISCOVERY_HISTORY_SIZE;
import static org.apache.ignite.IgniteSystemProperties.getInteger;
/**
* Utility methods for queries.
*/
public class QueryUtils {
/** Default schema. */
public static final String DFLT_SCHEMA = "PUBLIC";
/** Schema for system view. */
public static final String SCHEMA_SYS = "IGNITE";
/** Field name for key. */
public static final String KEY_FIELD_NAME = "_KEY";
/** Field name for value. */
public static final String VAL_FIELD_NAME = "_VAL";
/** Version field name. */
public static final String VER_FIELD_NAME = "_VER";
/** Well-known template name for PARTITIONED cache. */
public static final String TEMPLATE_PARTITIONED = "PARTITIONED";
/** Well-known template name for REPLICATED cache. */
public static final String TEMPLATE_REPLICATED = "REPLICATED";
/** Discovery history size. */
private static final int DISCO_HIST_SIZE = getInteger(IGNITE_INDEXING_DISCOVERY_HISTORY_SIZE, 1000);
/** */
private static final Class<?> GEOMETRY_CLASS = U.classForName("org.locationtech.jts.geom.Geometry", null);
/** */
private static final Set<Class<?>> SQL_TYPES = createSqlTypes();
/**
* Creates SQL types set.
*
* @return SQL types set.
*/
@NotNull private static Set<Class<?>> createSqlTypes() {
Set<Class<?>> sqlClasses = new HashSet<>(Arrays.<Class<?>>asList(
Integer.class,
Boolean.class,
Byte.class,
Short.class,
Long.class,
BigDecimal.class,
Double.class,
Float.class,
Time.class,
Timestamp.class,
Date.class,
java.sql.Date.class,
String.class,
UUID.class,
byte[].class
));
sqlClasses.addAll(Jsr310Java8DateTimeApiUtils.jsr310ApiClasses());
return sqlClasses;
}
/**
* Get table name for entity.
*
* @param entity Entity.
* @return Table name.
*/
public static String tableName(QueryEntity entity) {
String res = entity.getTableName();
if (res == null) {
String valTyp = entity.findValueType();
if (valTyp == null)
throw new IgniteException("Value type cannot be null or empty [queryEntity=" + entity + ']');
res = typeName(entity.findValueType());
}
return res;
}
/**
* Get index name.
*
* @param entity Query entity.
* @param idx Index.
* @return Index name.
*/
public static String indexName(QueryEntity entity, QueryIndex idx) {
return indexName(tableName(entity), idx);
}
/**
* Get index name.
*
* @param tblName Table name.
* @param idx Index.
* @return Index name.
*/
public static String indexName(String tblName, QueryIndex idx) {
String res = idx.getName();
if (res == null) {
StringBuilder idxName = new StringBuilder(tblName + "_");
for (Map.Entry<String, Boolean> field : idx.getFields().entrySet()) {
idxName.append(field.getKey());
idxName.append('_');
idxName.append(field.getValue() ? "asc_" : "desc_");
}
for (int i = 0; i < idxName.length(); i++) {
char ch = idxName.charAt(i);
if (Character.isWhitespace(ch))
idxName.setCharAt(i, '_');
else
idxName.setCharAt(i, Character.toLowerCase(ch));
}
idxName.append("idx");
return idxName.toString();
}
return res;
}
/**
* Normalize cache query entities.
*
* @param entities Query entities.
* @param cfg Cache config.
* @return Normalized query entities.
*/
public static Collection<QueryEntity> normalizeQueryEntities(Collection<QueryEntity> entities,
CacheConfiguration<?, ?> cfg) {
Collection<QueryEntity> normalEntities = new ArrayList<>(entities.size());
for (QueryEntity entity : entities) {
if (!F.isEmpty(entity.getNotNullFields()))
checkNotNullAllowed(cfg);
normalEntities.add(normalizeQueryEntity(entity, cfg.isSqlEscapeAll()));
}
return normalEntities;
}
/**
* Normalize query entity. If "escape" flag is set, nothing changes. Otherwise we convert all object names to
* upper case and replace inner class separator characters ('$' for Java and '.' for .NET) with underscore.
*
* @param entity Query entity.
* @param escape Escape flag taken form configuration.
* @return Normalized query entity.
*/
public static QueryEntity normalizeQueryEntity(QueryEntity entity, boolean escape) {
if (escape) {
String tblName = tableName(entity);
entity.setTableName(tblName);
Map<String, String> aliases = new HashMap<>(entity.getAliases());
for (String fieldName : entity.getFields().keySet()) {
String fieldAlias = entity.getAliases().get(fieldName);
if (fieldAlias == null) {
fieldAlias = aliasForFieldName(fieldName);
aliases.put(fieldName, fieldAlias);
}
}
entity.setAliases(aliases);
for (QueryIndex idx : entity.getIndexes())
idx.setName(indexName(tblName, idx));
validateQueryEntity(entity);
return entity;
}
QueryEntity normalEntity = entity instanceof QueryEntityEx ? new QueryEntityEx() : new QueryEntity();
// Propagate plain properties.
normalEntity.setKeyType(entity.getKeyType());
normalEntity.setValueType(entity.getValueType());
normalEntity.setFields(entity.getFields());
normalEntity.setKeyFields(entity.getKeyFields());
normalEntity.setKeyFieldName(entity.getKeyFieldName());
normalEntity.setValueFieldName(entity.getValueFieldName());
normalEntity.setNotNullFields(entity.getNotNullFields());
normalEntity.setDefaultFieldValues(entity.getDefaultFieldValues());
normalEntity.setFieldsPrecision(entity.getFieldsPrecision());
normalEntity.setFieldsScale(entity.getFieldsScale());
// Normalize table name.
String normalTblName = entity.getTableName();
if (normalTblName == null)
// Replace special characters for auto-generated table name.
normalTblName = normalizeObjectName(tableName(entity), true);
else
// No replaces for manually defined table.
normalTblName = normalizeObjectName(normalTblName, false);
normalEntity.setTableName(normalTblName);
// Normalize field names through aliases.
Map<String, String> normalAliases = new HashMap<>(normalEntity.getAliases());
for (String fieldName : normalEntity.getFields().keySet()) {
String fieldAlias = entity.getAliases().get(fieldName);
if (fieldAlias == null)
fieldAlias = aliasForFieldName(fieldName);
assert fieldAlias != null;
normalAliases.put(fieldName, normalizeObjectName(fieldAlias, false));
}
normalEntity.setAliases(normalAliases);
// Normalize indexes.
Collection<QueryIndex> normalIdxs = new LinkedList<>();
for (QueryIndex idx : entity.getIndexes()) {
QueryIndex normalIdx = new QueryIndex();
normalIdx.setFields(idx.getFields());
normalIdx.setIndexType(idx.getIndexType());
normalIdx.setInlineSize(idx.getInlineSize());
normalIdx.setName(normalizeObjectName(indexName(normalTblName, idx), false));
normalIdxs.add(normalIdx);
}
normalEntity.setIndexes(normalIdxs);
validateQueryEntity(normalEntity);
return normalEntity;
}
/**
* Stores rule for constructing schemaName according to cache configuration.
*
* @param cacheName Cache name.
* @param schemaName Schema name.
* @return Proper schema name according to ANSI-99 standard.
*/
public static String normalizeSchemaName(String cacheName, @Nullable String schemaName) {
boolean escape = false;
String res = schemaName;
if (res == null) {
res = cacheName;
// If schema name is not set explicitly, we will use escaped cache name. The reason is that cache name
// could contain weird characters, such as underscores, dots or non-Latin stuff, which are invalid from
// SQL synthax perspective. We do not want node to fail on startup due to this.
escape = true;
}
else {
if (res.startsWith("\"") && res.endsWith("\"")) {
res = res.substring(1, res.length() - 1);
escape = true;
}
}
if (!escape)
res = normalizeObjectName(res, false);
return res;
}
/**
* Get alias for the field name (i.e. last part of the property).
*
* @param fieldName Field name.
* @return Alias.
*/
private static String aliasForFieldName(String fieldName) {
int idx = fieldName.lastIndexOf('.');
if (idx >= 0)
fieldName = fieldName.substring(idx + 1);
return fieldName;
}
/**
* Normalize object name.
*
* @param str String.
* @param replace Whether to perform replace of special characters.
* @return Escaped string.
*/
public static @Nullable String normalizeObjectName(@Nullable String str, boolean replace) {
if (str == null)
return null;
if (replace)
str = str.replace('.', '_').replace('$', '_');
return str.toUpperCase();
}
/**
* Create type candidate for query entity.
*
* @param cacheName Cache name.
* @param schemaName Schema name.
* @param cctx Cache context.
* @param qryEntity Query entity.
* @param mustDeserializeClss Classes which must be deserialized.
* @param escape Escape flag.
* @return Type candidate.
* @throws IgniteCheckedException If failed.
*/
public static QueryTypeCandidate typeForQueryEntity(String cacheName, String schemaName, GridCacheContext cctx,
QueryEntity qryEntity, List<Class<?>> mustDeserializeClss, boolean escape) throws IgniteCheckedException {
GridKernalContext ctx = cctx.kernalContext();
CacheConfiguration<?,?> ccfg = cctx.config();
boolean binaryEnabled = ctx.cacheObjects().isBinaryEnabled(ccfg);
CacheObjectContext coCtx = binaryEnabled ? ctx.cacheObjects().contextForCache(ccfg) : null;
QueryTypeDescriptorImpl desc = new QueryTypeDescriptorImpl(cacheName, coCtx);
desc.schemaName(schemaName);
desc.aliases(qryEntity.getAliases());
// Key and value classes still can be available if they are primitive or JDK part.
// We need that to set correct types for _key and _val columns.
// We better box these types - otherwise, if user provides, say, raw 'byte' for
// key or value (which they could), we'll deem key or value as Object which clearly is not right.
Class<?> keyCls = U.box(U.classForName(qryEntity.findKeyType(), null, true));
Class<?> valCls = U.box(U.classForName(qryEntity.findValueType(), null, true));
// If local node has the classes and they are externalizable, we must use reflection properties.
boolean keyMustDeserialize = mustDeserializeBinary(ctx, keyCls);
boolean valMustDeserialize = mustDeserializeBinary(ctx, valCls);
boolean keyOrValMustDeserialize = keyMustDeserialize || valMustDeserialize;
if (keyCls == null)
keyCls = Object.class;
String simpleValType = ((valCls == null) ? typeName(qryEntity.findValueType()) : typeName(valCls));
desc.name(simpleValType);
desc.tableName(qryEntity.getTableName());
if (binaryEnabled && !keyOrValMustDeserialize) {
// Safe to check null.
if (SQL_TYPES.contains(valCls))
desc.valueClass(valCls);
else
desc.valueClass(Object.class);
if (SQL_TYPES.contains(keyCls))
desc.keyClass(keyCls);
else
desc.keyClass(Object.class);
}
else {
if (valCls == null)
throw new IgniteCheckedException("Failed to find value class in the node classpath " +
"(use default marshaller to enable binary objects) : " + qryEntity.findValueType());
desc.valueClass(valCls);
desc.keyClass(keyCls);
}
desc.keyTypeName(qryEntity.findKeyType());
desc.valueTypeName(qryEntity.findValueType());
desc.keyFieldName(qryEntity.getKeyFieldName());
desc.valueFieldName(qryEntity.getValueFieldName());
if (binaryEnabled && keyOrValMustDeserialize) {
if (keyMustDeserialize)
mustDeserializeClss.add(keyCls);
if (valMustDeserialize)
mustDeserializeClss.add(valCls);
}
QueryTypeIdKey typeId;
QueryTypeIdKey altTypeId = null;
int valTypeId = ctx.cacheObjects().typeId(qryEntity.findValueType());
if (valCls == null || (binaryEnabled && !keyOrValMustDeserialize)) {
processBinaryMeta(ctx, qryEntity, desc);
typeId = new QueryTypeIdKey(cacheName, valTypeId);
if (valCls != null)
altTypeId = new QueryTypeIdKey(cacheName, valCls);
String affField = null;
// Need to setup affinity key for distributed joins.
String keyType = qryEntity.getKeyType();
if (!cctx.customAffinityMapper() && keyType != null) {
if (coCtx != null) {
CacheDefaultBinaryAffinityKeyMapper mapper =
(CacheDefaultBinaryAffinityKeyMapper)coCtx.defaultAffMapper();
BinaryField field = mapper.affinityKeyField(keyType);
if (field != null)
affField = field.name();
}
}
if (affField != null) {
if (!escape)
affField = normalizeObjectName(affField, false);
desc.affinityKey(affField);
}
}
else {
processClassMeta(qryEntity, desc, coCtx);
AffinityKeyMapper keyMapper = cctx.config().getAffinityMapper();
if (keyMapper instanceof GridCacheDefaultAffinityKeyMapper) {
String affField =
((GridCacheDefaultAffinityKeyMapper)keyMapper).affinityKeyPropertyName(desc.keyClass());
if (affField != null) {
if (!escape)
affField = normalizeObjectName(affField, false);
desc.affinityKey(affField);
}
}
typeId = new QueryTypeIdKey(cacheName, valCls);
altTypeId = new QueryTypeIdKey(cacheName, valTypeId);
}
desc.typeId(valTypeId);
return new QueryTypeCandidate(typeId, altTypeId, desc);
}
/**
* Processes declarative metadata for binary object.
*
* @param ctx Kernal context.
* @param qryEntity Declared metadata.
* @param d Type descriptor.
* @throws IgniteCheckedException If failed.
*/
public static void processBinaryMeta(GridKernalContext ctx, QueryEntity qryEntity, QueryTypeDescriptorImpl d)
throws IgniteCheckedException {
LinkedHashMap<String, String> fields = qryEntity.getFields();
Set<String> keyFields = qryEntity.getKeyFields();
Set<String> notNulls = qryEntity.getNotNullFields();
Map<String, Object> dlftVals = qryEntity.getDefaultFieldValues();
Map<String, Integer> precision = qryEntity.getFieldsPrecision();
Map<String, Integer> scale = qryEntity.getFieldsScale();
// We have to distinguish between empty and null keyFields when the key is not of SQL type -
// when a key is not of SQL type, absence of a field in nonnull keyFields tell us that this field
// is a value field, and null keyFields tells us that current configuration
// does not tell us anything about this field's ownership.
boolean hasKeyFields = (keyFields != null);
boolean isKeyClsSqlType = isSqlType(d.keyClass());
if (hasKeyFields && !isKeyClsSqlType) {
//ensure that 'keyFields' is case sensitive subset of 'fields'
for (String keyField : keyFields) {
if (!fields.containsKey(keyField))
throw new IgniteCheckedException("QueryEntity 'keyFields' property must be a subset of keys " +
"from 'fields' property (case sensitive): " + keyField);
}
}
for (Map.Entry<String, String> entry : fields.entrySet()) {
Boolean isKeyField;
if (isKeyClsSqlType) // We don't care about keyFields in this case - it might be null, or empty, or anything
isKeyField = false;
else
isKeyField = (hasKeyFields ? keyFields.contains(entry.getKey()) : null);
boolean notNull = notNulls != null && notNulls.contains(entry.getKey());
Object dfltVal = dlftVals != null ? dlftVals.get(entry.getKey()) : null;
QueryBinaryProperty prop = buildBinaryProperty(ctx, entry.getKey(),
U.classForName(entry.getValue(), Object.class, true),
d.aliases(), isKeyField, notNull, dfltVal,
precision == null ? -1 : precision.getOrDefault(entry.getKey(), -1),
scale == null ? -1 : scale.getOrDefault(entry.getKey(), -1));
d.addProperty(prop, false);
}
String keyFieldName = qryEntity.getKeyFieldName();
if (keyFieldName == null)
keyFieldName = KEY_FIELD_NAME;
if (!F.isEmpty(precision) && precision.containsKey(keyFieldName) &&
!fields.containsKey(keyFieldName)) {
addKeyValueValidationProperty(ctx, qryEntity, d, keyFieldName, true);
}
String valFieldName = qryEntity.getValueFieldName();
if (valFieldName == null)
valFieldName = VAL_FIELD_NAME;
if (!F.isEmpty(precision) && precision.containsKey(valFieldName) &&
!fields.containsKey(valFieldName)) {
addKeyValueValidationProperty(ctx, qryEntity, d, valFieldName, false);
}
processIndexes(qryEntity, d);
}
/**
* Add validate property to QueryTypeDescriptor.
*
* @param ctx Kernel context.
* @param qryEntity Query entity.
* @param d Descriptor.
* @param name Field name.
* @throws IgniteCheckedException
*/
private static void addKeyValueValidationProperty(GridKernalContext ctx, QueryEntity qryEntity, QueryTypeDescriptorImpl d,
String name, boolean isKey) throws IgniteCheckedException {
Map<String, Object> dfltVals = qryEntity.getDefaultFieldValues();
Map<String, Integer> precision = qryEntity.getFieldsPrecision();
Map<String, Integer> scale = qryEntity.getFieldsScale();
String typeName = isKey ? qryEntity.getKeyType() : qryEntity.getValueType();
Object dfltVal = dfltVals.get(name);
QueryBinaryProperty prop = buildBinaryProperty(
ctx,
name,
U.classForName(typeName, Object.class, true),
d.aliases(),
isKey,
true,
dfltVal,
precision == null ? -1 : precision.getOrDefault(name, -1),
scale == null ? -1 : scale.getOrDefault(name, -1));
d.addProperty(prop, true, false);
}
/**
* Processes declarative metadata for binary object.
*
* @param qryEntity Declared metadata.
* @param d Type descriptor.
* @throws IgniteCheckedException If failed.
*/
public static void processClassMeta(QueryEntity qryEntity, QueryTypeDescriptorImpl d, CacheObjectContext coCtx)
throws IgniteCheckedException {
Set<String> notNulls = qryEntity.getNotNullFields();
for (Map.Entry<String, String> entry : qryEntity.getFields().entrySet()) {
GridQueryProperty prop = buildProperty(
d.keyClass(),
d.valueClass(),
d.keyFieldName(),
d.valueFieldName(),
entry.getKey(),
U.classForName(entry.getValue(), Object.class),
d.aliases(),
notNulls != null && notNulls.contains(entry.getKey()),
coCtx);
d.addProperty(prop, false);
}
processIndexes(qryEntity, d);
}
/**
* Processes indexes based on query entity.
*
* @param qryEntity Query entity to process.
* @param d Type descriptor to populate.
* @throws IgniteCheckedException If failed to build index information.
*/
private static void processIndexes(QueryEntity qryEntity, QueryTypeDescriptorImpl d) throws IgniteCheckedException {
if (!F.isEmpty(qryEntity.getIndexes())) {
for (QueryIndex idx : qryEntity.getIndexes())
processIndex(idx, d);
}
}
/**
* Process dynamic index change.
*
* @param idx Index.
* @param d Type descriptor to populate.
* @throws IgniteCheckedException If failed to build index information.
*/
public static void processDynamicIndexChange(String idxName, @Nullable QueryIndex idx, QueryTypeDescriptorImpl d)
throws IgniteCheckedException {
d.dropIndex(idxName);
if (idx != null)
processIndex(idx, d);
}
/**
* Create index descriptor.
*
* @param typeDesc Type descriptor.
* @param idx Index.
* @return Index descriptor.
* @throws IgniteCheckedException If failed.
*/
public static QueryIndexDescriptorImpl createIndexDescriptor(QueryTypeDescriptorImpl typeDesc, QueryIndex idx)
throws IgniteCheckedException {
String idxName = indexName(typeDesc.tableName(), idx);
QueryIndexType idxTyp = idx.getIndexType();
assert idxTyp == QueryIndexType.SORTED || idxTyp == QueryIndexType.GEOSPATIAL;
QueryIndexDescriptorImpl res = new QueryIndexDescriptorImpl(typeDesc, idxName, idxTyp, idx.getInlineSize());
int i = 0;
for (Map.Entry<String, Boolean> entry : idx.getFields().entrySet()) {
String field = entry.getKey();
boolean asc = entry.getValue();
String alias = typeDesc.aliases().get(field);
if (alias != null)
field = alias;
res.addField(field, i++, !asc);
}
return res;
}
/**
* Process single index.
*
* @param idx Index.
* @param d Type descriptor to populate.
* @throws IgniteCheckedException If failed to build index information.
*/
private static void processIndex(QueryIndex idx, QueryTypeDescriptorImpl d) throws IgniteCheckedException {
QueryIndexType idxTyp = idx.getIndexType();
if (idxTyp == QueryIndexType.SORTED || idxTyp == QueryIndexType.GEOSPATIAL) {
QueryIndexDescriptorImpl idxDesc = createIndexDescriptor(d, idx);
d.addIndex(idxDesc);
}
else if (idxTyp == QueryIndexType.FULLTEXT){
for (String field : idx.getFields().keySet()) {
String alias = d.aliases().get(field);
if (alias != null)
field = alias;
d.addFieldToTextIndex(field);
}
}
else if (idxTyp != null)
throw new IllegalArgumentException("Unsupported index type [idx=" + idx.getName() +
", typ=" + idxTyp + ']');
else
throw new IllegalArgumentException("Index type is not set: " + idx.getName());
}
/**
* Builds binary object property.
*
* @param ctx Kernal context.
* @param pathStr String representing path to the property. May contains dots '.' to identify
* nested fields.
* @param resType Result type.
* @param aliases Aliases.
* @param isKeyField Key ownership flag, as defined in {@link QueryEntity#keyFields}: {@code true} if field belongs
* to key, {@code false} if it belongs to value, {@code null} if QueryEntity#keyFields is null.
* @param notNull {@code true} if {@code null} value is not allowed.
* @param dlftVal Default value.
* @param precision Precision.
* @param scale Scale.
* @return Binary property.
*/
public static QueryBinaryProperty buildBinaryProperty(GridKernalContext ctx, String pathStr,
Class<?> resType, Map<String, String> aliases, @Nullable Boolean isKeyField, boolean notNull, Object dlftVal,
int precision, int scale) {
String[] path = pathStr.split("\\.");
QueryBinaryProperty res = null;
StringBuilder fullName = new StringBuilder();
for (String prop : path) {
if (fullName.length() != 0)
fullName.append('.');
fullName.append(prop);
String alias = aliases.get(fullName.toString());
// The key flag that we've found out is valid for the whole path.
res = new QueryBinaryProperty(ctx, prop, res, resType, isKeyField, alias, notNull, dlftVal,
precision, scale);
}
return res;
}
/**
* @param keyCls Key class.
* @param valCls Value class.
* @param pathStr Path string.
* @param resType Result type.
* @param aliases Aliases.
* @param notNull {@code true} if {@code null} value is not allowed.
* @param coCtx Cache object context.
* @return Class property.
* @throws IgniteCheckedException If failed.
*/
public static QueryClassProperty buildClassProperty(Class<?> keyCls, Class<?> valCls, String pathStr,
Class<?> resType, Map<String,String> aliases, boolean notNull, CacheObjectContext coCtx)
throws IgniteCheckedException {
QueryClassProperty res = buildClassProperty(
true,
keyCls,
pathStr,
resType,
aliases,
notNull,
coCtx);
if (res == null) // We check key before value consistently with BinaryProperty.
res = buildClassProperty(false, valCls, pathStr, resType, aliases, notNull, coCtx);
if (res == null)
throw new IgniteCheckedException(propertyInitializationExceptionMessage(keyCls, valCls, pathStr, resType));
return res;
}
/**
* @param keyCls Key class.
* @param valCls Value class.
* @param keyFieldName Key Field.
* @param valueFieldName Value Field.
* @param pathStr Path string.
* @param resType Result type.
* @param aliases Aliases.
* @param notNull {@code true} if {@code null} value is not allowed.
* @param coCtx Cache object context.
* @return Class property.
* @throws IgniteCheckedException If failed.
*/
public static GridQueryProperty buildProperty(Class<?> keyCls, Class<?> valCls, String keyFieldName,
String valueFieldName, String pathStr, Class<?> resType, Map<String,String> aliases, boolean notNull,
CacheObjectContext coCtx) throws IgniteCheckedException {
if (pathStr.equals(keyFieldName))
return new KeyOrValProperty(true, pathStr, keyCls);
if (pathStr.equals(valueFieldName))
return new KeyOrValProperty(false, pathStr, valCls);
return buildClassProperty(keyCls,
valCls,
pathStr,
resType,
aliases,
notNull,
coCtx);
}
/**
* Exception message to compare in tests.
*
* @param keyCls key class
* @param valCls value class
* @param pathStr property name
* @param resType property type
* @return Exception message.
*/
public static String propertyInitializationExceptionMessage(Class<?> keyCls, Class<?> valCls, String pathStr,
Class<?> resType) {
return "Failed to initialize property '" + pathStr + "' of type '" +
resType.getName() + "' for key class '" + keyCls + "' and value class '" + valCls + "'. " +
"Make sure that one of these classes contains respective getter method or field.";
}
/**
* @param key If this is a key property.
* @param cls Source type class.
* @param pathStr String representing path to the property. May contains dots '.' to identify nested fields.
* @param resType Expected result type.
* @param aliases Aliases.
* @param notNull {@code true} if {@code null} value is not allowed.
* @param coCtx Cache object context.
* @return Property instance corresponding to the given path.
*/
@SuppressWarnings("ConstantConditions")
public static QueryClassProperty buildClassProperty(boolean key, Class<?> cls, String pathStr, Class<?> resType,
Map<String,String> aliases, boolean notNull, CacheObjectContext coCtx) {
String[] path = pathStr.split("\\.");
QueryClassProperty res = null;
StringBuilder fullName = new StringBuilder();
for (String prop : path) {
if (fullName.length() != 0)
fullName.append('.');
fullName.append(prop);
String alias = aliases.get(fullName.toString());
QueryPropertyAccessor accessor = findProperty(prop, cls);
if (accessor == null)
return null;
QueryClassProperty tmp = new QueryClassProperty(accessor, key, alias, notNull, coCtx);
tmp.parent(res);
cls = tmp.type();
res = tmp;
}
if (!U.box(resType).isAssignableFrom(U.box(res.type())))
return null;
return res;
}
/**
* Find a member (either a getter method or a field) with given name of given class.
* @param prop Property name.
* @param cls Class to search for a member in.
* @return Member for given name.
*/
@Nullable private static QueryPropertyAccessor findProperty(String prop, Class<?> cls) {
StringBuilder getBldr = new StringBuilder("get");
getBldr.append(prop);
getBldr.setCharAt(3, Character.toUpperCase(getBldr.charAt(3)));
StringBuilder setBldr = new StringBuilder("set");
setBldr.append(prop);
setBldr.setCharAt(3, Character.toUpperCase(setBldr.charAt(3)));
try {
Method getter = cls.getMethod(getBldr.toString());
Method setter;
try {
// Setter has to have the same name like 'setXxx' and single param of the same type
// as the return type of the getter.
setter = cls.getMethod(setBldr.toString(), getter.getReturnType());
}
catch (NoSuchMethodException ignore) {
// Have getter, but no setter - return read-only accessor.
return new QueryReadOnlyMethodsAccessor(getter, prop);
}
return new QueryMethodsAccessor(getter, setter, prop);
}
catch (NoSuchMethodException ignore) {
// No-op.
}
getBldr = new StringBuilder("is");
getBldr.append(prop);
getBldr.setCharAt(2, Character.toUpperCase(getBldr.charAt(2)));
// We do nothing about setBldr here as it corresponds to setProperty name which is what we need
// for boolean property setter as well
try {
Method getter = cls.getMethod(getBldr.toString());
Method setter;
try {
// Setter has to have the same name like 'setXxx' and single param of the same type
// as the return type of the getter.
setter = cls.getMethod(setBldr.toString(), getter.getReturnType());
}
catch (NoSuchMethodException ignore) {
// Have getter, but no setter - return read-only accessor.
return new QueryReadOnlyMethodsAccessor(getter, prop);
}
return new QueryMethodsAccessor(getter, setter, prop);
}
catch (NoSuchMethodException ignore) {
// No-op.
}
Class cls0 = cls;
while (cls0 != null)
try {
return new QueryFieldAccessor(cls0.getDeclaredField(prop));
}
catch (NoSuchFieldException ignored) {
cls0 = cls0.getSuperclass();
}
try {
Method getter = cls.getMethod(prop);
Method setter;
try {
// Setter has to have the same name and single param of the same type
// as the return type of the getter.
setter = cls.getMethod(prop, getter.getReturnType());
}
catch (NoSuchMethodException ignore) {
// Have getter, but no setter - return read-only accessor.
return new QueryReadOnlyMethodsAccessor(getter, prop);
}
return new QueryMethodsAccessor(getter, setter, prop);
}
catch (NoSuchMethodException ignored) {
// No-op.
}
// No luck.
return null;
}
/**
* Check whether type still must be deserialized when binary marshaller is set.
*
* @param ctx Kernal context.
* @param cls Class.
* @return {@code True} if will be deserialized.
*/
private static boolean mustDeserializeBinary(GridKernalContext ctx, Class cls) {
if (cls != null && cls != Object.class && ctx.config().getMarshaller() instanceof BinaryMarshaller) {
CacheObjectBinaryProcessorImpl proc0 = (CacheObjectBinaryProcessorImpl)ctx.cacheObjects();
return proc0.binaryContext().mustDeserialize(cls);
}
else
return false;
}
/**
* Checks if the given class can be mapped to a simple SQL type.
*
* @param cls Class.
* @return {@code true} If can.
*/
public static boolean isSqlType(Class<?> cls) {
cls = U.box(cls);
return SQL_TYPES.contains(cls) || QueryUtils.isGeometryClass(cls);
}
/**
* Checks if the given class is GEOMETRY.
*
* @param cls Class.
* @return {@code true} If this is geometry.
*/
public static boolean isGeometryClass(Class<?> cls) {
return GEOMETRY_CLASS != null && GEOMETRY_CLASS.isAssignableFrom(cls);
}
/**
* Gets type name by class.
*
* @param clsName Class name.
* @return Type name.
*/
public static String typeName(String clsName) {
int pkgEnd = clsName.lastIndexOf('.');
if (pkgEnd >= 0 && pkgEnd < clsName.length() - 1)
clsName = clsName.substring(pkgEnd + 1);
if (clsName.endsWith("[]"))
clsName = clsName.substring(0, clsName.length() - 2) + "_array";
int parentEnd = clsName.lastIndexOf('$');
if (parentEnd >= 0)
clsName = clsName.substring(parentEnd + 1);
parentEnd = clsName.lastIndexOf('+'); // .NET parent
if (parentEnd >= 0)
clsName = clsName.substring(parentEnd + 1);
return clsName;
}
/**
* Gets type name by class.
*
* @param cls Class.
* @return Type name.
*/
public static String typeName(Class<?> cls) {
String typeName = cls.getSimpleName();
// To protect from failure on anonymous classes.
if (F.isEmpty(typeName)) {
String pkg = cls.getPackage().getName();
typeName = cls.getName().substring(pkg.length() + (pkg.isEmpty() ? 0 : 1));
}
if (cls.isArray()) {
assert typeName.endsWith("[]");
typeName = typeName.substring(0, typeName.length() - 2) + "_array";
}
return typeName;
}
/**
* @param timeout Timeout.
* @param timeUnit Time unit.
* @return Converted time.
*/
public static int validateTimeout(int timeout, TimeUnit timeUnit) {
A.ensure(timeUnit != TimeUnit.MICROSECONDS && timeUnit != TimeUnit.NANOSECONDS,
"timeUnit minimal resolution is millisecond.");
A.ensure(timeout >= 0, "timeout value should be non-negative.");
long tmp = TimeUnit.MILLISECONDS.convert(timeout, timeUnit);
return (int) tmp;
}
/**
* @param ccfg Cache configuration.
* @return {@code true} If query index must be enabled for this cache.
*/
public static boolean isEnabled(CacheConfiguration<?,?> ccfg) {
return !F.isEmpty(ccfg.getIndexedTypes()) ||
!F.isEmpty(ccfg.getQueryEntities());
}
/**
* Discovery history size.
*
* @return Discovery history size.
*/
public static int discoveryHistorySize() {
return DISCO_HIST_SIZE;
}
/**
* Wrap schema exception if needed.
*
* @param e Original exception.
* @return Schema exception.
*/
@Nullable public static SchemaOperationException wrapIfNeeded(@Nullable Exception e) {
if (e == null)
return null;
if (e instanceof SchemaOperationException)
return (SchemaOperationException)e;
return new SchemaOperationException("Unexpected exception.", e);
}
/**
* Check given {@link CacheConfiguration} for conflicts in table and index names from any query entities
* found in collection of {@link DynamicCacheDescriptor}s and belonging to the same schema.
*
* @param ccfg New cache configuration.
* @param descs Cache descriptors.
* @return Exception message describing found conflict or {@code null} if none found.
*/
public static SchemaOperationException checkQueryEntityConflicts(CacheConfiguration<?, ?> ccfg,
Collection<DynamicCacheDescriptor> descs) {
String schema = QueryUtils.normalizeSchemaName(ccfg.getName(), ccfg.getSqlSchema());
Set<String> idxNames = new HashSet<>();
Set<String> tblNames = new HashSet<>();
for (DynamicCacheDescriptor desc : descs) {
if (F.eq(ccfg.getName(), desc.cacheName()))
continue;
String descSchema = QueryUtils.normalizeSchemaName(desc.cacheName(),
desc.cacheConfiguration().getSqlSchema());
if (!F.eq(schema, descSchema))
continue;
for (QueryEntity e : desc.schema().entities()) {
tblNames.add(e.getTableName());
for (QueryIndex idx : e.getIndexes())
idxNames.add(idx.getName());
}
}
for (QueryEntity e : ccfg.getQueryEntities()) {
if (!tblNames.add(e.getTableName()))
return new SchemaOperationException(SchemaOperationException.CODE_TABLE_EXISTS, e.getTableName());
for (QueryIndex idx : e.getIndexes())
if (!idxNames.add(idx.getName()))
return new SchemaOperationException(SchemaOperationException.CODE_INDEX_EXISTS, idx.getName());
}
return null;
}
/**
* Validate query entity.
*
* @param entity Entity.
*/
private static void validateQueryEntity(QueryEntity entity) {
if (F.isEmpty(entity.findValueType()))
throw new IgniteException("Value type cannot be null or empty [queryEntity=" + entity + ']');
String keyFieldName = entity.getKeyFieldName();
if (keyFieldName != null && !entity.getFields().containsKey(keyFieldName)) {
throw new IgniteException("Key field is not in the field list [queryEntity=" + entity +
", keyFieldName=" + keyFieldName + "]");
}
String valFieldName = entity.getValueFieldName();
if (valFieldName != null && !entity.getFields().containsKey(valFieldName)) {
throw new IgniteException("Value field is not in the field list [queryEntity=" + entity +
", valFieldName=" + valFieldName + "]");
}
Collection<QueryIndex> idxs = entity.getIndexes();
if (!F.isEmpty(idxs)) {
Set<String> idxNames = new HashSet<>();
for (QueryIndex idx : idxs) {
String idxName = idx.getName();
if (idxName == null)
idxName = indexName(entity, idx);
assert !F.isEmpty(idxName);
if (!idxNames.add(idxName))
throw new IgniteException("Duplicate index name [queryEntity=" + entity +
", queryIdx=" + idx + ']');
if (idx.getIndexType() == null)
throw new IgniteException("Index type is not set [queryEntity=" + entity +
", queryIdx=" + idx + ']');
}
}
Map<String, Object> dfltVals = entity.getDefaultFieldValues();
Map<String, Integer> precision = entity.getFieldsPrecision();
if (!F.isEmpty(precision)) {
for (String fld : precision.keySet()) {
if (!dfltVals.containsKey(fld))
continue;
Object dfltVal = dfltVals.get(fld);
if (dfltVal == null)
continue;
if (dfltVal.toString().length() > precision.get(fld)) {
throw new IgniteSQLException("Default value '" + dfltVal +
"' is longer than maximum length " + precision.get(fld));
}
}
}
}
/**
* Construct cache name for table.
*
* @param schemaName Schema name.
* @param tblName Table name.
* @return Cache name.
*/
public static String createTableCacheName(String schemaName, String tblName) {
return "SQL_" + schemaName + "_" + tblName;
}
/**
* Construct value type name for table.
*
* @param schemaName Schema name.
* @param tblName Table name.
* @return Value type name.
*/
public static String createTableValueTypeName(String schemaName, String tblName) {
return createTableCacheName(schemaName, tblName) + "_" + UUID.randomUUID().toString().replace("-", "_");
}
/**
* Construct key type name for table.
*
* @param valTypeName Value type name.
* @return Key type name.
*/
public static String createTableKeyTypeName(String valTypeName) {
return valTypeName + "_KEY";
}
/**
* Copy query entity.
*
* @param entity Query entity.
* @return Copied entity.
*/
public static QueryEntity copy(QueryEntity entity) {
QueryEntity res;
if (entity instanceof QueryEntityEx)
res = new QueryEntityEx(entity);
else
res = new QueryEntity(entity);
return res;
}
/**
* Performs checks to forbid cache configurations that are not compatible with NOT NULL query fields.
* See {@link QueryEntity#setNotNullFields(Set)}.
*
* @param cfg Cache configuration.
*/
public static void checkNotNullAllowed(CacheConfiguration cfg) {
if (cfg.isReadThrough())
throw new IgniteSQLException("NOT NULL constraint is not supported when CacheConfiguration.readThrough " +
"is enabled.", IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
if (cfg.getInterceptor() != null)
throw new IgniteSQLException("NOT NULL constraint is not supported when CacheConfiguration.interceptor " +
"is set.", IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
}
/**
* Checks if given column can be removed from table using its {@link QueryEntity}.
*
* @param entity Query entity.
* @param fieldName Name of the field of the key or value object.
* @param colName Name of the column.
* @return {@code null} if it's OK to remove the column and exception otherwise.
*/
public static SchemaOperationException validateDropColumn(QueryEntity entity, String fieldName, String colName) {
if (F.eq(fieldName, entity.getKeyFieldName()) || KEY_FIELD_NAME.equalsIgnoreCase(fieldName))
return new SchemaOperationException("Cannot drop column \"" + colName +
"\" because it represents an entire cache key");
if (F.eq(fieldName, entity.getValueFieldName()) || VAL_FIELD_NAME.equalsIgnoreCase(fieldName))
return new SchemaOperationException("Cannot drop column \"" + colName +
"\" because it represents an entire cache value");
Set<String> keyFields = entity.getKeyFields();
if (keyFields != null && keyFields.contains(fieldName))
return new SchemaOperationException("Cannot drop column \"" + colName +
"\" because it is a part of a cache key");
Collection<QueryIndex> indexes = entity.getIndexes();
if (indexes != null) {
for (QueryIndex idxDesc : indexes) {
if (idxDesc.getFields().containsKey(fieldName))
return new SchemaOperationException("Cannot drop column \"" + colName +
"\" because an index exists (\"" + idxDesc.getName() + "\") that uses the column.");
}
}
return null;
}
/**
* Checks if given column can be removed from the table using its {@link GridQueryTypeDescriptor}.
*
* @param type Type descriptor.
* @param colName Name of the column.
* @return {@code null} if it's OK to remove the column and exception otherwise.
*/
public static SchemaOperationException validateDropColumn(GridQueryTypeDescriptor type, String colName) {
if (F.eq(colName, type.keyFieldName()) || KEY_FIELD_NAME.equalsIgnoreCase(colName))
return new SchemaOperationException("Cannot drop column \"" + colName +
"\" because it represents an entire cache key");
if (F.eq(colName, type.valueFieldName()) || VAL_FIELD_NAME.equalsIgnoreCase(colName))
return new SchemaOperationException("Cannot drop column \"" + colName +
"\" because it represents an entire cache value");
GridQueryProperty prop = type.property(colName);
if (prop != null && prop.key())
return new SchemaOperationException("Cannot drop column \"" + colName +
"\" because it is a part of a cache key");
Collection<GridQueryIndexDescriptor> indexes = type.indexes().values();
for (GridQueryIndexDescriptor idxDesc : indexes) {
if (idxDesc.fields().contains(colName))
return new SchemaOperationException("Cannot drop column \"" + colName +
"\" because an index exists (\"" + idxDesc.name() + "\") that uses the column.");
}
return null;
}
/**
* Private constructor.
*/
private QueryUtils() {
// No-op.
}
/** Property used for keyFieldName or valueFieldName */
public static class KeyOrValProperty implements GridQueryProperty {
/** */
boolean isKey;
/** */
String name;
/** */
Class<?> cls;
/** */
public KeyOrValProperty(boolean key, String name, Class<?> cls) {
this.isKey = key;
this.name = name;
this.cls = cls;
}
/** {@inheritDoc} */
@Override public Object value(Object key, Object val) throws IgniteCheckedException {
return isKey ? key : val;
}
/** {@inheritDoc} */
@Override public void setValue(Object key, Object val, Object propVal) throws IgniteCheckedException {
//No-op
}
/** {@inheritDoc} */
@Override public String name() {
return name;
}
/** {@inheritDoc} */
@Override public Class<?> type() {
return cls;
}
/** {@inheritDoc} */
@Override public boolean key() {
return isKey;
}
/** {@inheritDoc} */
@Override public GridQueryProperty parent() {
return null;
}
/** {@inheritDoc} */
@Override public boolean notNull() {
return true;
}
/** {@inheritDoc} */
@Override public Object defaultValue() {
return null;
}
/** {@inheritDoc} */
@Override public int precision() {
return -1;
}
/** {@inheritDoc} */
@Override public int scale() {
return -1;
}
}
}