/* * 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.cassandra.cql3; import java.nio.ByteBuffer; import java.util.*; import java.util.stream.Collectors; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.marshal.*; import org.apache.cassandra.db.rows.CellPath; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.ByteBufferUtil; import static org.apache.cassandra.cql3.Constants.UNSET_VALUE; /** * Static helper methods and classes for user types. */ public abstract class UserTypes { private UserTypes() {} public static ColumnSpecification fieldSpecOf(ColumnSpecification column, int field) { UserType ut = (UserType)column.type; return new ColumnSpecification(column.ksName, column.cfName, new ColumnIdentifier(column.name + "." + ut.fieldName(field), true), ut.fieldType(field)); } public static <T extends AssignmentTestable> void validateUserTypeAssignableTo(ColumnSpecification receiver, Map<FieldIdentifier, T> entries) { if (!receiver.type.isUDT()) throw new InvalidRequestException(String.format("Invalid user type literal for %s of type %s", receiver, receiver.type.asCQL3Type())); UserType ut = (UserType) receiver.type; for (int i = 0; i < ut.size(); i++) { FieldIdentifier field = ut.fieldName(i); T value = entries.get(field); if (value == null) continue; ColumnSpecification fieldSpec = fieldSpecOf(receiver, i); if (!value.testAssignment(receiver.ksName, fieldSpec).isAssignable()) { throw new InvalidRequestException(String.format("Invalid user type literal for %s: field %s is not of type %s", receiver, field, fieldSpec.type.asCQL3Type())); } } } /** * Tests that the map with the specified entries can be assigned to the specified column. * * @param receiver the receiving column * @param entries the map entries */ public static <T extends AssignmentTestable> AssignmentTestable.TestResult testUserTypeAssignment(ColumnSpecification receiver, Map<FieldIdentifier, T> entries) { try { validateUserTypeAssignableTo(receiver, entries); return AssignmentTestable.TestResult.WEAKLY_ASSIGNABLE; } catch (InvalidRequestException e) { return AssignmentTestable.TestResult.NOT_ASSIGNABLE; } } /** * Create a {@code String} representation of the user type from the specified items associated to * the user type entries. * * @param items items associated to the user type entries * @return a {@code String} representation of the user type */ public static <T> String userTypeToString(Map<FieldIdentifier, T> items) { return userTypeToString(items, Object::toString); } /** * Create a {@code String} representation of the user type from the specified items associated to * the user type entries. * * @param items items associated to the user type entries * @return a {@code String} representation of the user type */ public static <T> String userTypeToString(Map<FieldIdentifier, T> items, java.util.function.Function<T, String> mapper) { return items.entrySet() .stream() .map(p -> String.format("%s: %s", p.getKey(), mapper.apply(p.getValue()))) .collect(Collectors.joining(", ", "{", "}")); } public static class Literal extends Term.Raw { public final Map<FieldIdentifier, Term.Raw> entries; public Literal(Map<FieldIdentifier, Term.Raw> entries) { this.entries = entries; } public Term prepare(String keyspace, ColumnSpecification receiver) throws InvalidRequestException { validateAssignableTo(keyspace, receiver); UserType ut = (UserType)receiver.type; boolean allTerminal = true; List<Term> values = new ArrayList<>(entries.size()); int foundValues = 0; for (int i = 0; i < ut.size(); i++) { FieldIdentifier field = ut.fieldName(i); Term.Raw raw = entries.get(field); if (raw == null) raw = Constants.NULL_LITERAL; else ++foundValues; Term value = raw.prepare(keyspace, fieldSpecOf(receiver, i)); if (value instanceof Term.NonTerminal) allTerminal = false; values.add(value); } if (foundValues != entries.size()) { // We had some field that are not part of the type for (FieldIdentifier id : entries.keySet()) { if (!ut.fieldNames().contains(id)) throw new InvalidRequestException(String.format("Unknown field '%s' in value of user defined type %s", id, ut.getNameAsString())); } } DelayedValue value = new DelayedValue(((UserType)receiver.type), values); return allTerminal ? value.bind(QueryOptions.DEFAULT) : value; } private void validateAssignableTo(String keyspace, ColumnSpecification receiver) throws InvalidRequestException { validateUserTypeAssignableTo(receiver, entries); } public AssignmentTestable.TestResult testAssignment(String keyspace, ColumnSpecification receiver) { return testUserTypeAssignment(receiver, entries); } public AbstractType<?> getExactTypeIfKnown(String keyspace) { return null; } public String getText() { return userTypeToString(entries, Term.Raw::getText); } } public static class Value extends Term.MultiItemTerminal { private final UserType type; public final ByteBuffer[] elements; public Value(UserType type, ByteBuffer[] elements) { this.type = type; this.elements = elements; } public static Value fromSerialized(ByteBuffer bytes, UserType type) { ByteBuffer[] values = type.split(bytes); if (values.length > type.size()) { throw new InvalidRequestException(String.format( "UDT value contained too many fields (expected %s, got %s)", type.size(), values.length)); } return new Value(type, type.split(bytes)); } public ByteBuffer get(ProtocolVersion protocolVersion) { return TupleType.buildValue(elements); } public boolean equals(UserType userType, Value v) { if (elements.length != v.elements.length) return false; for (int i = 0; i < elements.length; i++) if (userType.fieldType(i).compare(elements[i], v.elements[i]) != 0) return false; return true; } public List<ByteBuffer> getElements() { return Arrays.asList(elements); } } public static class DelayedValue extends Term.NonTerminal { private final UserType type; private final List<Term> values; public DelayedValue(UserType type, List<Term> values) { this.type = type; this.values = values; } public void addFunctionsTo(List<Function> functions) { Terms.addFunctions(values, functions); } public boolean containsBindMarker() { for (Term t : values) if (t.containsBindMarker()) return true; return false; } public void collectMarkerSpecification(VariableSpecifications boundNames) { for (int i = 0; i < type.size(); i++) values.get(i).collectMarkerSpecification(boundNames); } private ByteBuffer[] bindInternal(QueryOptions options) throws InvalidRequestException { if (values.size() > type.size()) { throw new InvalidRequestException(String.format( "UDT value contained too many fields (expected %s, got %s)", type.size(), values.size())); } ByteBuffer[] buffers = new ByteBuffer[values.size()]; for (int i = 0; i < type.size(); i++) { buffers[i] = values.get(i).bindAndGet(options); // Since a frozen UDT value is always written in its entirety Cassandra can't preserve a pre-existing // value by 'not setting' the new value. Reject the query. if (!type.isMultiCell() && buffers[i] == ByteBufferUtil.UNSET_BYTE_BUFFER) throw new InvalidRequestException(String.format("Invalid unset value for field '%s' of user defined type %s", type.fieldNameAsString(i), type.getNameAsString())); } return buffers; } public Value bind(QueryOptions options) throws InvalidRequestException { return new Value(type, bindInternal(options)); } @Override public ByteBuffer bindAndGet(QueryOptions options) throws InvalidRequestException { return UserType.buildValue(bindInternal(options)); } } public static class Marker extends AbstractMarker { protected Marker(int bindIndex, ColumnSpecification receiver) { super(bindIndex, receiver); assert receiver.type.isUDT(); } public Terminal bind(QueryOptions options) throws InvalidRequestException { ByteBuffer value = options.getValues().get(bindIndex); if (value == null) return null; if (value == ByteBufferUtil.UNSET_BYTE_BUFFER) return UNSET_VALUE; return Value.fromSerialized(value, (UserType) receiver.type); } } public static class Setter extends Operation { public Setter(ColumnMetadata column, Term t) { super(column, t); } public void execute(DecoratedKey partitionKey, UpdateParameters params) throws InvalidRequestException { Term.Terminal value = t.bind(params.options); if (value == UNSET_VALUE) return; Value userTypeValue = (Value) value; if (column.type.isMultiCell()) { // setting a whole UDT at once means we overwrite all cells, so delete existing cells params.setComplexDeletionTimeForOverwrite(column); if (value == null) return; Iterator<FieldIdentifier> fieldNameIter = userTypeValue.type.fieldNames().iterator(); for (ByteBuffer buffer : userTypeValue.elements) { assert fieldNameIter.hasNext(); FieldIdentifier fieldName = fieldNameIter.next(); if (buffer == null) continue; CellPath fieldPath = userTypeValue.type.cellPathForField(fieldName); params.addCell(column, fieldPath, buffer); } } else { // for frozen UDTs, we're overwriting the whole cell value if (value == null) params.addTombstone(column); else params.addCell(column, value.get(params.options.getProtocolVersion())); } } } public static class SetterByField extends Operation { private final FieldIdentifier field; public SetterByField(ColumnMetadata column, FieldIdentifier field, Term t) { super(column, t); this.field = field; } public void execute(DecoratedKey partitionKey, UpdateParameters params) throws InvalidRequestException { // we should not get here for frozen UDTs assert column.type.isMultiCell() : "Attempted to set an individual field on a frozen UDT"; Term.Terminal value = t.bind(params.options); if (value == UNSET_VALUE) return; CellPath fieldPath = ((UserType) column.type).cellPathForField(field); if (value == null) params.addTombstone(column, fieldPath); else params.addCell(column, fieldPath, value.get(params.options.getProtocolVersion())); } } public static class DeleterByField extends Operation { private final FieldIdentifier field; public DeleterByField(ColumnMetadata column, FieldIdentifier field) { super(column, null); this.field = field; } public void execute(DecoratedKey partitionKey, UpdateParameters params) throws InvalidRequestException { // we should not get here for frozen UDTs assert column.type.isMultiCell() : "Attempted to delete a single field from a frozen UDT"; CellPath fieldPath = ((UserType) column.type).cellPathForField(field); params.addTombstone(column, fieldPath); } } }