Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@
UNKNOWN_DATABASE("42F63"),
UNION_INCORRECT_COLUMN_COUNT("42F64"),
UNION_INCOMPATIBLE_COLUMNS("42F65"),
INVALID_DATABASE("42F66"),

Check notice on line 154 in fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/exceptions/ErrorCode.java

View workflow job for this annotation

GitHub Actions / coverage

File coverage: 98.8% (84/85 lines) | Changed lines: 100.0% (1/1 lines)
VIEW_NOT_UPDATABLE("42F67"),
// Class 53 - Insufficient Resources
TRANSACTION_TIMEOUT("53F00"),
// Class 54 Program Limit Exceeded
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
import com.apple.foundationdb.record.query.plan.cascades.expressions.LogicalUnionExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.SelectExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.TempTableInsertExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.TempTableScanExpression;

Check notice on line 42 in fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/LogicalOperator.java

View workflow job for this annotation

GitHub Actions / coverage

File coverage: 94.9% (282/297 lines) | Changed lines: 100.0% (2/2 lines)
import com.apple.foundationdb.record.query.plan.cascades.predicates.QueryPredicate;
import com.apple.foundationdb.record.query.plan.cascades.typing.PseudoField;
import com.apple.foundationdb.record.query.plan.cascades.typing.Type;
import com.apple.foundationdb.record.query.plan.cascades.values.CountValue;
Expand Down Expand Up @@ -464,12 +465,24 @@
@Nonnull Optional<Identifier> alias,
@Nonnull Set<CorrelationIdentifier> outerCorrelations,
boolean isForDdl) {
return generateSimpleSelect(output, logicalOperators, where, alias, outerCorrelations, ImmutableList.of(), isForDdl);

Check warning on line 468 in fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/LogicalOperator.java

View check run for this annotation

fdb.teamscale.io / Teamscale | Findings

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/LogicalOperator.java#L468

Use `java.util.List.of` instead https://fdb.teamscale.io/findings/details/foundationdb-fdb-record-layer?id=AF444A45D667018DA9C485FD9676071D&t=FORK_MR%2F4279%2Farnaud-lacurie%2Fview-dml%3AHEAD
}

