Skip to content
Closed
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 @@ -59,6 +59,7 @@
import java.sql.Struct;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
Expand Down Expand Up @@ -152,13 +153,13 @@
}

private AstNormalizer(@Nonnull final PreparedParams preparedStatementParameters, boolean caseSensitive,
@Nonnull final PlanHashable.PlanHashMode currentPlanHashMode) {
@Nonnull final PlanHashable.PlanHashMode currentPlanHashMode, boolean forExplain) {
hashFunction = Hashing.murmur3_32_fixed().newHasher();
parameterHash = Hashing.murmur3_32_fixed().newHasher().putInt("ParameterHash".hashCode());
parameterHashSupplier = Suppliers.memoize(() -> parameterHash.hash().asInt())::get;
sqlCanonicalizer = new StringBuilder();
// needed to collect information that guide query execution (explain flag, continuation string, offset int, and limit int).
queryHasherContextBuilder = NormalizedQueryExecutionContext.newBuilder().setPlanHashMode(currentPlanHashMode);
queryHasherContextBuilder = NormalizedQueryExecutionContext.newBuilder().setPlanHashMode(currentPlanHashMode).setForExplain(forExplain);
this.preparedStatementParameters = preparedStatementParameters;
allowTokenAddition = true;
allowLiteralAddition = true;
Expand Down Expand Up @@ -237,11 +238,13 @@
}

@Override
public Void visitFullDescribeStatement(@Nonnull RelationalParser.FullDescribeStatementContext ctx) {
// (yhatem) this is probably not needed, since a cached physical plan _knows_ it is either forExplain or not.
// we should remove this, but ok for now.
queryHasherContextBuilder.setForExplain(ctx.EXPLAIN() != null);
return visitChildren(ctx);
// we are stripping Explain/Describe prefix from the request before passing it to the parser
throw Assert.failUnchecked("Explain/Describe statement should not appear at the parser lavel");
// // (yhatem) this is probably not needed, since a cached physical plan _knows_ it is either forExplain or not.
// // we should remove this, but ok for now.
// queryHasherContextBuilder.setForExplain(ctx.EXPLAIN() != null);
// return visitChildren(ctx);

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

View check run for this annotation

fdb.teamscale.io / Teamscale | Findings

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/AstNormalizer.java#L244-L247

Commented Out Code https://fdb.teamscale.io/findings/details/foundationdb-fdb-record-layer?t=FORK_MR%2F3991%2Fsergei-pustovykh%2Freuse-cache-for-expalain%3AHEAD&id=F3CA707E9A0A82D30D2FFD1612B22976
}

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

View check run for this annotation

fdb.teamscale.io / Teamscale | Test Gaps

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/AstNormalizer.java#L241-L248

[Test Gap] Changed method `visitFullDescribeStatement` has not been tested. https://fdb.teamscale.io/metrics/code/foundationdb-fdb-record-layer/fdb-relational-core%2Fsrc%2Fmain%2Fjava%2Fcom%2Fapple%2Ffoundationdb%2Frelational%2Frecordlayer%2Fquery%2FAstNormalizer.java?coverage-mode=test-gap&t=FORK_MR%2F3991%2Fsergei-pustovykh%2Freuse-cache-for-expalain%3AHEAD&selection=char-10605-11211&merge-request=FoundationDB%2Ffdb-record-layer%2F3991

@Override
Expand Down Expand Up @@ -569,6 +572,87 @@
}
}

