fix mysql tenant database schema cleanup

This commit is contained in:
richjohnrambo
2026-08-15 15:48:17 +08:00
parent 672fc68cfa
commit d8d1639d5b
6 changed files with 868 additions and 81 deletions
+735
View File
@@ -0,0 +1,735 @@
-- IM product database changes summary, chapters 1-11.
-- Target:
-- public backend database: hotgo
-- default tenant business database: tinode
-- Notes:
-- 1. Public tenant registry tables live in hotgo.
-- 2. Tenant-scoped business tables live in each tenant database and do not include tenant_id.
-- 3. This file is for review and manual execution. Existing legacy columns/indexes should be checked before ALTER/DROP.
-- 4. Table definitions are aligned with server/db/mysql/adapter.go plus im_verification_codes used by chapter 1 verification APIs.
SET NAMES utf8mb4;
CREATE DATABASE IF NOT EXISTS hotgo CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
CREATE DATABASE IF NOT EXISTS tinode CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
-- ============================================================
-- Public database: tenant registry.
-- ============================================================
CREATE TABLE IF NOT EXISTS hotgo.im_tenant (
id BIGINT NOT NULL AUTO_INCREMENT,
code VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
tenant_desc VARCHAR(256),
db_name VARCHAR(128) NOT NULL DEFAULT '',
db_dsn VARCHAR(1024) NOT NULL DEFAULT '',
state SMALLINT NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
created_by BIGINT NOT NULL,
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
updated_by BIGINT NOT NULL,
PRIMARY KEY (id),
UNIQUE INDEX uk_im_tenant_code (code),
INDEX idx_im_tenant_db_name (db_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS hotgo.im_tenant_config (
id BIGINT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
created_by BIGINT NOT NULL,
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
updated_by BIGINT NOT NULL,
PRIMARY KEY (id),
UNIQUE INDEX uk_im_tenant_config_tenant_id (tenant_id),
CONSTRAINT fk_im_tenant_config_tenant FOREIGN KEY (tenant_id) REFERENCES hotgo.im_tenant(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ============================================================
-- Tenant database: chapter 1, tenant-aware login, verification, security.
-- ============================================================
CREATE TABLE IF NOT EXISTS tinode.im_user_security (
user_id BIGINT NOT NULL,
auth_version INT NOT NULL DEFAULT 1,
password_updated_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_verification_codes (
id BIGINT NOT NULL AUTO_INCREMENT,
challenge_id VARCHAR(64) NOT NULL,
scene VARCHAR(32) NOT NULL,
action VARCHAR(64) NOT NULL DEFAULT '',
phone_hash CHAR(64) NOT NULL,
phone_mask VARCHAR(32) NOT NULL,
target_user BIGINT,
code_hash CHAR(64) NOT NULL,
verification_token_hash CHAR(64),
state SMALLINT NOT NULL,
attempts INT NOT NULL DEFAULT 0,
max_attempts INT NOT NULL,
expires_at DATETIME(3) NOT NULL,
resend_after DATETIME(3) NOT NULL,
verified_at DATETIME(3),
consumed_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_verification_challenge (challenge_id),
INDEX idx_im_verification_scene_phone_state (scene, phone_hash, state, expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_realname_verifications (
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
submission_id VARCHAR(64) NOT NULL,
status VARCHAR(16) NOT NULL,
name_cipher TEXT NOT NULL,
id_type VARCHAR(32) NOT NULL,
id_number_cipher TEXT NOT NULL,
id_number_hash CHAR(64) NOT NULL,
masked_name VARCHAR(64) NOT NULL,
masked_id_number VARCHAR(64) NOT NULL,
review_reason VARCHAR(256),
verified_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_realname_submission (submission_id),
INDEX idx_im_realname_user_latest (user_id, created_at),
INDEX idx_im_realname_id_hash (id_number_hash)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_action_tokens (
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
token_id VARCHAR(64) NOT NULL,
action VARCHAR(64) NOT NULL,
target VARCHAR(128) NOT NULL DEFAULT '',
token_hash CHAR(64) NOT NULL,
state SMALLINT NOT NULL,
expires_at DATETIME(3) NOT NULL,
consumed_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_action_tokens_token (token_id),
INDEX idx_im_action_tokens_user_action (user_id, action, target, state, expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_youth_mode_settings (
user_id BIGINT NOT NULL,
purpose VARCHAR(64) NOT NULL,
password_hash VARBINARY(255) NOT NULL,
enabled TINYINT NOT NULL DEFAULT 0,
policy_version INT NOT NULL DEFAULT 1,
restrictions JSON,
enabled_at DATETIME(3),
failed_attempts INT NOT NULL DEFAULT 0,
locked_until DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (user_id, purpose)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ============================================================
-- Tenant database: chapters 2-7, notifications, messages, friends, files, favorites.
-- ============================================================
CREATE TABLE IF NOT EXISTS tinode.im_system_notifications (
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
event_id VARCHAR(64) NOT NULL,
event_type VARCHAR(64) NOT NULL,
title VARCHAR(128) NOT NULL,
summary VARCHAR(512) NOT NULL,
action JSON,
read_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_system_notifications_event (user_id, event_id),
INDEX idx_im_system_notifications_inbox (user_id, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_message_revisions (
id BIGINT NOT NULL AUTO_INCREMENT,
createdat DATETIME(3) NOT NULL,
updatedat DATETIME(3) NOT NULL,
topic CHAR(25) NOT NULL,
seqid INT NOT NULL,
version INT NOT NULL,
actor BIGINT NOT NULL,
head JSON,
content JSON,
reason VARCHAR(256) NOT NULL DEFAULT '',
PRIMARY KEY (id),
UNIQUE INDEX uk_im_message_revisions_version (topic, seqid, version)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_friend_requests (
id BIGINT NOT NULL AUTO_INCREMENT,
request_id VARCHAR(64) NOT NULL,
applicant_id BIGINT NOT NULL,
target_id BIGINT NOT NULL,
message VARCHAR(200) NOT NULL DEFAULT '',
source VARCHAR(32) NOT NULL DEFAULT '',
source_topic VARCHAR(64) NOT NULL DEFAULT '',
status VARCHAR(16) NOT NULL,
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
expires_at DATETIME(3) NOT NULL,
handled_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_friend_requests_request (request_id),
UNIQUE INDEX uk_im_friend_requests_idempotency (applicant_id, idempotency_key),
INDEX idx_im_friend_requests_pair (applicant_id, target_id, status, created_at),
INDEX idx_im_friend_requests_inbox (target_id, status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_friendships (
user_id BIGINT NOT NULL,
peer_id BIGINT NOT NULL,
status VARCHAR(16) NOT NULL,
remark VARCHAR(64) NOT NULL DEFAULT '',
topic CHAR(25) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (user_id, peer_id),
INDEX idx_im_friendships_peer (peer_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_user_blocks (
blocker_id BIGINT NOT NULL,
blocked_id BIGINT NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (blocker_id, blocked_id),
INDEX idx_im_user_blocks_blocked (blocked_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_favorites (
id BIGINT NOT NULL AUTO_INCREMENT,
favorite_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
item_type VARCHAR(16) NOT NULL,
source_topic CHAR(25) NOT NULL,
source_seq INT NOT NULL,
snapshot JSON,
attachment_refs JSON,
availability VARCHAR(16) NOT NULL DEFAULT 'available',
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
collected_at DATETIME(3) NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_favorites_favorite (favorite_id),
UNIQUE INDEX uk_im_favorites_source (user_id, source_topic, source_seq),
UNIQUE INDEX uk_im_favorites_idempotency (user_id, ((NULLIF(idempotency_key, '')))),
INDEX idx_im_favorites_inbox (user_id, id DESC),
INDEX idx_im_favorites_type (user_id, item_type, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Existing Tinode tables must match the current schema for chapters 2-7:
-- topics.aux, fileuploads.etag, and filemsglinks.topic/userid are required by current code.
SET @im_sql = IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'client_msg_id') = 0,
'ALTER TABLE tinode.messages ADD COLUMN client_msg_id VARCHAR(64) AFTER `from`',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
SET @im_sql = IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'version') = 0,
'ALTER TABLE tinode.messages ADD COLUMN version INT NOT NULL DEFAULT 1 AFTER client_msg_id',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
SET @im_sql = IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'recalled_at') = 0,
'ALTER TABLE tinode.messages ADD COLUMN recalled_at DATETIME(3) AFTER version',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
SET @im_sql = IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'recalled_by') = 0,
'ALTER TABLE tinode.messages ADD COLUMN recalled_by BIGINT AFTER recalled_at',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
SET @im_sql = IF(
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND INDEX_NAME = 'messages_sender_client_mid') = 0,
'CREATE UNIQUE INDEX messages_sender_client_mid ON tinode.messages(`from`, client_msg_id)',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
-- Independent-DB tenant isolation: tenant business tables must not keep tenant_id.
DROP TABLE IF EXISTS tinode.im_tenant_config;
DROP TABLE IF EXISTS tinode.im_tenant;
SET @im_sql = IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'auth' AND COLUMN_NAME = 'tenant_id') > 0,
'ALTER TABLE tinode.auth DROP COLUMN tenant_id',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
SET @im_sql = IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'credentials' AND COLUMN_NAME = 'tenant_id') > 0,
'ALTER TABLE tinode.credentials DROP COLUMN tenant_id',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
-- ============================================================
-- Tenant database: chapter 8, realtime call records.
-- ============================================================
CREATE TABLE IF NOT EXISTS tinode.im_call_records (
id BIGINT NOT NULL AUTO_INCREMENT,
call_id VARCHAR(64) NOT NULL,
topic CHAR(25) NOT NULL,
seq_id INT NOT NULL,
caller_id BIGINT NOT NULL,
callee_id BIGINT NOT NULL,
call_type VARCHAR(16) NOT NULL,
status VARCHAR(32) NOT NULL,
end_reason VARCHAR(64) NOT NULL DEFAULT '',
duration_ms BIGINT NOT NULL DEFAULT 0,
started_at DATETIME(3) NOT NULL,
accepted_at DATETIME(3),
finished_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_call_records_call (call_id),
UNIQUE INDEX uk_im_call_records_topic_seq (topic, seq_id),
INDEX idx_im_call_records_caller (caller_id, id DESC),
INDEX idx_im_call_records_callee (callee_id, id DESC),
INDEX idx_im_call_records_status (status, updated_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ============================================================
-- Tenant database: chapter 9, workbench and office collaboration.
-- ============================================================
CREATE TABLE IF NOT EXISTS tinode.im_workbench_apps (
id BIGINT NOT NULL AUTO_INCREMENT,
code VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
icon_file_id VARCHAR(64) NOT NULL DEFAULT '',
route VARCHAR(128) NOT NULL,
sort_order INT NOT NULL DEFAULT 0,
unread_key VARCHAR(64) NOT NULL DEFAULT '',
enabled TINYINT NOT NULL DEFAULT 1,
permission VARCHAR(64) NOT NULL DEFAULT 'enabled',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_workbench_apps_code (code),
INDEX idx_im_workbench_apps_enabled_order (enabled, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_org_departments (
id BIGINT NOT NULL AUTO_INCREMENT,
department_id VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
parent_id VARCHAR(64) NOT NULL DEFAULT 'root',
sort_order INT NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 1,
state VARCHAR(16) NOT NULL DEFAULT 'active',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_org_departments_department (department_id),
INDEX idx_im_org_departments_parent (parent_id, state, sort_order, id),
INDEX idx_im_org_departments_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_org_members (
id BIGINT NOT NULL AUTO_INCREMENT,
department_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
display_name VARCHAR(128) NOT NULL DEFAULT '',
position VARCHAR(128) NOT NULL DEFAULT '',
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
mobile VARCHAR(32) NOT NULL DEFAULT '',
email VARCHAR(128) NOT NULL DEFAULT '',
employment_state VARCHAR(16) NOT NULL DEFAULT 'active',
contactable TINYINT NOT NULL DEFAULT 1,
sort_order INT NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_org_members_department_user (department_id, user_id),
INDEX idx_im_org_members_department (department_id, employment_state, sort_order, id),
INDEX idx_im_org_members_user (user_id),
INDEX idx_im_org_members_display_name (display_name),
CONSTRAINT fk_im_org_members_department FOREIGN KEY (department_id) REFERENCES tinode.im_org_departments(department_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_meetings (
id BIGINT NOT NULL AUTO_INCREMENT,
meeting_id VARCHAR(64) NOT NULL,
organizer_id BIGINT NOT NULL,
title VARCHAR(200) NOT NULL,
start_at DATETIME(3) NOT NULL,
end_at DATETIME(3) NOT NULL,
location VARCHAR(256) NOT NULL DEFAULT '',
state VARCHAR(16) NOT NULL DEFAULT 'scheduled',
reminder_minutes JSON,
idempotency_key VARCHAR(128),
version INT NOT NULL DEFAULT 1,
canceled_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_meetings_meeting (meeting_id),
UNIQUE INDEX uk_im_meetings_idempotency (organizer_id, idempotency_key),
INDEX idx_im_meetings_organizer (organizer_id, start_at),
INDEX idx_im_meetings_state_time (state, start_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_meeting_participants (
id BIGINT NOT NULL AUTO_INCREMENT,
meeting_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
response VARCHAR(16) NOT NULL DEFAULT 'pending',
response_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_meeting_participants_user (meeting_id, user_id),
INDEX idx_im_meeting_participants_user_time (user_id, meeting_id),
INDEX idx_im_meeting_participants_response (meeting_id, response),
CONSTRAINT fk_im_meeting_participants_meeting FOREIGN KEY (meeting_id) REFERENCES tinode.im_meetings(meeting_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_daily_reports (
id BIGINT NOT NULL AUTO_INCREMENT,
report_id VARCHAR(64) NOT NULL,
author_id BIGINT NOT NULL,
report_date DATE NOT NULL,
content TEXT NOT NULL,
attachments JSON,
status VARCHAR(16) NOT NULL DEFAULT 'draft',
read_state VARCHAR(16) NOT NULL DEFAULT 'unread',
version INT NOT NULL DEFAULT 1,
submitted_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_daily_reports_report (report_id),
UNIQUE INDEX uk_im_daily_reports_author_date (author_id, report_date),
INDEX idx_im_daily_reports_author_id (author_id, id DESC),
INDEX idx_im_daily_reports_date_status (report_date, status, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_monthly_reports (
id BIGINT NOT NULL AUTO_INCREMENT,
report_id VARCHAR(64) NOT NULL,
author_id BIGINT NOT NULL,
report_month CHAR(7) NOT NULL,
work_summary TEXT NOT NULL,
next_plan TEXT NOT NULL,
attachments JSON,
status VARCHAR(16) NOT NULL DEFAULT 'draft',
read_state VARCHAR(16) NOT NULL DEFAULT 'unread',
version INT NOT NULL DEFAULT 1,
submitted_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_monthly_reports_report (report_id),
UNIQUE INDEX uk_im_monthly_reports_author_month (author_id, report_month),
INDEX idx_im_monthly_reports_author_id (author_id, id DESC),
INDEX idx_im_monthly_reports_month_status (report_month, status, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_mailbox_config (
id BIGINT NOT NULL AUTO_INCREMENT,
enabled TINYINT NOT NULL DEFAULT 1,
allow_anonymous TINYINT NOT NULL DEFAULT 1,
required_fields JSON,
max_length INT NOT NULL DEFAULT 5000,
attachment_policy JSON,
notice_title VARCHAR(128) NOT NULL DEFAULT '',
notice_content TEXT NOT NULL,
notice_updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_mailbox_submissions (
id BIGINT NOT NULL AUTO_INCREMENT,
submission_id VARCHAR(64) NOT NULL,
sender_id BIGINT NOT NULL,
subject VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
anonymous TINYINT NOT NULL DEFAULT 0,
attachment_file_ids JSON,
notice_version VARCHAR(64) NOT NULL DEFAULT '',
idempotency_key VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'received',
submitted_at DATETIME(3) NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_mailbox_submissions_submission (submission_id),
UNIQUE INDEX uk_im_mailbox_submissions_idempotency (sender_id, idempotency_key),
INDEX idx_im_mailbox_submissions_sender (sender_id, id DESC),
INDEX idx_im_mailbox_submissions_status (status, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ============================================================
-- Tenant database: chapter 10, profile, privacy, app release, push, youth mode.
-- ============================================================
CREATE TABLE IF NOT EXISTS tinode.im_user_profiles (
user_id BIGINT NOT NULL,
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
nickname VARCHAR(64) NOT NULL DEFAULT '',
birth_date DATE,
signature VARCHAR(256) NOT NULL DEFAULT '',
gender VARCHAR(16) NOT NULL DEFAULT '',
region VARCHAR(128) NOT NULL DEFAULT '',
public_account VARCHAR(64) NOT NULL DEFAULT '',
version INT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_user_privacy_settings (
user_id BIGINT NOT NULL,
add_me_methods JSON,
profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
last_seen_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
read_receipts TINYINT NOT NULL DEFAULT 1,
version INT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_app_releases (
id BIGINT NOT NULL AUTO_INCREMENT,
platform VARCHAR(32) NOT NULL,
channel VARCHAR(32) NOT NULL DEFAULT 'stable',
latest_version VARCHAR(64) NOT NULL,
min_supported_version VARCHAR(64) NOT NULL,
update_type VARCHAR(16) NOT NULL DEFAULT 'none',
release_notes TEXT NOT NULL,
store_url VARCHAR(512) NOT NULL DEFAULT '',
package_url VARCHAR(512) NOT NULL DEFAULT '',
checksum VARCHAR(128) NOT NULL DEFAULT '',
remind_after_seconds INT NOT NULL DEFAULT 0,
published_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_app_releases_platform_channel (platform, channel)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_device_push_tokens (
device_id VARCHAR(128) NOT NULL,
user_id BIGINT NOT NULL,
provider VARCHAR(32) NOT NULL,
token VARCHAR(512) NOT NULL,
app_version VARCHAR(64) NOT NULL DEFAULT '',
platform VARCHAR(32) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (device_id),
UNIQUE INDEX uk_im_device_push_tokens_provider_token (provider, token),
INDEX idx_im_device_push_tokens_user (user_id, updated_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ============================================================
-- Tenant database: chapter 11, risk challenge, abuse report, feedback.
-- ============================================================
CREATE TABLE IF NOT EXISTS tinode.im_risk_challenges (
id BIGINT NOT NULL AUTO_INCREMENT,
challenge_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
action VARCHAR(64) NOT NULL,
level VARCHAR(16) NOT NULL,
notice_code VARCHAR(64) NOT NULL,
notice_params JSON,
state VARCHAR(16) NOT NULL DEFAULT 'pending',
expires_at DATETIME(3) NOT NULL,
decision VARCHAR(16) NOT NULL DEFAULT '',
action_token_id VARCHAR(64) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_risk_challenges_challenge (challenge_id),
INDEX idx_im_risk_challenges_user_state (user_id, state, expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_abuse_reasons (
id BIGINT NOT NULL AUTO_INCREMENT,
target_type VARCHAR(16) NOT NULL,
reason_code VARCHAR(64) NOT NULL,
label VARCHAR(128) NOT NULL,
requires_description TINYINT NOT NULL DEFAULT 0,
allows_evidence TINYINT NOT NULL DEFAULT 1,
sort_order INT NOT NULL DEFAULT 0,
enabled TINYINT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_abuse_reasons_type_code (target_type, reason_code),
INDEX idx_im_abuse_reasons_type_order (target_type, enabled, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_abuse_reports (
id BIGINT NOT NULL AUTO_INCREMENT,
report_id VARCHAR(64) NOT NULL,
reporter_id BIGINT NOT NULL,
target_type VARCHAR(16) NOT NULL,
target_user_id BIGINT NOT NULL DEFAULT 0,
target_topic VARCHAR(32) NOT NULL DEFAULT '',
target_seq INT NOT NULL DEFAULT 0,
reason_code VARCHAR(64) NOT NULL,
description TEXT NOT NULL,
evidence_file_ids JSON,
snapshot JSON,
idempotency_key VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'received',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_abuse_reports_report (report_id),
UNIQUE INDEX uk_im_abuse_reports_idempotency (reporter_id, idempotency_key),
INDEX idx_im_abuse_reports_target (target_type, target_topic, target_seq, target_user_id),
INDEX idx_im_abuse_reports_reporter (reporter_id, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_feedback_config (
id BIGINT NOT NULL AUTO_INCREMENT,
categories JSON,
max_length INT NOT NULL DEFAULT 1000,
attachment_policy JSON,
allow_contact TINYINT NOT NULL DEFAULT 1,
diagnostics_notice TEXT NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_feedback_submissions (
id BIGINT NOT NULL AUTO_INCREMENT,
feedback_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
category VARCHAR(64) NOT NULL,
content TEXT NOT NULL,
contact VARCHAR(128) NOT NULL DEFAULT '',
attachment_file_ids JSON,
client_context JSON,
include_diagnostics TINYINT NOT NULL DEFAULT 0,
idempotency_key VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'received',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_feedback_submissions_feedback (feedback_id),
UNIQUE INDEX uk_im_feedback_submissions_idempotency (user_id, idempotency_key),
INDEX idx_im_feedback_submissions_user (user_id, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ============================================================
-- Seed data.
-- ============================================================
INSERT INTO hotgo.im_tenant(id, code, name, tenant_desc, db_name, db_dsn, state, created_by, updated_by)
VALUES (1, 'default', 'Default Tenant', NULL, 'tinode', '', 1, 0, 0)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
db_name = VALUES(db_name),
state = VALUES(state),
updated_at = CURRENT_TIMESTAMP(3);
INSERT INTO hotgo.im_tenant_config(tenant_id, max_users, max_groups, max_group_members, created_by, updated_by)
SELECT id, 0, 0, 0, 0, 0 FROM hotgo.im_tenant WHERE code = 'default'
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP(3);
INSERT INTO tinode.im_workbench_apps(code, name, route, sort_order, unread_key, enabled, permission)
VALUES
('organization', '组织架构', '/organization', 10, 'organization', 1, 'enabled'),
('meetings', '会议通知', '/meetings', 20, 'meetings', 1, 'enabled'),
('daily_reports', '日报', '/reports/daily', 30, 'daily_reports', 1, 'enabled'),
('monthly_reports', '月报', '/reports/monthly', 40, 'monthly_reports', 1, 'enabled'),
('executive_mailbox', '总经理信箱', '/executive-mailbox', 50, 'executive_mailbox', 1, 'enabled')
ON DUPLICATE KEY UPDATE
name = VALUES(name),
route = VALUES(route),
sort_order = VALUES(sort_order),
unread_key = VALUES(unread_key),
updated_at = CURRENT_TIMESTAMP(3);
INSERT INTO tinode.im_mailbox_config(id, enabled, allow_anonymous, required_fields, max_length, attachment_policy, notice_title, notice_content)
VALUES (1, 1, 1, '["subject","content"]', 5000, '{"max_files":5,"purpose":"executive_mailbox"}', '总经理信箱', '请如实填写诉求,匿名提交将按企业政策保护展示身份。')
ON DUPLICATE KEY UPDATE updated_at = updated_at;
INSERT INTO tinode.im_app_releases(platform, channel, latest_version, min_supported_version, update_type, release_notes, remind_after_seconds)
VALUES
('ios', 'stable', '1.0.0', '1.0.0', 'none', '当前已是最新版本。', 0),
('android', 'stable', '1.0.0', '1.0.0', 'none', '当前已是最新版本。', 0)
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP(3);
INSERT INTO tinode.im_abuse_reasons(target_type, reason_code, label, requires_description, allows_evidence, sort_order)
VALUES
('user', 'fraud', '诈骗欺诈', 0, 1, 10),
('user', 'harassment', '骚扰辱骂', 0, 1, 20),
('user', 'illegal', '违法违规', 1, 1, 30),
('group', 'fraud', '诈骗欺诈', 0, 1, 10),
('group', 'spam', '垃圾广告', 0, 1, 20),
('group', 'illegal', '违法违规', 1, 1, 30),
('message', 'fraud', '诈骗欺诈', 0, 1, 10),
('message', 'spam', '垃圾广告', 0, 1, 20),
('message', 'harassment', '骚扰辱骂', 0, 1, 30),
('message', 'illegal', '违法违规', 1, 1, 40)
ON DUPLICATE KEY UPDATE
label = VALUES(label),
requires_description = VALUES(requires_description),
allows_evidence = VALUES(allows_evidence),
sort_order = VALUES(sort_order),
updated_at = CURRENT_TIMESTAMP(3);
INSERT INTO tinode.im_feedback_config(id, categories, max_length, attachment_policy, allow_contact, diagnostics_notice)
VALUES (1, '["bug","suggestion","complaint","other"]', 1000, '{"max_files":5,"purpose":"feedback"}', 1, '仅在你同意时上传脱敏诊断信息。')
ON DUPLICATE KEY UPDATE updated_at = updated_at;
-- Current MySQL adapter schema version expected by server/db/mysql/adapter.go.
UPDATE tinode.kvmeta SET `value` = 123 WHERE `key` = 'version';
+5 -74
View File
@@ -57,48 +57,9 @@ func main() {
CONSTRAINT fk_im_tenant_config_tenant FOREIGN KEY(tenant_id) REFERENCES hotgo.im_tenant(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
"CREATE DATABASE IF NOT EXISTS tinode CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci",
// The main server binary only creates tinode.im_tenant/im_tenant_config
// (see createTenantRegistry in server/db/mysql/adapter.go) as part of its
// version-gated schema upgrade chain. A tinode DB that was already at the
// target schema version before this table was introduced never runs that
// step, so the ALTERs below would otherwise fail with "table doesn't
// exist". Create it here too (schema kept identical to createTenantRegistry).
`CREATE TABLE IF NOT EXISTS tinode.im_tenant(
id BIGINT NOT NULL AUTO_INCREMENT,
code VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
tenant_desc VARCHAR(256),
db_name VARCHAR(128) NOT NULL DEFAULT '',
db_dsn VARCHAR(1024) NOT NULL DEFAULT '',
state SMALLINT NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
created_by BIGINT NOT NULL,
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
updated_by BIGINT NOT NULL,
PRIMARY KEY(id),
UNIQUE INDEX uk_im_tenant_code(code),
INDEX idx_im_tenant_db_name(db_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
`CREATE TABLE IF NOT EXISTS tinode.im_tenant_config(
id BIGINT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
created_by BIGINT NOT NULL,
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
updated_by BIGINT NOT NULL,
PRIMARY KEY(id),
UNIQUE INDEX uk_im_tenant_config_tenant_id(tenant_id),
CONSTRAINT fk_im_tenant_config_tenant FOREIGN KEY(tenant_id) REFERENCES tinode.im_tenant(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
`INSERT INTO tinode.im_tenant(id,code,name,tenant_desc,db_name,db_dsn,state,created_by,updated_by)
VALUES(1,'default','Default Tenant',NULL,'tinode','',1,0,0)
ON DUPLICATE KEY UPDATE db_name=VALUES(db_name), updated_at=CURRENT_TIMESTAMP(3)`,
// Same root cause as im_tenant above: createIMSecurityTables, createIMNotificationTables
// and createIMRelationTables (server/db/mysql/adapter.go) are only reached from CreateDb's
// fresh-install path, which our pre-existing tinode DB never runs. Recreate their tables here.
// createIMSecurityTables, createIMNotificationTables and createIMRelationTables
// (server/db/mysql/adapter.go) are only reached from CreateDb's fresh-install
// path, which our pre-existing tinode DB never runs. Recreate their tables here.
`CREATE TABLE IF NOT EXISTS tinode.im_realname_verifications(
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
@@ -559,21 +520,10 @@ func main() {
"ALTER TABLE hotgo.im_tenant ADD COLUMN tenant_desc VARCHAR(256) AFTER name",
"ALTER TABLE hotgo.im_tenant ADD COLUMN db_name VARCHAR(128) NOT NULL DEFAULT '' AFTER tenant_desc",
"ALTER TABLE hotgo.im_tenant ADD COLUMN db_dsn VARCHAR(1024) NOT NULL DEFAULT '' AFTER db_name",
"ALTER TABLE tinode.im_tenant ADD COLUMN tenant_desc VARCHAR(256) AFTER name",
"ALTER TABLE tinode.im_tenant ADD COLUMN db_name VARCHAR(128) NOT NULL DEFAULT '' AFTER tenant_desc",
"ALTER TABLE tinode.im_tenant ADD COLUMN db_dsn VARCHAR(1024) NOT NULL DEFAULT '' AFTER db_name",
"ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN enabled TINYINT NOT NULL DEFAULT 0 AFTER password_hash",
"ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN policy_version INT NOT NULL DEFAULT 1 AFTER enabled",
"ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN restrictions JSON AFTER policy_version",
"ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN enabled_at DATETIME(3) AFTER restrictions",
// auth/credentials: server/db/mysql/adapter.go's CreateDb never defined a
// tenant_id column here even on a fresh install, yet every auth/credentials
// query unconditionally filters/inserts by tenant_id - a latent bug in the
// fresh-schema definition, not something version-skip alone explains. Backfill
// existing rows to the default tenant (id=1); adapter.go's CREATE TABLE was
// fixed alongside this to include the column on future fresh installs too.
"ALTER TABLE tinode.auth ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id",
"ALTER TABLE tinode.credentials ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id",
} {
if _, err = db.Exec(stmt); err != nil && !strings.Contains(err.Error(), "Duplicate column name") {
fatal(fmt.Errorf("exec failed: %s: %w", stmt, err))
@@ -611,9 +561,6 @@ func main() {
ON DUPLICATE KEY UPDATE updated_at=CURRENT_TIMESTAMP(3)`); err != nil {
fatal(err)
}
if _, err = db.Exec("UPDATE tinode.im_tenant SET db_name='tinode' WHERE code='default' AND (db_name='' OR db_name IS NULL)"); err != nil {
fatal(err)
}
if _, err = db.Exec(`INSERT INTO tinode.im_abuse_reasons(target_type,reason_code,label,requires_description,allows_evidence,sort_order)
VALUES
('user','fraud','诈骗欺诈',0,1,10),
@@ -644,13 +591,6 @@ func main() {
fatal(err)
}
fmt.Printf("hotgo.im_tenant default: code=%s db_name=%s state=%d\n", code, dbName, state)
var columnCount int
if err = db.QueryRow(`SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA='tinode' AND TABLE_NAME='im_tenant' AND COLUMN_NAME IN ('tenant_desc','db_name','db_dsn')`).Scan(&columnCount); err != nil {
fatal(err)
}
fmt.Printf("tinode.im_tenant routing columns: %d/3\n", columnCount)
rows, err := db.Query(`SELECT table_name FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA='tinode' AND COLUMN_NAME='tenant_id'
ORDER BY table_name`)
@@ -681,17 +621,10 @@ func fatal(v any) {
os.Exit(1)
}
// keptTenantIDTables lists tables whose tenant_id column must survive
// dropTenantIDColumns: server/db/mysql/adapter.go queries auth and credentials
// by tenant_id unconditionally (not gated behind tenant_db_routing), so unlike
// the newer per-tenant-DB feature tables, these two are row-tagged by design.
var keptTenantIDTables = "'auth','credentials'"
func dropTenantIDColumns(db *sql.DB, schema string) error {
fks, err := queryPairs(db, `SELECT DISTINCT table_name, constraint_name
FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA=? AND COLUMN_NAME='tenant_id' AND REFERENCED_TABLE_NAME IS NOT NULL
AND table_name NOT IN (`+keptTenantIDTables+`)`,
WHERE TABLE_SCHEMA=? AND COLUMN_NAME='tenant_id' AND REFERENCED_TABLE_NAME IS NOT NULL`,
schema)
if err != nil {
return err
@@ -704,8 +637,7 @@ func dropTenantIDColumns(db *sql.DB, schema string) error {
indexes, err := queryPairs(db, `SELECT DISTINCT table_name, index_name
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA=? AND COLUMN_NAME='tenant_id' AND INDEX_NAME<>'PRIMARY'
AND table_name NOT IN (`+keptTenantIDTables+`)`,
WHERE TABLE_SCHEMA=? AND COLUMN_NAME='tenant_id' AND INDEX_NAME<>'PRIMARY'`,
schema)
if err != nil {
return err
@@ -718,7 +650,6 @@ func dropTenantIDColumns(db *sql.DB, schema string) error {
rows, err := db.Query(`SELECT table_name FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA=? AND COLUMN_NAME='tenant_id'
AND table_name NOT IN (`+keptTenantIDTables+`)
ORDER BY table_name`, schema)
if err != nil {
return err
+49
View File
@@ -0,0 +1,49 @@
//go:build ignore
package main
import (
"database/sql"
"fmt"
"os"
"strings"
_ "github.com/go-sql-driver/mysql"
)
func main() {
dsn := strings.TrimPrefix(os.Getenv("IM_MYSQL_DSN"), "mysql://")
sqlFile := os.Getenv("IM_SQL_FILE")
if dsn == "" || sqlFile == "" {
fatal("IM_MYSQL_DSN and IM_SQL_FILE are required")
}
if !strings.Contains(dsn, "multiStatements=") {
sep := "?"
if strings.Contains(dsn, "?") {
sep = "&"
}
dsn += sep + "multiStatements=true"
}
body, err := os.ReadFile(sqlFile)
if err != nil {
fatal(err)
}
db, err := sql.Open("mysql", dsn)
if err != nil {
fatal(err)
}
defer db.Close()
if err = db.Ping(); err != nil {
fatal(err)
}
if _, err = db.Exec(string(body)); err != nil {
fatal(err)
}
fmt.Println("applied", sqlFile)
}
func fatal(v any) {
fmt.Fprintln(os.Stderr, v)
os.Exit(1)
}
+61
View File
@@ -0,0 +1,61 @@
//go:build ignore
package main
import (
"database/sql"
"fmt"
"os"
"strings"
_ "github.com/go-sql-driver/mysql"
)
func main() {
dsn := strings.TrimPrefix(os.Getenv("IM_MYSQL_DSN"), "mysql://")
query := os.Getenv("IM_SQL_QUERY")
if dsn == "" || query == "" {
fatal("IM_MYSQL_DSN and IM_SQL_QUERY are required")
}
db, err := sql.Open("mysql", dsn)
if err != nil {
fatal(err)
}
defer db.Close()
rows, err := db.Query(query)
if err != nil {
fatal(err)
}
defer rows.Close()
cols, err := rows.Columns()
if err != nil {
fatal(err)
}
fmt.Println(strings.Join(cols, "\t"))
for rows.Next() {
values := make([]sql.NullString, len(cols))
scan := make([]any, len(cols))
for i := range values {
scan[i] = &values[i]
}
if err := rows.Scan(scan...); err != nil {
fatal(err)
}
out := make([]string, len(cols))
for i, value := range values {
if value.Valid {
out[i] = value.String
}
}
fmt.Println(strings.Join(out, "\t"))
}
if err := rows.Err(); err != nil {
fatal(err)
}
}
func fatal(v any) {
fmt.Fprintln(os.Stderr, v)
os.Exit(1)
}
-2
View File
@@ -658,7 +658,6 @@ func (a *adapter) CreateDb(reset bool) error {
if _, err = tx.Exec(
`CREATE TABLE auth(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
uname VARCHAR(32) NOT NULL,
userid BIGINT NOT NULL,
scheme VARCHAR(16) NOT NULL,
@@ -836,7 +835,6 @@ func (a *adapter) CreateDb(reset bool) error {
if _, err = tx.Exec(
`CREATE TABLE credentials(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
createdat DATETIME(3) NOT NULL,
updatedat DATETIME(3) NOT NULL,
deletedat DATETIME(3),
+18 -5
View File
@@ -105,15 +105,28 @@ func TestDispatchHello(t *testing.T) {
}
}
func TestDispatchHelloRequiresTenant(t *testing.T) {
func TestDispatchHelloDefaultsTenant(t *testing.T) {
oldTenants := store.Tenants
store.Tenants = testTenantStore{tenant: &types.Tenant{
ID: 1,
Code: "default",
Name: "Default Tenant",
State: types.TenantStateActive,
}}
defer func() { store.Tenants = oldTenants }()
s := &Session{send: make(chan any, 1)}
s.dispatch(&ClientComMessage{Hi: &MsgClientHi{Id: "123", Version: "1"}})
resp := (<-s.send).(*ServerComMessage)
if resp.Ctrl.Code != http.StatusBadRequest {
t.Fatalf("response code: expected %d, got %d", http.StatusBadRequest, resp.Ctrl.Code)
if resp.Ctrl.Code != http.StatusCreated {
t.Fatalf("response code: expected %d, got %d", http.StatusCreated, resp.Ctrl.Code)
}
if !s.tenantID.IsZero() || s.ver != 0 {
t.Fatalf("invalid hello must not bind session: tenant=%d version=%d", s.tenantID, s.ver)
if s.tenantID != 1 || s.tenantCode != "default" || s.ver == 0 {
t.Fatalf("default hello did not bind session: tenant=%d code=%q version=%d", s.tenantID, s.tenantCode, s.ver)
}
tenantParams := resp.Ctrl.Params.(map[string]any)["tenant"].(map[string]any)
if tenantParams["code"] != "default" || tenantParams["name"] != "Default Tenant" {
t.Fatalf("unexpected tenant params: %#v", tenantParams)
}
}