All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.apple.foundationdb.relational.yamltests.command.QueryConfig Maven / Gradle / Ivy

The newest version!
/*
 * QueryConfig.java
 *
 * This source file is part of the FoundationDB open source project
 *
 * Copyright 2021-2024 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.yamltests.command;

import com.apple.foundationdb.record.query.plan.cascades.debug.BrowserHelper;
import com.apple.foundationdb.relational.api.RelationalResultSet;
import com.apple.foundationdb.relational.recordlayer.ErrorCapturingResultSet;
import com.apple.foundationdb.relational.util.Assert;
import com.apple.foundationdb.relational.yamltests.CustomYamlConstructor;
import com.apple.foundationdb.relational.yamltests.Matchers;
import com.apple.foundationdb.relational.yamltests.YamlConnection;
import com.apple.foundationdb.relational.yamltests.YamlExecutionContext;
import com.apple.foundationdb.relational.yamltests.block.FileOptions;
import com.apple.foundationdb.relational.yamltests.generated.stats.PlannerMetricsProto;
import com.apple.foundationdb.relational.yamltests.server.SemanticVersion;
import com.apple.foundationdb.relational.yamltests.server.SupportedVersionCheck;
import com.apple.foundationdb.tuple.ByteArrayUtil2;
import com.github.difflib.text.DiffRow;
import com.github.difflib.text.DiffRowGenerator;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Range;
import com.google.common.collect.RangeSet;
import com.google.common.collect.TreeRangeSet;
import com.google.protobuf.Descriptors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.jupiter.api.Assertions;
import org.opentest4j.AssertionFailedError;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;

import static com.apple.foundationdb.relational.yamltests.command.QueryCommand.reportTestFailure;

/**
 * A {@link QueryConfig} defines how the query is to be run and how the output of the query execution or the error
 * (in case of failure in query execution) is to be tested. To do so, it utilizes 2 methods
 * 
    *
  • {@code decorateQuery}: takes in the canonical query string and changes it as is needed to be run
  • *
  • {@code checkResult}: checks for the result from the query execution.
  • *
  • {@code checkError}: checks for the error.
  • *
*/ @SuppressWarnings({"PMD.GuardLogStatement", "PMD.AvoidCatchingThrowable"}) public abstract class QueryConfig { private static final Logger logger = LogManager.getLogger(QueryConfig.class); public static final String QUERY_CONFIG_RESULT = "result"; public static final String QUERY_CONFIG_UNORDERED_RESULT = "unorderedResult"; public static final String QUERY_CONFIG_EXPLAIN = "explain"; public static final String QUERY_CONFIG_EXPLAIN_CONTAINS = "explainContains"; public static final String QUERY_CONFIG_COUNT = "count"; public static final String QUERY_CONFIG_ERROR = "error"; public static final String QUERY_CONFIG_PLAN_HASH = "planHash"; public static final String QUERY_CONFIG_NO_CHECKS = "noChecks"; public static final String QUERY_CONFIG_MAX_ROWS = "maxRows"; public static final String QUERY_CONFIG_SUPPORTED_VERSION = FileOptions.SUPPORTED_VERSION_OPTION; public static final String QUERY_CONFIG_INITIAL_VERSION_AT_LEAST = "initialVersionAtLeast"; public static final String QUERY_CONFIG_INITIAL_VERSION_LESS_THAN = "initialVersionLessThan"; public static final String QUERY_CONFIG_NO_OP = "noOp"; private static final Set RESULT_CONFIGS = ImmutableSet.of(QUERY_CONFIG_ERROR, QUERY_CONFIG_COUNT, QUERY_CONFIG_RESULT, QUERY_CONFIG_UNORDERED_RESULT); private static final Set VERSION_DEPENDENT_RESULT_CONFIGS = ImmutableSet.of(QUERY_CONFIG_INITIAL_VERSION_AT_LEAST, QUERY_CONFIG_INITIAL_VERSION_LESS_THAN); @Nullable private final Object value; private final int lineNumber; @Nonnull private final YamlExecutionContext executionContext; @Nullable private final String configName; private QueryConfig(@Nullable String configName, @Nullable Object value, int lineNumber, @Nonnull YamlExecutionContext executionContext) { this.configName = configName; this.value = value; this.lineNumber = lineNumber; this.executionContext = executionContext; } int getLineNumber() { return lineNumber; } @Nullable Object getVal() { return value; } @Nullable String getConfigName() { return configName; } String getValueString() { if (value instanceof byte[]) { return ByteArrayUtil2.loggable((byte[]) value); } else if (value != null) { return value.toString(); } else { return ""; } } String decorateQuery(@Nonnull String query) { return query; } final void checkResult(@Nonnull String currentQuery, @Nonnull Object actual, @Nonnull String queryDescription, @Nonnull YamlConnection connection) { try { checkResultInternal(currentQuery, actual, queryDescription); } catch (AssertionFailedError e) { throw executionContext.wrapContext(e, () -> "‼️Check result failed in config at line " + getLineNumber() + " against connection for versions " + connection.getVersions(), String.format(Locale.ROOT, "config [%s: %s] ", getConfigName(), getVal()), getLineNumber()); } catch (Throwable e) { throw executionContext.wrapContext(e, () -> "‼️Failed to test config at line " + getLineNumber() + " against connection for versions " + connection.getVersions(), String.format(Locale.ROOT, "config [%s: %s] ", getConfigName(), getVal()), getLineNumber()); } } final void checkError(@Nonnull SQLException actual, @Nonnull String queryDescription, final YamlConnection connection) { try { checkErrorInternal(actual, queryDescription); } catch (AssertionFailedError e) { throw executionContext.wrapContext(e, () -> "‼️Check result failed in config at line " + getLineNumber() + " against connection for versions " + connection.getVersions(), String.format(Locale.ROOT, "config [%s: %s] ", getConfigName(), getVal()), getLineNumber()); } catch (Throwable e) { throw executionContext.wrapContext(e, () -> "‼️Failed to test config at line " + getLineNumber() + " against connection for versions " + connection.getVersions(), String.format(Locale.ROOT, "config [%s: %s] ", getConfigName(), getVal()), getLineNumber()); } } abstract void checkResultInternal(@Nonnull String currentQuery, @Nonnull Object actual, @Nonnull String queryDescription) throws SQLException; void checkErrorInternal(@Nonnull SQLException e, @Nonnull String queryDescription) throws SQLException { final var diffMessage = String.format(Locale.ROOT, "‼️ statement failed with the following error at line %s:%n" + "⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤%n" + "%s%n" + "⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤%n", getLineNumber(), e.getMessage()); logger.error(diffMessage); throw e; } private static QueryConfig getCheckResultConfig(boolean isExpectedOrdered, @Nullable String configName, @Nullable Object value, int lineNumber, @Nonnull YamlExecutionContext executionContext) { return new QueryConfig(configName, value, lineNumber, executionContext) { @Override void checkResultInternal(@Nonnull String currentQuery, @Nonnull Object actual, @Nonnull String queryDescription) throws SQLException { logger.debug("⛳️ Matching results of query '{}'", queryDescription); try (RelationalResultSet resultSet = (RelationalResultSet)actual) { final var matchResult = Matchers.matchResultSet(getVal(), resultSet, isExpectedOrdered); if (!matchResult.getLeft().equals(Matchers.ResultSetMatchResult.success())) { var toReport = "‼️ result mismatch at line " + getLineNumber() + ":\n" + "⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤\n" + Matchers.notNull(matchResult.getLeft().getExplanation(), "failure error message") + "\n" + "⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤\n"; final var valueString = getValueString(); if (!valueString.isEmpty()) { toReport += "↪ expected result:\n" + getValueString() + "\n"; } if (matchResult.getRight() != null) { toReport += "⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤\n" + "↩ actual result left to be matched:\n" + Matchers.notNull(matchResult.getRight(), "failure error actual result set"); } reportTestFailure(toReport); } else { logger.debug("✅ results match!"); } } } }; } private static QueryConfig getCheckExplainConfig(boolean isExact, @Nonnull String blockName, @Nonnull String configName, @Nullable Object value, int lineNumber, @Nonnull YamlExecutionContext executionContext) { return new QueryConfig(configName, value, lineNumber, executionContext) { @Override String decorateQuery(@Nonnull String query) { return "EXPLAIN " + query; } @SuppressWarnings({"PMD.CloseResource", "PMD.EmptyWhileStmt"}) // lifetime of autocloseable resource persists beyond method @Override void checkResultInternal(@Nonnull String currentQuery, @Nonnull Object actual, @Nonnull String queryDescription) throws SQLException { logger.debug("⛳️ Matching plan for query '{}'", queryDescription); final var resultSet = (RelationalResultSet) actual; resultSet.next(); final var actualPlan = resultSet.getString(1); var success = isExact ? getVal().equals(actualPlan) : actualPlan.contains((String) getVal()); final var actualDot = resultSet.getString(3); final var metricsMap = executionContext.getMetricsMap(); final var identifier = PlannerMetricsProto.Identifier.newBuilder() .setBlockName(blockName) .setQuery(currentQuery) .build(); final var expectedPlannerMetricsInfo = metricsMap.get(identifier); if (success) { logger.debug("✅️ plan match!"); } else { if (executionContext.shouldShowPlanOnDiff() && actualDot != null && expectedPlannerMetricsInfo != null) { BrowserHelper.browse("/showPlanDiff.html", ImmutableMap.of("$SQL", queryDescription, "$DOT_EXPECTED", expectedPlannerMetricsInfo.getDot(), "$DOT_ACTUAL", actualDot)); } final var expectedPlan = getValueString(); final var diffGenerator = DiffRowGenerator.create() .showInlineDiffs(true) .inlineDiffByWord(true) .newTag(f -> f ? CommandUtil.Color.RED.toString() : CommandUtil.Color.RESET.toString()) .oldTag(f -> f ? CommandUtil.Color.GREEN.toString() : CommandUtil.Color.RESET.toString()) .build(); final List diffRows = diffGenerator.generateDiffRows( Collections.singletonList(expectedPlan), Collections.singletonList(actualPlan)); final var planDiffs = new StringBuilder(); for (final var diffRow : diffRows) { planDiffs.append(diffRow.getOldLine()).append('\n').append(diffRow.getNewLine()).append('\n'); } if (isExact && executionContext.shouldCorrectExplains()) { if (!executionContext.correctExplain(getLineNumber() - 1, actualPlan)) { reportTestFailure("‼️ Cannot correct explain plan at line " + getLineNumber()); } else { logger.debug("⭐️ Successfully replaced plan at line {}", getLineNumber()); } } else { final var diffMessage = String.format(Locale.ROOT, "‼️ plan mismatch at line %d:%n" + "⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤%n%s" + "⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤%n" + "↪ expected plan %s:%n%s%n" + "⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤%n" + "↩ actual plan:%n%s", getLineNumber(), planDiffs, (!isExact ? "fragment" : ""), getValueString(), actualPlan); reportTestFailure(diffMessage); } } final var actualPlannerMetrics = resultSet.getStruct(6); if (isExact && actualPlannerMetrics != null) { Objects.requireNonNull(actualDot); final var taskCount = actualPlannerMetrics.getLong(1); Verify.verify(taskCount > 0); final var taskTotalTimeInNs = actualPlannerMetrics.getLong(2); Verify.verify(taskTotalTimeInNs > 0); if (expectedPlannerMetricsInfo == null && !executionContext.shouldCorrectMetrics()) { reportTestFailure("‼️ No planner metrics for line " + getLineNumber()); } final var actualInfo = PlannerMetricsProto.Info.newBuilder() .setExplain(actualPlan) .setDot(actualDot) .setCountersAndTimers(PlannerMetricsProto.CountersAndTimers.newBuilder() .setTaskCount(taskCount) .setTaskTotalTimeNs(taskTotalTimeInNs) .setTransformCount(actualPlannerMetrics.getLong(3)) .setTransformTimeNs(actualPlannerMetrics.getLong(4)) .setTransformYieldCount(actualPlannerMetrics.getLong(5)) .setInsertTimeNs(actualPlannerMetrics.getLong(6)) .setInsertNewCount(actualPlannerMetrics.getLong(7)) .setInsertReusedCount(actualPlannerMetrics.getLong(8))) .build(); if (expectedPlannerMetricsInfo == null) { executionContext.putMetrics(blockName, currentQuery, lineNumber, actualInfo, true); executionContext.markDirty(); logger.debug("⭐️ Successfully inserted new planner metrics at line {}", getLineNumber()); } else { final var expectedCountersAndTimers = expectedPlannerMetricsInfo.getCountersAndTimers(); final var actualCountersAndTimers = actualInfo.getCountersAndTimers(); final var metricsDescriptor = expectedCountersAndTimers.getDescriptorForType(); boolean isDifferent = isMetricDifferent(expectedCountersAndTimers, actualCountersAndTimers, metricsDescriptor.findFieldByName("task_count"), lineNumber) | isMetricDifferent(expectedCountersAndTimers, actualCountersAndTimers, metricsDescriptor.findFieldByName("transform_count"), lineNumber) | isMetricDifferent(expectedCountersAndTimers, actualCountersAndTimers, metricsDescriptor.findFieldByName("transform_yield_count"), lineNumber) | isMetricDifferent(expectedCountersAndTimers, actualCountersAndTimers, metricsDescriptor.findFieldByName("insert_new_count"), lineNumber) | isMetricDifferent(expectedCountersAndTimers, actualCountersAndTimers, metricsDescriptor.findFieldByName("insert_reused_count"), lineNumber); executionContext.putMetrics(blockName, currentQuery, lineNumber, actualInfo, isDifferent); if (isDifferent) { if (executionContext.shouldCorrectMetrics()) { executionContext.markDirty(); logger.debug("⭐️ Successfully updated planner metrics at line {}", getLineNumber()); } else { reportTestFailure("‼️ Planner metrics have changed for line " + getLineNumber()); } } } } } }; } private static boolean isMetricDifferent(@Nonnull final PlannerMetricsProto.CountersAndTimers expected, @Nonnull final PlannerMetricsProto.CountersAndTimers actual, @Nonnull final Descriptors.FieldDescriptor fieldDescriptor, int lineNumber) { final long expectedMetric = (long)expected.getField(fieldDescriptor); final long actualMetric = (long)actual.getField(fieldDescriptor); if (expectedMetric != actualMetric) { logger.warn("‼️ metric {} differs; lineNumber = {}; expected = {}; actual = {}", fieldDescriptor.getName(), lineNumber, expectedMetric, actualMetric); return true; } return false; } private static QueryConfig getCheckErrorConfig(@Nullable Object value, int lineNumber, @Nonnull YamlExecutionContext executionContext) { return new QueryConfig(QUERY_CONFIG_ERROR, value, lineNumber, executionContext) { @Override void checkResultInternal(@Nonnull String currentQuery, @Nonnull Object actual, @Nonnull String queryDescription) throws SQLException { Matchers.ResultSetPrettyPrinter resultSetPrettyPrinter = new Matchers.ResultSetPrettyPrinter(); if (actual instanceof ErrorCapturingResultSet) { Matchers.printRemaining((ErrorCapturingResultSet) actual, resultSetPrettyPrinter); reportTestFailure(String.format( "‼️ expecting statement to throw an error, however it returned a result set at line %d%n" + "⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤%n" + "%s%n" + "⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤%n", getLineNumber(), resultSetPrettyPrinter)); } else if (actual instanceof Integer) { reportTestFailure(String.format( "‼️ expecting statement to throw an error, however it returned a count at line %d%n" + "⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤%n" + "%s%n" + "⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤%n", getLineNumber(), actual)); } else { reportTestFailure(String.format(Locale.ROOT, "‼️ unexpected query result of type '%s' (expecting '%s') at line %d%n", actual.getClass().getSimpleName(), ErrorCapturingResultSet.class.getSimpleName(), getLineNumber())); } } @Override void checkErrorInternal(@Nonnull SQLException e, @Nonnull String queryDescription) { logger.debug("⛳️ Checking error code resulted from executing '{}'", queryDescription); if (!e.getSQLState().equals(getVal())) { reportTestFailure(String.format(Locale.ROOT, "‼️ expecting '%s' error code, got '%s' instead at line %d!", getVal(), e.getSQLState(), getLineNumber()), e); } else { logger.debug("✅ error codes '{}' match!", getVal()); } } }; } private static QueryConfig getCheckCountConfig(@Nullable Object value, int lineNumber, @Nonnull YamlExecutionContext executionContext) { return new QueryConfig(QUERY_CONFIG_COUNT, value, lineNumber, executionContext) { @Override void checkResultInternal(@Nonnull String currentQuery, @Nonnull Object actual, @Nonnull String queryDescription) { logger.debug("⛳️ Matching count of update query '{}'", queryDescription); if (!Matchers.matches(getVal(), actual)) { reportTestFailure(String.format(Locale.ROOT, "‼️ Expected count value %d, but got %d at line %d", (Integer) getVal(), (Integer) actual, getLineNumber())); } else { logger.debug("✅ Results match!"); } } }; } private static QueryConfig getCheckPlanHashConfig(@Nullable Object value, int lineNumber, @Nonnull YamlExecutionContext executionContext) { return new QueryConfig(QUERY_CONFIG_PLAN_HASH, value, lineNumber, executionContext) { @Override String decorateQuery(@Nonnull String query) { return "EXPLAIN " + query; } @SuppressWarnings("PMD.CloseResource") // lifetime of autocloseable persists beyond method @Override void checkResultInternal(@Nonnull String currentQuery, @Nonnull Object actual, @Nonnull String queryDescription) throws SQLException { logger.debug("⛳️ Matching plan hash of query '{}'", queryDescription); final var resultSet = (RelationalResultSet) actual; resultSet.next(); final var actualPlanHash = resultSet.getInt(2); if (!Matchers.matches(getVal(), actualPlanHash)) { reportTestFailure("‼️ Incorrect plan hash: expecting " + getVal() + " got " + actualPlanHash + " at line " + getLineNumber()); } logger.debug("✅️ Plan hash matches!"); } }; } public static QueryConfig getNoCheckConfig(int lineNumber, @Nonnull YamlExecutionContext executionContext) { return new QueryConfig(QUERY_CONFIG_NO_CHECKS, null, lineNumber, executionContext) { @SuppressWarnings("PMD.CloseResource") // lifetime of autocloseable persists beyond method @Override void checkResultInternal(@Nonnull String currentQuery, @Nonnull Object actual, @Nonnull String queryDescription) throws SQLException { if (actual instanceof RelationalResultSet) { final var resultSet = (RelationalResultSet) actual; // slurp boolean valid = true; while (valid) { // suppress check style valid = ((RelationalResultSet) actual).next(); } resultSet.close(); } } }; } private static QueryConfig getMaxRowConfig(@Nonnull Object value, int lineNumber, @Nonnull YamlExecutionContext executionContext) { return new QueryConfig(QUERY_CONFIG_MAX_ROWS, value, lineNumber, executionContext) { @Override void checkResultInternal(@Nonnull String currentQuery, @Nonnull Object actual, @Nonnull String queryDescription) throws SQLException { Assert.failUnchecked("No results to check on a maxRow config"); } }; } @Nonnull public static QueryConfig getSupportedVersionConfig(Object rawVersion, final int lineNumber, final YamlExecutionContext executionContext) { final SupportedVersionCheck check = SupportedVersionCheck.parse(rawVersion, executionContext); if (!check.isSupported()) { return new SkipConfig(QUERY_CONFIG_SUPPORTED_VERSION, rawVersion, lineNumber, executionContext, check.getMessage()); } return new QueryConfig(QUERY_CONFIG_SUPPORTED_VERSION, rawVersion, lineNumber, executionContext) { @Override void checkResultInternal(@Nonnull final String currentQuery, @Nonnull final Object actual, @Nonnull final String queryDescription) throws SQLException { // Nothing to do, this query is supported // SupportedVersion configs are not executed Assertions.fail("Supported version configs are not meant to be executed."); } }; } /** * Return a NoOp config - a config that does nothing. * This config can be executed but will perform no action. Use in cases where we need to continue running (e.g. * the command and config are legal and supprted) but some conditions make execution unneeded. * @param lineNumber the line number in the test file * @param executionContext the execution context for the test * @return an instance of a NoOp config */ public static QueryConfig getNoOpConfig(int lineNumber, @Nonnull YamlExecutionContext executionContext) { return new QueryConfig(QUERY_CONFIG_NO_OP, null, lineNumber, executionContext) { @SuppressWarnings("PMD.CloseResource") // lifetime of autocloseable persists beyond method @Override void checkResultInternal(@Nonnull String currentQuery, @Nonnull Object actual, @Nonnull String queryDescription) throws SQLException { // This should not be executed Assertions.fail("NoOp Config should not be executed"); } }; } @Nonnull public static List parseConfigs(String blockName, int commandLineNumber, @Nonnull List objects, @Nonnull YamlExecutionContext executionContext) { List configs = new ArrayList<>(); // After the first result config, require all future results are also result configs. That is, we should // not interleave explain, maxRows, etc., and result configurations, and the results should be the last boolean requireResults = false; for (Object object : objects) { final var configEntry = Matchers.notNull(Matchers.firstEntry(object, "query configuration"), "query configuration"); final var linedObject = CustomYamlConstructor.LinedObject.cast(configEntry.getKey(), () -> "Invalid config key-value pair: " + configEntry); final var lineNumber = linedObject.getLineNumber(); try { final var key = Matchers.notNull(Matchers.string(linedObject.getObject(), "query configuration"), "query configuration"); final var value = Matchers.notNull(configEntry, "query configuration").getValue(); boolean resultOrVersionConfig = RESULT_CONFIGS.contains(key) || VERSION_DEPENDENT_RESULT_CONFIGS.contains(key); if (requireResults && !resultOrVersionConfig) { throw new IllegalArgumentException("Only result configurations can follow first result or version specification config"); } requireResults |= resultOrVersionConfig; configs.add(parseConfig(blockName, key, value, lineNumber, executionContext)); } catch (Exception e) { throw executionContext.wrapContext(e, () -> "‼️ Error parsing the query config at line " + lineNumber, "config", lineNumber); } } validateConfigs(configs, commandLineNumber, executionContext); return configs; } private static QueryConfig getInitialVersionCheckConfig(Object key, Object value, int lineNumber, YamlExecutionContext executionContext) { try { SemanticVersion versionArgument = FileOptions.parseVersion(value); if (QUERY_CONFIG_INITIAL_VERSION_AT_LEAST.equals(key)) { return new InitialVersionCheckConfig(QueryConfig.QUERY_CONFIG_INITIAL_VERSION_AT_LEAST, value, lineNumber, executionContext, versionArgument, SemanticVersion.max()); } else if (QUERY_CONFIG_INITIAL_VERSION_LESS_THAN.equals(key)) { return new InitialVersionCheckConfig(QueryConfig.QUERY_CONFIG_INITIAL_VERSION_LESS_THAN, value, lineNumber, executionContext, SemanticVersion.min(), versionArgument); } else { throw new IllegalArgumentException("Unknown version constraint " + key); } } catch (Exception e) { throw executionContext.wrapContext(e, () -> "‼️ Unable to parse version constraint information at line " + lineNumber, "version constraint", lineNumber); } } private static QueryConfig parseConfig(String blockName, String key, Object value, int lineNumber, YamlExecutionContext executionContext) { if (QUERY_CONFIG_SUPPORTED_VERSION.equals(key)) { return getSupportedVersionConfig(value, lineNumber, executionContext); } else if (VERSION_DEPENDENT_RESULT_CONFIGS.contains(key)) { return getInitialVersionCheckConfig(key, value, lineNumber, executionContext); } else if (QUERY_CONFIG_COUNT.equals(key)) { return getCheckCountConfig(value, lineNumber, executionContext); } else if (QUERY_CONFIG_ERROR.equals(key)) { return getCheckErrorConfig(value, lineNumber, executionContext); } else if (QUERY_CONFIG_EXPLAIN.equals(key)) { if (shouldExecuteExplain(executionContext)) { return getCheckExplainConfig(true, blockName, key, value, lineNumber, executionContext); } else { return getNoOpConfig(lineNumber, executionContext); } } else if (QUERY_CONFIG_EXPLAIN_CONTAINS.equals(key)) { if (shouldExecuteExplain(executionContext)) { return getCheckExplainConfig(false, blockName, key, value, lineNumber, executionContext); } else { return getNoOpConfig(lineNumber, executionContext); } } else if (QUERY_CONFIG_PLAN_HASH.equals(key)) { if (shouldExecuteExplain(executionContext)) { return getCheckPlanHashConfig(value, lineNumber, executionContext); } else { return getNoOpConfig(lineNumber, executionContext); } } else if (QUERY_CONFIG_RESULT.equals(key)) { return getCheckResultConfig(true, key, value, lineNumber, executionContext); } else if (QUERY_CONFIG_UNORDERED_RESULT.equals(key)) { return getCheckResultConfig(false, key, value, lineNumber, executionContext); } else if (QUERY_CONFIG_MAX_ROWS.equals(key)) { return getMaxRowConfig(value, lineNumber, executionContext); } else { throw Assert.failUnchecked("‼️ '%s' is not a valid configuration"); } } private static void validateConfigs(List configs, int lineNumber, YamlExecutionContext executionContext) { Assert.thatUnchecked(configs.stream().skip(1) .noneMatch(config -> QueryConfig.QUERY_CONFIG_SUPPORTED_VERSION.equals(config.getConfigName())), "supported_version must be the first config in a query (after the query itself)"); // Validate that the results check each version comprehensively by making sure the set of // covered ranges spans the range [MIN_VERSION, MAX_VERSION) if (configs.stream().anyMatch(config -> config instanceof QueryConfig.InitialVersionCheckConfig)) { // Creating an interval set including each covered range RangeSet rangeSet = TreeRangeSet.create(); configs.stream().filter(config -> config instanceof QueryConfig.InitialVersionCheckConfig) .map(config -> (QueryConfig.InitialVersionCheckConfig)config) .forEach(config -> rangeSet.add(Range.closedOpen(config.getMinVersion(), config.getMaxVersion()))); // Get the set of uncovered ranges that span over [MIN_VERSION, MAX_VERSION) Set> uncovered = rangeSet.complement() .subRangeSet(Range.closedOpen(SemanticVersion.min(), SemanticVersion.max())) .asRanges(); if (!uncovered.isEmpty()) { IllegalArgumentException e = new IllegalArgumentException("Test case does not cover complete set of versions as it is missing: " + uncovered); throw executionContext.wrapContext(e, () -> "‼️ Non-comprehensive test case found at line " + lineNumber, "config", lineNumber); } } } public static class SkipConfig extends QueryConfig { private final String message; public SkipConfig(final String configMap, final Object value, final int lineNumber, final YamlExecutionContext executionContext, final String message) { super(configMap, value, lineNumber, executionContext); this.message = message; } @Override void checkResultInternal(@Nonnull final String currentQuery, @Nonnull final Object actual, @Nonnull final String queryDescription) throws SQLException { Assertions.fail("Skipped config should not be executed: Line: " + getLineNumber() + " " + message); } public String getMessage() { return message; } } public static class InitialVersionCheckConfig extends QueryConfig { private final SemanticVersion minVersion; private final SemanticVersion maxVersion; public InitialVersionCheckConfig(final String configName, final Object value, final int lineNumber, final YamlExecutionContext executionContext, SemanticVersion minVersion, SemanticVersion maxVersion) { super(configName, value, lineNumber, executionContext); this.minVersion = minVersion; this.maxVersion = maxVersion; } public boolean shouldExecute(YamlConnection connection) { SemanticVersion initialVersion = connection.getInitialVersion(); return initialVersion.compareTo(minVersion) >= 0 && initialVersion.compareTo(maxVersion) < 0; } @Override void checkResultInternal(@Nonnull final String currentQuery, @Nonnull final Object actual, @Nonnull final String queryDescription) throws SQLException { Assertions.fail("Check version config should not be executed: Line: " + getLineNumber()); } @Nonnull public SemanticVersion getMinVersion() { return minVersion; } @Nonnull public SemanticVersion getMaxVersion() { return maxVersion; } } private static boolean shouldExecuteExplain(final YamlExecutionContext executionContext) { return (! executionContext.getConnectionFactory().isMultiServer()); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy