/* * 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.db.view; import java.nio.ByteBuffer; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import javax.annotation.Nullable; import com.google.common.collect.Iterables; import org.apache.cassandra.cql3.*; import org.apache.cassandra.cql3.statements.ParsedStatement; import org.apache.cassandra.cql3.statements.SelectStatement; import org.apache.cassandra.db.*; import org.apache.cassandra.config.*; import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.partitions.*; import org.apache.cassandra.db.rows.*; import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.pager.QueryPager; import org.apache.cassandra.transport.Server; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.btree.BTreeSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A View copies data from a base table into a view table which can be queried independently from the * base. Every update which targets the base table must be fed through the {@link ViewManager} to ensure * that if a view needs to be updated, the updates are properly created and fed into the view. */ public class View { private static final Logger logger = LoggerFactory.getLogger(View.class); public final String name; private volatile ViewDefinition definition; private final ColumnFamilyStore baseCfs; public volatile List<ColumnDefinition> baseNonPKColumnsInViewPK; private final boolean includeAllColumns; private ViewBuilder builder; // Only the raw statement can be final, because the statement cannot always be prepared when the MV is initialized. // For example, during startup, this view will be initialized as part of the Keyspace.open() work; preparing a statement // also requires the keyspace to be open, so this results in double-initialization problems. private final SelectStatement.RawStatement rawSelect; private SelectStatement select; private ReadQuery query; public View(ViewDefinition definition, ColumnFamilyStore baseCfs) { this.baseCfs = baseCfs; this.name = definition.viewName; this.includeAllColumns = definition.includeAllColumns; this.rawSelect = definition.select; updateDefinition(definition); } public ViewDefinition getDefinition() { return definition; } /** * This updates the columns stored which are dependent on the base CFMetaData. * * @return true if the view contains only columns which are part of the base's primary key; false if there is at * least one column which is not. */ public void updateDefinition(ViewDefinition definition) { this.definition = definition; CFMetaData viewCfm = definition.metadata; List<ColumnDefinition> nonPKDefPartOfViewPK = new ArrayList<>(); for (ColumnDefinition baseColumn : baseCfs.metadata.allColumns()) { ColumnDefinition viewColumn = getViewColumn(baseColumn); if (viewColumn != null && !baseColumn.isPrimaryKeyColumn() && viewColumn.isPrimaryKeyColumn()) nonPKDefPartOfViewPK.add(baseColumn); } this.baseNonPKColumnsInViewPK = nonPKDefPartOfViewPK; } /** * The view column corresponding to the provided base column. This <b>can</b> * return {@code null} if the column is denormalized in the view. */ public ColumnDefinition getViewColumn(ColumnDefinition baseColumn) { return definition.metadata.getColumnDefinition(baseColumn.name); } /** * The base column corresponding to the provided view column. This should * never return {@code null} since a view can't have its "own" columns. */ public ColumnDefinition getBaseColumn(ColumnDefinition viewColumn) { ColumnDefinition baseColumn = baseCfs.metadata.getColumnDefinition(viewColumn.name); assert baseColumn != null; return baseColumn; } /** * Whether the view might be affected by the provided update. * <p> * Note that having this method return {@code true} is not an absolute guarantee that the view will be * updated, just that it most likely will, but a {@code false} return guarantees it won't be affected). * * @param partitionKey the partition key that is updated. * @param update the update being applied. * @return {@code false} if we can guarantee that inserting {@code update} for key {@code partitionKey} * won't affect the view in any way, {@code true} otherwise. */ public boolean mayBeAffectedBy(DecoratedKey partitionKey, Row update) { // We can guarantee that the view won't be affected if: // - the clustering is excluded by the view filter (note that this isn't true of the filter on regular columns: // even if an update don't match a view condition on a regular column, that update can still invalidate an pre-existing // entry). // - or the update don't modify any of the columns impacting the view (where "impacting" the view means that column is // neither included in the view, nor used by the view filter). if (!getReadQuery().selectsClustering(partitionKey, update.clustering())) return false; // We want to find if the update modify any of the columns that are part of the view (in which case the view is affected). // But if the view include all the base table columns, or the update has either a row deletion or a row liveness (note // that for the liveness, it would be more "precise" to check if it's live, but pushing an update that is already expired // is dump so it's ok not to optimize for it and it saves us from having to pass nowInSec to the method), we know the view // is affected right away. if (includeAllColumns || !update.deletion().isLive() || !update.primaryKeyLivenessInfo().isEmpty()) return true; for (ColumnData data : update) { if (definition.metadata.getColumnDefinition(data.column().name) != null) return true; } return false; } /** * Whether a given base row matches the view filter (and thus if is should have a corresponding entry). * <p> * Note that this differs from {@link #mayBeAffectedBy} in that the provide row <b>must</b> be the current * state of the base row, not just some updates to it. This method also has no false positive: a base * row either do or don't match the view filter. * * @param partitionKey the partition key that is updated. * @param baseRow the current state of a particular base row. * @param nowInSec the current time in seconds (to decide what is live and what isn't). * @return {@code true} if {@code baseRow} matches the view filters, {@code false} otherwise. */ public boolean matchesViewFilter(DecoratedKey partitionKey, Row baseRow, int nowInSec) { return getReadQuery().selectsClustering(partitionKey, baseRow.clustering()) && getSelectStatement().rowFilterForInternalCalls().isSatisfiedBy(baseCfs.metadata, partitionKey, baseRow, nowInSec); } /** * Returns the SelectStatement used to populate and filter this view. Internal users should access the select * statement this way to ensure it has been prepared. */ public SelectStatement getSelectStatement() { if (select == null) { ClientState state = ClientState.forInternalCalls(); state.setKeyspace(baseCfs.keyspace.getName()); rawSelect.prepareKeyspace(state); ParsedStatement.Prepared prepared = rawSelect.prepare(true); select = (SelectStatement) prepared.statement; } return select; } /** * Returns the ReadQuery used to filter this view. Internal users should access the query this way to ensure it * has been prepared. */ public ReadQuery getReadQuery() { if (query == null) query = getSelectStatement().getQuery(QueryOptions.forInternalCalls(Collections.emptyList()), FBUtilities.nowInSeconds()); return query; } public synchronized void build() { if (this.builder != null) { this.builder.stop(); this.builder = null; } this.builder = new ViewBuilder(baseCfs, this); CompactionManager.instance.submitViewBuilder(builder); } @Nullable public static CFMetaData findBaseTable(String keyspace, String viewName) { ViewDefinition view = Schema.instance.getView(keyspace, viewName); return (view == null) ? null : Schema.instance.getCFMetaData(view.baseTableId); } public static Iterable<ViewDefinition> findAll(String keyspace, String baseTable) { KeyspaceMetadata ksm = Schema.instance.getKSMetaData(keyspace); final UUID baseId = Schema.instance.getId(keyspace, baseTable); return Iterables.filter(ksm.views, view -> view.baseTableId.equals(baseId)); } /** * Builds the string text for a materialized view's SELECT statement. */ public static String buildSelectStatement(String cfName, Collection<ColumnDefinition> includedColumns, String whereClause) { StringBuilder rawSelect = new StringBuilder("SELECT "); if (includedColumns == null || includedColumns.isEmpty()) rawSelect.append("*"); else rawSelect.append(includedColumns.stream().map(id -> id.name.toCQLString()).collect(Collectors.joining(", "))); rawSelect.append(" FROM \"").append(cfName).append("\" WHERE ") .append(whereClause).append(" ALLOW FILTERING"); return rawSelect.toString(); } public static String relationsToWhereClause(List<Relation> whereClause) { List<String> expressions = new ArrayList<>(whereClause.size()); for (Relation rel : whereClause) { StringBuilder sb = new StringBuilder(); if (rel.isMultiColumn()) { sb.append(((MultiColumnRelation) rel).getEntities().stream() .map(ColumnIdentifier.Raw::toCQLString) .collect(Collectors.joining(", ", "(", ")"))); } else { sb.append(((SingleColumnRelation) rel).getEntity().toCQLString()); } sb.append(" ").append(rel.operator()).append(" "); if (rel.isIN()) { sb.append(rel.getInValues().stream() .map(Term.Raw::getText) .collect(Collectors.joining(", ", "(", ")"))); } else { sb.append(rel.getValue().getText()); } expressions.add(sb.toString()); } return expressions.stream().collect(Collectors.joining(" AND ")); } }