Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement restore points #4169

Closed
Show file tree
Hide file tree
Changes from 1 commit
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
16 changes: 16 additions & 0 deletions h2/src/docsrc/help/information_schema.csv
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ Contains statistics of queries when query statistics gathering is enabled.
Contains additional information about referential constraints.
"

"RESTORE_POINTS",,"
Contains information about restore points.
"

"RIGHTS",,"
Contains information about granted rights and roles.
"
Expand Down Expand Up @@ -662,6 +666,18 @@ The rule for UPDATE in referenced table ('RESTRICT', 'CASCADE', 'SET DEFAULT', o
The rule for DELETE in referenced table ('RESTRICT', 'CASCADE', 'SET DEFAULT', or 'SET NULL').
"

"RESTORE_POINTS",RESTORE_POINT_NAME,"
The name of a restore point.
"

"RESTORE_POINTS",CREATED_AT,"
The timestamp at which a restore point has been created.
"

"RESTORE_POINTS",DATABASE_VERSION,"
The database version a particular restore point references.
"

"RIGHTS","GRANTEETYPE","
'USER' if grantee is a user, 'ROLE' if grantee is a role.
"
Expand Down
20 changes: 19 additions & 1 deletion h2/src/main/org/h2/api/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -2261,7 +2261,25 @@ public class ErrorCode {
*/
public static final int GROUP_BY_NOT_IN_THE_RESULT = 90157;

// next is 90158
/**
* The error with code <code>90158</code> is thrown when a restore point is
* being created, but a restore point with the given name already exists.
*/
public static final int RESTORE_POINT_ALREADY_EXISTS = 90158;

/**
* The error with code <code>90159</code> is thrown when a restore point
* is being referenced that does not exist.
*/
public static final int RESTORE_POINT_NOT_FOUND = 90159;

/**
* The error with code <code>90160</code> is thrown when a restore point is being
* returned to via RESTORE TO POINT, but switching the database to exclusive mode failed.
*/
public static final int COULD_NOT_SWITCH_DATABASE_TO_EXCLUSIVE_MODE = 90160;
i-am-not-giving-my-name-to-a-machine marked this conversation as resolved.
Show resolved Hide resolved

// next is 90161

private ErrorCode() {
// utility class
Expand Down
15 changes: 15 additions & 0 deletions h2/src/main/org/h2/command/CommandInterface.java
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,21 @@ public interface CommandInterface extends AutoCloseable {
*/
int ALTER_TYPE = 105;

/**
* The type of a CREATE RESTORE POINT statement.
*/
int CREATE_RESTORE_POINT = 106;

/**
* The type of a RESTORE TO POINT statement.
*/
int RESTORE_TO_POINT = 107;

/**
* The type of a DROP RESTORE POINT statement.
*/
int DROP_RESTORE_POINT = 108;

/**
* Get command type.
*
Expand Down
24 changes: 24 additions & 0 deletions h2/src/main/org/h2/command/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@
import org.h2.command.dml.Merge;
import org.h2.command.dml.MergeUsing;
import org.h2.command.dml.NoOperation;
import org.h2.command.dml.RestorePointCommand;
import org.h2.command.dml.RunScriptCommand;
import org.h2.command.dml.ScriptCommand;
import org.h2.command.dml.Set;
Expand Down Expand Up @@ -722,6 +723,8 @@ private Prepared parsePrepared() {
c = parseReplace(start);
} else if (readIf("REFRESH")) {
c = parseRefresh(start);
} else if (readIf("RESTORE")) {
c = parseRestoreTo();
}
break;
case 'S':
Expand Down Expand Up @@ -1653,6 +1656,12 @@ private Merge parseReplace(int start) {
return command;
}

private RestorePointCommand parseRestoreTo() {
read("TO");
read("POINT");
return new RestorePointCommand(session, CommandInterface.RESTORE_TO_POINT, readIdentifier());
}

/**
* REFRESH MATERIALIZED VIEW
*/
Expand Down Expand Up @@ -2218,6 +2227,8 @@ private Prepared parseDrop() {
ifExists = readIfExists(ifExists);
command.setIfExists(ifExists);
return command;
} else if (readIf("RESTORE")) {
return parseDropRestorePoint();
}
throw getSyntaxError();
}
Expand Down Expand Up @@ -2246,6 +2257,11 @@ private DropAggregate parseDropAggregate() {
return command;
}

private RestorePointCommand parseDropRestorePoint() {
read("POINT");
return new RestorePointCommand(session, CommandInterface.DROP_RESTORE_POINT, readIdentifier());
}

private TableFilter readTableReference() {
for (TableFilter top, last = top = readTablePrimary(), join;; last = join) {
switch (currentTokenType) {
Expand Down Expand Up @@ -6340,6 +6356,9 @@ private Prepared parseCreate() {
if (readIf(OR, "REPLACE")) {
orReplace = true;
}
if (readIf("RESTORE")) {
return parseCreateRestorePoint();
}
boolean force = readIf("FORCE");
if (readIf("VIEW")) {
return parseCreateView(force, orReplace);
Expand Down Expand Up @@ -6633,6 +6652,11 @@ private Call parseCall() {
return command;
}

private RestorePointCommand parseCreateRestorePoint() {
read("POINT");
return new RestorePointCommand(session, CommandInterface.CREATE_RESTORE_POINT, readIdentifier());
}

private CreateRole parseCreateRole() {
CreateRole command = new CreateRole(session);
command.setIfNotExists(readIfNotExists());
Expand Down
158 changes: 158 additions & 0 deletions h2/src/main/org/h2/command/dml/RestorePointCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* Copyright 2004-2024 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (https://h2database.com/html/license.html).
* Initial Developer: Enno Thieleke
*/
package org.h2.command.dml;

import org.h2.api.ErrorCode;
import org.h2.command.CommandInterface;
import org.h2.command.Prepared;
import org.h2.engine.Database;
import org.h2.engine.SessionLocal;
import org.h2.message.DbException;
import org.h2.mvstore.MVStore;
import org.h2.mvstore.db.Store;
import org.h2.mvstore.tx.TransactionStore;
import org.h2.mvstore.type.RestorePoint;
import org.h2.result.ResultInterface;
import org.h2.value.ValueBigint;
import org.h2.value.ValueTimestampTimeZone;

/**
* This class represents the statements for dealing with restore points.
*/
public class RestorePointCommand extends Prepared {

private final int type;
private final String name;

public RestorePointCommand(SessionLocal session, int type, String name) {
super(session);
this.type = type;
this.name = name;
}

@Override
public long update() {
session.getUser().checkAdmin();
switch (type) {
case CommandInterface.CREATE_RESTORE_POINT:
createRestorePoint();
break;
case CommandInterface.RESTORE_TO_POINT:
restoreToPoint();
break;
case CommandInterface.DROP_RESTORE_POINT:
dropRestorePoint();
break;
default: throw DbException.getInternalError("type=" + type);
i-am-not-giving-my-name-to-a-machine marked this conversation as resolved.
Show resolved Hide resolved
}
return 0;
}

private void createRestorePoint() {
Database db = getDatabase();
MVStore mvStore = db.getStore().getMvStore();
mvStore.lock();
int autoCommitDelay = mvStore.getAutoCommitDelay();
try {
if (mvStore.findRestorePoint(name) != null) {
throw DbException.get(ErrorCode.RESTORE_POINT_ALREADY_EXISTS, name);
}
mvStore.setAutoCommitDelay(0);
session.commit(false); // Commit pending changes.
ValueTimestampTimeZone createdAt = db.currentTimestamp();
ValueBigint databaseVersion = ValueBigint.get(mvStore.getCurrentVersion() + 1);
// The oldest database version to keep across all restore points is always the same.
long temp = mvStore.getOldestRestorePointVersion();
ValueBigint oldestDatabaseVersionToKeep;
if (temp == Long.MAX_VALUE) {
oldestDatabaseVersionToKeep = databaseVersion;
} else {
oldestDatabaseVersionToKeep = ValueBigint.get(temp);
}
RestorePoint rp = new RestorePoint(name, createdAt, oldestDatabaseVersionToKeep, databaseVersion);
mvStore.addRestorePoint(rp.getDatabaseVersion().getLong(), rp);
mvStore.commit(); // Flush changes to disk.
db.getNextModificationMetaId(); // To invalidate previous results of selects from restore points.
assert databaseVersion.getLong() == mvStore.getCurrentVersion();
} finally {
mvStore.setAutoCommitDelay(autoCommitDelay);
mvStore.unlock();
}
}

private void restoreToPoint() {
Database db = getDatabase();
try {
if (!db.setExclusiveSession(session, true)) {
throw DbException.get(ErrorCode.COULD_NOT_SWITCH_DATABASE_TO_EXCLUSIVE_MODE);
}
RestorePoint rp = db.getStore().getMvStore().findRestorePoint(name);
if (rp == null) {
throw DbException.get(ErrorCode.RESTORE_POINT_NOT_FOUND, name);
}
session.rollback(); // Discard the implicitly started transaction.
Store store = db.getStore();
MVStore mvStore = store.getMvStore();
mvStore.lock();
int autoCommitDelay = mvStore.getAutoCommitDelay();
try {
mvStore.setAutoCommitDelay(0);
mvStore.rollbackTo(rp.getDatabaseVersion().getLong());

TransactionStore txStore = store.getTransactionStore();
txStore.reinit(); // This reads any leftover transactions back into the store...
txStore.endLeftoverTransactions(); // ...and this ends them (i.e. removes unwanted data).

/* This might have brought back restore points that have been deleted.
* Or it might have removed restore points, which hadn't been created at the point we're returning to.
*/
db.executeMeta(session);
} finally {
mvStore.setAutoCommitDelay(autoCommitDelay);
mvStore.unlock();
}
} finally {
db.unsetExclusiveSession(session);
}
}

private void dropRestorePoint() {
Database db = getDatabase();
MVStore mvStore = db.getStore().getMvStore();
mvStore.lock();
try {
RestorePoint rp = mvStore.findRestorePoint(name);
if (rp == null) {
throw DbException.get(ErrorCode.RESTORE_POINT_NOT_FOUND, name);
}
mvStore.removeRestorePoint(rp.getDatabaseVersion().getLong());
session.commit(false);
db.getNextModificationMetaId(); // To invalidate previous results of selects from restore points.
} finally {
mvStore.unlock();
}
}

@Override
public boolean isTransactional() {
return false;
}

@Override
public boolean needRecompile() {
return false;
}

@Override
public ResultInterface queryMeta() {
return null;
}

@Override
public int getType() {
return type;
}
}
Loading
Loading