|
17 | 17 | package com.google.cloud.spanner.connection;
|
18 | 18 |
|
19 | 19 | import com.google.api.core.ApiFuture;
|
| 20 | +import com.google.cloud.spanner.Dialect; |
20 | 21 | import com.google.cloud.spanner.ErrorCode;
|
21 | 22 | import com.google.cloud.spanner.Options.QueryOption;
|
22 | 23 | import com.google.cloud.spanner.ReadContext;
|
23 | 24 | import com.google.cloud.spanner.ResultSet;
|
24 | 25 | import com.google.cloud.spanner.SpannerException;
|
25 | 26 | import com.google.cloud.spanner.SpannerExceptionFactory;
|
26 | 27 | import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
|
| 28 | +import com.google.common.annotations.VisibleForTesting; |
27 | 29 | import com.google.common.base.Preconditions;
|
| 30 | +import com.google.common.collect.ImmutableList; |
28 | 31 | import com.google.spanner.v1.SpannerGrpc;
|
| 32 | +import java.util.LinkedList; |
| 33 | +import java.util.Objects; |
| 34 | +import javax.annotation.Nonnull; |
29 | 35 |
|
30 | 36 | /**
|
31 | 37 | * Base class for {@link Connection}-based transactions that can be used for multiple read and
|
32 | 38 | * read/write statements.
|
33 | 39 | */
|
34 | 40 | abstract class AbstractMultiUseTransaction extends AbstractBaseUnitOfWork {
|
35 | 41 |
|
| 42 | + /** In-memory savepoint implementation that is used by the Connection API. */ |
| 43 | + static class Savepoint { |
| 44 | + private final String name; |
| 45 | + |
| 46 | + static Savepoint of(String name) { |
| 47 | + return new Savepoint(name); |
| 48 | + } |
| 49 | + |
| 50 | + Savepoint(String name) { |
| 51 | + this.name = name; |
| 52 | + } |
| 53 | + |
| 54 | + /** Returns the index of the first statement that was executed after this savepoint. */ |
| 55 | + int getStatementPosition() { |
| 56 | + return -1; |
| 57 | + } |
| 58 | + |
| 59 | + /** Returns the index of the first mutation that was executed after this savepoint. */ |
| 60 | + int getMutationPosition() { |
| 61 | + return -1; |
| 62 | + } |
| 63 | + |
| 64 | + @Override |
| 65 | + public boolean equals(Object o) { |
| 66 | + if (!(o instanceof Savepoint)) { |
| 67 | + return false; |
| 68 | + } |
| 69 | + return Objects.equals(((Savepoint) o).name, name); |
| 70 | + } |
| 71 | + |
| 72 | + @Override |
| 73 | + public int hashCode() { |
| 74 | + return name.hashCode(); |
| 75 | + } |
| 76 | + |
| 77 | + @Override |
| 78 | + public String toString() { |
| 79 | + return name; |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + private final LinkedList<Savepoint> savepoints = new LinkedList<>(); |
| 84 | + |
36 | 85 | AbstractMultiUseTransaction(Builder<?, ? extends AbstractMultiUseTransaction> builder) {
|
37 | 86 | super(builder);
|
38 | 87 | }
|
@@ -94,4 +143,53 @@ public void abortBatch() {
|
94 | 143 | throw SpannerExceptionFactory.newSpannerException(
|
95 | 144 | ErrorCode.FAILED_PRECONDITION, "Run batch is not supported for transactions");
|
96 | 145 | }
|
| 146 | + |
| 147 | + abstract Savepoint savepoint(String name); |
| 148 | + |
| 149 | + abstract void rollbackToSavepoint(Savepoint savepoint); |
| 150 | + |
| 151 | + @VisibleForTesting |
| 152 | + ImmutableList<Savepoint> getSavepoints() { |
| 153 | + return ImmutableList.copyOf(savepoints); |
| 154 | + } |
| 155 | + |
| 156 | + @Override |
| 157 | + public void savepoint(@Nonnull String name, @Nonnull Dialect dialect) { |
| 158 | + if (dialect != Dialect.POSTGRESQL) { |
| 159 | + // Check that there is no savepoint with this name. Note that PostgreSQL allows multiple |
| 160 | + // savepoints in a transaction with the same name, so we don't execute this check for PG. |
| 161 | + if (savepoints.stream().anyMatch(savepoint -> savepoint.name.equals(name))) { |
| 162 | + throw SpannerExceptionFactory.newSpannerException( |
| 163 | + ErrorCode.INVALID_ARGUMENT, "Savepoint with name " + name + " already exists"); |
| 164 | + } |
| 165 | + } |
| 166 | + savepoints.add(savepoint(name)); |
| 167 | + } |
| 168 | + |
| 169 | + @Override |
| 170 | + public void releaseSavepoint(@Nonnull String name) { |
| 171 | + // Remove the given savepoint and all later savepoints from the transaction. |
| 172 | + savepoints.subList(getSavepointIndex(name), savepoints.size()).clear(); |
| 173 | + } |
| 174 | + |
| 175 | + @Override |
| 176 | + public void rollbackToSavepoint( |
| 177 | + @Nonnull String name, @Nonnull SavepointSupport savepointSupport) { |
| 178 | + int index = getSavepointIndex(name); |
| 179 | + rollbackToSavepoint(savepoints.get(index)); |
| 180 | + if (index < (savepoints.size() - 1)) { |
| 181 | + // Remove all savepoints that come after this savepoint from the transaction. |
| 182 | + // Rolling back to a savepoint does not remove that savepoint, only the ones that come after. |
| 183 | + savepoints.subList(index + 1, savepoints.size()).clear(); |
| 184 | + } |
| 185 | + } |
| 186 | + |
| 187 | + private int getSavepointIndex(String name) { |
| 188 | + int index = savepoints.lastIndexOf(savepoint(name)); |
| 189 | + if (index == -1) { |
| 190 | + throw SpannerExceptionFactory.newSpannerException( |
| 191 | + ErrorCode.INVALID_ARGUMENT, "Savepoint with name " + name + " does not exist"); |
| 192 | + } |
| 193 | + return index; |
| 194 | + } |
97 | 195 | }
|
0 commit comments