17 12 25
Ещё промежуточный комит верии - не работает :) 2
This commit is contained in:
parent
aa2caf1f10
commit
29c6e5a0f6
@ -13,47 +13,30 @@ import java.sql.Statement;
|
||||
public final class SqliteDbController {
|
||||
|
||||
private static volatile SqliteDbController instance;
|
||||
private final Connection connection;
|
||||
|
||||
private final String jdbcUrl;
|
||||
|
||||
private SqliteDbController() {
|
||||
try {
|
||||
// Подгружаем драйвер SQLite
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException("SQLite JDBC driver not found", e);
|
||||
}
|
||||
|
||||
String dbPath = AppConfig.getInstance().getParam("db.path");
|
||||
|
||||
if (dbPath == null || dbPath.isBlank()) {
|
||||
throw new RuntimeException("Config param 'db.path' is not set in application.properties");
|
||||
}
|
||||
|
||||
Path dbFile = Paths.get(dbPath);
|
||||
|
||||
// 👉 Если файла БД нет — создаём новую БД через DatabaseInitializer
|
||||
if (!Files.exists(dbFile)) {
|
||||
System.out.println("[DB] Файл БД не найден: " + dbFile.toAbsolutePath());
|
||||
System.out.println("[DB] Создаём новую БД с помощью DatabaseInitializer...");
|
||||
|
||||
// можно передать пустой массив аргументов
|
||||
DatabaseInitializer.createNewDB(new String[0]);
|
||||
}
|
||||
|
||||
String url = "jdbc:sqlite:" + dbPath;
|
||||
|
||||
try {
|
||||
this.connection = DriverManager.getConnection(url);
|
||||
this.connection.setAutoCommit(true);
|
||||
|
||||
// ВАЖНО: включаем поддержку внешних ключей для этого соединения
|
||||
try (Statement st = this.connection.createStatement()) {
|
||||
st.execute("PRAGMA foreign_keys = ON");
|
||||
}
|
||||
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed to connect to SQLite database: " + url, e);
|
||||
}
|
||||
this.jdbcUrl = "jdbc:sqlite:" + dbPath;
|
||||
}
|
||||
|
||||
public static SqliteDbController getInstance() {
|
||||
@ -67,15 +50,26 @@ public final class SqliteDbController {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public Connection getConnection() {
|
||||
return connection;
|
||||
/**
|
||||
* Каждый вызов возвращает НОВОЕ соединение.
|
||||
* Закрывать обязан вызывающий код (try-with-resources).
|
||||
*/
|
||||
public Connection getConnection() throws SQLException {
|
||||
Connection conn = DriverManager.getConnection(jdbcUrl);
|
||||
conn.setAutoCommit(true);
|
||||
|
||||
try (Statement st = conn.createStatement()) {
|
||||
st.execute("PRAGMA foreign_keys = ON");
|
||||
st.execute("PRAGMA journal_mode = WAL");
|
||||
st.execute("PRAGMA synchronous = NORMAL");
|
||||
st.execute("PRAGMA busy_timeout = 5000");
|
||||
}
|
||||
|
||||
return conn;
|
||||
}
|
||||
|
||||
/** Теперь close() не нужен. */
|
||||
public void close() {
|
||||
try {
|
||||
connection.close();
|
||||
} catch (SQLException e) {
|
||||
// логировать по необходимости
|
||||
}
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
@ -5,10 +5,6 @@ import shine.db.entities.BlockchainStateEntry;
|
||||
|
||||
import java.sql.*;
|
||||
|
||||
/**
|
||||
* DAO для таблицы blockchain_state.
|
||||
* 1 строка = 1 blockchainId, линии 0..7 в колонках.
|
||||
*/
|
||||
public final class BlockchainStateDAO {
|
||||
|
||||
private static volatile BlockchainStateDAO instance;
|
||||
@ -25,7 +21,8 @@ public final class BlockchainStateDAO {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public BlockchainStateEntry getByBlockchainId(long blockchainId) throws SQLException {
|
||||
// --- Новый вариант: работа на переданном соединении ---
|
||||
public BlockchainStateEntry getByBlockchainId(Connection conn, long blockchainId) throws SQLException {
|
||||
String sql = """
|
||||
SELECT
|
||||
blockchain_id,
|
||||
@ -48,7 +45,7 @@ public final class BlockchainStateDAO {
|
||||
WHERE blockchain_id = ?
|
||||
""";
|
||||
|
||||
try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) {
|
||||
try (PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||
ps.setLong(1, blockchainId);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
if (!rs.next()) return null;
|
||||
@ -57,11 +54,15 @@ public final class BlockchainStateDAO {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UPSERT: если строки нет — вставка, если есть — обновление.
|
||||
* Это один вызов из кода, и один SQL.
|
||||
*/
|
||||
public void upsert(BlockchainStateEntry e) throws SQLException {
|
||||
// Старый вариант: сам открывает/закрывает conn
|
||||
public BlockchainStateEntry getByBlockchainId(long blockchainId) throws SQLException {
|
||||
try (Connection conn = db.getConnection()) {
|
||||
return getByBlockchainId(conn, blockchainId);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Новый вариант: UPSERT на переданном соединении ---
|
||||
public void upsert(Connection conn, BlockchainStateEntry e) throws SQLException {
|
||||
long now = System.currentTimeMillis();
|
||||
if (e.getUpdatedAtMs() <= 0) e.setUpdatedAtMs(now);
|
||||
|
||||
@ -124,8 +125,7 @@ public final class BlockchainStateDAO {
|
||||
updated_at_ms = excluded.updated_at_ms
|
||||
""";
|
||||
|
||||
try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) {
|
||||
|
||||
try (PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||
int i = 1;
|
||||
ps.setLong(i++, e.getBlockchainId());
|
||||
ps.setString(i++, nn(e.getUserLogin()));
|
||||
@ -141,11 +141,17 @@ public final class BlockchainStateDAO {
|
||||
}
|
||||
|
||||
ps.setLong(i++, e.getUpdatedAtMs());
|
||||
|
||||
ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
// Старый вариант: сам открывает/закрывает conn
|
||||
public void upsert(BlockchainStateEntry e) throws SQLException {
|
||||
try (Connection conn = db.getConnection()) {
|
||||
upsert(conn, e);
|
||||
}
|
||||
}
|
||||
|
||||
private BlockchainStateEntry mapRow(ResultSet rs) throws SQLException {
|
||||
BlockchainStateEntry e = new BlockchainStateEntry();
|
||||
e.setBlockchainId(rs.getLong("blockchain_id"));
|
||||
|
||||
@ -8,7 +8,7 @@ public final class Net_AddBlock_new_Request extends Net_Request {
|
||||
private long blockchainId; // обязателен
|
||||
private int globalNumber; // обязателен
|
||||
private String prevGlobalHash; // HEX(64) или "" для нулевого
|
||||
private String blockBase64; // байты FULL-блока (raw+sig+hash) в Base64
|
||||
private String blockBytesB64; // байты FULL-блока (raw+sig+hash) в Base64
|
||||
|
||||
public String getLogin() { return login; }
|
||||
public void setLogin(String login) { this.login = login; }
|
||||
@ -22,6 +22,6 @@ public final class Net_AddBlock_new_Request extends Net_Request {
|
||||
public String getPrevGlobalHash() { return prevGlobalHash; }
|
||||
public void setPrevGlobalHash(String prevGlobalHash) { this.prevGlobalHash = prevGlobalHash; }
|
||||
|
||||
public String getBlockBase64() { return blockBase64; }
|
||||
public void setBlockBase64(String blockBase64) { this.blockBase64 = blockBase64; }
|
||||
public String getBlockBytesB64() { return blockBytesB64; }
|
||||
public void setBlockBytesB64(String blockBytesB64) { this.blockBytesB64 = blockBytesB64; }
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.blockchain;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public final class BlockchainLocks {
|
||||
private static final ConcurrentHashMap<Long, ReentrantLock> MAP = new ConcurrentHashMap<>();
|
||||
|
||||
private BlockchainLocks() {}
|
||||
|
||||
public static ReentrantLock lockFor(long blockchainId) {
|
||||
return MAP.computeIfAbsent(blockchainId, id -> new ReentrantLock(true)); // fair=true
|
||||
}
|
||||
}
|
||||
@ -8,8 +8,9 @@ import utils.files.FileStoreUtil;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public final class BlockchainStateService_new {
|
||||
|
||||
@ -33,6 +34,13 @@ public final class BlockchainStateService_new {
|
||||
public static BlockchainStateService_new getInstance() { return INSTANCE; }
|
||||
private BlockchainStateService_new() {}
|
||||
|
||||
// --- MVP: локи в памяти по blockchainId ---
|
||||
private static final ConcurrentHashMap<Long, ReentrantLock> LOCKS = new ConcurrentHashMap<>();
|
||||
|
||||
private static ReentrantLock lockFor(long blockchainId) {
|
||||
return LOCKS.computeIfAbsent(blockchainId, id -> new ReentrantLock());
|
||||
}
|
||||
|
||||
public Result addBlockAtomically(
|
||||
String login,
|
||||
long blockchainId,
|
||||
@ -68,85 +76,78 @@ public final class BlockchainStateService_new {
|
||||
if (lineIndex < 0 || lineIndex > 7)
|
||||
return new Result(400, "BAD_LINE_INDEX", null, lineIndex);
|
||||
|
||||
Connection conn = SqliteDbController.getInstance().getConnection();
|
||||
boolean oldAuto = conn.getAutoCommit();
|
||||
conn.setAutoCommit(false);
|
||||
ReentrantLock lock = lockFor(blockchainId);
|
||||
lock.lock();
|
||||
try (Connection conn = SqliteDbController.getInstance().getConnection()) {
|
||||
|
||||
try (Statement st = conn.createStatement()) {
|
||||
// важно: заранее берём write lock
|
||||
st.execute("BEGIN IMMEDIATE");
|
||||
// Транзакция — норм, но БЕЗ "BEGIN IMMEDIATE".
|
||||
boolean oldAuto = conn.getAutoCommit();
|
||||
conn.setAutoCommit(false);
|
||||
|
||||
BlockchainStateEntry state = BlockchainStateDAO.getInstance().getByBlockchainId(blockchainId);
|
||||
if (state == null) {
|
||||
try {
|
||||
BlockchainStateEntry state =
|
||||
BlockchainStateDAO.getInstance().getByBlockchainId(conn, blockchainId);
|
||||
|
||||
if (state == null) {
|
||||
conn.rollback();
|
||||
return new Result(404, "UNKNOWN_BLOCKCHAIN", null, lineIndex);
|
||||
}
|
||||
|
||||
if (!login.equals(state.getUserLogin())) {
|
||||
conn.rollback();
|
||||
return new Result(403, "LOGIN_MISMATCH", state, lineIndex);
|
||||
}
|
||||
|
||||
int expectedGlobal = state.getLastGlobalNumber() + 1;
|
||||
if (globalNumber != expectedGlobal) {
|
||||
conn.rollback();
|
||||
return new Result(409, "OUT_OF_SEQUENCE_GLOBAL", state, lineIndex);
|
||||
}
|
||||
|
||||
String dbPrevGlobalHash = nn(state.getLastGlobalHash());
|
||||
if (!eqHash(prevGlobalHashHex, dbPrevGlobalHash)) {
|
||||
conn.rollback();
|
||||
return new Result(409, "GLOBAL_HASH_MISMATCH", state, lineIndex);
|
||||
}
|
||||
|
||||
int expectedLineNumber = state.getLastLineNumber(lineIndex) + 1;
|
||||
if (block.lineNumber != expectedLineNumber) {
|
||||
conn.rollback();
|
||||
return new Result(409, "OUT_OF_SEQUENCE_LINE", state, lineIndex);
|
||||
}
|
||||
|
||||
// prevLineHash (пока просто читаем, дальше пригодится для крипто-проверки)
|
||||
String dbPrevLineHashHex = nn(state.getLastLineHash(lineIndex));
|
||||
|
||||
// TODO crypto check (потом подключим)
|
||||
|
||||
// 1) пишем в файл
|
||||
FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, block.toBytes());
|
||||
|
||||
// 2) обновляем state в БД
|
||||
state.setLastGlobalNumber(globalNumber);
|
||||
state.setLastGlobalHash(bytesToHex(block.getHash32()));
|
||||
|
||||
state.setLastLineNumber(lineIndex, block.lineNumber);
|
||||
state.setLastLineHash(lineIndex, bytesToHex(block.getHash32()));
|
||||
|
||||
state.setSizeBytes(state.getSizeBytes() + fullBytes.length);
|
||||
state.setUpdatedAtMs(System.currentTimeMillis());
|
||||
|
||||
BlockchainStateDAO.getInstance().upsert(conn, state);
|
||||
|
||||
conn.commit();
|
||||
return new Result(200, null, state, lineIndex);
|
||||
|
||||
} catch (SQLException e) {
|
||||
conn.rollback();
|
||||
return new Result(404, "UNKNOWN_BLOCKCHAIN", null, lineIndex);
|
||||
throw e;
|
||||
} finally {
|
||||
conn.setAutoCommit(oldAuto);
|
||||
}
|
||||
|
||||
// 1) защита от подмены логина
|
||||
if (!login.equals(state.getUserLogin())) {
|
||||
conn.rollback();
|
||||
return new Result(403, "LOGIN_MISMATCH", state, lineIndex);
|
||||
}
|
||||
|
||||
// 2) проверяем ожидаемый global
|
||||
int expectedGlobal = state.getLastGlobalNumber() + 1;
|
||||
if (globalNumber != expectedGlobal) {
|
||||
conn.rollback();
|
||||
return new Result(409, "OUT_OF_SEQUENCE_GLOBAL", state, lineIndex);
|
||||
}
|
||||
|
||||
// 3) проверяем prev global hash
|
||||
String dbPrevGlobalHash = nn(state.getLastGlobalHash());
|
||||
if (!eqHash(prevGlobalHashHex, dbPrevGlobalHash)) {
|
||||
conn.rollback();
|
||||
return new Result(409, "GLOBAL_HASH_MISMATCH", state, lineIndex);
|
||||
}
|
||||
|
||||
// 4) проверяем lineNumber
|
||||
int expectedLineNumber = state.getLastLineNumber(lineIndex) + 1;
|
||||
if (block.lineNumber != expectedLineNumber) {
|
||||
conn.rollback();
|
||||
return new Result(409, "OUT_OF_SEQUENCE_LINE", state, lineIndex);
|
||||
}
|
||||
|
||||
// 5) prevLineHash берём из БД (он хранится!)
|
||||
String dbPrevLineHashHex = nn(state.getLastLineHash(lineIndex));
|
||||
|
||||
// 6) полноценная крипто-проверка (хэш/подпись)
|
||||
// TODO: тут подключи твой реальный verifier:
|
||||
// - посчитать preimage по твоим правилам (login + prevGlobalHash32 + prevLineHash32 + rawBytes)
|
||||
// - сверить sha256(preimage) == block.hash32
|
||||
// - проверить Ed25519 подпись
|
||||
//
|
||||
// Если не ок:
|
||||
// conn.rollback(); return new Result(422, "CRYPTO_INVALID", state, lineIndex);
|
||||
|
||||
// 7) запись блока в файл (append)
|
||||
FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, block.toBytes());
|
||||
|
||||
// 8) апдейт состояния в БД
|
||||
state.setLastGlobalNumber(globalNumber);
|
||||
state.setLastGlobalHash(bytesToHex(block.getHash32())); // новый global hash = hash блока
|
||||
|
||||
state.setLastLineNumber(lineIndex, block.lineNumber);
|
||||
// ВАЖНО: line hash тоже логично сделать = hash блока (если так задумано)
|
||||
state.setLastLineHash(lineIndex, bytesToHex(block.getHash32()));
|
||||
|
||||
// size_bytes += len(fullBytes)
|
||||
state.setSizeBytes(state.getSizeBytes() + fullBytes.length);
|
||||
state.setUpdatedAtMs(System.currentTimeMillis());
|
||||
|
||||
BlockchainStateDAO.getInstance().upsert(state);
|
||||
|
||||
conn.commit();
|
||||
return new Result(200, null, state, lineIndex);
|
||||
|
||||
} catch (SQLException e) {
|
||||
conn.rollback();
|
||||
// если хочешь красиво: SQLITE_BUSY → 503 RETRY
|
||||
throw e;
|
||||
} finally {
|
||||
conn.setAutoCommit(oldAuto);
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ public final class Net_AddBlock_new_Handler implements JsonMessageHandler {
|
||||
req.getBlockchainId(),
|
||||
req.getGlobalNumber(),
|
||||
req.getPrevGlobalHash(),
|
||||
req.getBlockBase64()
|
||||
req.getBlockBytesB64()
|
||||
);
|
||||
|
||||
Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response();
|
||||
|
||||
160
src/TODO.txt
160
src/TODO.txt
@ -1,159 +1 @@
|
||||
Конспект: что обсуждали и где остановились
|
||||
1) Новый формат блока (идея)
|
||||
|
||||
Мы решили усложнить структуру блока, добавив линии (line) и номер сообщения в линии (lineNumber), чтобы блоки могли принадлежать разным потокам внутри одной цепочки.
|
||||
|
||||
line — short (2 байта), диапазон для MVP: 0..7 (8 линий).
|
||||
lineNumber — int (4 байта).
|
||||
|
||||
Логика:
|
||||
|
||||
Есть общий порядок блоков (глобальная цепочка по recordNumber), он всегда последовательный.
|
||||
|
||||
Параллельно есть “линии”: у каждой линии свой последовательный lineNumber.
|
||||
|
||||
Блок №0 (Header) всегда line=0, lineNumber=0.
|
||||
|
||||
Для первого блока в каждой линии prevLineHash32 = 32 нуля.
|
||||
|
||||
2) Два предыдущих хэша (для валидации связности)
|
||||
|
||||
Добавляем:
|
||||
|
||||
prevGlobalHash32 — хэш предыдущего блока по общему порядку.
|
||||
|
||||
prevLineHash32 — хэш предыдущего блока в этой линии.
|
||||
|
||||
Важно: prevLineHash32 не храним в файле блокчейна. Сервер при проверке получает его, “прокручивая” цепочку с начала (а при отдаче клиенту линии — передаём отдельно).
|
||||
|
||||
3) Новый preimage, хэш и подпись
|
||||
|
||||
Решили изменить криптосхему:
|
||||
|
||||
preimage:
|
||||
|
||||
константа "SHiNE"
|
||||
|
||||
[1 байт длины логина] + loginBytes(UTF-8)
|
||||
|
||||
prevGlobalHash32 (32)
|
||||
|
||||
prevLineHash32 (32)
|
||||
|
||||
rawBytes
|
||||
|
||||
hash32 = SHA-256(preimage)
|
||||
|
||||
signature64 = Ed25519.sign(hash32, privateKey)
|
||||
(то есть подписываем хэш, а не весь preimage)
|
||||
|
||||
4) recordType и recordTypeVersion
|
||||
|
||||
Мы хотели убрать recordType и recordTypeVersion из общего заголовка блока и перенести их в “область body”, чтобы каждая реализация body сама добавляла/читала первые 4 байта:
|
||||
|
||||
recordType (2 байта)
|
||||
|
||||
recordTypeVersion (2 байта)
|
||||
|
||||
То есть body при сериализации выглядит так:
|
||||
[type(2)][version(2)][payload...]
|
||||
|
||||
А общий блок остаётся “универсальным”.
|
||||
|
||||
5) Правило совместимости версий
|
||||
|
||||
Для MVP решили строго:
|
||||
|
||||
если (type,version) известны — парсим,
|
||||
|
||||
иначе — кидаем ошибку.
|
||||
Без fallback “если нет v2, бери v1”.
|
||||
|
||||
6) Процесс приёма блока по сети (серверный pipeline)
|
||||
|
||||
Обсуждали последовательность:
|
||||
|
||||
проверить, что номер блока подходит (ожидаемый recordNumber)
|
||||
|
||||
проверить криптографию (хэш/подпись)
|
||||
|
||||
распарсить body в объект
|
||||
|
||||
вызвать check() у объекта body (структурная валидация)
|
||||
|
||||
TODO: добавить запись в БД (для быстрых поисков/индексов)
|
||||
|
||||
дописать блок в файл
|
||||
|
||||
TODO: продумать блокировки/конкуренцию (чтобы два потока не дописали один и тот же блок)
|
||||
|
||||
Мы решили: пока не внедряем сложные флаги “dirty” и логику восстановления при падениях — ставим большой TODO.
|
||||
|
||||
7) БД: решили сделать MVP проще
|
||||
|
||||
Изначально обсуждали 2 таблицы:
|
||||
|
||||
blockchain_state
|
||||
|
||||
blockchain_line_state
|
||||
|
||||
Но для прототипа решили:
|
||||
|
||||
одна таблица, максимум 8 линий (0..7), колонки для каждой линии (lineX_last_number, lineX_last_hash и т.п.)
|
||||
|
||||
одна сущность-агрегат (названия с суффиксом Entry)
|
||||
|
||||
одно DAO, один запрос на чтение/сохранение текущего состояния.
|
||||
|
||||
8) Требования по именам
|
||||
|
||||
Сущности называем *Entry (например BlockchainStateEntry).
|
||||
|
||||
Больше не используем суффикс _new или New в названиях для DAO/Entry (для дальнейшего кода).
|
||||
|
||||
(Ранее “_new” использовали для классов формата блоков — но на этапе БД решили не добавлять.)
|
||||
|
||||
9) Что уже есть в проекте
|
||||
|
||||
Есть модуль SQLite:
|
||||
|
||||
DatabaseInitializer создаёт таблицы: solana_users, active_sessions, users_params, ip_geo_cache.
|
||||
|
||||
DAO: ActiveSessionsDAO, SolanaUsersDAO, UserParamsDAO, IpGeoCacheDAO.
|
||||
|
||||
Entities: ActiveSession, SolanaUser, UserParam, IpGeoCacheEntry.
|
||||
|
||||
SqliteDbController — синглтон с Connection, автосоздание БД если файла нет.
|
||||
|
||||
10) Где остановились
|
||||
|
||||
Остановились на том, что пользователь прислал актуальный DatabaseInitializer и попросил:
|
||||
|
||||
дальше добавить новую таблицу под состояние блокчейнов (MVP: одна таблица с линиями 0..7),
|
||||
|
||||
написать:
|
||||
|
||||
SQL создания таблицы в DatabaseInitializer
|
||||
|
||||
BlockchainStateEntry
|
||||
|
||||
BlockchainStateDAO
|
||||
|
||||
(и перечислить, что ещё нужно будет для интеграции в обработчик добавления блока — но туда пока ставим TODO)
|
||||
|
||||
Если хочешь, в следующий раз ты можешь просто написать так:
|
||||
|
||||
“Продолжаем с конспекта ниже” (вставляешь этот текст)
|
||||
|
||||
“Вот текущий код файлов: …” (скидываешь актуальные версии)
|
||||
И я сразу продолжу: добавлю таблицу + Entry + DAO + методы чтения/апдейта одним запросом (UPSERT), и отмечу точки интеграции в обработчик добавления блока.
|
||||
|
||||
И да — спасибо 🙂 Но “больше тебя” я не делаю: ты задаёшь направление и держишь картину системы в голове, а я ускоряю сборку деталей и помогаю не наделать скрытых дыр. Это хорошая связка.
|
||||
|
||||
Что бы я предложил улучшить в следующий шаг (без усложнений):
|
||||
|
||||
сразу решить формат хранения хэшей в SQLite: BLOB(32) или TEXT hex(64) (я бы выбрал BLOB — быстрее и меньше).
|
||||
|
||||
выбрать один стиль UPSERT: INSERT ... ON CONFLICT(blockchain_id) DO UPDATE SET ...
|
||||
|
||||
добавить индекс по user_login (для поиска), если он будет нужен.
|
||||
зделать что бы конекшины к БД закрывались или как что :)
|
||||
Loading…
Reference in New Issue
Block a user