Каналы: типы 0/1/100/200, CreateChannel v3, state для chat200, новые API и деплой на prod

This commit is contained in:
AidarKC
2026-05-13 01:17:23 +03:00
parent 97d37a2eb6
commit e95f65ac78
36 changed files with 1764 additions and 187 deletions
@@ -397,11 +397,13 @@ public final class DatabaseInitializer {
// 9) channel_names_state (global normalized channel names)
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS channel_names_state (
slug TEXT NOT NULL PRIMARY KEY,
slug TEXT NOT NULL,
display_name TEXT NOT NULL,
channel_description TEXT NOT NULL DEFAULT '',
owner_login TEXT NOT NULL,
owner_bch_name TEXT NOT NULL,
channel_type_code INTEGER NOT NULL DEFAULT 1,
channel_type_version INTEGER NOT NULL DEFAULT 1,
channel_root_block_number INTEGER NOT NULL,
channel_root_block_hash BLOB NOT NULL,
created_at_ms INTEGER NOT NULL
@@ -409,8 +411,8 @@ public final class DatabaseInitializer {
""");
st.executeUpdate("""
CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_names_state_slug
ON channel_names_state (slug);
CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_names_state_owner_type_slug
ON channel_names_state (owner_bch_name, channel_type_code, slug);
""");
st.executeUpdate("""
@@ -423,6 +425,48 @@ public final class DatabaseInitializer {
ON channel_names_state (owner_login, owner_bch_name);
""");
// 9.1) chat200_state
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS chat200_state (
owner_login TEXT NOT NULL,
owner_bch_name TEXT NOT NULL,
channel_root_block_number INTEGER NOT NULL,
channel_root_block_hash BLOB NOT NULL,
channel_name TEXT NOT NULL,
channel_type_version INTEGER NOT NULL,
chat_title TEXT NOT NULL DEFAULT '',
updated_at_ms INTEGER NOT NULL,
PRIMARY KEY (owner_bch_name, channel_root_block_number)
);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_chat200_state_owner
ON chat200_state (owner_login, owner_bch_name);
""");
// 9.2) chat200_members_state
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS chat200_members_state (
owner_bch_name TEXT NOT NULL,
channel_root_block_number INTEGER NOT NULL,
member_login TEXT NOT NULL,
member_channel_name TEXT NOT NULL,
is_active INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL,
updated_by_block_number INTEGER NOT NULL,
PRIMARY KEY (
owner_bch_name,
channel_root_block_number,
member_login,
member_channel_name
)
);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_chat200_members_owner
ON chat200_members_state (owner_bch_name, channel_root_block_number, is_active);
""");
// 10) direct_messages
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS direct_messages (
@@ -14,7 +14,7 @@ import java.sql.Statement;
public final class SqliteDbController {
private static volatile SqliteDbController instance;
private static final int LATEST_SCHEMA_VERSION = 2;
private static final int LATEST_SCHEMA_VERSION = 3;
private final String jdbcUrl;
@@ -85,6 +85,7 @@ public final class SqliteDbController {
switch (targetVersion) {
case 1 -> migrateToV1();
case 2 -> migrateToV2();
case 3 -> migrateToV3();
default -> throw new RuntimeException("Unknown DB migration target version: " + targetVersion);
}
}
@@ -107,6 +108,7 @@ public final class SqliteDbController {
}
ensureChannelNamesDescriptionColumn(c, st);
ensureChannelNamesTypeColumns(c, st);
ensureSignedMessageReceiptUniq(c, st);
DatabaseTriggersInstaller.createAllTriggers(st);
setSchemaVersion(c, 1);
@@ -147,6 +149,69 @@ public final class SqliteDbController {
}
}
private void migrateToV3() {
try (Connection c = DriverManager.getConnection(jdbcUrl);
Statement st = c.createStatement()) {
c.setAutoCommit(false);
try {
ensureChat200StateTables(st);
setSchemaVersion(c, 3);
c.commit();
} catch (Exception e) {
try { c.rollback(); } catch (Exception ignored) {}
throw new RuntimeException("DB migration to v3 failed", e);
} finally {
try { c.setAutoCommit(true); } catch (Exception ignored) {}
}
} catch (SQLException e) {
throw new RuntimeException("DB migration to v3 failed", e);
}
}
private static void ensureChat200StateTables(Statement st) throws SQLException {
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS chat200_state (
owner_login TEXT NOT NULL,
owner_bch_name TEXT NOT NULL,
channel_root_block_number INTEGER NOT NULL,
channel_root_block_hash BLOB NOT NULL,
channel_name TEXT NOT NULL,
channel_type_version INTEGER NOT NULL,
chat_title TEXT NOT NULL DEFAULT '',
updated_at_ms INTEGER NOT NULL,
PRIMARY KEY (owner_bch_name, channel_root_block_number)
);
""");
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS chat200_members_state (
owner_bch_name TEXT NOT NULL,
channel_root_block_number INTEGER NOT NULL,
member_login TEXT NOT NULL,
member_channel_name TEXT NOT NULL,
is_active INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL,
updated_by_block_number INTEGER NOT NULL,
PRIMARY KEY (
owner_bch_name,
channel_root_block_number,
member_login,
member_channel_name
)
);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_chat200_state_owner
ON chat200_state (owner_login, owner_bch_name);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_chat200_members_owner
ON chat200_members_state (owner_bch_name, channel_root_block_number, is_active);
""");
}
private int getCurrentSchemaVersion() {
try (Connection c = DriverManager.getConnection(jdbcUrl)) {
if (!tableExists(c, DatabaseInitializer.DB_SCHEMA_VERSION_TABLE)) {
@@ -256,11 +321,13 @@ public final class SqliteDbController {
private static void ensureChannelNamesStateTable(Statement st) throws SQLException {
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS channel_names_state (
slug TEXT NOT NULL PRIMARY KEY,
slug TEXT NOT NULL,
display_name TEXT NOT NULL,
channel_description TEXT NOT NULL DEFAULT '',
owner_login TEXT NOT NULL,
owner_bch_name TEXT NOT NULL,
channel_type_code INTEGER NOT NULL DEFAULT 1,
channel_type_version INTEGER NOT NULL DEFAULT 1,
channel_root_block_number INTEGER NOT NULL,
channel_root_block_hash BLOB NOT NULL,
created_at_ms INTEGER NOT NULL
@@ -286,10 +353,33 @@ public final class SqliteDbController {
}
}
private static void ensureChannelNamesTypeColumns(Connection c, Statement st) throws SQLException {
boolean hasTypeCode = false;
boolean hasTypeVersion = false;
try (Statement probe = c.createStatement();
ResultSet rs = probe.executeQuery("PRAGMA table_info(channel_names_state)")) {
while (rs.next()) {
String name = rs.getString("name");
if ("channel_type_code".equalsIgnoreCase(name)) hasTypeCode = true;
if ("channel_type_version".equalsIgnoreCase(name)) hasTypeVersion = true;
}
}
if (!hasTypeCode) {
st.executeUpdate("ALTER TABLE channel_names_state ADD COLUMN channel_type_code INTEGER NOT NULL DEFAULT 1");
}
if (!hasTypeVersion) {
st.executeUpdate("ALTER TABLE channel_names_state ADD COLUMN channel_type_version INTEGER NOT NULL DEFAULT 1");
}
}
private static void ensureChannelNamesIndexes(Statement st) throws SQLException {
st.executeUpdate("DROP INDEX IF EXISTS uq_channel_names_state_slug");
st.executeUpdate("DROP INDEX IF EXISTS uq_channel_names_state_owner_slug");
st.executeUpdate("""
CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_names_state_slug
ON channel_names_state (slug);
CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_names_state_owner_type_slug
ON channel_names_state (owner_bch_name, channel_type_code, slug);
""");
st.executeUpdate("""
CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_names_state_target
@@ -24,19 +24,26 @@ public final class ChannelNameStateDAO {
return instance;
}
public boolean existsBySlug(Connection c, String slug) throws SQLException {
String sql = "SELECT 1 FROM channel_names_state WHERE slug = ? LIMIT 1";
public boolean existsByOwnerTypeAndSlug(Connection c, String ownerBchName, int channelTypeCode, String slug) throws SQLException {
String sql = """
SELECT 1
FROM channel_names_state
WHERE owner_bch_name = ? AND channel_type_code = ? AND slug = ?
LIMIT 1
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, slug);
ps.setString(1, ownerBchName);
ps.setInt(2, channelTypeCode);
ps.setString(3, slug);
try (ResultSet rs = ps.executeQuery()) {
return rs.next();
}
}
}
public boolean existsBySlug(String slug) throws SQLException {
public boolean existsByOwnerTypeAndSlug(String ownerBchName, int channelTypeCode, String slug) throws SQLException {
try (Connection c = db.getConnection()) {
return existsBySlug(c, slug);
return existsByOwnerTypeAndSlug(c, ownerBchName, channelTypeCode, slug);
}
}
@@ -54,10 +61,12 @@ public final class ChannelNameStateDAO {
channel_description,
owner_login,
owner_bch_name,
channel_type_code,
channel_type_version,
channel_root_block_number,
channel_root_block_hash,
created_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, entry.getSlug());
@@ -65,9 +74,11 @@ public final class ChannelNameStateDAO {
ps.setString(3, entry.getChannelDescription() == null ? "" : entry.getChannelDescription());
ps.setString(4, entry.getOwnerLogin());
ps.setString(5, entry.getOwnerBlockchainName());
ps.setInt(6, entry.getChannelRootBlockNumber());
ps.setBytes(7, entry.getChannelRootBlockHash());
ps.setLong(8, entry.getCreatedAtMs());
ps.setInt(6, entry.getChannelTypeCode());
ps.setInt(7, entry.getChannelTypeVersion());
ps.setInt(8, entry.getChannelRootBlockNumber());
ps.setBytes(9, entry.getChannelRootBlockHash());
ps.setLong(10, entry.getCreatedAtMs());
ps.executeUpdate();
}
}
@@ -8,6 +8,8 @@ public class ChannelNameStateEntry {
private String channelDescription;
private String ownerLogin;
private String ownerBlockchainName;
private int channelTypeCode;
private int channelTypeVersion;
private int channelRootBlockNumber;
private byte[] channelRootBlockHash;
private long createdAtMs;
@@ -52,6 +54,22 @@ public class ChannelNameStateEntry {
this.ownerBlockchainName = ownerBlockchainName;
}
public int getChannelTypeCode() {
return channelTypeCode;
}
public void setChannelTypeCode(int channelTypeCode) {
this.channelTypeCode = channelTypeCode;
}
public int getChannelTypeVersion() {
return channelTypeVersion;
}
public void setChannelTypeVersion(int channelTypeVersion) {
this.channelTypeVersion = channelTypeVersion;
}
public int getChannelRootBlockNumber() {
return channelRootBlockNumber;
}