Skip to content

Commit 757cc42

Browse files
authored
Introduce option to setup transaction before executing queries (#3471)
This adds the ability to specific a transaction setup to yamsql. More specifically for an individual query you can have: `setup:` which will be executed at the start of every transaction for that query To avoid writing the same setup repeatedly, it also adds a new block: `transaction_setups:` which allows you to create references for the setups. Inside the query, it can now be referenced via the key, using `setupReference:`. This can be useful if you want to test with temporary functions, but still have all the benefits of randomized, and forced-continuations. This currently only supports a single query in the setup, but it wouldn't be hard to extend it to take an array.
1 parent 8cfcff6 commit 757cc42

36 files changed

+1210
-43
lines changed

scripts/YAML-SQL.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@
5353
multi_repetition_parallelized" />
5454
<keywords4 keywords="connect:;query:;load schema template:;set schema state:;result:;unorderedResult:;explain:;
5555
explainContains:;count:;error:;planHash:;setup:;schema_template:;supported_version:;test_block:;options:;tests:;
56-
maxRows:;initialVersionAtLeast:;initialVersionLessThan:;;mode:;repetition:;seed:;
57-
check_cache:;connection_lifecycle:;steps:;preset:;statement_type:;!r;!in;!a;" />
56+
maxRows:;initialVersionAtLeast:;initialVersionLessThan:;;mode:;repetition:;seed:;setup:;transaction_setups:;
57+
setupReference:;check_cache:;connection_lifecycle:;steps:;preset:;statement_type:;!r;!in;!a;" />
5858
</highlighting>
5959
<extensionMap>
6060
<mapping ext="yamsql" />

yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/CustomYamlConstructor.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.apple.foundationdb.relational.yamltests.block.FileOptions;
2626
import com.apple.foundationdb.relational.yamltests.block.SetupBlock;
2727
import com.apple.foundationdb.relational.yamltests.block.TestBlock;
28+
import com.apple.foundationdb.relational.yamltests.block.TransactionSetupsBlock;
2829
import com.apple.foundationdb.relational.yamltests.command.Command;
2930
import com.apple.foundationdb.relational.yamltests.command.QueryConfig;
3031
import org.yaml.snakeyaml.LoaderOptions;
@@ -61,6 +62,7 @@ public CustomYamlConstructor(LoaderOptions loaderOptions) {
6162
requireLineNumber.add(FileOptions.OPTIONS);
6263
requireLineNumber.add(SetupBlock.SETUP_BLOCK);
6364
requireLineNumber.add(SetupBlock.SchemaTemplateBlock.SCHEMA_TEMPLATE_BLOCK);
65+
requireLineNumber.add(TransactionSetupsBlock.TRANSACTION_SETUP);
6466
requireLineNumber.add(TestBlock.TEST_BLOCK);
6567
// commands
6668
requireLineNumber.add(Command.COMMAND_LOAD_SCHEMA_TEMPLATE);
@@ -78,6 +80,8 @@ public CustomYamlConstructor(LoaderOptions loaderOptions) {
7880
requireLineNumber.add(QueryConfig.QUERY_CONFIG_SUPPORTED_VERSION);
7981
requireLineNumber.add(QueryConfig.QUERY_CONFIG_INITIAL_VERSION_LESS_THAN);
8082
requireLineNumber.add(QueryConfig.QUERY_CONFIG_INITIAL_VERSION_AT_LEAST);
83+
requireLineNumber.add(QueryConfig.QUERY_CONFIG_SETUP);
84+
requireLineNumber.add(QueryConfig.QUERY_CONFIG_SETUP_REFERENCE);
8185
requireLineNumber.add(QueryConfig.QUERY_CONFIG_DEBUGGER);
8286
}
8387

yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/SimpleYamlConnection.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424
import com.apple.foundationdb.relational.api.RelationalConnection;
2525
import com.apple.foundationdb.relational.api.RelationalPreparedStatement;
2626
import com.apple.foundationdb.relational.api.RelationalStatement;
27+
import com.apple.foundationdb.relational.api.exceptions.RelationalException;
2728
import com.apple.foundationdb.relational.api.metrics.MetricCollector;
2829
import com.apple.foundationdb.relational.recordlayer.EmbeddedRelationalConnection;
30+
import com.apple.foundationdb.relational.yamltests.command.SQLFunction;
2931
import com.apple.foundationdb.relational.yamltests.server.SemanticVersion;
3032
import com.google.common.collect.Iterables;
3133
import org.junit.jupiter.api.Assumptions;
@@ -117,6 +119,24 @@ public SemanticVersion getInitialVersion() {
117119
return versions.get(0);
118120
}
119121

122+
@Override
123+
public <T> T executeTransactionally(final SQLFunction<YamlConnection, T> transactionalWork) throws SQLException, RelationalException {
124+
underlying.setAutoCommit(false);
125+
T result;
126+
try {
127+
result = transactionalWork.apply(this);
128+
} catch (final SQLException | RelationalException e) {
129+
underlying.rollback();
130+
throw e;
131+
} finally {
132+
// enabling autoCommit will commit if there is outstanding work
133+
// It would probably be good to commit earlier, but https://github.com/FoundationDB/fdb-record-layer/pull/3477
134+
// causes the setAutoCommit to fail if you do that
135+
underlying.setAutoCommit(true);
136+
}
137+
return result;
138+
}
139+
120140
@Override
121141
public String toString() {
122142
return connectionLabel;

yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/YamlConnection.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424
import com.apple.foundationdb.relational.api.RelationalConnection;
2525
import com.apple.foundationdb.relational.api.RelationalPreparedStatement;
2626
import com.apple.foundationdb.relational.api.RelationalStatement;
27+
import com.apple.foundationdb.relational.api.exceptions.RelationalException;
2728
import com.apple.foundationdb.relational.api.metrics.MetricCollector;
2829
import com.apple.foundationdb.relational.recordlayer.EmbeddedRelationalConnection;
30+
import com.apple.foundationdb.relational.yamltests.command.SQLFunction;
2931
import com.apple.foundationdb.relational.yamltests.server.SemanticVersion;
3032

3133
import javax.annotation.Nonnull;
@@ -104,4 +106,6 @@ public interface YamlConnection extends AutoCloseable {
104106
*/
105107
@Nonnull
106108
SemanticVersion getInitialVersion();
109+
110+
<T> T executeTransactionally(SQLFunction<YamlConnection, T> transactionalWork) throws SQLException, RelationalException;
107111
}

yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/YamlExecutionContext.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public final class YamlExecutionContext {
8484
private final List<String> connectionURIs = new ArrayList<>();
8585
// Additional options that can be set by the runners to impact test execution
8686
private final ContextOptions additionalOptions;
87+
private final Map<String, String> transactionSetups = new HashMap<>();
8788

8889
public static class YamlExecutionError extends RuntimeException {
8990

@@ -248,6 +249,20 @@ public URI inferConnectionURI(@Nullable Object connectObject) {
248249
}
249250
}
250251

252+
public void registerTransactionSetup(final String name, final String command) {
253+
// Note: at the time of writing, this is only called by code that is iterating over a Map from yaml, so it will
254+
// not prevent two entries in the yaml file itself
255+
Assert.thatUnchecked(!transactionSetups.containsKey(name), ErrorCode.INTERNAL_ERROR,
256+
() -> "Transaction setup " + name + " is defined multiple times.");
257+
transactionSetups.put(name, command);
258+
}
259+
260+
public String getTransactionSetup(final Object name) {
261+
return Matchers.notNull(
262+
transactionSetups.get(Matchers.string(name, "setup reference")),
263+
"transaction setup " + name + " is not defined");
264+
}
265+
251266
@Nonnull
252267
public List<Block> getFinalizeBlocks() {
253268
return finalizeBlocks;

yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/YamlRunner.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ public void run() throws Exception {
7272
LoaderOptions loaderOptions = new LoaderOptions();
7373
loaderOptions.setAllowDuplicateKeys(true);
7474
DumperOptions dumperOptions = new DumperOptions();
75-
final var yaml = new Yaml(new CustomYamlConstructor(loaderOptions), new Representer(dumperOptions), new DumperOptions(), loaderOptions, new Resolver());
75+
final var yaml = new Yaml(new CustomYamlConstructor(loaderOptions), new Representer(dumperOptions),
76+
new DumperOptions(), loaderOptions, new Resolver());
7677

7778
final var testBlocks = new ArrayList<TestBlock>();
7879
int blockNumber = 0;

yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/block/Block.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
* </ul>
4242
*/
4343
public interface Block {
44+
4445
/**
4546
* Looks at the block to determine if its one of the valid blocks. If it is a valid one, parses it to that. This
4647
* method dispatches the execution to the right block which takes care of reading from the block and initializing
@@ -62,12 +63,14 @@ static Block parse(@Nonnull Object document, int blockNumber, @Nonnull YamlExecu
6263
switch (blockKey) {
6364
case SetupBlock.SETUP_BLOCK:
6465
return SetupBlock.ManualSetupBlock.parse(lineNumber, entry.getValue(), executionContext);
66+
case TransactionSetupsBlock.TRANSACTION_SETUP:
67+
return TransactionSetupsBlock.parse(lineNumber, entry.getValue(), executionContext);
6568
case TestBlock.TEST_BLOCK:
6669
return TestBlock.parse(blockNumber, lineNumber, entry.getValue(), executionContext);
6770
case SetupBlock.SchemaTemplateBlock.SCHEMA_TEMPLATE_BLOCK:
6871
return SetupBlock.SchemaTemplateBlock.parse(lineNumber, entry.getValue(), executionContext);
6972
case FileOptions.OPTIONS:
70-
Assert.thatUnchecked(blockNumber == 0,
73+
Assert.that(blockNumber == 0,
7174
"File level options must be the first block, but found one at line " + lineNumber);
7275
return FileOptions.parse(lineNumber, entry.getValue(), executionContext);
7376
default:
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* TransactionSetupsBlock.java
3+
*
4+
* This source file is part of the FoundationDB open source project
5+
*
6+
* Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
21+
package com.apple.foundationdb.relational.yamltests.block;
22+
23+
import com.apple.foundationdb.relational.yamltests.Matchers;
24+
import com.apple.foundationdb.relational.yamltests.YamlExecutionContext;
25+
26+
import java.util.Map;
27+
28+
public class TransactionSetupsBlock {
29+
public static final String TRANSACTION_SETUP = "transaction_setups";
30+
31+
public static Block parse(final int lineNumber,
32+
final Object document,
33+
final YamlExecutionContext executionContext) {
34+
final Map<?, ?> map = Matchers.map(document);
35+
for (final Map.Entry<?, ?> entry : map.entrySet()) {
36+
final String transactionSetupName = Matchers.string(entry.getKey(), "transaction setup name");
37+
final String transactionSetupCommand = Matchers.string(entry.getValue(), "transaction setup command");
38+
executionContext.registerTransactionSetup(transactionSetupName, transactionSetupCommand);
39+
}
40+
return new NoOpBlock(lineNumber);
41+
}
42+
43+
}

yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/command/QueryCommand.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,18 @@ private void executeInternal(@Nonnull final YamlConnection connection, boolean c
207207
} else if (QueryConfig.QUERY_CONFIG_NO_OP.equals(queryConfig.getConfigName())) {
208208
// Do nothing for noop execution.
209209
continue;
210+
} else if (QueryConfig.QUERY_CONFIG_SETUP.equals(queryConfig.getConfigName())) {
211+
Assert.that(!queryIsRunning, "Transaction setup should not be intermingled with query results");
212+
final String setupStatement = Matchers.notNull(Matchers.string(Matchers.notNull(queryConfig.getVal(),
213+
"Setup Config Val"), "Transaction setup"), "Transaction setup");
214+
// we restrict transaction setups to CREATE TEMPORARY FUNCTION, because other mutations could be hard
215+
// to reason about when running in a parallel world. It's possible the right answer is that we shouldn't
216+
// commit after the query, or that we shouldn't allow things that modify state in the database. This will
217+
// become clearer as we have more related tests, and for now, just try to stop people from being confused.
218+
final String allowedStatement = "CREATE TEMPORARY FUNCTION";
219+
Assert.that(setupStatement.regionMatches(true, 0, allowedStatement, 0, allowedStatement.length()),
220+
"Only \"CREATE TEMPORARY FUNCTION\" is allowed for transaction setups");
221+
executor.addSetup(setupStatement);
210222
} else if (!QueryConfig.QUERY_CONFIG_SUPPORTED_VERSION.equals(queryConfig.getConfigName()) &&
211223
!QueryConfig.QUERY_CONFIG_DEBUGGER.equals(queryConfig.getConfigName())) {
212224
if (QueryConfig.QUERY_CONFIG_ERROR.equals(queryConfig.getConfigName())) {

yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/command/QueryConfig.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ public abstract class QueryConfig {
8585
public static final String QUERY_CONFIG_INITIAL_VERSION_AT_LEAST = "initialVersionAtLeast";
8686
public static final String QUERY_CONFIG_INITIAL_VERSION_LESS_THAN = "initialVersionLessThan";
8787
public static final String QUERY_CONFIG_NO_OP = "noOp";
88+
public static final String QUERY_CONFIG_SETUP = "setup";
89+
public static final String QUERY_CONFIG_SETUP_REFERENCE = "setupReference";
8890
public static final String QUERY_CONFIG_DEBUGGER = "debugger";
8991

9092
private static final Set<String> RESULT_CONFIGS = ImmutableSet.of(QUERY_CONFIG_ERROR, QUERY_CONFIG_COUNT, QUERY_CONFIG_RESULT, QUERY_CONFIG_UNORDERED_RESULT);
@@ -466,6 +468,16 @@ void checkResultInternal(@Nonnull String currentQuery, @Nonnull Object actual, @
466468
};
467469
}
468470

471+
private static QueryConfig getSetupConfig(final Object value, final int lineNumber, final YamlExecutionContext executionContext) {
472+
return new QueryConfig(QUERY_CONFIG_SETUP, value, lineNumber, executionContext) {
473+
@Override
474+
void checkResultInternal(@Nonnull final String currentQuery, @Nonnull final Object actual,
475+
@Nonnull final String queryDescription) throws SQLException {
476+
Assert.failUnchecked("No results to check on a setup config");
477+
}
478+
};
479+
}
480+
469481
private static QueryConfig getDebuggerConfig(@Nonnull Object value, int lineNumber, @Nonnull YamlExecutionContext executionContext) {
470482
return new QueryConfig(QUERY_CONFIG_DEBUGGER, DebuggerImplementation.valueOf(((String)value).toUpperCase(Locale.ROOT)),
471483
lineNumber, executionContext) {
@@ -590,10 +602,14 @@ private static QueryConfig parseConfig(String blockName, String key, Object valu
590602
return getCheckResultConfig(false, key, value, lineNumber, executionContext);
591603
} else if (QUERY_CONFIG_MAX_ROWS.equals(key)) {
592604
return getMaxRowConfig(value, lineNumber, executionContext);
605+
} else if (QUERY_CONFIG_SETUP.equals(key)) {
606+
return getSetupConfig(value, lineNumber, executionContext);
607+
} else if (QUERY_CONFIG_SETUP_REFERENCE.equals(key)) {
608+
return getSetupConfig(executionContext.getTransactionSetup(value), lineNumber, executionContext);
593609
} else if (QUERY_CONFIG_DEBUGGER.equals(key)) {
594610
return getDebuggerConfig(value, lineNumber, executionContext);
595611
} else {
596-
throw Assert.failUnchecked("‼️ '%s' is not a valid configuration");
612+
throw Assert.failUnchecked("‼️ '" + key + "' is not a valid configuration");
597613
}
598614
}
599615

0 commit comments

Comments
 (0)