@VisibleForTesting
public static class ExplainParser {

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

View check run for this annotation

fdb.teamscale.io / Teamscale | Findings

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/AstNormalizer.java#L576

Make this class `final` or add a public constructor https://fdb.teamscale.io/findings/details/foundationdb-fdb-record-layer?t=FORK_MR%2F3991%2Fsergei-pustovykh%2Freuse-cache-for-expalain%3AHEAD&id=595DD0642433E6F7FA024F2B3FF691FA

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

View check run for this annotation

fdb.teamscale.io / Teamscale | Findings

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/AstNormalizer.java#L576

Reduce this class from 66 lines to the maximum allowed 25 or externalize it in a public class https://fdb.teamscale.io/findings/details/foundationdb-fdb-record-layer?t=FORK_MR%2F3991%2Fsergei-pustovykh%2Freuse-cache-for-expalain%3AHEAD&id=9671B4FACFE47DE2E2910342D89A6530
private ExplainParser() {
}

private static class Word {
public int begin;

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

View check run for this annotation

fdb.teamscale.io / Teamscale | Findings

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/AstNormalizer.java#L581

Make begin a static final constant or non-public and provide accessors if needed https://fdb.teamscale.io/findings/details/foundationdb-fdb-record-layer?t=FORK_MR%2F3991%2Fsergei-pustovykh%2Freuse-cache-for-expalain%3AHEAD&id=F17FD0AFC1AC678474662F5F8ECE6F47
public int end;

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

View check run for this annotation

fdb.teamscale.io / Teamscale | Findings

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/AstNormalizer.java#L582

Make end a static final constant or non-public and provide accessors if needed https://fdb.teamscale.io/findings/details/foundationdb-fdb-record-layer?t=FORK_MR%2F3991%2Fsergei-pustovykh%2Freuse-cache-for-expalain%3AHEAD&id=E6C44A5B02E1A94D6203E222E922820C

public Word(int b, int e) {
begin = b;
end = e;
}
}

private static int getNotWhitespace(@Nonnull final String query, int pos) {
while (pos < query.length() && Character.isWhitespace(query.charAt(pos))) {
pos++;
}
return pos;
}

private static int getWhitespaceEqual(@Nonnull final String query, int pos) {
while (pos < query.length() && !Character.isWhitespace(query.charAt(pos)) && query.charAt(pos) != '=') {
pos++;
}
return pos;
}

private static Word getWordFromString(@Nonnull final String query, int start) {
int pos = getNotWhitespace(query, start);
return new Word(pos, getWhitespaceEqual(query, pos));
}

private static boolean matchWord(@Nonnull final String query, @Nonnull final Word w, @Nonnull final String word) {
return w.end - w.begin == word.length() && query.regionMatches(true, w.begin, word, 0, word.length());
}

private static boolean matchWordFromList(@Nonnull final String query, @Nonnull final Word w, @Nonnull final List<String> list) {
for (final var word : list) {
if (matchWord(query, w, word)) {
return true;
}
}
return false;
}

public static String truncateExplainStatement(@Nonnull final String query) {
final List<String> explainList = List.of("EXPLAIN", "DESCRIBE", "DESC");
final String shemaString = "SCHEMA";

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

View check run for this annotation

fdb.teamscale.io / Teamscale | Findings

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/AstNormalizer.java#L624

Move the declaration of "shemaString" closer to the code that uses it https://fdb.teamscale.io/findings/details/foundationdb-fdb-record-layer?t=FORK_MR%2F3991%2Fsergei-pustovykh%2Freuse-cache-for-expalain%3AHEAD&id=342ECE4AA84D9B5FB42D1733B501A3AB
final List<String> extendedList = List.of("EXTENDED", "PARTITIONS", "FORMAT");

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

View check run for this annotation

fdb.teamscale.io / Teamscale | Findings

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/AstNormalizer.java#L625

Move the declaration of "extendedList" closer to the code that uses it https://fdb.teamscale.io/findings/details/foundationdb-fdb-record-layer?t=FORK_MR%2F3991%2Fsergei-pustovykh%2Freuse-cache-for-expalain%3AHEAD&id=13F1603A811B948BEB6C99FF4F8DB641
final List<String> traditionalList = List.of("TRADITIONAL", "JSON");

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

View check run for this annotation

fdb.teamscale.io / Teamscale | Findings

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/AstNormalizer.java#L626

Move the declaration of "traditionalList" closer to the code that uses it https://fdb.teamscale.io/findings/details/foundationdb-fdb-record-layer?t=FORK_MR%2F3991%2Fsergei-pustovykh%2Freuse-cache-for-expalain%3AHEAD&id=60E6480EA23A902B20F74CFB3B5583C9


final var w0 = getWordFromString(query, 0);
if (!matchWordFromList(query, w0, explainList)) { // EXPLAIN
return query;
}

final var w1 = getWordFromString(query, w0.end);
if (matchWord(query, w1, shemaString)) { // EXPLAIN SCHEMA
return query;
}
if (!matchWordFromList(query, w1, extendedList)) { // EXPLAIN EXTENDED
int pos = getNotWhitespace(query, w1.begin);
Assert.thatUnchecked(pos != query.length(), ErrorCode.SYNTAX_ERROR, "no statement to explain");
return query.substring(pos);
}

int pos = getNotWhitespace(query, w1.end); // EXPLAIN EXTENDED=
Assert.thatUnchecked(query.charAt(pos) == '=', ErrorCode.SYNTAX_ERROR, "equal (=) not found after EXTENDED/PARTITIONS/FORMAT");

final var w2 = getWordFromString(query, pos + 1); // EXPLAIN EXTENDED=TRADITIONAL
Assert.thatUnchecked(matchWordFromList(query, w2, traditionalList), ErrorCode.SYNTAX_ERROR, "value of EXTENDED/PARTITIONS/FORMAT is not TRADITIONAL/JSON");

pos = getNotWhitespace(query, w2.end);
Assert.thatUnchecked(pos != query.length(), ErrorCode.SYNTAX_ERROR, "no statement to explain");
return query.substring(w2.end);
}
}