@Nonnull
public static LogicalOperator generateSimpleSelect(@Nonnull Expressions output,
@Nonnull LogicalOperators logicalOperators,
@Nonnull Optional<Expression> where,

Check failure on line 474 in fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/LogicalOperator.java

View check run for this annotation

fdb.teamscale.io / Teamscale | Findings

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/LogicalOperator.java#L474

The following lines contain 2 findings of the type: Specify a `Expression` parameter instead (Line 474: https://fdb.teamscale.io/findings/details/foundationdb-fdb-record-layer?id=0EF576C2FD63D2E768F9ACDF5D316984&t=FORK_MR%2F4279%2Farnaud-lacurie%2Fview-dml%3AHEAD) Specify a `Identifier` parameter instead (Line 475: https://fdb.teamscale.io/findings/details/foundationdb-fdb-record-layer?id=BC4CD4513E28C018020F2C9E3D30C636&t=FORK_MR%2F4279%2Farnaud-lacurie%2Fview-dml%3AHEAD)
@Nonnull Optional<Identifier> alias,
@Nonnull Set<CorrelationIdentifier> outerCorrelations,
@Nonnull List<? extends QueryPredicate> additionalPredicates,
boolean isForDdl) {
final var quantifiers = logicalOperators.getQuantifiers();
final var selectBuilder = GraphExpansion.builder().addAllQuantifiers(quantifiers);
where.ifPresent(predicate -> {
final var localAliases = quantifiers.stream().map(Quantifier::getAlias).collect(ImmutableSet.toImmutableSet());
selectBuilder.addPredicate(Expression.Utils.toUnderlyingPredicate(predicate, localAliases, isForDdl));
});
selectBuilder.addAllPredicates(additionalPredicates);
final var expandedOutput = output.expanded();
SelectExpression selectExpression;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1172,7 +1172,15 @@
final var recordLayerView = Assert.castUnchecked(view, RecordLayerView.class);
return recordLayerView.getCompilableViewSupplier().apply(isCaseSensitive);
}

Check notice on line 1175 in fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/SemanticAnalyzer.java

View workflow job for this annotation

GitHub Actions / coverage

File coverage: 86.4% (508/588 lines) | Changed lines: 100.0% (7/7 lines)
@Nonnull
public ViewUpdatabilityInfo resolveUpdatableView(@Nonnull final Identifier viewIdentifier) {
final LogicalOperator viewOp = resolveView(viewIdentifier);
return ViewUpdatabilityAnalyzer.analyze(viewOp, metadataCatalog)
.orElseThrow(() -> Assert.failUnchecked(ErrorCode.VIEW_NOT_UPDATABLE,
String.format(Locale.ROOT, "View '%s' is not updatable", viewIdentifier.getName())));
}

// TODO: this will be removed once we unify both Java- and SQL-UDFs.
public boolean isJavaCallFunction(@Nonnull final String functionName) {
return functionCatalog.isJavaCallFunction(functionName);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*

Check notice on line 1 in fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ViewUpdatabilityAnalyzer.java

View workflow job for this annotation

GitHub Actions / coverage

File coverage: 80.9% (38/47 lines) | Changed lines: 80.9% (38/47 lines)
* ViewUpdatabilityAnalyzer.java
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
*
* Licensed 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 com.apple.foundationdb.relational.recordlayer.query;

import com.apple.foundationdb.annotation.API;
import com.apple.foundationdb.record.query.plan.cascades.Quantifier;
import com.apple.foundationdb.record.query.plan.cascades.expressions.GroupByExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.LogicalDistinctExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.LogicalSortExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.LogicalTypeFilterExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.LogicalUnionExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.SelectExpression;
import com.apple.foundationdb.relational.api.exceptions.RelationalException;
import com.apple.foundationdb.relational.api.metadata.SchemaTemplate;
import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerTable;

import com.google.common.collect.Iterables;

import javax.annotation.Nonnull;
import java.util.Optional;

/**
* Analyzes the compiled logical plan of a view to determine whether it is updatable.
*
* <p>A view is updatable when its defining query satisfies all of the following conditions:
* <ol>
* <li>The top-level expression is a {@link SelectExpression} with exactly one child quantifier (no joins).</li>
* <li>There are no set operations ({@link LogicalUnionExpression}).</li>
* <li>There are no aggregations ({@link GroupByExpression}).</li>
* <li>There is no {@code DISTINCT} ({@link LogicalDistinctExpression}).</li>
* <li>The single child quantifier ranges over a {@link LogicalTypeFilterExpression} targeting
* exactly one base record type.</li>
* <li>The projection covers every column of the base table ({@code SELECT *} — no partial
* projections, no computed expressions, no {@code DISTINCT} without a GroupBy that would
* have been caught earlier).</li>
* </ol>
*
* <p>When all conditions are met, {@link #analyze} returns a populated {@link ViewUpdatabilityInfo}
* containing the base table, its identifier, any view-level predicates (the view's own WHERE
* clause), and the quantifier alias that those predicates reference.
*/
@API(API.Status.EXPERIMENTAL)
public final class ViewUpdatabilityAnalyzer {

private ViewUpdatabilityAnalyzer() {
}

/**
* Returns {@link ViewUpdatabilityInfo} if {@code viewOp} represents an updatable view,
* or {@link Optional#empty()} if the view is read-only.
*/
@Nonnull
public static Optional<ViewUpdatabilityInfo> analyze(@Nonnull final LogicalOperator viewOp,
@Nonnull final SchemaTemplate catalog) {
// The compiled view plan is always headed by a forEach quantifier whose target is the
// SelectExpression produced by the view's query. When the view is loaded from FDB and
// compiled via RoutineParser (a fresh BaseVisitor with no parent plan fragment), the
// isTopLevel flag is true, so generateSelect wraps the SelectExpression in a
// LogicalSortExpression.unsorted(). We look through that transparent wrapper here.
RelationalExpression topExpr = viewOp.getQuantifier().getRangesOver().get();
if (topExpr instanceof LogicalSortExpression) {
final LogicalSortExpression sortExpr = (LogicalSortExpression) topExpr;
if (sortExpr.getQuantifiers().size() != 1) {
return Optional.empty();
}
topExpr = Iterables.getOnlyElement(sortExpr.getQuantifiers()).getRangesOver().get();
}

// Rule 1: must be a simple SELECT (no GROUP BY, DISTINCT, UNION as top-level).
if (!(topExpr instanceof SelectExpression)) {
return Optional.empty();
}
final SelectExpression selectExpr = (SelectExpression) topExpr;

// Rule 2: exactly one source quantifier (no joins, no multi-table FROM).
if (selectExpr.getQuantifiers().size() != 1) {
return Optional.empty();
}

// Rule 3: no disqualifying expressions in the subtree.
final Quantifier innerQun = Iterables.getOnlyElement(selectExpr.getQuantifiers());
if (containsDisqualifyingExpression(innerQun.getRangesOver().get())) {
return Optional.empty();
}

// Rule 4: the single inner quantifier must range over a LogicalTypeFilterExpression,
// which is the signature of a plain base-table scan.
final RelationalExpression innerExpr = innerQun.getRangesOver().get();
if (!(innerExpr instanceof LogicalTypeFilterExpression)) {
return Optional.empty();
}
final LogicalTypeFilterExpression typeFilter = (LogicalTypeFilterExpression) innerExpr;

// Rule 5: the type filter must cover exactly one record type (single base table).
if (typeFilter.getRecordTypes().size() != 1) {
return Optional.empty();
}
final String storageName = Iterables.getOnlyElement(typeFilter.getRecordTypes());

// Locate the corresponding RecordLayerTable in the catalog.
final Optional<RecordLayerTable> baseTableOpt = findTableByStorageName(catalog, storageName);
if (baseTableOpt.isEmpty()) {
return Optional.empty();
}
final RecordLayerTable baseTable = baseTableOpt.get();

// Rule 6: the projection must cover all base-table columns — partial projections are
// non-updatable because the DML rewrite cannot determine default values for
// omitted columns. A SELECT * view expands to exactly as many result values as the
// base table has columns.
if (selectExpr.getResultValues().size() != baseTable.getColumns().size()) {
return Optional.empty();
}

return Optional.of(new ViewUpdatabilityInfo(
baseTable,
Identifier.of(baseTable.getName()),
selectExpr.getPredicates(),
innerQun.getAlias()));
}

/**
* Returns {@code true} if {@code expr} or any expression reachable through its quantifiers
* is a disqualifying expression type (aggregation, DISTINCT, or set operation).
*/
private static boolean containsDisqualifyingExpression(@Nonnull final RelationalExpression expr) {
if (expr instanceof GroupByExpression ||
expr instanceof LogicalDistinctExpression ||
expr instanceof LogicalUnionExpression) {
return true;
}
for (final Quantifier qun : expr.getQuantifiers()) {
if (containsDisqualifyingExpression(qun.getRangesOver().get())) {
return true;
}
}
return false;
}

@Nonnull
private static Optional<RecordLayerTable> findTableByStorageName(@Nonnull final SchemaTemplate catalog,
@Nonnull final String storageName) {
try {
for (final var table : catalog.getTables()) {
if (table instanceof RecordLayerTable) {
final RecordLayerTable rlt = (RecordLayerTable) table;
if (storageName.equals(rlt.getType().getStorageName())) {
return Optional.of(rlt);
}
}
}
return Optional.empty();
} catch (RelationalException e) {
throw e.toUncheckedWrappedException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*

Check notice on line 1 in fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ViewUpdatabilityInfo.java

View workflow job for this annotation

GitHub Actions / coverage

File coverage: 100.0% (10/10 lines) | Changed lines: 100.0% (10/10 lines)
* ViewUpdatabilityInfo.java
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
*
* Licensed 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 com.apple.foundationdb.relational.recordlayer.query;

import com.apple.foundationdb.annotation.API;
import com.apple.foundationdb.record.query.plan.cascades.CorrelationIdentifier;
import com.apple.foundationdb.record.query.plan.cascades.predicates.QueryPredicate;
import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerTable;

import javax.annotation.Nonnull;
import java.util.List;

/**
* Carries the information extracted from an updatable view's compiled plan that DML visitors need to
* rewrite INSERT/UPDATE/DELETE targeting the view into equivalent DML against the base table.
*/
@API(API.Status.EXPERIMENTAL)
public final class ViewUpdatabilityInfo {

@Nonnull
private final RecordLayerTable baseTable;

@Nonnull
private final Identifier baseTableIdentifier;

/**
* Predicates from the view's SelectExpression (the view's own WHERE clause, if any).
* These reference the view's inner quantifier alias ({@link #viewInnerAlias}).
*/
@Nonnull
private final List<? extends QueryPredicate> viewPredicates;

/**
* The alias of the quantifier that flows through the view's SelectExpression. View predicates
* reference this alias and must be translated before use in a different quantifier context.
*/
@Nonnull
private final CorrelationIdentifier viewInnerAlias;

public ViewUpdatabilityInfo(@Nonnull final RecordLayerTable baseTable,
@Nonnull final Identifier baseTableIdentifier,
@Nonnull final List<? extends QueryPredicate> viewPredicates,
@Nonnull final CorrelationIdentifier viewInnerAlias) {
this.baseTable = baseTable;
this.baseTableIdentifier = baseTableIdentifier;
this.viewPredicates = viewPredicates;
this.viewInnerAlias = viewInnerAlias;
}

@Nonnull
public RecordLayerTable baseTable() {
return baseTable;
}

@Nonnull
public Identifier baseTableIdentifier() {
return baseTableIdentifier;
}

@Nonnull
public List<? extends QueryPredicate> viewPredicates() {
return viewPredicates;
}

@Nonnull
public CorrelationIdentifier viewInnerAlias() {
return viewInnerAlias;
}
}
Loading
Loading