поменял все названия таблиц и полей в таблицах на стиль только маленькие буквы и разделение через "_"  . Все тесты проходят норм
This commit is contained in:
AidarKC 2026-01-06 01:49:26 +03:00
parent 93c007b2b9
commit 8fd7f4676b
16 changed files with 467 additions and 532 deletions

View File

@ -1,132 +1,209 @@
1) Перечень таблиц и кратко о каждой SHiNE — структура БД (актуальная версия)
solana_users — справочник пользователей: логин + ключ устройства + (опционально) Solana-ключ. База для FK почти во всех таблицах. Перечень таблиц и назначение
active_sessions — активные сессии авторизации/работы клиента: пароли сессии/хранилища, времена, push-данные, IP/инфо клиента.
users_params — “последние значения параметров” пользователя. Для каждого (login,param) хранится актуальная версия по time_ms (старые обновления игнорируются).
ip_geo_cache — кеш геолокации по IP, чтобы не дергать внешние сервисы постоянно.
blockchain_state — агрегатное состояние блокчейна по blockchainName: лимиты/размер, последний глобальный блок и последние блоки по линиям 0..7.
blocks — локальное хранилище блоков/сообщений: привязка к пользователю и блокчейну, номера/хэши, тип/подтип, тело блока (BLOB), и опциональная “ссылка на другой блок” через поля to*. (PK сейчас намеренно убран.)
2) Таблицы подробно: параметры по строкам
solana_users solana_users
Справочник пользователей: логин + ключ устройства + (опционально) Solana-ключ.
login — TEXT PK — уникальный логин пользователя. Базовая таблица, используется как FK почти везде.
deviceKey — TEXT NOT NULL — публичный ключ устройства (обычно Base64(32) или HEX(64)).
solanaKey — TEXT NULL — публичный ключ Solana-аккаунта (если используется).
active_sessions active_sessions
Активные сессии авторизации/работы клиента: секреты, тайминги, WebPush-данные, IP и информация о клиенте.
sessionId — TEXT PK — идентификатор сессии (строка; по смыслу base64(32)).
login — TEXT NOT NULL, FK → solana_users(login) — владелец сессии.
sessionPwd — TEXT NOT NULL — пароль/секрет сессии (как хранится — строкой).
storagePwd — TEXT NOT NULL — пароль/секрет для storage (как хранится — строкой).
sessionCreatedAtMs — INTEGER NOT NULL — время создания сессии (Unix ms).
lastAuthirificatedAtMs — INTEGER NOT NULL — время последней авторизации/refresh (Unix ms).
pushEndpoint — TEXT NULL — endpoint для WebPush.
pushP256dhKey — TEXT NULL — p256dh ключ для WebPush.
pushAuthKey — TEXT NULL — auth ключ для WebPush.
clientIp — TEXT NULL — IP клиента на auth/refresh.
clientInfoFromClient — TEXT NULL — строка, присланная клиентом (PWA).
clientInfoFromRequest — TEXT NULL — строка, собранная сервером из запроса.
userLanguage — TEXT NULL — язык пользователя (например ru-RU).
users_params users_params
Хранилище актуальных параметров пользователя.
login — TEXT NOT NULL, FK → solana_users(login) — владелец параметра. Для каждой пары (login, param) хранится только самая новая версия по time_ms.
param — TEXT NOT NULL — имя параметра (ключ).
time_ms — INTEGER NOT NULL — версия/время параметра (Unix ms), используется для “только если новее”.
value — TEXT NOT NULL — значение параметра.
device_key — TEXT NULL — каким ключом подписано (если используешь), строковый формат.
signature — TEXT NULL — подпись (если используешь), строковый формат.
Ограничение: UNIQUE(login, param) — один актуальный параметр на пару.
ip_geo_cache ip_geo_cache
Кеш геолокации по IP для снижения нагрузки на внешние сервисы.
ip — TEXT PK — IP-адрес (строкой).
geo — TEXT NULL — гео-строка (Country/City или как договоритесь).
updated_at_ms — INTEGER NOT NULL — когда кеш обновлялся (Unix ms).
blockchain_state blockchain_state
Агрегированное состояние блокчейна по blockchain_name:
blockchainName — TEXT PK — имя/ID блокчейна (уникальное). лимиты, текущий размер, последний глобальный блок и состояние линий 0..7.
login — TEXT NOT NULL, FK → solana_users(login) — владелец блокчейна.
blockchainKey — TEXT NOT NULL — публичный ключ блокчейна (по смыслу Base64(32)).
size_limit — INTEGER NOT NULL — лимит размера (по смыслу bytes; в Java это long).
file_size_bytes — INTEGER NOT NULL — текущий размер файла блокчейна в байтах.
last_global_number — INTEGER NOT NULL — последний глобальный номер блока (genesis = -1).
last_global_hash — TEXT NOT NULL — хэш последнего глобального блока (или пустая строка).
updated_at_ms — INTEGER NOT NULL — время обновления состояния (Unix ms).
Линии 0..7 (для каждой линии две колонки):
line0_last_number — INTEGER NOT NULL — последний номер в линии 0.
line0_last_hash — TEXT NOT NULL — последний хэш в линии 0.
line1_last_number — INTEGER NOT NULL — последний номер в линии 1.
line1_last_hash — TEXT NOT NULL — последний хэш в линии 1.
line2_last_number — INTEGER NOT NULL — последний номер в линии 2.
line2_last_hash — TEXT NOT NULL — последний хэш в линии 2.
line3_last_number — INTEGER NOT NULL — последний номер в линии 3.
line3_last_hash — TEXT NOT NULL — последний хэш в линии 3.
line4_last_number — INTEGER NOT NULL — последний номер в линии 4.
line4_last_hash — TEXT NOT NULL — последний хэш в линии 4.
line5_last_number — INTEGER NOT NULL — последний номер в линии 5.
line5_last_hash — TEXT NOT NULL — последний хэш в линии 5.
line6_last_number — INTEGER NOT NULL — последний номер в линии 6.
line6_last_hash — TEXT NOT NULL — последний хэш в линии 6.
line7_last_number — INTEGER NOT NULL — последний номер в линии 7.
line7_last_hash — TEXT NOT NULL — последний хэш в линии 7.
blocks blocks
Журнал всех блоков и сообщений.
login — TEXT NOT NULL, FK → solana_users(login) — чей блок (логин). Содержит историю событий: тексты, реакции, ответы, связи.
bchName — TEXT NOT NULL, FK → blockchain_state(blockchainName) — к какому блокчейну относится блок. PRIMARY KEY намеренно отсутствует.
blockGlobalNumber — INTEGER NOT NULL — глобальный номер блока.
blockGlobalPreHashe — TEXT NOT NULL — хэш предыдущего глобального блока.
blockLineIndex — INTEGER NOT NULL — индекс линии (0..7), по смыслу int16.
blockLineNumber — INTEGER NOT NULL — номер блока в линии.
blockLinePreHashe — TEXT NOT NULL — хэш предыдущего блока в линии.
msgType — INTEGER NOT NULL — общий тип сообщения/блока (по смыслу uint16).
msgSubType — INTEGER NOT NULL — подтип внутри msgType (по смыслу uint16).
blockByte — BLOB NULL — сырой байтовый блок/тело сообщения.
Ссылка на другой блок (nullable, для ответов/репостов/связей и т.п.):
to_login — TEXT NULL — логин “на кого/кому/с кем” (смысловая цель).
toBchName — TEXT NULL — целевой блокчейн.
toBlockGlobalNumber — INTEGER NULL — целевой глобальный номер.
toBlockHashe — TEXT NULL — хэш целевого блока.
connections_state ⭐ connections_state ⭐
Текущее состояние связей пользователя. Актуальное состояние связей между пользователями
login — TEXT NOT NULL — владелец связи. (друг / контакт / подписка).
relType — INTEGER NOT NULL — тип связи: Обновляется автоматически на основе событий из blocks.
10 = FRIEND, 20 = CONTACT, 30 = FOLLOW.
to_login — TEXT NOT NULL — с кем связь. message_stats ⭐
toBchName — TEXT NOT NULL — блокчейн цели. Агрегированные счётчики лайков и ответов на конкретные сообщения.
toBlockGlobalNumber — INTEGER NULL — последний известный номер блока цели. Поддерживается триггерами из blocks.
toBlockHashe — TEXT NULL — последний известный хэш блока цели.
Таблицы подробно
solana_users
login — TEXT PK — уникальный логин пользователя
device_key — TEXT NOT NULL — публичный ключ устройства (Base64(32) / HEX(64))
solana_key — TEXT NULL — публичный ключ Solana-аккаунта
active_sessions
session_id — TEXT PK — идентификатор сессии
login — TEXT NOT NULL, FK → solana_users(login)
session_pwd — TEXT NOT NULL — секрет сессии
storage_pwd — TEXT NOT NULL — секрет storage
session_created_at_ms — INTEGER NOT NULL
last_authirificated_at_ms — INTEGER NOT NULL
push_endpoint — TEXT NULL
push_p256dh_key — TEXT NULL
push_auth_key — TEXT NULL
client_ip — TEXT NULL
client_info_from_client — TEXT NULL
client_info_from_request — TEXT NULL
user_language — TEXT NULL
users_params
login — TEXT NOT NULL, FK → solana_users(login)
param — TEXT NOT NULL
time_ms — INTEGER NOT NULL
value — TEXT NOT NULL
device_key — TEXT NULL
signature — TEXT NULL
Ограничение: Ограничение:
UNIQUE(login, relType, to_login) — одно актуальное состояние связи. UNIQUE(login, param)
3) Триггер логики связей Логика:
обновление принимается только если excluded.time_ms > users_params.time_ms
Триггер trg_blocks_connection_state_ai срабатывает AFTER INSERT ON blocks: ip_geo_cache
если msgType = 3 (ConnectionBody) ip — TEXT PK
msgSubType IN (10,20,30) → geo — TEXT NULL
добавить или обновить запись в connections_state updated_at_ms — INTEGER NOT NULL
msgSubType IN (11,21,31) →
удалить соответствующую связь (10/20/30) blockchain_state
повторные добавления и удаления не вызывают ошибок blockchain_name — TEXT PK
Таким образом: login — TEXT NOT NULL, FK → solana_users(login)
blockchain_key — TEXT NOT NULL
size_limit — INTEGER NOT NULL
file_size_bytes — INTEGER NOT NULL
last_global_number — INTEGER NOT NULL (-1 = genesis)
last_global_hash — TEXT NOT NULL
updated_at_ms — INTEGER NOT NULL
Линии 0..7:
для каждой линии:
lineX_last_number
lineX_last_hash
blocks
login — TEXT NOT NULL
bch_name — TEXT NOT NULL
block_global_number — INTEGER NOT NULL
block_global_pre_hash — TEXT NOT NULL
block_line_index — INTEGER NOT NULL
block_line_number — INTEGER NOT NULL
block_line_pre_hash — TEXT NOT NULL
msg_type — INTEGER NOT NULL
msg_sub_type — INTEGER NOT NULL
block_bytes — BLOB NULL
Ссылка на другой блок (nullable):
to_login
to_bch_name
to_block_global_number
to_block_hash
connections_state ⭐
Текущее агрегированное состояние связей.
login — TEXT NOT NULL
rel_type — INTEGER NOT NULL
10 = FRIEND
20 = CONTACT
30 = FOLLOW
to_login — TEXT NOT NULL
to_bch_name — TEXT NOT NULL
to_block_global_number — INTEGER NULL
to_block_hash — TEXT NULL
Ограничение:
UNIQUE(login, rel_type, to_login)
message_stats ⭐
Счётчики активности по целевому сообщению.
to_login — TEXT NOT NULL
to_bch_name — TEXT NOT NULL
to_block_global_number — INTEGER NOT NULL
to_block_hash — TEXT 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)
Триггеры БД (полная логика)
3.1 Связи пользователей
trg_blocks_connection_state_ai
AFTER INSERT ON blocks
Условие:
msg_type = 3 (connection)
Добавление / обновление связи
msg_sub_type IN (10,20,30)
выполняется UPSERT в connections_state
Удаление связи
msg_sub_type IN (11,21,31)
удаляется соответствующая связь:
11 → 10
21 → 20
31 → 30
Итог:
blocks — журнал событий blocks — журнал событий
connections_state — всегда актуальное состояние connections_state — всегда актуальное состояние
Индексы (кратко, чтобы было понятно зачем) 3.2 Подсчёт лайков ⭐
idx_solana_users_login(login) — быстрый поиск по логину. trg_blocks_message_stats_like_ai
idx_active_sessions_login(login) — быстро получить сессии пользователя. AFTER INSERT ON blocks
idx_users_params_login(login) — быстро получить параметры пользователя.
idx_ip_geo_cache_updated_at(updated_at_ms) — чистка старых записей. Условие:
idx_blockchain_state_login(login) — блокчейны пользователя. msg_type = 2 (reaction)
idx_blockchain_state_updated_at(updated_at_ms) — выборки/обслуживание по “свежести”. msg_sub_type = 1 (like)
idx_blocks_chain_global(login,bchName,blockGlobalNumber) — выборки блоков по цепочке.
idx_blocks_to_target(to_login,toBchName,toBlockGlobalNumber) — быстрые выборки “по ссылке”. Действие:
определяется цель по to_bch_name, to_block_global_number, to_block_hash
to_login вычисляется как
substr(to_bch_name, 1, length(to_bch_name) - 3)
выполняется UPSERT в message_stats
likes_count += 1
3.3 Подсчёт ответов ⭐
trg_blocks_message_stats_reply_ai
AFTER INSERT ON blocks
Условие:
msg_type = 1 (text)
msg_sub_type = 2 (reply)
Действие:
цель определяется аналогично лайкам
выполняется UPSERT в message_stats
replies_count += 1
Индексы (смысл)
idx_solana_users_login — поиск пользователя
idx_active_sessions_login — сессии пользователя
idx_users_params_login — параметры пользователя
idx_ip_geo_cache_updated_at — чистка кеша
idx_blockchain_state_login — блокчейны пользователя
idx_blockchain_state_updated_at — обслуживание
idx_blocks_chain_global — чтение цепочки
idx_blocks_to_target — реакции / ответы
idx_message_stats_target — быстрый доступ к счётчикам
Итоговая модель мышления
blocks — неизменяемый журнал событий
connections_state — проекция связей
message_stats — проекция активности
всё вычисляется детерминированно через триггеры