/**
* Normalizes the SQL query using a given planning context.
* @param context The planning context, captures all the state required to plan and execution query such as prepared parameter values.
Expand All @@ -583,9 +667,10 @@
boolean isCaseSensitive,
@Nonnull final PlanHashable.PlanHashMode currentPlanHashMode) throws RelationalException {
// lexing, parsing, and normalization are profiled through the metric collector.
final var truncQuery = ExplainParser.truncateExplainStatement(query);
final var metricCollector = context.getMetricsCollector();
final var rootContext = metricCollector.clock(RelationalMetric.RelationalEvent.LEX_PARSE,
() -> QueryParser.parse(query).getRootContext());
() -> QueryParser.parse(truncQuery).getRootContext());
return metricCollector.clock(RelationalMetric.RelationalEvent.NORMALIZE_QUERY,
() -> normalizeAst(
context.getSchemaTemplate(), rootContext,
Expand All @@ -594,7 +679,8 @@
context.getPlannerConfiguration(),
isCaseSensitive,
currentPlanHashMode,
query
query,
!Objects.equals(query, truncQuery)
));
}

Expand All @@ -607,8 +693,9 @@
@Nonnull final PlannerConfiguration plannerConfiguration,
boolean caseSensitive,
@Nonnull final PlanHashable.PlanHashMode currentPlanHashMode,
@Nonnull final String query) throws RelationalException {
final var astNormalizer = new AstNormalizer(preparedStatementParameters, caseSensitive, currentPlanHashMode);
@Nonnull final String query,
boolean forExplain) throws RelationalException {
final var astNormalizer = new AstNormalizer(preparedStatementParameters, caseSensitive, currentPlanHashMode, forExplain);
astNormalizer.visit(context);
final var recordLayerSchemaTemplate = Assert.castUnchecked(schemaTemplate, RecordLayerSchemaTemplate.class);

Expand Down Expand Up @@ -637,7 +724,7 @@
plannerConfiguration,
caseSensitive,
currentPlanHashMode,
recordLayerRoutine.getDescription());
recordLayerRoutine.getDescription(), forExplain);
astNormalizer.queryHasherContextBuilder.getLiteralsBuilder().importLiterals(functionAstResult.queryExecutionContext.getLiterals());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ private Plan<?> generatePhysicalPlanForCompilableStatement(@Nonnull AstNormalize

final var planGenerationContext = new MutablePlanGenerationContext(planContext.getPreparedStatementParameters(),
currentPlanHashMode, ast.getQuery(), ast.getQueryCacheKey().getCanonicalQueryString(), parameterHash);
planGenerationContext.setForExplain(ast.getQueryExecutionContext().isForExplain());
final var metadata = Assert.castUnchecked(planContext.getSchemaTemplate(), RecordLayerSchemaTemplate.class);
try {
final var maybePlan = planContext.getMetricsCollector().clock(RelationalMetric.RelationalEvent.GENERATE_LOGICAL_PLAN, () ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -597,13 +597,15 @@

@Nonnull
@Override
public QueryPlan.LogicalQueryPlan visitFullDescribeStatement(@Nonnull RelationalParser.FullDescribeStatementContext ctx) {
getDelegate().getPlanGenerationContext().setForExplain(ctx.EXPLAIN() != null);
final var logicalOperator = Assert.castUnchecked(ctx.describeObjectClause().accept(this), LogicalOperator.class);
// Capture semantic type structure as StructType with field names
final var semanticStructType = logicalOperator.getOutput().getStructType();
return QueryPlan.LogicalQueryPlan.of(logicalOperator.getQuantifier().getRangesOver().get(),
getDelegate().getPlanGenerationContext(), getDelegate().getPlanGenerationContext().getQuery(), semanticStructType);
// we are stripping Explain/Describe prefix from the request before passing it to the parser
throw Assert.failUnchecked("Explain/Describe statement should not appear at the parser lavel");
// getDelegate().getPlanGenerationContext().setForExplain(ctx.EXPLAIN() != null);
// final var logicalOperator = Assert.castUnchecked(ctx.describeObjectClause().accept(this), LogicalOperator.class);
// // Capture semantic type structure as StructType with field names
// final var semanticStructType = logicalOperator.getOutput().getStructType();
// return QueryPlan.LogicalQueryPlan.of(logicalOperator.getQuantifier().getRangesOver().get(),
// getDelegate().getPlanGenerationContext(), getDelegate().getPlanGenerationContext().getQuery(), semanticStructType);

Check warning on line 608 in fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/QueryVisitor.java

View check run for this annotation

fdb.teamscale.io / Teamscale | Findings

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/QueryVisitor.java#L603-L608

Commented Out Code https://fdb.teamscale.io/findings/details/foundationdb-fdb-record-layer?t=FORK_MR%2F3991%2Fsergei-pustovykh%2Freuse-cache-for-expalain%3AHEAD&id=517CA406E6C84C68A77501ABE8A4C658
}

Check warning on line 609 in fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/QueryVisitor.java

View check run for this annotation

fdb.teamscale.io / Teamscale | Test Gaps

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/QueryVisitor.java#L600-L609

[Test Gap] Changed method `visitFullDescribeStatement` has not been tested. https://fdb.teamscale.io/metrics/code/foundationdb-fdb-record-layer/fdb-relational-core%2Fsrc%2Fmain%2Fjava%2Fcom%2Fapple%2Ffoundationdb%2Frelational%2Frecordlayer%2Fquery%2Fvisitors%2FQueryVisitor.java?coverage-mode=test-gap&t=FORK_MR%2F3991%2Fsergei-pustovykh%2Freuse-cache-for-expalain%3AHEAD&selection=char-36782-37730&merge-request=FoundationDB%2Ffdb-record-layer%2F3991

@Nonnull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,26 @@

package com.apple.foundationdb.relational.recordlayer;

import com.apple.foundationdb.record.PlanHashable;
import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStoreBase;
import com.apple.foundationdb.relational.api.Options;
import com.apple.foundationdb.relational.api.exceptions.ErrorCode;
import com.apple.foundationdb.relational.api.exceptions.RelationalException;
import com.apple.foundationdb.relational.api.exceptions.UncheckedRelationalException;
import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerSchemaTemplate;
import com.apple.foundationdb.relational.recordlayer.query.MutablePlanGenerationContext;
import com.apple.foundationdb.relational.recordlayer.query.Plan;
import com.apple.foundationdb.relational.recordlayer.query.PlanContext;
import com.apple.foundationdb.relational.recordlayer.query.PlanGenerator;
import com.apple.foundationdb.relational.recordlayer.query.QueryParser;
import com.apple.foundationdb.relational.recordlayer.query.QueryPlan;
import com.apple.foundationdb.relational.recordlayer.query.visitors.BaseVisitor;
import com.apple.foundationdb.relational.util.Assert;
import com.apple.foundationdb.relational.utils.SimpleDatabaseRule;
import com.apple.foundationdb.relational.utils.TestSchemas;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
Expand All @@ -46,6 +55,7 @@
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
* A series of tests that target the following.
Expand Down Expand Up @@ -216,4 +226,46 @@ void queryTestHarness(int ignored, @Nonnull String query, @Nullable String error
}
}
}

@Test
void queryVisitorThrowsWhenExplainReachesParser() throws Exception {
// This test covers test gap
// QueryVisitor.visitFullDescribeStatement must never be reached because EXPLAIN is stripped
// before parsing. This test verifies the defensive assertion fires if that invariant is broken.
final var explainQuery = "EXPLAIN SELECT * FROM restaurant";
final String schemaName = connection.getSchema();
final EmbeddedRelationalConnection embeddedConnection = (EmbeddedRelationalConnection) connection.connection;
embeddedConnection.setAutoCommit(false);
embeddedConnection.createNewTransaction();
try {
final AbstractDatabase db = embeddedConnection.getRecordLayerDatabase();
final FDBRecordStoreBase<?> store = db.loadSchema(schemaName).loadStore().unwrap(FDBRecordStoreBase.class);
final PlanContext planContext = PlanContext.Builder
.create()
.fromDatabase(db)
.fromRecordStore(store, connection.getOptions())
.withSchemaTemplate(embeddedConnection.getSchemaTemplate())
.withMetricsCollector(embeddedConnection.getMetricCollector())
.build();
final var parseTree = QueryParser.parse(explainQuery).getRootContext();
final var planGenContext = new MutablePlanGenerationContext(
planContext.getPreparedStatementParameters(),
PlanHashable.PlanHashMode.VC0,
explainQuery,
explainQuery,
0);
final var metadata = Assert.castUnchecked(planContext.getSchemaTemplate(), RecordLayerSchemaTemplate.class);
final var baseVisitor = new BaseVisitor(planGenContext, metadata,
planContext.getDdlQueryFactory(), planContext.getConstantActionFactory(),
planContext.getDbUri(), false);
assertThatThrownBy(() -> baseVisitor.generateLogicalPlan(parseTree))
.isInstanceOf(UncheckedRelationalException.class)
.hasMessageContaining("Explain/Describe statement should not appear at the parser")
.extracting(e -> ((UncheckedRelationalException) e).unwrap().getErrorCode())
.isEqualTo(ErrorCode.INTERNAL_ERROR);
} finally {
embeddedConnection.rollback();
embeddedConnection.setAutoCommit(true);
}
}
}
Loading
Loading