SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java
AidarKC e2b89da2fa 08 01 25
Вынес константы, начал переделаывать тесты
2026-01-08 13:24:55 +03:00

435 lines
18 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package shine.db;
import utils.config.AppConfig;
import utils.config.MsgSubType;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.*;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
/**
* DatabaseInitializer — создание новой SQLite-БД по схеме SHiNE.
*
* Таблицы:
* - solana_users
* - active_sessions
* - users_params
* - ip_geo_cache
* - blockchain_state
* - blocks
* - connections_state
* - message_stats
*/
public class DatabaseInitializer {
public static void createNewDB(String[] args) {
AppConfig config = AppConfig.getInstance();
String dbPath = config.getParam("db.path");
if (dbPath == null || dbPath.isBlank()) {
System.err.println("Параметр db.path не задан в application.properties");
return;
}
Path dbFile = Paths.get(dbPath);
try {
Path parent = dbFile.getParent();
if (parent != null && !Files.exists(parent)) {
Files.createDirectories(parent);
}
if (Files.exists(dbFile)) {
System.out.println("Файл базы данных уже существует: " + dbFile.toAbsolutePath());
System.out.print("Пересоздать БД (СТАРАЯ БУДЕТ УДАЛЕНА)? [y/N]: ");
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String answer = reader.readLine();
if (!"y".equalsIgnoreCase(answer) && !"yes".equalsIgnoreCase(answer)) {
System.out.println("Операция отменена. БД не изменена.");
return;
}
Files.delete(dbFile);
System.out.println("Старый файл БД удалён.");
}
createSchema("jdbc:sqlite:" + dbPath);
System.out.println("Новая БД успешно создана по пути: " + dbFile.toAbsolutePath());
} catch (IOException e) {
System.err.println("Ошибка работы с файлом БД: " + e.getMessage());
} catch (SQLException e) {
System.err.println("Ошибка создания схемы БД: " + e.getMessage());
}
}
private static void createSchema(String jdbcUrl) throws SQLException {
try {
Class.forName("org.sqlite.JDBC");
} catch (ClassNotFoundException e) {
throw new RuntimeException("SQLite JDBC driver not found", e);
}
try (Connection conn = DriverManager.getConnection(jdbcUrl);
Statement st = conn.createStatement()) {
st.execute("PRAGMA foreign_keys = ON");
// 1. solana_users
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS solana_users (
login TEXT NOT NULL PRIMARY KEY,
device_key TEXT NOT NULL,
solana_key TEXT
);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_solana_users_login
ON solana_users (login);
""");
// 2. active_sessions
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS active_sessions (
session_id TEXT NOT NULL PRIMARY KEY,
login TEXT NOT NULL,
session_pwd TEXT NOT NULL,
storage_pwd TEXT NOT NULL,
session_created_at_ms INTEGER NOT NULL,
last_authirificated_at_ms INTEGER NOT NULL,
push_endpoint TEXT,
push_p256dh_key TEXT,
push_auth_key TEXT,
client_ip TEXT,
client_info_from_client TEXT,
client_info_from_request TEXT,
user_language TEXT,
FOREIGN KEY (login) REFERENCES solana_users(login)
);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_active_sessions_login
ON active_sessions (login);
""");
// 3. users_params
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS users_params (
login TEXT NOT NULL,
param TEXT NOT NULL,
time_ms INTEGER NOT NULL,
value TEXT NOT NULL,
device_key TEXT,
signature TEXT,
FOREIGN KEY (login) REFERENCES solana_users(login),
UNIQUE (login, param)
);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_users_params_login
ON users_params (login);
""");
// 4. ip_geo_cache
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS ip_geo_cache (
ip TEXT NOT NULL PRIMARY KEY,
geo TEXT,
updated_at_ms INTEGER NOT NULL
);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_ip_geo_cache_updated_at
ON ip_geo_cache (updated_at_ms);
""");
// 5. blockchain_state (хэши -> BLOB NULLABLE)
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS blockchain_state (
blockchain_name TEXT NOT NULL PRIMARY KEY,
login TEXT NOT NULL,
blockchain_key TEXT NOT NULL,
size_limit INTEGER NOT NULL,
file_size_bytes INTEGER NOT NULL,
last_global_number INTEGER NOT NULL,
last_global_hash BLOB,
updated_at_ms INTEGER NOT NULL,
line0_last_number INTEGER NOT NULL,
line0_last_hash BLOB,
line1_last_number INTEGER NOT NULL,
line1_last_hash BLOB,
line2_last_number INTEGER NOT NULL,
line2_last_hash BLOB,
line3_last_number INTEGER NOT NULL,
line3_last_hash BLOB,
line4_last_number INTEGER NOT NULL,
line4_last_hash BLOB,
line5_last_number INTEGER NOT NULL,
line5_last_hash BLOB,
line6_last_number INTEGER NOT NULL,
line6_last_hash BLOB,
line7_last_number INTEGER NOT NULL,
line7_last_hash BLOB,
FOREIGN KEY (login) REFERENCES solana_users(login)
);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_blockchain_state_login
ON blockchain_state (login);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_blockchain_state_updated_at
ON blockchain_state (updated_at_ms);
""");
// 6. blocks (хэши/подпись -> BLOB, + edited_by_block_global_number)
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS blocks (
login TEXT NOT NULL,
bch_name TEXT NOT NULL,
block_global_number INTEGER NOT NULL,
block_global_pre_hashe BLOB NOT NULL,
block_line_index INTEGER NOT NULL,
block_line_number INTEGER NOT NULL,
block_line_pre_hashe BLOB NOT NULL,
msg_type INTEGER NOT NULL,
msg_sub_type INTEGER NOT NULL,
block_byte BLOB,
-- Ссылка на целевой блок (для reply/like/edit и т.д.)
to_login TEXT,
to_bch_name TEXT,
to_block_global_number INTEGER,
to_block_hashe BLOB,
-- Собственные данные блока (по просьбе)
block_hash BLOB NOT NULL,
block_signature BLOB NOT NULL,
-- Последний edit, который изменил этот блок (NULL если не редактировали)
edited_by_block_global_number INTEGER,
FOREIGN KEY (login) REFERENCES solana_users(login),
FOREIGN KEY (bch_name) REFERENCES blockchain_state(blockchain_name)
);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_blocks_chain_global
ON blocks (login, bch_name, block_global_number);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_blocks_to_target
ON blocks (to_login, to_bch_name, to_block_global_number);
""");
// 7) connections_state (to_block_hashe -> BLOB)
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS connections_state (
login TEXT NOT NULL,
rel_type INTEGER NOT NULL,
to_login TEXT NOT NULL,
to_bch_name TEXT NOT NULL,
to_block_global_number INTEGER,
to_block_hashe BLOB,
FOREIGN KEY (login) REFERENCES solana_users(login),
UNIQUE (login, rel_type, to_login)
);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_connections_state_login
ON connections_state (login);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_connections_state_to_login
ON connections_state (to_login);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_connections_state_pair
ON connections_state (login, to_login);
""");
// 8) Trigger: connection state
st.executeUpdate("""
CREATE TRIGGER IF NOT EXISTS trg_blocks_connection_state_ai
AFTER INSERT ON blocks
WHEN NEW.msg_type = 3
BEGIN
INSERT INTO connections_state (
login, rel_type, to_login, to_bch_name, to_block_global_number, to_block_hashe
)
SELECT
NEW.login,
NEW.msg_sub_type,
NEW.to_login,
NEW.to_bch_name,
NEW.to_block_global_number,
NEW.to_block_hashe
WHERE NEW.msg_sub_type IN (%d, %d, %d)
AND NEW.to_login IS NOT NULL
AND NEW.to_bch_name IS NOT NULL
ON CONFLICT(login, rel_type, to_login)
DO UPDATE SET
to_bch_name = excluded.to_bch_name,
to_block_global_number = excluded.to_block_global_number,
to_block_hashe = excluded.to_block_hashe;
DELETE FROM connections_state
WHERE login = NEW.login
AND to_login = NEW.to_login
AND rel_type = CASE NEW.msg_sub_type
WHEN %d THEN %d
WHEN %d THEN %d
WHEN %d THEN %d
ELSE rel_type
END
AND NEW.msg_sub_type IN (%d, %d, %d);
END;
""".formatted(
(int) MsgSubType.CONNECTION_FRIEND,
(int) MsgSubType.CONNECTION_FOLLOW,
(int) MsgSubType.CONNECTION_BLOCK,
(int) MsgSubType.CONNECTION_UNFRIEND, (int) MsgSubType.CONNECTION_FRIEND,
(int) MsgSubType.CONNECTION_UNFOLLOW, (int) MsgSubType.CONNECTION_FOLLOW,
(int) MsgSubType.CONNECTION_UNBLOCK, (int) MsgSubType.CONNECTION_BLOCK,
(int) MsgSubType.CONNECTION_UNFRIEND,
(int) MsgSubType.CONNECTION_UNFOLLOW,
(int) MsgSubType.CONNECTION_UNBLOCK
));
// 9) message_stats (to_block_hash -> BLOB)
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS message_stats (
to_login TEXT NOT NULL,
to_bch_name TEXT NOT NULL,
to_block_global_number INTEGER NOT NULL,
to_block_hash BLOB NOT NULL,
likes_count INTEGER NOT NULL DEFAULT 0,
replies_count INTEGER NOT NULL DEFAULT 0,
UNIQUE (
to_login,
to_bch_name,
to_block_global_number,
to_block_hash
)
);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_message_stats_target
ON message_stats (to_bch_name, to_block_global_number, to_block_hash);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_message_stats_login
ON message_stats (to_login);
""");
// 10) Trigger: LIKE
st.executeUpdate("""
CREATE TRIGGER IF NOT EXISTS trg_blocks_message_stats_like_ai
AFTER INSERT ON blocks
WHEN NEW.msg_type = 2 AND NEW.msg_sub_type = %d
BEGIN
INSERT INTO message_stats (
to_login,
to_bch_name,
to_block_global_number,
to_block_hash,
likes_count,
replies_count
)
SELECT
substr(NEW.to_bch_name, 1, length(NEW.to_bch_name) - 3),
NEW.to_bch_name,
NEW.to_block_global_number,
NEW.to_block_hashe,
1,
0
WHERE NEW.to_bch_name IS NOT NULL
AND length(NEW.to_bch_name) > 3
AND NEW.to_block_global_number IS NOT NULL
AND NEW.to_block_hashe IS NOT NULL
ON CONFLICT(to_login, to_bch_name, to_block_global_number, to_block_hash)
DO UPDATE SET
likes_count = message_stats.likes_count + 1;
END;
""".formatted((int) MsgSubType.REACTION_LIKE));
// 11) Trigger: REPLY
st.executeUpdate("""
CREATE TRIGGER IF NOT EXISTS trg_blocks_message_stats_reply_ai
AFTER INSERT ON blocks
WHEN NEW.msg_type = 1 AND NEW.msg_sub_type = %d
BEGIN
INSERT INTO message_stats (
to_login,
to_bch_name,
to_block_global_number,
to_block_hash,
likes_count,
replies_count
)
SELECT
substr(NEW.to_bch_name, 1, length(NEW.to_bch_name) - 3),
NEW.to_bch_name,
NEW.to_block_global_number,
NEW.to_block_hashe,
0,
1
WHERE NEW.to_bch_name IS NOT NULL
AND length(NEW.to_bch_name) > 3
AND NEW.to_block_global_number IS NOT NULL
AND NEW.to_block_hashe IS NOT NULL
ON CONFLICT(to_login, to_bch_name, to_block_global_number, to_block_hash)
DO UPDATE SET
replies_count = message_stats.replies_count + 1;
END;
""".formatted((int) MsgSubType.TEXT_REPLY));
// 12) Trigger: EDIT — пометить исходный блок
st.executeUpdate("""
CREATE TRIGGER IF NOT EXISTS trg_blocks_edit_apply_ai
AFTER INSERT ON blocks
WHEN NEW.msg_type = 1 AND NEW.msg_sub_type = %d
BEGIN
UPDATE blocks
SET edited_by_block_global_number = NEW.block_global_number
WHERE login = NEW.login
AND bch_name = NEW.bch_name
AND block_global_number = NEW.to_block_global_number;
END;
""".formatted((int) MsgSubType.TEXT_EDIT));
}
}
}