View File

@ -21,8 +21,8 @@ import java.sql.Statement;
* - ip_geo_cache * - ip_geo_cache
* - blockchain_state * - blockchain_state
* - blocks * - blocks
* - connections_state (текущее состояние связей) * - connections_state
* - message_stats (счётчики лайков/ответов на сообщения) * - message_stats
*/ */
public class DatabaseInitializer { public class DatabaseInitializer {
@ -83,8 +83,8 @@ public class DatabaseInitializer {
st.executeUpdate(""" st.executeUpdate("""
CREATE TABLE IF NOT EXISTS solana_users ( CREATE TABLE IF NOT EXISTS solana_users (
login TEXT NOT NULL PRIMARY KEY, login TEXT NOT NULL PRIMARY KEY,
deviceKey TEXT NOT NULL, device_key TEXT NOT NULL,
solanaKey TEXT solana_key TEXT
); );
"""); """);
@ -96,19 +96,19 @@ public class DatabaseInitializer {
// 2. active_sessions // 2. active_sessions
st.executeUpdate(""" st.executeUpdate("""
CREATE TABLE IF NOT EXISTS active_sessions ( CREATE TABLE IF NOT EXISTS active_sessions (
sessionId TEXT NOT NULL PRIMARY KEY, session_id TEXT NOT NULL PRIMARY KEY,
login TEXT NOT NULL, login TEXT NOT NULL,
sessionPwd TEXT NOT NULL, session_pwd TEXT NOT NULL,
storagePwd TEXT NOT NULL, storage_pwd TEXT NOT NULL,
sessionCreatedAtMs INTEGER NOT NULL, session_created_at_ms INTEGER NOT NULL,
lastAuthirificatedAtMs INTEGER NOT NULL, last_authirificated_at_ms INTEGER NOT NULL,
pushEndpoint TEXT, push_endpoint TEXT,
pushP256dhKey TEXT, push_p256dh_key TEXT,
pushAuthKey TEXT, push_auth_key TEXT,
clientIp TEXT, client_ip TEXT,
clientInfoFromClient TEXT, client_info_from_client TEXT,
clientInfoFromRequest TEXT, client_info_from_request TEXT,
userLanguage TEXT, user_language TEXT,
FOREIGN KEY (login) REFERENCES solana_users(login) FOREIGN KEY (login) REFERENCES solana_users(login)
); );
"""); """);
@ -154,9 +154,9 @@ public class DatabaseInitializer {
// 5. blockchain_state // 5. blockchain_state
st.executeUpdate(""" st.executeUpdate("""
CREATE TABLE IF NOT EXISTS blockchain_state ( CREATE TABLE IF NOT EXISTS blockchain_state (
blockchainName TEXT NOT NULL PRIMARY KEY, blockchain_name TEXT NOT NULL PRIMARY KEY,
login TEXT NOT NULL, login TEXT NOT NULL,
blockchainKey TEXT NOT NULL, blockchain_key TEXT NOT NULL,
size_limit INTEGER NOT NULL, size_limit INTEGER NOT NULL,
file_size_bytes INTEGER NOT NULL, file_size_bytes INTEGER NOT NULL,
@ -200,54 +200,52 @@ public class DatabaseInitializer {
st.executeUpdate(""" st.executeUpdate("""
CREATE TABLE IF NOT EXISTS blocks ( CREATE TABLE IF NOT EXISTS blocks (
login TEXT NOT NULL, login TEXT NOT NULL,
bchName TEXT NOT NULL, bch_name TEXT NOT NULL,
blockGlobalNumber INTEGER NOT NULL, block_global_number INTEGER NOT NULL,
blockGlobalPreHashe TEXT NOT NULL, block_global_pre_hashe TEXT NOT NULL,
blockLineIndex INTEGER NOT NULL, block_line_index INTEGER NOT NULL,
blockLineNumber INTEGER NOT NULL, block_line_number INTEGER NOT NULL,
blockLinePreHashe TEXT NOT NULL, block_line_pre_hashe TEXT NOT NULL,
msgType INTEGER NOT NULL, msg_type INTEGER NOT NULL,
msgSubType INTEGER NOT NULL, msg_sub_type INTEGER NOT NULL,
blockByte BLOB, block_byte BLOB,
to_login TEXT, to_login TEXT,
toBchName TEXT, to_bch_name TEXT,
toBlockGlobalNumber INTEGER, to_block_global_number INTEGER,
toBlockHashe TEXT, to_block_hashe TEXT,
FOREIGN KEY (login) REFERENCES solana_users(login), FOREIGN KEY (login) REFERENCES solana_users(login),
FOREIGN KEY (bchName) REFERENCES blockchain_state(blockchainName) FOREIGN KEY (bch_name) REFERENCES blockchain_state(blockchain_name)
); );
"""); """);
st.executeUpdate(""" st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_blocks_chain_global CREATE INDEX IF NOT EXISTS idx_blocks_chain_global
ON blocks (login, bchName, blockGlobalNumber); ON blocks (login, bch_name, block_global_number);
"""); """);
st.executeUpdate(""" st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_blocks_to_target CREATE INDEX IF NOT EXISTS idx_blocks_to_target
ON blocks (to_login, toBchName, toBlockGlobalNumber); ON blocks (to_login, to_bch_name, to_block_global_number);
"""); """);
// ===================================================================== // 7) connections_state
// 7) connections_state текущее состояние "кто с кем и какая связь"
// =====================================================================
st.executeUpdate(""" st.executeUpdate("""
CREATE TABLE IF NOT EXISTS connections_state ( CREATE TABLE IF NOT EXISTS connections_state (
login TEXT NOT NULL, login TEXT NOT NULL,
relType INTEGER NOT NULL, -- 10/20/30 (FRIEND/CONTACT/FOLLOW) rel_type INTEGER NOT NULL,
to_login TEXT NOT NULL, to_login TEXT NOT NULL,
toBchName TEXT NOT NULL, to_bch_name TEXT NOT NULL,
toBlockGlobalNumber INTEGER, to_block_global_number INTEGER,
toBlockHashe TEXT, to_block_hashe TEXT,
FOREIGN KEY (login) REFERENCES solana_users(login), FOREIGN KEY (login) REFERENCES solana_users(login),
UNIQUE (login, relType, to_login) UNIQUE (login, rel_type, to_login)
); );
"""); """);
@ -266,54 +264,47 @@ public class DatabaseInitializer {
ON connections_state (login, to_login); ON connections_state (login, to_login);
"""); """);
// ===================================================================== // 8) Trigger: connection state
// 8) Trigger: при вставке connection-блоков в blocks обновлять connections_state
// =====================================================================
st.executeUpdate(""" st.executeUpdate("""
CREATE TRIGGER IF NOT EXISTS trg_blocks_connection_state_ai CREATE TRIGGER IF NOT EXISTS trg_blocks_connection_state_ai
AFTER INSERT ON blocks AFTER INSERT ON blocks
WHEN NEW.msgType = 3 WHEN NEW.msg_type = 3
BEGIN BEGIN
INSERT INTO connections_state ( INSERT INTO connections_state (
login, relType, to_login, toBchName, toBlockGlobalNumber, toBlockHashe login, rel_type, to_login, to_bch_name, to_block_global_number, to_block_hashe
) )
SELECT SELECT
NEW.login, NEW.login,
NEW.msgSubType, NEW.msg_sub_type,
NEW.to_login, NEW.to_login,
NEW.toBchName, NEW.to_bch_name,
NEW.toBlockGlobalNumber, NEW.to_block_global_number,
NEW.toBlockHashe NEW.to_block_hashe
WHERE NEW.msgSubType IN (10, 20, 30) WHERE NEW.msg_sub_type IN (10, 20, 30)
AND NEW.to_login IS NOT NULL AND NEW.to_login IS NOT NULL
AND NEW.toBchName IS NOT NULL AND NEW.to_bch_name IS NOT NULL
ON CONFLICT(login, relType, to_login) ON CONFLICT(login, rel_type, to_login)
DO UPDATE SET DO UPDATE SET
toBchName = excluded.toBchName, to_bch_name = excluded.to_bch_name,
toBlockGlobalNumber = excluded.toBlockGlobalNumber, to_block_global_number = excluded.to_block_global_number,
toBlockHashe = excluded.toBlockHashe; to_block_hashe = excluded.to_block_hashe;
DELETE FROM connections_state DELETE FROM connections_state
WHERE login = NEW.login WHERE login = NEW.login
AND to_login = NEW.to_login AND to_login = NEW.to_login
AND relType = CASE NEW.msgSubType AND rel_type = CASE NEW.msg_sub_type
WHEN 11 THEN 10 WHEN 11 THEN 10
WHEN 21 THEN 20 WHEN 21 THEN 20
WHEN 31 THEN 30 WHEN 31 THEN 30
ELSE relType ELSE rel_type
END END
AND NEW.msgSubType IN (11, 21, 31); AND NEW.msg_sub_type IN (11, 21, 31);
END; END;
"""); """);
// ===================================================================== // 9) message_stats
// 9) message_stats счётчики лайков/ответов на конкретный блок-цель
//
// Правило системы:
// - to_login берём из toBchName: отрезаем последние 3 символа
// =====================================================================
st.executeUpdate(""" st.executeUpdate("""
CREATE TABLE IF NOT EXISTS message_stats ( CREATE TABLE IF NOT EXISTS message_stats (
to_login TEXT NOT NULL, to_login TEXT NOT NULL,
@ -343,15 +334,11 @@ public class DatabaseInitializer {
ON message_stats (to_login); ON message_stats (to_login);
"""); """);
// ===================================================================== // 10) Trigger: LIKE
// 10) Trigger: LIKE (Reaction)
// - msgType=2 (REACTION)
// - msgSubType=1 (LIKE)
// =====================================================================
st.executeUpdate(""" st.executeUpdate("""
CREATE TRIGGER IF NOT EXISTS trg_blocks_message_stats_like_ai CREATE TRIGGER IF NOT EXISTS trg_blocks_message_stats_like_ai
AFTER INSERT ON blocks AFTER INSERT ON blocks
WHEN NEW.msgType = 2 AND NEW.msgSubType = 1 WHEN NEW.msg_type = 2 AND NEW.msg_sub_type = 1
BEGIN BEGIN
INSERT INTO message_stats ( INSERT INTO message_stats (
to_login, to_login,
@ -362,31 +349,27 @@ public class DatabaseInitializer {
replies_count replies_count
) )
SELECT SELECT
substr(NEW.toBchName, 1, length(NEW.toBchName) - 3), substr(NEW.to_bch_name, 1, length(NEW.to_bch_name) - 3),
NEW.toBchName, NEW.to_bch_name,
NEW.toBlockGlobalNumber, NEW.to_block_global_number,
NEW.toBlockHashe, NEW.to_block_hashe,
1, 1,
0 0
WHERE NEW.toBchName IS NOT NULL WHERE NEW.to_bch_name IS NOT NULL
AND length(NEW.toBchName) > 3 AND length(NEW.to_bch_name) > 3
AND NEW.toBlockGlobalNumber IS NOT NULL AND NEW.to_block_global_number IS NOT NULL
AND NEW.toBlockHashe 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) ON CONFLICT(to_login, to_bch_name, to_block_global_number, to_block_hash)
DO UPDATE SET DO UPDATE SET
likes_count = message_stats.likes_count + 1; likes_count = message_stats.likes_count + 1;
END; END;
"""); """);
// ===================================================================== // 11) Trigger: REPLY
// 11) Trigger: REPLY (Text)
// - msgType=1 (TEXT)
// - msgSubType=2 (REPLY)
// =====================================================================
st.executeUpdate(""" st.executeUpdate("""
CREATE TRIGGER IF NOT EXISTS trg_blocks_message_stats_reply_ai CREATE TRIGGER IF NOT EXISTS trg_blocks_message_stats_reply_ai
AFTER INSERT ON blocks AFTER INSERT ON blocks
WHEN NEW.msgType = 1 AND NEW.msgSubType = 2 WHEN NEW.msg_type = 1 AND NEW.msg_sub_type = 2
BEGIN BEGIN
INSERT INTO message_stats ( INSERT INTO message_stats (
to_login, to_login,
@ -397,16 +380,16 @@ public class DatabaseInitializer {
replies_count replies_count
) )
SELECT SELECT
substr(NEW.toBchName, 1, length(NEW.toBchName) - 3), substr(NEW.to_bch_name, 1, length(NEW.to_bch_name) - 3),
NEW.toBchName, NEW.to_bch_name,
NEW.toBlockGlobalNumber, NEW.to_block_global_number,
NEW.toBlockHashe, NEW.to_block_hashe,
0, 0,
1 1
WHERE NEW.toBchName IS NOT NULL WHERE NEW.to_bch_name IS NOT NULL
AND length(NEW.toBchName) > 3 AND length(NEW.to_bch_name) > 3
AND NEW.toBlockGlobalNumber IS NOT NULL AND NEW.to_block_global_number IS NOT NULL
AND NEW.toBlockHashe 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) ON CONFLICT(to_login, to_bch_name, to_block_global_number, to_block_hash)
DO UPDATE SET DO UPDATE SET
replies_count = message_stats.replies_count + 1; replies_count = message_stats.replies_count + 1;

View File

@ -50,10 +50,6 @@ public final class SqliteDbController {
return instance; return instance;
} }
/**
* Каждый вызов возвращает НОВОЕ соединение.
* Закрывать обязан вызывающий код (try-with-resources).
*/
public Connection getConnection() throws SQLException { public Connection getConnection() throws SQLException {
Connection conn = DriverManager.getConnection(jdbcUrl); Connection conn = DriverManager.getConnection(jdbcUrl);
conn.setAutoCommit(true); conn.setAutoCommit(true);
@ -68,7 +64,6 @@ public final class SqliteDbController {
return conn; return conn;
} }
/** Теперь close() не нужен. */
public void close() { public void close() {
// no-op // no-op
} }

View File

@ -36,19 +36,19 @@ public final class ActiveSessionsDAO {
public void insert(Connection c, ActiveSessionEntry session) throws SQLException { public void insert(Connection c, ActiveSessionEntry session) throws SQLException {
String sql = """ String sql = """
INSERT INTO active_sessions ( INSERT INTO active_sessions (
sessionId, session_id,
login, login,
sessionPwd, session_pwd,
storagePwd, storage_pwd,
sessionCreatedAtMs, session_created_at_ms,
lastAuthirificatedAtMs, last_authirificated_at_ms,
pushEndpoint, push_endpoint,
pushP256dhKey, push_p256dh_key,
pushAuthKey, push_auth_key,
clientIp, client_ip,
clientInfoFromClient, client_info_from_client,
clientInfoFromRequest, client_info_from_request,
userLanguage user_language
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""; """;
@ -83,21 +83,21 @@ public final class ActiveSessionsDAO {
public ActiveSessionEntry getBySessionId(Connection c, String sessionId) throws SQLException { public ActiveSessionEntry getBySessionId(Connection c, String sessionId) throws SQLException {
String sql = """ String sql = """
SELECT SELECT
sessionId, session_id,
login, login,
sessionPwd, session_pwd,
storagePwd, storage_pwd,
sessionCreatedAtMs, session_created_at_ms,
lastAuthirificatedAtMs, last_authirificated_at_ms,
pushEndpoint, push_endpoint,
pushP256dhKey, push_p256dh_key,
pushAuthKey, push_auth_key,
clientIp, client_ip,
clientInfoFromClient, client_info_from_client,
clientInfoFromRequest, client_info_from_request,
userLanguage user_language
FROM active_sessions FROM active_sessions
WHERE sessionId = ? WHERE session_id = ?
"""; """;
try (PreparedStatement ps = c.prepareStatement(sql)) { try (PreparedStatement ps = c.prepareStatement(sql)) {
@ -120,19 +120,19 @@ public final class ActiveSessionsDAO {
public List<ActiveSessionEntry> getByLogin(Connection c, String login) throws SQLException { public List<ActiveSessionEntry> getByLogin(Connection c, String login) throws SQLException {
String sql = """ String sql = """
SELECT SELECT
sessionId, session_id,
login, login,
sessionPwd, session_pwd,
storagePwd, storage_pwd,
sessionCreatedAtMs, session_created_at_ms,
lastAuthirificatedAtMs, last_authirificated_at_ms,
pushEndpoint, push_endpoint,
pushP256dhKey, push_p256dh_key,
pushAuthKey, push_auth_key,
clientIp, client_ip,
clientInfoFromClient, client_info_from_client,
clientInfoFromRequest, client_info_from_request,
userLanguage user_language
FROM active_sessions FROM active_sessions
WHERE login = ? WHERE login = ?
"""; """;
@ -162,8 +162,8 @@ public final class ActiveSessionsDAO {
public void updateLastAuthirificatedAtMs(Connection c, String sessionId, long lastAuthMs) throws SQLException { public void updateLastAuthirificatedAtMs(Connection c, String sessionId, long lastAuthMs) throws SQLException {
String sql = """ String sql = """
UPDATE active_sessions UPDATE active_sessions
SET lastAuthirificatedAtMs = ? SET last_authirificated_at_ms = ?
WHERE sessionId = ? WHERE session_id = ?
"""; """;
try (PreparedStatement ps = c.prepareStatement(sql)) { try (PreparedStatement ps = c.prepareStatement(sql)) {
@ -194,12 +194,12 @@ public final class ActiveSessionsDAO {
String sql = """ String sql = """
UPDATE active_sessions UPDATE active_sessions
SET SET
lastAuthirificatedAtMs = ?, last_authirificated_at_ms = ?,
clientIp = ?, client_ip = ?,
clientInfoFromClient = ?, client_info_from_client = ?,
clientInfoFromRequest = ?, client_info_from_request = ?,
userLanguage = ? user_language = ?
WHERE sessionId = ? WHERE session_id = ?
"""; """;
try (PreparedStatement ps = c.prepareStatement(sql)) { try (PreparedStatement ps = c.prepareStatement(sql)) {
@ -231,7 +231,7 @@ public final class ActiveSessionsDAO {
/** Удалить по sessionId с внешним соединением. Соединение НЕ закрывает. */ /** Удалить по sessionId с внешним соединением. Соединение НЕ закрывает. */
public void deleteBySessionId(Connection c, String sessionId) throws SQLException { public void deleteBySessionId(Connection c, String sessionId) throws SQLException {
String sql = "DELETE FROM active_sessions WHERE sessionId = ?"; String sql = "DELETE FROM active_sessions WHERE session_id = ?";
try (PreparedStatement ps = c.prepareStatement(sql)) { try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, sessionId); ps.setString(1, sessionId);
@ -249,19 +249,19 @@ public final class ActiveSessionsDAO {
// -------------------- MAPPER -------------------- // -------------------- MAPPER --------------------
private ActiveSessionEntry mapRow(ResultSet rs) throws SQLException { private ActiveSessionEntry mapRow(ResultSet rs) throws SQLException {
String sessionId = rs.getString("sessionId"); String sessionId = rs.getString("session_id");
String login = rs.getString("login"); String login = rs.getString("login");
String sessionPwd = rs.getString("sessionPwd"); String sessionPwd = rs.getString("session_pwd");
String storagePwd = rs.getString("storagePwd"); String storagePwd = rs.getString("storage_pwd");
long sessionCreatedAtMs = rs.getLong("sessionCreatedAtMs"); long sessionCreatedAtMs = rs.getLong("session_created_at_ms");
long lastAuthirificatedAtMs = rs.getLong("lastAuthirificatedAtMs"); long lastAuthirificatedAtMs = rs.getLong("last_authirificated_at_ms");
String pushEndpoint = rs.getString("pushEndpoint"); String pushEndpoint = rs.getString("push_endpoint");
String pushP256dhKey = rs.getString("pushP256dhKey"); String pushP256dhKey = rs.getString("push_p256dh_key");
String pushAuthKey = rs.getString("pushAuthKey"); String pushAuthKey = rs.getString("push_auth_key");
String clientIp = rs.getString("clientIp"); String clientIp = rs.getString("client_ip");
String clientInfoFromClient = rs.getString("clientInfoFromClient"); String clientInfoFromClient = rs.getString("client_info_from_client");
String clientInfoFromRequest = rs.getString("clientInfoFromRequest"); String clientInfoFromRequest = rs.getString("client_info_from_request");
String userLanguage = rs.getString("userLanguage"); String userLanguage = rs.getString("user_language");
return new ActiveSessionEntry( return new ActiveSessionEntry(
sessionId, sessionId,

View File

@ -32,9 +32,9 @@ public final class BlockchainStateDAO {
public BlockchainStateEntry getByBlockchainName(Connection c, String blockchainName) throws SQLException { public BlockchainStateEntry getByBlockchainName(Connection c, String blockchainName) throws SQLException {
String sql = """ String sql = """
SELECT SELECT
blockchainName, blockchain_name,
login, login,
blockchainKey, blockchain_key,
size_limit, size_limit,
file_size_bytes, file_size_bytes,
last_global_number, last_global_number,
@ -49,7 +49,7 @@ public final class BlockchainStateDAO {
line6_last_number, line6_last_hash, line6_last_number, line6_last_hash,
line7_last_number, line7_last_hash line7_last_number, line7_last_hash
FROM blockchain_state FROM blockchain_state
WHERE blockchainName = ? WHERE blockchain_name = ?
"""; """;
try (PreparedStatement ps = c.prepareStatement(sql)) { try (PreparedStatement ps = c.prepareStatement(sql)) {
@ -79,9 +79,9 @@ public final class BlockchainStateDAO {
String sql = """ String sql = """
INSERT INTO blockchain_state ( INSERT INTO blockchain_state (
blockchainName, blockchain_name,
login, login,
blockchainKey, blockchain_key,
size_limit, size_limit,
file_size_bytes, file_size_bytes,
last_global_number, last_global_number,
@ -106,10 +106,10 @@ public final class BlockchainStateDAO {
?,?, ?,?,
?,? ?,?
) )
ON CONFLICT(blockchainName) ON CONFLICT(blockchain_name)
DO UPDATE SET DO UPDATE SET
login = excluded.login, login = excluded.login,
blockchainKey = excluded.blockchainKey, blockchain_key = excluded.blockchain_key,
size_limit = excluded.size_limit, size_limit = excluded.size_limit,
file_size_bytes = excluded.file_size_bytes, file_size_bytes = excluded.file_size_bytes,
last_global_number = excluded.last_global_number, last_global_number = excluded.last_global_number,
@ -158,12 +158,6 @@ public final class BlockchainStateDAO {
/** /**
* Атомарно увеличить file_size_bytes на deltaBytes, но только если НЕ превысим size_limit. * Атомарно увеличить file_size_bytes на deltaBytes, но только если НЕ превысим size_limit.
*
* Возвращает:
* - true если обновили (лимит не превышен)
* - false если лимит превышается или blockchainName не найден
*
* ВАЖНО: это именно тот механизм, который надо дергать при добавлении блока.
*/ */
public boolean tryIncreaseFileSizeWithinLimit(Connection c, String blockchainName, long deltaBytes, long nowMs) throws SQLException { public boolean tryIncreaseFileSizeWithinLimit(Connection c, String blockchainName, long deltaBytes, long nowMs) throws SQLException {
String sql = """ String sql = """
@ -172,7 +166,7 @@ public final class BlockchainStateDAO {
file_size_bytes = file_size_bytes + ?, file_size_bytes = file_size_bytes + ?,
updated_at_ms = ? updated_at_ms = ?
WHERE WHERE
blockchainName = ? blockchain_name = ?
AND (file_size_bytes + ?) <= size_limit AND (file_size_bytes + ?) <= size_limit
"""; """;
@ -202,11 +196,10 @@ public final class BlockchainStateDAO {
private BlockchainStateEntry mapRow(ResultSet rs) throws SQLException { private BlockchainStateEntry mapRow(ResultSet rs) throws SQLException {
BlockchainStateEntry e = new BlockchainStateEntry(); BlockchainStateEntry e = new BlockchainStateEntry();
e.setBlockchainName(rs.getString("blockchainName")); e.setBlockchainName(rs.getString("blockchain_name"));
e.setLogin(rs.getString("login")); e.setLogin(rs.getString("login"));
e.setBlockchainKey(rs.getString("blockchainKey")); e.setBlockchainKey(rs.getString("blockchain_key"));
// size_limit теперь long
e.setSizeLimit(rs.getLong("size_limit")); e.setSizeLimit(rs.getLong("size_limit"));
e.setFileSizeBytes(rs.getLong("file_size_bytes")); e.setFileSizeBytes(rs.getLong("file_size_bytes"));

View File

@ -38,19 +38,19 @@ public final class BlocksDAO {
String sql = """ String sql = """
INSERT INTO blocks ( INSERT INTO blocks (
login, login,
bchName, bch_name,
blockGlobalNumber, block_global_number,
blockGlobalPreHashe, block_global_pre_hashe,
blockLineIndex, block_line_index,
blockLineNumber, block_line_number,
blockLinePreHashe, block_line_pre_hashe,
msgType, msg_type,
msgSubType, msg_sub_type,
blockByte, block_byte,
to_login, to_login,
toBchName, to_bch_name,
toBlockGlobalNumber, to_block_global_number,
toBlockHashe to_block_hashe
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""; """;
@ -69,16 +69,11 @@ public final class BlocksDAO {
// -------------------- UPSERT (UPDATE -> INSERT) -------------------- // -------------------- UPSERT (UPDATE -> INSERT) --------------------
/**
* Сохранить (условный upsert) с внешним соединением. Соединение НЕ закрывает.
* Без PK/UNIQUE делаем: UPDATE по "ключевым" полям -> если 0 строк, то INSERT.
*/
public void upsert(Connection c, BlockEntry e) throws SQLException { public void upsert(Connection c, BlockEntry e) throws SQLException {
int updated = update(c, e); int updated = update(c, e);
if (updated == 0) insert(c, e); if (updated == 0) insert(c, e);
} }
/** Сохранить (upsert) без внешнего соединения. Сам открывает/закрывает. */
public void upsert(BlockEntry e) throws SQLException { public void upsert(BlockEntry e) throws SQLException {
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {
upsert(c, e); upsert(c, e);
@ -87,7 +82,6 @@ public final class BlocksDAO {
// -------------------- SELECT -------------------- // -------------------- SELECT --------------------
/** Получить блок по "PK-подобному" набору полей с внешним соединением. Соединение НЕ закрывает. */
public BlockEntry getByPk(Connection c, public BlockEntry getByPk(Connection c,
String login, String login,
String bchName, String bchName,
@ -98,26 +92,26 @@ public final class BlocksDAO {
String sql = """ String sql = """
SELECT SELECT
login, login,
bchName, bch_name,
blockGlobalNumber, block_global_number,
blockGlobalPreHashe, block_global_pre_hashe,
blockLineIndex, block_line_index,
blockLineNumber, block_line_number,
blockLinePreHashe, block_line_pre_hashe,
msgType, msg_type,
msgSubType, msg_sub_type,
blockByte, block_byte,
to_login, to_login,
toBchName, to_bch_name,
toBlockGlobalNumber, to_block_global_number,
toBlockHashe to_block_hashe
FROM blocks FROM blocks
WHERE WHERE
login = ? login = ?
AND bchName = ? AND bch_name = ?
AND blockGlobalNumber = ? AND block_global_number = ?
AND blockLineIndex = ? AND block_line_index = ?
AND blockLineNumber = ? AND block_line_number = ?
LIMIT 1 LIMIT 1
"""; """;
@ -135,7 +129,6 @@ public final class BlocksDAO {
} }
} }
/** Получить блок по "PK-подобному" набору полей без внешнего соединения. Сам открывает/закрывает. */
public BlockEntry getByPk(String login, public BlockEntry getByPk(String login,
String bchName, String bchName,
int blockGlobalNumber, int blockGlobalNumber,
@ -148,29 +141,25 @@ public final class BlocksDAO {
// -------------------- UPDATE -------------------- // -------------------- UPDATE --------------------
/**
* Обновить (строго UPDATE) по "PK-подобному" набору полей с внешним соединением. Соединение НЕ закрывает.
* Может обновить >1 строк, если в таблице появились дубликаты.
*/
public int update(Connection c, BlockEntry e) throws SQLException { public int update(Connection c, BlockEntry e) throws SQLException {
String sql = """ String sql = """
UPDATE blocks UPDATE blocks
SET SET
blockGlobalPreHashe = ?, block_global_pre_hashe = ?,
blockLinePreHashe = ?, block_line_pre_hashe = ?,
msgType = ?, msg_type = ?,
msgSubType = ?, msg_sub_type = ?,
blockByte = ?, block_byte = ?,
to_login = ?, to_login = ?,
toBchName = ?, to_bch_name = ?,
toBlockGlobalNumber = ?, to_block_global_number = ?,
toBlockHashe = ? to_block_hashe = ?
WHERE WHERE
login = ? login = ?
AND bchName = ? AND bch_name = ?
AND blockGlobalNumber = ? AND block_global_number = ?
AND blockLineIndex = ? AND block_line_index = ?
AND blockLineNumber = ? AND block_line_number = ?
"""; """;
try (PreparedStatement ps = c.prepareStatement(sql)) { try (PreparedStatement ps = c.prepareStatement(sql)) {
@ -207,7 +196,6 @@ public final class BlocksDAO {
} }
} }
/** Обновить без внешнего соединения. Сам открывает/закрывает. */
public int update(BlockEntry e) throws SQLException { public int update(BlockEntry e) throws SQLException {
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {
return update(c, e); return update(c, e);
@ -216,10 +204,6 @@ public final class BlocksDAO {
// -------------------- DELETE -------------------- // -------------------- DELETE --------------------
/**
* Удалить по "PK-подобному" набору полей с внешним соединением. Соединение НЕ закрывает.
* Может удалить >1 строк, если есть дубликаты.
*/
public int deleteByPk(Connection c, public int deleteByPk(Connection c,
String login, String login,
String bchName, String bchName,
@ -231,10 +215,10 @@ public final class BlocksDAO {
DELETE FROM blocks DELETE FROM blocks
WHERE WHERE
login = ? login = ?
AND bchName = ? AND bch_name = ?
AND blockGlobalNumber = ? AND block_global_number = ?
AND blockLineIndex = ? AND block_line_index = ?
AND blockLineNumber = ? AND block_line_number = ?
"""; """;
try (PreparedStatement ps = c.prepareStatement(sql)) { try (PreparedStatement ps = c.prepareStatement(sql)) {
@ -247,7 +231,6 @@ public final class BlocksDAO {
} }
} }
/** Удалить по "PK-подобному" набору полей без внешнего соединения. Сам открывает/закрывает. */
public int deleteByPk(String login, public int deleteByPk(String login,
String bchName, String bchName,
int blockGlobalNumber, int blockGlobalNumber,
@ -260,7 +243,6 @@ public final class BlocksDAO {
// -------------------- INTERNAL -------------------- // -------------------- INTERNAL --------------------
/** Единая привязка параметров под INSERT — чтобы не разъезжалось. */
private static void bindAll(PreparedStatement ps, BlockEntry e) throws SQLException { private static void bindAll(PreparedStatement ps, BlockEntry e) throws SQLException {
int i = 1; int i = 1;
@ -297,29 +279,29 @@ public final class BlocksDAO {
BlockEntry e = new BlockEntry(); BlockEntry e = new BlockEntry();
e.setLogin(rs.getString("login")); e.setLogin(rs.getString("login"));
e.setBchName(rs.getString("bchName")); e.setBchName(rs.getString("bch_name"));
e.setBlockGlobalNumber(rs.getInt("blockGlobalNumber")); e.setBlockGlobalNumber(rs.getInt("block_global_number"));
e.setBlockGlobalPreHashe(rs.getString("blockGlobalPreHashe")); e.setBlockGlobalPreHashe(rs.getString("block_global_pre_hashe"));
e.setBlockLineIndex(rs.getInt("blockLineIndex")); e.setBlockLineIndex(rs.getInt("block_line_index"));
e.setBlockLineNumber(rs.getInt("blockLineNumber")); e.setBlockLineNumber(rs.getInt("block_line_number"));
e.setBlockLinePreHashe(rs.getString("blockLinePreHashe")); e.setBlockLinePreHashe(rs.getString("block_line_pre_hashe"));
e.setMsgType(rs.getInt("msgType")); e.setMsgType(rs.getInt("msg_type"));
e.setMsgSubType(rs.getInt("msgSubType")); e.setMsgSubType(rs.getInt("msg_sub_type"));
e.setBlockByte(rs.getBytes("blockByte")); e.setBlockByte(rs.getBytes("block_byte"));
e.setToLogin(rs.getString("to_login")); e.setToLogin(rs.getString("to_login"));
String toBchName = rs.getString("toBchName"); String toBchName = rs.getString("to_bch_name");
if (rs.wasNull()) toBchName = null; if (rs.wasNull()) toBchName = null;
e.setToBchName(toBchName); e.setToBchName(toBchName);
Integer toBlockGlobalNumber = (Integer) rs.getObject("toBlockGlobalNumber"); Integer toBlockGlobalNumber = (Integer) rs.getObject("to_block_global_number");
e.setToBlockGlobalNumber(toBlockGlobalNumber); e.setToBlockGlobalNumber(toBlockGlobalNumber);
String toBlockHashe = rs.getString("toBlockHashe"); String toBlockHashe = rs.getString("to_block_hashe");
if (rs.wasNull()) toBlockHashe = null; if (rs.wasNull()) toBlockHashe = null;
e.setToBlockHashe(toBlockHashe); e.setToBlockHashe(toBlockHashe);

View File

@ -8,10 +8,10 @@ import java.sql.*;
/** /**
* DAO для таблицы ip_geo_cache. * DAO для таблицы ip_geo_cache.
* *
* * Таблица: * Таблица:
* * - ip TEXT PRIMARY KEY * - ip TEXT PRIMARY KEY
* * - geo TEXT * - geo TEXT
* * - updated_at_ms INTEGER NOT NULL * - updated_at_ms INTEGER NOT NULL
* *
* Правило: * Правило:
* - методы с Connection НЕ закрывают соединение * - методы с Connection НЕ закрывают соединение

View File

@ -14,18 +14,8 @@ import java.util.List;
* *
* Колонки: * Колонки:
* - login TEXT PRIMARY KEY * - login TEXT PRIMARY KEY
* Уникальный логин пользователя (case-insensitive используется на уровне запросов). * - device_key TEXT NOT NULL
* * - solana_key TEXT NULLABLE
* - deviceKey TEXT NOT NULL
* Публичный ключ устройства пользователя.
* Хранится в Base64(32 bytes) или HEX(64 chars).
*
* - solanaKey TEXT NULLABLE
* Публичный ключ Solana-аккаунта пользователя (если есть).
*
* Назначение таблицы:
* - локальное сопоставление login deviceKey / solanaKey
* - используется для аутентификации, валидации подписей и связки с блокчейном
* *
* Правило работы с соединениями: * Правило работы с соединениями:
* - методы с Connection НЕ закрывают соединение * - методы с Connection НЕ закрывают соединение
@ -52,7 +42,7 @@ public final class SolanaUsersDAO {
/** Вставка с внешним соединением. Соединение НЕ закрывает. */ /** Вставка с внешним соединением. Соединение НЕ закрывает. */
public void insert(Connection c, SolanaUserEntry user) throws SQLException { public void insert(Connection c, SolanaUserEntry user) throws SQLException {
String sql = """ String sql = """
INSERT INTO solana_users (login, deviceKey, solanaKey) INSERT INTO solana_users (login, device_key, solana_key)
VALUES (?, ?, ?) VALUES (?, ?, ?)
"""; """;
@ -102,7 +92,7 @@ public final class SolanaUsersDAO {
/** Получить по login (case-insensitive) с внешним соединением. Соединение НЕ закрывает. */ /** Получить по login (case-insensitive) с внешним соединением. Соединение НЕ закрывает. */
public SolanaUserEntry getByLogin(Connection c, String login) throws SQLException { public SolanaUserEntry getByLogin(Connection c, String login) throws SQLException {
String sql = """ String sql = """
SELECT login, deviceKey, solanaKey SELECT login, device_key, solana_key
FROM solana_users FROM solana_users
WHERE LOWER(login) = LOWER(?) WHERE LOWER(login) = LOWER(?)
"""; """;
@ -126,7 +116,7 @@ public final class SolanaUsersDAO {
/** Поиск по префиксу с внешним соединением. Соединение НЕ закрывает. */ /** Поиск по префиксу с внешним соединением. Соединение НЕ закрывает. */
public List<SolanaUserEntry> searchByLoginPrefix(Connection c, String prefix) throws SQLException { public List<SolanaUserEntry> searchByLoginPrefix(Connection c, String prefix) throws SQLException {
String sql = """ String sql = """
SELECT login, deviceKey, solanaKey SELECT login, device_key, solana_key
FROM solana_users FROM solana_users
WHERE LOWER(login) LIKE ? WHERE LOWER(login) LIKE ?
ORDER BY login ORDER BY login
@ -157,10 +147,10 @@ public final class SolanaUsersDAO {
private SolanaUserEntry mapRow(ResultSet rs) throws SQLException { private SolanaUserEntry mapRow(ResultSet rs) throws SQLException {
SolanaUserEntry e = new SolanaUserEntry( SolanaUserEntry e = new SolanaUserEntry(
rs.getString("login"), rs.getString("login"),
rs.getString("deviceKey") rs.getString("device_key")
); );
String solanaKey = rs.getString("solanaKey"); String solanaKey = rs.getString("solana_key");
if (rs.wasNull()) solanaKey = null; if (rs.wasNull()) solanaKey = null;
e.setSolanaKey(solanaKey); e.setSolanaKey(solanaKey);

View File

@ -8,8 +8,8 @@ import java.sql.*;
/** /**
* UserCreateDAO атомарное добавление пользователя: * UserCreateDAO атомарное добавление пользователя:
* - solana_users (login, deviceKey) * - solana_users (login, device_key)
* - blockchain_state (blockchainName, login, blockchainKey, size_limit, ... last_global_number=-1 ...) * - blockchain_state (blockchain_name, login, blockchain_key, size_limit, ... last_global_number=-1 ...)
* *
* ВАЖНО: * ВАЖНО:
* - только INSERT * - только INSERT
@ -86,8 +86,6 @@ public final class UserCreateDAO {
} catch (SQLException e) { } catch (SQLException e) {
c.rollback(); c.rollback();
// SQLITE_CONSTRAINT -> "уже существует"
// Мы не делаем UPDATE, только insert.
String msg = e.getMessage() == null ? "" : e.getMessage().toLowerCase(); String msg = e.getMessage() == null ? "" : e.getMessage().toLowerCase();
if (msg.contains("constraint")) { if (msg.contains("constraint")) {
return false; return false;

View File

@ -17,11 +17,6 @@ import java.util.List;
* ЛОГИКА time_ms: * ЛОГИКА time_ms:
* - БД принимает запись только если она "новее" (time_ms строго больше текущего). * - БД принимает запись только если она "новее" (time_ms строго больше текущего).
* - Реализовано атомарно одним SQL: UPSERT + WHERE users_params.time_ms < excluded.time_ms * - Реализовано атомарно одним SQL: UPSERT + WHERE users_params.time_ms < excluded.time_ms
*
* Возврат результата:
* - upsertIfNewer(...) возвращает количество изменённых строк:
* 1 = вставили/обновили
* 0 = проигнорировали (запись уже новее или равная)
*/ */
public final class UserParamsDAO { public final class UserParamsDAO {
@ -41,11 +36,6 @@ public final class UserParamsDAO {
// -------------------- UPSERT (IF NEWER) -------------------- // -------------------- UPSERT (IF NEWER) --------------------
/**
* Атомарный UPSERT "только если новее".
*
* @return 1 если вставили/обновили; 0 если запись не тронули (existing.time_ms >= incoming.time_ms).
*/
public int upsertIfNewer(Connection c, UserParamEntry e) throws SQLException { public int upsertIfNewer(Connection c, UserParamEntry e) throws SQLException {
String sql = """ String sql = """
INSERT INTO users_params ( INSERT INTO users_params (
@ -77,11 +67,10 @@ public final class UserParamsDAO {
if (e.getSignature() != null) ps.setString(6, e.getSignature()); if (e.getSignature() != null) ps.setString(6, e.getSignature());
else ps.setNull(6, Types.VARCHAR); else ps.setNull(6, Types.VARCHAR);
return ps.executeUpdate(); // 1 или 0 return ps.executeUpdate();
} }
} }
/** То же самое, но сам открывает/закрывает соединение. */
public int upsertIfNewer(UserParamEntry e) throws SQLException { public int upsertIfNewer(UserParamEntry e) throws SQLException {
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {
return upsertIfNewer(c, e); return upsertIfNewer(c, e);
@ -90,7 +79,6 @@ public final class UserParamsDAO {
// -------------------- SELECT -------------------- // -------------------- SELECT --------------------
/** Получить параметр по (login,param) с внешним соединением. Соединение НЕ закрывает. */
public UserParamEntry getByLoginAndParam(Connection c, String login, String param) throws SQLException { public UserParamEntry getByLoginAndParam(Connection c, String login, String param) throws SQLException {
String sql = """ String sql = """
SELECT SELECT
@ -116,14 +104,12 @@ public final class UserParamsDAO {
} }
} }
/** Получить параметр по (login,param) без внешнего соединения. Сам открывает/закрывает. */
public UserParamEntry getByLoginAndParam(String login, String param) throws SQLException { public UserParamEntry getByLoginAndParam(String login, String param) throws SQLException {
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {
return getByLoginAndParam(c, login, param); return getByLoginAndParam(c, login, param);
} }
} }
/** Получить все параметры пользователя с внешним соединением. */
public List<UserParamEntry> getByLogin(Connection c, String login) throws SQLException { public List<UserParamEntry> getByLogin(Connection c, String login) throws SQLException {
String sql = """ String sql = """
SELECT SELECT
@ -148,7 +134,6 @@ public final class UserParamsDAO {
return list; return list;
} }
/** Получить все параметры пользователя без внешнего соединения. */
public List<UserParamEntry> getByLogin(String login) throws SQLException { public List<UserParamEntry> getByLogin(String login) throws SQLException {
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {
return getByLogin(c, login); return getByLogin(c, login);

View File

@ -2,27 +2,23 @@ package shine.db.entities;
/** /**
* Модель активной сессии (таблица active_sessions). * Модель активной сессии (таблица active_sessions).
*
* Теперь вместо loginId:
* - login TEXT NOT NULL (FK -> solana_users(login))
*/ */
public class ActiveSessionEntry { public class ActiveSessionEntry {
private String sessionId; // TEXT base64(32 bytes) private String sessionId;
private String login; // TEXT NOT NULL private String login;
private String sessionPwd; // TEXT private String sessionPwd;
private String storagePwd; // TEXT private String storagePwd;
private long sessionCreatedAtMs; // INTEGER private long sessionCreatedAtMs;
private long lastAuthirificatedAtMs; // INTEGER private long lastAuthirificatedAtMs;
private String pushEndpoint; // TEXT (nullable) private String pushEndpoint;
private String pushP256dhKey; // TEXT (nullable) private String pushP256dhKey;
private String pushAuthKey; // TEXT (nullable) private String pushAuthKey;
// Новые поля private String clientIp;
private String clientIp; // IP клиента при auth/refresh private String clientInfoFromClient;
private String clientInfoFromClient; // строка от клиента (PWA) private String clientInfoFromRequest;
private String clientInfoFromRequest; // строка, собранная на сервере private String userLanguage;
private String userLanguage; // prefer-language (например, "ru-RU")
public ActiveSessionEntry() { public ActiveSessionEntry() {
} }

View File

@ -2,41 +2,28 @@ package shine.db.entities;
/** /**
* Запись блока (таблица blocks). * Запись блока (таблица blocks).
*
* Теперь:
* - login TEXT NOT NULL
* - bchName TEXT NOT NULL (идёт сразу после login)
* - to_login TEXT nullable
* - toBchName TEXT nullable
* - toBlockGlobalNumber INTEGER nullable
* - toBlockHashe TEXT nullable
*
* ДОБАВЛЕНО:
* - msgSubType INTEGER (uint16 по смыслу, храним как int)
*
* PRIMARY KEY пока убран вообще.
*/ */
public class BlockEntry { public class BlockEntry {
private String login; // TEXT private String login;
private String bchName; // TEXT private String bchName;
private int blockGlobalNumber; // int32 private int blockGlobalNumber;
private String blockGlobalPreHashe; // TEXT private String blockGlobalPreHashe;
private int blockLineIndex; // int16 (храним как int) private int blockLineIndex;
private int blockLineNumber; // int32 private int blockLineNumber;
private String blockLinePreHashe; // TEXT private String blockLinePreHashe;
private int msgType; // int16 (храним как int) private int msgType;
private int msgSubType; // int16 (храним как int) private int msgSubType;
private byte[] blockByte; // BLOB private byte[] blockByte;
private String toLogin; // TEXT nullable private String toLogin;
private String toBchName; // TEXT nullable private String toBchName;
private Integer toBlockGlobalNumber; // INTEGER nullable private Integer toBlockGlobalNumber;
private String toBlockHashe; // TEXT nullable private String toBlockHashe;
public BlockEntry() {} public BlockEntry() {}

View File

@ -5,29 +5,22 @@ import java.util.Base64;
/** /**
* Агрегатная сущность текущего состояния блокчейна. * Агрегатная сущность текущего состояния блокчейна.
* 1 строка = 1 blockchainName, плюс состояние линий 0..7. * 1 строка = 1 blockchain_name, плюс состояние линий 0..7.
*/ */
public final class BlockchainStateEntry { public final class BlockchainStateEntry {
private String blockchainName; private String blockchainName;
private String login; private String login;
/** Ключ блокчейна (pubkey), которым подписываются блоки. Base64(32 bytes). */
private String blockchainKey; private String blockchainKey;
/** Лимит (теперь long). */
private long sizeLimit; private long sizeLimit;
/** Размер файла блокчейна в байтах (то, что будем сверять/чинить при старте). */
private long fileSizeBytes; private long fileSizeBytes;
private int lastGlobalNumber; private int lastGlobalNumber;
private String lastGlobalHash; // HEX(64) либо пустая строка для "нулевого" private String lastGlobalHash;
/** line 0..7 */
private final int[] lastLineNumbers = new int[8]; private final int[] lastLineNumbers = new int[8];
/** line 0..7 */
private final String[] lastLineHashes = new String[8]; private final String[] lastLineHashes = new String[8];
private long updatedAtMs; private long updatedAtMs;
@ -78,7 +71,6 @@ public final class BlockchainStateEntry {
public String getBlockchainKey() { return blockchainKey; } public String getBlockchainKey() { return blockchainKey; }
public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; } public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
/** blockchainKey в байтах (32) или null, если битый. */
public byte[] getBlockchainKeyBytes() { public byte[] getBlockchainKeyBytes() {
if (blockchainKey == null) return null; if (blockchainKey == null) return null;
String s = blockchainKey.trim(); String s = blockchainKey.trim();
@ -103,7 +95,6 @@ public final class BlockchainStateEntry {
public String getLastGlobalHash() { return lastGlobalHash; } public String getLastGlobalHash() { return lastGlobalHash; }
public void setLastGlobalHash(String lastGlobalHash) { this.lastGlobalHash = lastGlobalHash == null ? "" : lastGlobalHash; } public void setLastGlobalHash(String lastGlobalHash) { this.lastGlobalHash = lastGlobalHash == null ? "" : lastGlobalHash; }
/** line in [0..7] */
public int getLastLineNumber(int line) { public int getLastLineNumber(int line) {
checkLine(line); checkLine(line);
return lastLineNumbers[line]; return lastLineNumbers[line];
@ -113,7 +104,6 @@ public final class BlockchainStateEntry {
lastLineNumbers[line] = value; lastLineNumbers[line] = value;
} }
/** line in [0..7] */
public String getLastLineHash(int line) { public String getLastLineHash(int line) {
checkLine(line); checkLine(line);
return lastLineHashes[line]; return lastLineHashes[line];

View File

@ -2,11 +2,6 @@ package shine.db.entities;
/** /**
* Запись в таблице ip_geo_cache. * Запись в таблице ip_geo_cache.
*
* Храним:
* - ip строка IP-адреса (PRIMARY KEY)
* - geo строка "Country, City" или любое текстовое описание
* - updatedAtMs время последнего обновления (Unix time в мс)
*/ */
public class IpGeoCacheEntry { public class IpGeoCacheEntry {

View File

@ -9,34 +9,14 @@ import java.util.Base64;
* *
* Поля: * Поля:
* - login PRIMARY KEY (TEXT) * - login PRIMARY KEY (TEXT)
* Уникальный логин пользователя. * - device_key TEXT NOT NULL
* * - solana_key TEXT NULLABLE
* - deviceKey TEXT NOT NULL
* Публичный ключ устройства.
* Поддерживаемые форматы:
* Base64 (32 байта)
* HEX (64 hex-символа)
*
* - solanaKey TEXT NULLABLE
* Публичный ключ Solana-аккаунта пользователя (если используется).
*
* Назначение:
* - хранение минимальной локальной информации о пользователе
* - используется для:
* проверки подписи запросов
* привязки пользователя к блокчейну
* сопоставления login ключи
*
* ВАЖНО:
* - deviceKey обязателен всегда
* - solanaKey может отсутствовать (null)
*/ */
public class SolanaUserEntry { public class SolanaUserEntry {
private String login; // TEXT PK private String login;
private String deviceKey; // TEXT NOT NULL (Base64(32 bytes)) private String deviceKey;
private String solanaKey; // TEXT private String solanaKey;
public SolanaUserEntry() {} public SolanaUserEntry() {}
@ -54,32 +34,22 @@ public class SolanaUserEntry {
public String getLogin() { return login; } public String getLogin() { return login; }
public void setLogin(String login) { this.login = login; } public void setLogin(String login) { this.login = login; }
/** Публичный ключ устройства (device key). */
public String getDeviceKey() { return deviceKey; } public String getDeviceKey() { return deviceKey; }
public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; } public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
public String getSolanaKey() { return solanaKey; } public String getSolanaKey() { return solanaKey; }
public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; } public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; }
/**
* Device key в байтах (32 байта) или null, если ключ битый/пустой.
*
* Поддержка форматов:
* - Base64 (предпочтительно)
* - HEX (ровно 64 hex-символа, без пробелов)
*/
public byte[] getDeviceKeyByte() { public byte[] getDeviceKeyByte() {
if (deviceKey == null) return null; if (deviceKey == null) return null;
String s = deviceKey.trim(); String s = deviceKey.trim();
if (s.isEmpty()) return null; if (s.isEmpty()) return null;
// 1) пробуем Base64
try { try {
byte[] b = Base64.getDecoder().decode(s); byte[] b = Base64.getDecoder().decode(s);
if (b != null && b.length == 32) return b; if (b != null && b.length == 32) return b;
} catch (IllegalArgumentException ignore) {} } catch (IllegalArgumentException ignore) {}
// 2) пробуем HEX (64 символа)
if (s.length() == 64 && s.matches("^[0-9a-fA-F]+$")) { if (s.length() == 64 && s.matches("^[0-9a-fA-F]+$")) {
byte[] out = new byte[32]; byte[] out = new byte[32];
for (int i = 0; i < 32; i++) { for (int i = 0; i < 32; i++) {

View File

@ -10,12 +10,6 @@ package shine.db.entities;
* - value TEXT NOT NULL * - value TEXT NOT NULL
* - device_key TEXT NULL * - device_key TEXT NULL
* - signature TEXT NULL * - signature TEXT NULL
*
* UNIQUE(login, param)
*
* Смысл:
* - в таблице всегда хранится "последнее" значение параметра по времени.
* - time_ms монотонно растёт для каждого (login,param) сервер не принимает более старые обновления.
*/ */
public class UserParamEntry { public class UserParamEntry {
@ -24,8 +18,8 @@ public class UserParamEntry {
private long timeMs; private long timeMs;
private String value; private String value;
private String deviceKey; // base64(32) можно хранить как "каким ключом подписано" private String deviceKey;
private String signature; // base64(64) private String signature;
public UserParamEntry() {} public UserParamEntry() {}