Добавить версионирование схемы БД и заблокировать прод-очистку БД

This commit is contained in:
AidarKC 2026-04-26 00:32:10 +03:00
parent e764a713c4
commit c8fa4a01a1
4 changed files with 111 additions and 37 deletions

View File

@ -1,2 +1,2 @@
client.version=1.2.2 client.version=1.2.3
server.version=1.2.2 server.version=1.2.3

View File

@ -191,25 +191,20 @@ tasks.register('deployServer', JavaExec) {
dependsOn testClasses dependsOn testClasses
} }
tasks.register('deployServerWithBackupCleanAndTests', JavaExec) { tasks.register('deployServerWithBackupCleanAndTests') {
group = "!!deployment" group = "!!deployment"
description = "DANGER: deploy + backup data + clean data + restart + run IT tests (с обязательным подтверждением)" description = "BLOCKED: удаление БД на проде запрещено, используйте только миграции"
classpath = sourceSets.test.runtimeClasspath doLast {
mainClass = "test.it.IT_DeployBackupCleanAndRunRemoteMain" def msg = """
[BLOCKED] Удаление базы данных на продакшен-сервере отключено.
dependsOn shadowJar Причина: в базе уже есть пользовательские сообщения.
systemProperty "it.remoteHost", System.getProperty("it.remoteHost", "194.87.0.247") Дальше используйте только миграции схемы БД.
systemProperty "it.remoteUser", System.getProperty("it.remoteUser", "user") Задача остановлена намеренно.
systemProperty "it.remoteDir", System.getProperty("it.remoteDir", "/home/user/docker/shine-server") """.stripIndent().trim()
systemProperty "it.remoteDataDir", System.getProperty("it.remoteDataDir", "/home/user/docker/shine-server/data") println msg
systemProperty "it.remoteBackupDir", System.getProperty("it.remoteBackupDir", "/home/user/docker/shine-server/backup") throw new GradleException(msg)
systemProperty "it.service", System.getProperty("it.service", "shine-server") }
systemProperty "it.localJar", System.getProperty("it.localJar", "build/libs/shine-server.jar")
systemProperty "it.wsUri", System.getProperty("it.wsUri", "wss://shineup.me/ws")
standardInput = System.in
dependsOn testClasses
} }
tasks.register('deployServerNoCleanNoTests', JavaExec) { tasks.register('deployServerNoCleanNoTests', JavaExec) {

View File

@ -24,6 +24,9 @@ import java.sql.Statement;
*/ */
public final class DatabaseInitializer { public final class DatabaseInitializer {
public static final String DB_SCHEMA_VERSION_TABLE = "db_schema_version";
public static final int SCHEMA_VERSION_1 = 1;
private DatabaseInitializer() {} private DatabaseInitializer() {}
/* ===================== TEXT (msg_type=1) ===================== */ /* ===================== TEXT (msg_type=1) ===================== */
@ -101,7 +104,15 @@ public final class DatabaseInitializer {
} }
} }
public static void ensureSchemaV1Structure(String jdbcUrl) throws SQLException {
createSchema(jdbcUrl, false);
}
private static void createSchema(String jdbcUrl) throws SQLException { private static void createSchema(String jdbcUrl) throws SQLException {
createSchema(jdbcUrl, true);
}
private static void createSchema(String jdbcUrl, boolean initializeVersionRow) throws SQLException {
try { try {
Class.forName("org.sqlite.JDBC"); Class.forName("org.sqlite.JDBC");
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
@ -586,6 +597,24 @@ public final class DatabaseInitializer {
ON signed_message_session_delivery (session_id, delivered); ON signed_message_session_delivery (session_id, delivered);
"""); """);
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS db_schema_version (
id INTEGER NOT NULL PRIMARY KEY CHECK (id = 1),
schema_version INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL
);
""");
if (initializeVersionRow) {
st.executeUpdate("""
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
VALUES (1, 1, CAST(strftime('%s','now') AS INTEGER) * 1000)
ON CONFLICT(id) DO UPDATE SET
schema_version = excluded.schema_version,
updated_at_ms = excluded.updated_at_ms;
""");
}
DatabaseTriggersInstaller.createAllTriggers(st); DatabaseTriggersInstaller.createAllTriggers(st);
} }
} }

View File

@ -14,6 +14,7 @@ import java.sql.Statement;
public final class SqliteDbController { public final class SqliteDbController {
private static volatile SqliteDbController instance; private static volatile SqliteDbController instance;
private static final int LATEST_SCHEMA_VERSION = DatabaseInitializer.SCHEMA_VERSION_1;
private final String jdbcUrl; private final String jdbcUrl;
@ -38,7 +39,7 @@ public final class SqliteDbController {
} }
this.jdbcUrl = "jdbc:sqlite:" + dbPath; this.jdbcUrl = "jdbc:sqlite:" + dbPath;
ensureSchemaUpgrades(); ensureSchemaMigrations();
} }
public static SqliteDbController getInstance() { public static SqliteDbController getInstance() {
@ -70,42 +71,91 @@ public final class SqliteDbController {
// no-op // no-op
} }
private void ensureSchemaUpgrades() { private void ensureSchemaMigrations() {
int currentVersion = getCurrentSchemaVersion();
while (currentVersion < LATEST_SCHEMA_VERSION) {
int nextVersion = currentVersion + 1;
applyMigration(nextVersion);
currentVersion = nextVersion;
}
}
private void applyMigration(int targetVersion) {
switch (targetVersion) {
case 1 -> migrateToV1();
default -> throw new RuntimeException("Unknown DB migration target version: " + targetVersion);
}
}
private void migrateToV1() {
try {
DatabaseInitializer.ensureSchemaV1Structure(jdbcUrl);
} catch (SQLException e) {
throw new RuntimeException("DB migration to v1 failed (base schema)", e);
}
try (Connection c = DriverManager.getConnection(jdbcUrl); try (Connection c = DriverManager.getConnection(jdbcUrl);
Statement st = c.createStatement()) { Statement st = c.createStatement()) {
c.setAutoCommit(false); c.setAutoCommit(false);
try { try {
st.execute("PRAGMA foreign_keys = OFF"); st.execute("PRAGMA foreign_keys = OFF");
ensureReactionsStateTable(st); if (tableExists(c, "connections_state") && needsConnectionsStateUpgrade(c)) {
ensureMessageViewsStateTable(st);
if (!tableExists(c, "connections_state")) {
createConnectionsStateTable(st);
} else if (needsConnectionsStateUpgrade(c)) {
rebuildConnectionsStateTable(st); rebuildConnectionsStateTable(st);
} }
ensureChannelNamesStateTable(st);
ensureChannelNamesDescriptionColumn(c, st);
ensureConnectionsIndexes(st);
ensureReactionsIndexes(st);
ensureMessageViewsIndexes(st);
ensureChannelNamesIndexes(st);
ensureSignedMessageReceiptUniq(c, st);
ensureChannelNamesDescriptionColumn(c, st);
ensureSignedMessageReceiptUniq(c, st);
DatabaseTriggersInstaller.createAllTriggers(st); DatabaseTriggersInstaller.createAllTriggers(st);
setSchemaVersion(c, 1);
st.execute("PRAGMA foreign_keys = ON"); st.execute("PRAGMA foreign_keys = ON");
c.commit(); c.commit();
} catch (Exception e) { } catch (Exception e) {
try { c.rollback(); } catch (Exception ignored) {} try { c.rollback(); } catch (Exception ignored) {}
throw new RuntimeException("DB schema upgrade failed", e); throw new RuntimeException("DB migration to v1 failed", e);
} finally { } finally {
try { c.setAutoCommit(true); } catch (Exception ignored) {} try { c.setAutoCommit(true); } catch (Exception ignored) {}
} }
} catch (SQLException e) { } catch (SQLException e) {
throw new RuntimeException("DB schema upgrade failed", e); throw new RuntimeException("DB migration to v1 failed", e);
}
}
private int getCurrentSchemaVersion() {
try (Connection c = DriverManager.getConnection(jdbcUrl)) {
if (!tableExists(c, DatabaseInitializer.DB_SCHEMA_VERSION_TABLE)) {
return 0;
}
try (var ps = c.prepareStatement("""
SELECT schema_version
FROM db_schema_version
WHERE id = 1
LIMIT 1
""");
ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return rs.getInt(1);
}
}
return 0;
} catch (SQLException e) {
throw new RuntimeException("Cannot read DB schema version", e);
}
}
private static void setSchemaVersion(Connection c, int version) throws SQLException {
try (var ps = c.prepareStatement("""
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
VALUES (1, ?, CAST(strftime('%s','now') AS INTEGER) * 1000)
ON CONFLICT(id) DO UPDATE SET
schema_version = excluded.schema_version,
updated_at_ms = excluded.updated_at_ms
""")) {
ps.setInt(1, version);
ps.executeUpdate();
} }
} }