/* * Licensed to CRATE Technology GmbH ("Crate") under one or more contributor * license agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. Crate 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. * * However, if you have executed another commercial license agreement * with Crate these terms will supersede the license and you may use the * software solely pursuant to the terms of the relevant commercial agreement. */ package io.crate.analyze; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import io.crate.analyze.ddl.GeoSettingsApplier; import io.crate.exceptions.InvalidColumnNameException; import io.crate.metadata.ColumnIdent; import io.crate.metadata.Reference; import io.crate.sql.tree.Expression; import io.crate.types.DataTypes; import org.elasticsearch.common.settings.Settings; import javax.annotation.Nullable; import java.util.*; public class AnalyzedColumnDefinition { private final static Set<String> UNSUPPORTED_PK_TYPES = Sets.newHashSet( DataTypes.OBJECT.getName(), DataTypes.GEO_POINT.getName(), DataTypes.GEO_SHAPE.getName() ); private final static Set<String> UNSUPPORTED_INDEX_TYPES = Sets.newHashSet( "array", DataTypes.OBJECT.getName(), DataTypes.GEO_POINT.getName(), DataTypes.GEO_SHAPE.getName() ); private final static Set<String> STRING_TYPES = ImmutableSet.of("string", "keyword", "text"); private final AnalyzedColumnDefinition parent; private ColumnIdent ident; private String name; private String dataType; private String collectionType; private Reference.IndexType indexType; private String geoTree; private String analyzer; @VisibleForTesting String objectType = "true"; // dynamic = true private boolean isPrimaryKey = false; private boolean isNotNull = false; private Settings analyzerSettings = Settings.EMPTY; private Settings geoSettings = Settings.EMPTY; private List<AnalyzedColumnDefinition> children = new ArrayList<>(); private boolean isIndex = false; private ArrayList<String> copyToTargets; private boolean isParentColumn; @Nullable private String formattedGeneratedExpression; @Nullable private Expression generatedExpression; public static void validateName(String name) { Preconditions.checkArgument(!name.startsWith("_"), "Column name must not start with '_'"); if (ColumnIdent.INVALID_COLUMN_NAME_PREDICATE.apply(name)) { throw new InvalidColumnNameException(name); } } AnalyzedColumnDefinition(@Nullable AnalyzedColumnDefinition parent) { this.parent = parent; } public void name(String name) { validateName(name); this.name = name; if (this.parent != null) { this.ident = ColumnIdent.getChild(this.parent.ident, name); } else { this.ident = new ColumnIdent(name); } } public void analyzer(String analyzer) { this.analyzer = analyzer; } void indexConstraint(Reference.IndexType indexType) { this.indexType = indexType; } @Nullable Reference.IndexType indexConstraint() { return indexType; } void geoTree(String geoTree) { this.geoTree = geoTree; } public void analyzerSettings(Settings settings) { this.analyzerSettings = settings; } void geoSettings(Settings settings) { this.geoSettings = settings; } public void dataType(String dataType) { switch (dataType) { case "timestamp": this.dataType = "date"; break; case "int": this.dataType = "integer"; break; default: this.dataType = dataType; } } public String dataType() { return this.dataType; } void objectType(String objectType) { this.objectType = objectType; } void collectionType(String type) { this.collectionType = type; } boolean isIndexColumn() { return isIndex; } void setAsIndexColumn() { this.isIndex = true; } void addChild(AnalyzedColumnDefinition analyzedColumnDefinition) { children.add(analyzedColumnDefinition); } boolean hasChildren() { return !children.isEmpty(); } public Settings analyzerSettings() { if (!children().isEmpty()) { Settings.Builder builder = Settings.builder(); builder.put(analyzerSettings); for (AnalyzedColumnDefinition child : children()) { builder.put(child.analyzerSettings()); } return builder.build(); } return analyzerSettings; } public void validate() { if (analyzer != null && !"not_analyzed".equals(analyzer) && !STRING_TYPES.contains(dataType)) { throw new IllegalArgumentException( String.format(Locale.ENGLISH, "Can't use an Analyzer on column %s because analyzers are only allowed on columns of type \"string\".", ident.sqlFqn() )); } if (indexType != null && UNSUPPORTED_INDEX_TYPES.contains(dataType)) { throw new IllegalArgumentException(String.format(Locale.ENGLISH, "INDEX constraint cannot be used on columns of type \"%s\"", dataType)); } if (hasPrimaryKeyConstraint()) { ensureTypeCanBeUsedAsKey(); } for (AnalyzedColumnDefinition child : children) { child.validate(); } } private void ensureTypeCanBeUsedAsKey() { if (collectionType != null) { throw new UnsupportedOperationException( String.format(Locale.ENGLISH, "Cannot use columns of type \"%s\" as primary key", collectionType)); } if (UNSUPPORTED_PK_TYPES.contains(dataType)) { throw new UnsupportedOperationException( String.format(Locale.ENGLISH, "Cannot use columns of type \"%s\" as primary key", dataType)); } if (isArrayOrInArray()) { throw new UnsupportedOperationException( String.format(Locale.ENGLISH, "Cannot use column \"%s\" as primary key within an array object", name)); } } public String name() { return name; } Map<String, Object> toMapping() { Map<String, Object> mapping = new HashMap<>(); String dataType = addTypeOptions(mapping); mapping.put("type", dataType); if (indexType == Reference.IndexType.NO) { mapping.put("index", "false"); } if (copyToTargets != null) { mapping.put("copy_to", copyToTargets); } if ("array".equals(collectionType)) { Map<String, Object> outerMapping = new HashMap<>(); outerMapping.put("type", "array"); if (dataType().equals("object")) { objectMapping(mapping); } outerMapping.put("inner", mapping); return outerMapping; } else if (dataType().equals("object")) { objectMapping(mapping); } return mapping; } /** * @return ES internal type name . * Usually this equals the crate type name, but for example string may become keyword or text */ private String addTypeOptions(Map<String, Object> mapping) { switch (dataType) { case "date": /* * We want 1000 not be be interpreted as year 1000AD but as 1970-01-01T00:00:01.000 * so prefer date mapping format epoch_millis over strict_date_optional_time */ mapping.put("format", "epoch_millis||strict_date_optional_time"); break; case "geo_shape": GeoSettingsApplier.applySettings(mapping, geoSettings, geoTree); break; case "string": if (analyzer == null) { return "keyword"; } mapping.put("analyzer", analyzer); return "text"; case "text": // explicit index definition if (analyzer != null) { mapping.put("analyzer", analyzer); } break; } return dataType; } private void objectMapping(Map<String, Object> mapping) { mapping.put("dynamic", objectType); Map<String, Object> childProperties = new HashMap<>(); for (AnalyzedColumnDefinition child : children) { childProperties.put(child.name(), child.toMapping()); } mapping.put("properties", childProperties); } public ColumnIdent ident() { return ident; } void setPrimaryKeyConstraint() { this.isPrimaryKey = true; } boolean hasPrimaryKeyConstraint() { return this.isPrimaryKey; } void setNotNullConstraint() { isNotNull = true; } boolean hasNotNullConstraint() { return isNotNull; } Map<String, Object> toMetaIndicesMapping() { return ImmutableMap.of(); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof AnalyzedColumnDefinition)) return false; AnalyzedColumnDefinition that = (AnalyzedColumnDefinition) o; return ident != null ? ident.equals(that.ident) : that.ident == null; } @Override public int hashCode() { return ident != null ? ident.hashCode() : 0; } @Override public String toString() { return MoreObjects.toStringHelper(this).add("ident", ident).toString(); } public List<AnalyzedColumnDefinition> children() { return children; } void addCopyTo(Set<String> targets) { this.copyToTargets = Lists.newArrayList(targets); } public void ident(ColumnIdent ident) { assert this.ident == null : "ident must be null"; this.ident = ident; } boolean isArrayOrInArray() { return collectionType != null || (parent != null && parent.isArrayOrInArray()); } void markAsParentColumn() { this.isParentColumn = true; } /** * @return true if this column has a defined child * (which is not coming from an object column definition payload in case of ADD COLUMN) */ boolean isParentColumn() { return isParentColumn; } public void formattedGeneratedExpression(String formattedGeneratedExpression) { this.formattedGeneratedExpression = formattedGeneratedExpression; } @Nullable public String formattedGeneratedExpression() { return formattedGeneratedExpression; } public void generatedExpression(Expression generatedExpression) { this.generatedExpression = generatedExpression; } @Nullable public Expression generatedExpression() { return generatedExpression; } }