content: /*
   * 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.statements;
  
  import java.nio.ByteBuffer;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.List;
  
  import com.google.common.base.Preconditions;
  import org.apache.commons.lang3.builder.ToStringBuilder;
  import org.apache.commons.lang3.builder.ToStringStyle;
  
  import org.apache.cassandra.audit.AuditLogContext;
  import org.apache.cassandra.audit.AuditLogEntryType;
  import org.apache.cassandra.cql3.Attributes;
  import org.apache.cassandra.cql3.ColumnIdentifier;
  import org.apache.cassandra.cql3.Relation;
  import org.apache.cassandra.cql3.terms.Constants;
  import org.apache.cassandra.cql3.Json;
  import org.apache.cassandra.cql3.Operation;
  import org.apache.cassandra.cql3.Operations;
  import org.apache.cassandra.cql3.Operator;
  import org.apache.cassandra.cql3.QualifiedName;
  import org.apache.cassandra.cql3.StatementSource;
  import org.apache.cassandra.cql3.terms.Term;
  import org.apache.cassandra.cql3.UpdateParameters;
  import org.apache.cassandra.cql3.VariableSpecifications;
  import org.apache.cassandra.cql3.WhereClause;
  import org.apache.cassandra.cql3.conditions.ColumnCondition;
  import org.apache.cassandra.cql3.conditions.Conditions;
  import org.apache.cassandra.cql3.constraints.ColumnConstraint;
  import org.apache.cassandra.cql3.restrictions.StatementRestrictions;
  import org.apache.cassandra.cql3.transactions.ReferenceOperation;
  import org.apache.cassandra.cql3.transactions.ReferenceValue;
  import org.apache.cassandra.db.Clustering;
  import org.apache.cassandra.db.Slice;
  import org.apache.cassandra.db.filter.IndexHints;
  import org.apache.cassandra.db.partitions.PartitionUpdate;
  import org.apache.cassandra.db.rows.Cell;
  import org.apache.cassandra.db.rows.Row;
  import org.apache.cassandra.schema.ColumnMetadata;
  import org.apache.cassandra.schema.TableMetadata;
  import org.apache.cassandra.service.ClientState;
  import org.apache.cassandra.service.accord.txn.TxnReferenceOperation;
  import org.apache.cassandra.utils.ByteBufferUtil;
  import org.apache.cassandra.utils.Pair;
  
  import static org.apache.cassandra.cql3.statements.RequestValidations.checkContainsNoDuplicates;
  import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse;
  
  /**
   * An <code>UPDATE</code> statement parsed from a CQL query statement.
   *
   */
  public class UpdateStatement extends ModificationStatement
  {
      public static final String UPDATING_PRIMARY_KEY_MESSAGE = "PRIMARY KEY part %s found in SET part";
      public static final String CANNOT_SET_KEY_WITH_REFERENCE_MESSAGE = "Value reference %s cannot be used to insert PRIMARY KEY column %s";
  
      private static final Constants.Value EMPTY = new Constants.Value(ByteBufferUtil.EMPTY_BYTE_BUFFER);
  
      private UpdateStatement(StatementType type,
                              VariableSpecifications bindVariables,
                              TableMetadata metadata,
                              Operations operations,
                              StatementRestrictions restrictions,
                              Conditions conditions,
                              Attributes attrs,
                              StatementSource source)
      {
          super(type, bindVariables, metadata, operations, restrictions, conditions, attrs, source);
      }
  
      @Override
      protected ModificationStatement withOperations(Operations operations)
      {
          return new UpdateStatement(type, bindVariables, metadata, operations, restrictions, conditions, attrs, source);
      }
  
      @Override
      public void addUpdateForKey(PartitionUpdate.Builder updateBuilder, Clustering<?> clustering, UpdateParameters params)
      {
          if (updatesRegularRows())
          {
              params.newRow(clustering);
  
              // We update the row timestamp only on INSERT (#6782)
              // Further, COMPACT tables semantic differs from "CQL3" ones in that a row exists only if it has
              // a non-null column, so we don't want to set the row timestamp for them.
              if (type.isInsert() && !metadata.isCompactTable())
                  params.addPrimaryKeyLivenessInfo();
  
              List<Operation> updates = getRegularOperations();
  
              // For compact table, we don't accept an insert/update that only sets the PK unless the is no
              // declared non-PK columns (which we recognize because in that case
              // the compact value is of type "EmptyType").
              if (metadata().isCompactTable() && updates.isEmpty())
              {
                  TableMetadata.CompactTableMetadata metadata = (TableMetadata.CompactTableMetadata) metadata();
                  RequestValidations.checkTrue(metadata.hasEmptyCompactValue(),
                                               "Column %s is mandatory for this COMPACT STORAGE table",
                                               metadata.compactValueColumn);
  
                  updates = Collections.singletonList(new Constants.Setter(metadata.compactValueColumn, EMPTY));
              }
  
              for (int i = 0, isize = updates.size(); i < isize; i++)
                  updates.get(i).execute(updateBuilder.partitionKey(), params);
  
              Row row = params.buildRow();
              evaluateConstraintsForRow(row, metadata);
              updateBuilder.add(row);
          }
  
          if (updatesStaticRow())
          {
              params.newRow(Clustering.STATIC_CLUSTERING);
              List<Operation> staticOps = getStaticOperations();
              for (int i = 0, isize = staticOps.size(); i < isize; i++)
                  staticOps.get(i).execute(updateBuilder.partitionKey(), params);
              updateBuilder.add(params.buildRow());
          }
      }
  
      @Override
      public void addUpdateForKey(PartitionUpdate.Builder update, Slice slice, UpdateParameters params)
      {
          throw new UnsupportedOperationException();
      }
  
      public static class ParsedInsert extends ModificationStatement.Parsed
      {
          private final List<ColumnIdentifier> columnNames;
          private final List<Term.Raw> columnValues;
          private final boolean isForTxn;
  
          /**
           * A parsed <code>INSERT</code> statement.
           *
           * @param name column family being operated on
           * @param attrs additional attributes for statement (CL, timestamp, timeToLive)
           * @param columnNames list of column names
           * @param columnValues list of column values (corresponds to names)
           * @param ifNotExists true if an IF NOT EXISTS condition was specified, false otherwise
           */
          public ParsedInsert(QualifiedName name,
                              Attributes.Raw attrs,
                              List<ColumnIdentifier> columnNames,
                              List<Term.Raw> columnValues,
                              boolean ifNotExists,
                              StatementSource source,
                              boolean isForTxn)
          {
              super(name, StatementType.INSERT, attrs, null, ifNotExists, false, source);
              this.columnNames = columnNames;
              this.columnValues = columnValues;
              this.isForTxn = isForTxn;
          }
  
          @Override
          protected ModificationStatement prepareInternal(ClientState state,
                                                          TableMetadata metadata,
                                                          VariableSpecifications bindVariables,
                                                          Conditions conditions,
                                                          Attributes attrs)
          {
  
              // Created from an INSERT
              checkFalse(metadata.isCounter(), "INSERT statements are not allowed on counter tables, use UPDATE instead");
  
              checkFalse(columnNames == null, "Column names for INSERT must be provided when using VALUES");
              checkFalse(columnNames.isEmpty(), "No columns provided to INSERT");
              checkFalse(columnNames.size() != columnValues.size(), "Unmatched column names/values");
              checkContainsNoDuplicates(columnNames, "The column names contains duplicates");
  
              WhereClause.Builder whereClause = new WhereClause.Builder();
              Operations operations = new Operations(type, isForTxn);
              boolean hasClusteringColumnsSet = false;
  
              for (int i = 0; i < columnNames.size(); i++)
              {
                  ColumnMetadata def = metadata.getExistingColumn(columnNames.get(i));
  
                  if (def.isClusteringColumn())
                      hasClusteringColumnsSet = true;
  
                  Term.Raw value = columnValues.get(i);
  
                  if (def.isPrimaryKeyColumn())
                  {
                      checkFalse(value instanceof ReferenceValue.Raw, String.format(CANNOT_SET_KEY_WITH_REFERENCE_MESSAGE, value, def));
                      whereClause.add(Relation.singleColumn(columnNames.get(i), Operator.EQ, value));
                  }
                  else if (value instanceof ReferenceValue.Raw)
                  {
                      ReferenceValue.Raw raw = (ReferenceValue.Raw) value;
                      ReferenceValue referenceValue = raw.prepare(def, bindVariables);
                      ReferenceOperation operation = new ReferenceOperation(def, metadata, TxnReferenceOperation.Kind.setterFor(def), null, null, referenceValue);
                      operations.add(def, operation);
                  }
                  else
                  {
                      Operation operation = new Operation.SetValue(value).prepare(metadata, def, !conditions.isEmpty());
                      operation.collectMarkerSpecification(bindVariables);
                      operations.add(operation, metadata);
                  }
              }
  
              boolean applyOnlyToStaticColumns = !hasClusteringColumnsSet && appliesOnlyToStaticColumns(operations, conditions);
  
              StatementRestrictions restrictions = new StatementRestrictions(state,
                                                                             type,
                                                                             metadata,
                                                                             IndexHints.NONE,
                                                                             whereClause.build(),
                                                                             bindVariables,
                                                                             Collections.emptyList(),
                                                                             applyOnlyToStaticColumns,
                                                                             false,
                                                                             false);
  
              return new UpdateStatement(type,
                                         bindVariables,
                                         metadata,
                                         operations,
                                         restrictions,
                                         conditions,
                                         attrs,
                                         source);
          }
      }
  
      /**
       * A parsed INSERT JSON statement.
       */
      public static class ParsedInsertJson extends ModificationStatement.Parsed
      {
          private final Json.Raw jsonValue;
          private final boolean defaultUnset;
          private final boolean isForTxn;
  
          public ParsedInsertJson(QualifiedName name, Attributes.Raw attrs, Json.Raw jsonValue, boolean defaultUnset, boolean ifNotExists, StatementSource source, boolean isForTxn)
          {
              super(name, StatementType.INSERT, attrs, null, ifNotExists, false, source);
              this.jsonValue = jsonValue;
              this.defaultUnset = defaultUnset;
              this.isForTxn = isForTxn;
          }
  
          @Override
          protected ModificationStatement prepareInternal(ClientState state,
                                                          TableMetadata metadata,
                                                          VariableSpecifications bindVariables,
                                                          Conditions conditions,
                                                          Attributes attrs)
          {
              checkFalse(metadata.isCounter(), "INSERT statements are not allowed on counter tables, use UPDATE instead");
  
              Collection<ColumnMetadata> defs = metadata.columns();
              Json.Prepared prepared = jsonValue.prepareAndCollectMarkers(metadata, defs, bindVariables);
  
              WhereClause.Builder whereClause = new WhereClause.Builder();
              Operations operations = new Operations(type, isForTxn);
              boolean hasClusteringColumnsSet = false;
  
              for (ColumnMetadata def : defs)
              {
                  if (def.isClusteringColumn())
                      hasClusteringColumnsSet = true;
  
                  Term.Raw raw = prepared.getRawTermForColumn(def, defaultUnset);
                  if (def.isPrimaryKeyColumn())
                  {
                      whereClause.add(Relation.singleColumn(def.name, Operator.EQ, raw));
                  }
                  else
                  {
                      Operation operation = new Operation.SetValue(raw).prepare(metadata, def, !conditions.isEmpty());
                      operation.collectMarkerSpecification(bindVariables);
                      operations.add(operation, metadata);
                  }
              }
  
              boolean applyOnlyToStaticColumns = !hasClusteringColumnsSet && appliesOnlyToStaticColumns(operations, conditions);
  
              StatementRestrictions restrictions = new StatementRestrictions(state,
                                                                             type,
                                                                             metadata,
                                                                             IndexHints.NONE,
                                                                             whereClause.build(),
                                                                             bindVariables,
                                                                             Collections.emptyList(),
                                                                             applyOnlyToStaticColumns,
                                                                             false,
                                                                             false);
  
              return new UpdateStatement(type,
                                         bindVariables,
                                         metadata,
                                         operations,
                                         restrictions,
                                         conditions,
                                         attrs,
                                         source);
          }
      }
  
      public static class OperationCollector
      {
          public final List<Pair<ColumnIdentifier, Operation.RawUpdate>> operations = new ArrayList<>();
          public final List<Pair<ColumnIdentifier, ReferenceOperation.Raw>> referenceOps = new ArrayList<>();
  
          public boolean conflictsWithExistingUpdate(ColumnIdentifier column, Operation.RawUpdate update)
          {
              for (Pair<ColumnIdentifier, Operation.RawUpdate> p : operations)
              {
                  if (p.left.equals(column) && !p.right.isCompatibleWith(update))
                      return true;
              }
              return false;
          }
  
          public boolean conflictsWithExistingSubstitution(ColumnIdentifier column)
          {
              for (Pair<ColumnIdentifier, ReferenceOperation.Raw> p : referenceOps)
              {
                  if (p.left.equals(column))
                      return true;
              }
              return false;
          }
  
          public void addRawUpdate(ColumnIdentifier column, Operation.RawUpdate update)
          {
              operations.add(Pair.create(column, update));
          }
  
          public boolean conflictsWithExistingUpdate(ColumnIdentifier column)
          {
              for (Pair<ColumnIdentifier, Operation.RawUpdate> p : operations)
              {
                  if (p.left.equals(column))
                      return true;
              }
              return false;
          }
  
          public void addRawReferenceOperation(ColumnIdentifier column, ReferenceOperation.Raw substitution)
          {
              // TODO: Make sure there's more than a tuple name here...i.e. an actual reference column?
              referenceOps.add(Pair.create(column, substitution));
          }
      }
  
      public static class ParsedUpdate extends ModificationStatement.Parsed
      {
          // Provided for an UPDATE
          private final OperationCollector updates;
          private final WhereClause whereClause;
          private final boolean isForTxn;
  
          /**
           * Creates a new UpdateStatement from a column family name, columns map, consistency
           * level, and key term.
           *
           * @param name column family being operated on
           * @param attrs additional attributes for statement (timestamp, timeToLive)
           * @param updates a map of column operations to perform
           * @param whereClause the where clause
           * @param ifExists flag to check if row exists
           * */
          public ParsedUpdate(QualifiedName name,
                              Attributes.Raw attrs,
                              OperationCollector updates,
                              WhereClause whereClause,
                              List<ColumnCondition.Raw> conditions,
                              boolean ifExists,
                              boolean isForTxn,
                              StatementSource source)
          {
              super(name, StatementType.UPDATE, attrs, conditions, false, ifExists, source);
              this.updates = updates;
              this.whereClause = whereClause;
              this.isForTxn = isForTxn;
          }
  
          @Override
          protected ModificationStatement prepareInternal(ClientState state,
                                                          TableMetadata metadata,
                                                          VariableSpecifications bindVariables,
                                                          Conditions conditions,
                                                          Attributes attrs)
          {
              Operations operations = new Operations(type, isForTxn);
  
              for (Pair<ColumnIdentifier, Operation.RawUpdate> entry : updates.operations)
              {
                  ColumnMetadata def = metadata.getExistingColumn(entry.left);
                  checkFalse(def.isPrimaryKeyColumn(), UPDATING_PRIMARY_KEY_MESSAGE, def.name);
                  Operation operation = entry.right.prepare(metadata, def, !conditions.isEmpty() || isForTxn);
                  operation.collectMarkerSpecification(bindVariables);
                  operations.add(operation, metadata);
              }
  
              Preconditions.checkState(updates.referenceOps.isEmpty() || isForTxn);
              for (Pair<ColumnIdentifier, ReferenceOperation.Raw> entry : updates.referenceOps)
              {
                  ColumnMetadata def = metadata.getExistingColumn(entry.left);
                  checkFalse(def.isPrimaryKeyColumn(), UPDATING_PRIMARY_KEY_MESSAGE, def.name);
                  ReferenceOperation operation = entry.right.prepare(metadata, bindVariables);
                  operations.add(def, operation);
              }
  
              StatementRestrictions restrictions = newRestrictions(state,
                                                                   metadata,
                                                                   bindVariables,
                                                                   operations,
                                                                   whereClause,
                                                                   conditions);
  
              return new UpdateStatement(type,
                                         bindVariables,
                                         metadata,
                                         operations,
                                         restrictions,
                                         conditions,
                                         attrs,
                                         source);
          }
      }
      
      @Override
      public String toString()
      {
          return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
      }
  
      @Override
      public AuditLogContext getAuditLogContext()
      {
          return new AuditLogContext(AuditLogEntryType.UPDATE, keyspace(), table());
      }
  
      public static void evaluateConstraintsForRow(Row row, TableMetadata metadata)
      {
          for (ColumnMetadata column : metadata.columnsWithConstraints)
          {
              Cell<?> cell = row.getCell(column);
              if (cell != null)
              {
                  ColumnMetadata columnMetadata = cell.column();
                  ByteBuffer cellData = cell.buffer();
                  evaluateConstraint(columnMetadata, cellData);
              }
          }
      }
  
      public static void evaluateConstraint(ColumnMetadata columnMetadata, ByteBuffer cellData)
      {
          for (ColumnConstraint<?> constraint : columnMetadata.getColumnConstraints().getConstraints())
              constraint.evaluate(columnMetadata.type, cellData);
      }
  }
  