-- DEPRECATED. -- -- 2026-08-13: the multi-tenant technical direction changed to -- "control database + one business database per tenant". Do not execute this -- shared-table tenant_id migration and do not continue adding tenant_id to -- tenant business tables. -- -- New design document: -- docs/IM-独立库多租户改造方案.md -- -- Original note: -- MySQL multi-tenant schema migration draft. -- REVIEW ONLY: do not execute before the application is fully tenant-aware. -- Target: MySQL 8.0, final schema version 119. -- -- This migration assigns all existing data to the default tenant, then makes -- tenant_id mandatory and replaces global uniqueness/foreign keys with -- tenant-scoped constraints. -- -- MySQL DDL auto-commits. Back up the database and rehearse this migration on -- a production-sized copy before running it in any environment. -- --------------------------------------------------------------------------- -- 1. Tenant registry and default tenant. -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS im_tenant ( id BIGINT NOT NULL AUTO_INCREMENT, code VARCHAR(64) NOT NULL, name VARCHAR(128) NOT NULL, `desc` VARCHAR(256) NULL, state SMALLINT 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 KEY uk_im_tenant_code (code) ) ENGINE=InnoDB; INSERT INTO im_tenant (code, name, `desc`, state) SELECT 'default', 'Default Tenant', NULL, 1 WHERE NOT EXISTS ( SELECT 1 FROM im_tenant WHERE code = 'default' ); SET @default_tenant_id := ( SELECT id FROM im_tenant WHERE code = 'default' LIMIT 1 ); -- Stop immediately if the default tenant could not be resolved. DELIMITER $$ CREATE PROCEDURE assert_default_tenant() BEGIN IF @default_tenant_id IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'default tenant is missing'; END IF; END$$ DELIMITER ; CALL assert_default_tenant(); DROP PROCEDURE assert_default_tenant; -- --------------------------------------------------------------------------- -- 2. Add nullable tenant_id columns first. -- --------------------------------------------------------------------------- ALTER TABLE im_users ADD COLUMN tenant_id BIGINT NULL AFTER id; ALTER TABLE im_usertags ADD COLUMN tenant_id BIGINT NULL AFTER id; ALTER TABLE im_devices ADD COLUMN tenant_id BIGINT NULL AFTER id; ALTER TABLE im_auth ADD COLUMN tenant_id BIGINT NULL AFTER id; ALTER TABLE im_topics ADD COLUMN tenant_id BIGINT NULL AFTER id; ALTER TABLE im_topictags ADD COLUMN tenant_id BIGINT NULL AFTER id; ALTER TABLE im_subscriptions ADD COLUMN tenant_id BIGINT NULL AFTER id; ALTER TABLE im_messages ADD COLUMN tenant_id BIGINT NULL AFTER id; ALTER TABLE im_dellog ADD COLUMN tenant_id BIGINT NULL AFTER id; ALTER TABLE im_credentials ADD COLUMN tenant_id BIGINT NULL AFTER id; ALTER TABLE im_fileuploads ADD COLUMN tenant_id BIGINT NULL AFTER id; ALTER TABLE im_filemsglinks ADD COLUMN tenant_id BIGINT NULL AFTER id; -- --------------------------------------------------------------------------- -- 3. Backfill all existing rows into the default tenant. -- --------------------------------------------------------------------------- UPDATE im_users SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL; UPDATE im_usertags SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL; UPDATE im_devices SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL; UPDATE im_auth SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL; UPDATE im_topics SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL; UPDATE im_topictags SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL; UPDATE im_subscriptions SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL; UPDATE im_messages SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL; UPDATE im_dellog SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL; UPDATE im_credentials SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL; UPDATE im_fileuploads SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL; UPDATE im_filemsglinks SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL; -- Validate the backfill before adding NOT NULL constraints. DELIMITER $$ CREATE PROCEDURE assert_no_null_tenant_id() BEGIN IF EXISTS (SELECT 1 FROM im_users WHERE tenant_id IS NULL LIMIT 1) OR EXISTS (SELECT 1 FROM im_usertags WHERE tenant_id IS NULL LIMIT 1) OR EXISTS (SELECT 1 FROM im_devices WHERE tenant_id IS NULL LIMIT 1) OR EXISTS (SELECT 1 FROM im_auth WHERE tenant_id IS NULL LIMIT 1) OR EXISTS (SELECT 1 FROM im_topics WHERE tenant_id IS NULL LIMIT 1) OR EXISTS (SELECT 1 FROM im_topictags WHERE tenant_id IS NULL LIMIT 1) OR EXISTS (SELECT 1 FROM im_subscriptions WHERE tenant_id IS NULL LIMIT 1) OR EXISTS (SELECT 1 FROM im_messages WHERE tenant_id IS NULL LIMIT 1) OR EXISTS (SELECT 1 FROM im_dellog WHERE tenant_id IS NULL LIMIT 1) OR EXISTS (SELECT 1 FROM im_credentials WHERE tenant_id IS NULL LIMIT 1) OR EXISTS (SELECT 1 FROM im_fileuploads WHERE tenant_id IS NULL LIMIT 1) OR EXISTS (SELECT 1 FROM im_filemsglinks WHERE tenant_id IS NULL LIMIT 1) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'tenant_id backfill is incomplete'; END IF; END$$ DELIMITER ; CALL assert_no_null_tenant_id(); DROP PROCEDURE assert_no_null_tenant_id; -- --------------------------------------------------------------------------- -- 4. Make tenant_id mandatory. -- No DEFAULT is retained: new code must always provide a tenant explicitly. -- --------------------------------------------------------------------------- ALTER TABLE im_users MODIFY COLUMN tenant_id BIGINT NOT NULL; ALTER TABLE im_usertags MODIFY COLUMN tenant_id BIGINT NOT NULL; ALTER TABLE im_devices MODIFY COLUMN tenant_id BIGINT NOT NULL; ALTER TABLE im_auth MODIFY COLUMN tenant_id BIGINT NOT NULL; ALTER TABLE im_topics MODIFY COLUMN tenant_id BIGINT NOT NULL; ALTER TABLE im_topictags MODIFY COLUMN tenant_id BIGINT NOT NULL; ALTER TABLE im_subscriptions MODIFY COLUMN tenant_id BIGINT NOT NULL; ALTER TABLE im_messages MODIFY COLUMN tenant_id BIGINT NOT NULL; ALTER TABLE im_dellog MODIFY COLUMN tenant_id BIGINT NOT NULL; ALTER TABLE im_credentials MODIFY COLUMN tenant_id BIGINT NOT NULL; ALTER TABLE im_fileuploads MODIFY COLUMN tenant_id BIGINT NOT NULL; ALTER TABLE im_filemsglinks MODIFY COLUMN tenant_id BIGINT NOT NULL; -- --------------------------------------------------------------------------- -- 5. Drop existing single-column foreign keys. -- Constraint names can differ between databases, so resolve them dynamically. -- --------------------------------------------------------------------------- DELIMITER $$ CREATE PROCEDURE drop_table_foreign_keys(IN table_name_value VARCHAR(64)) BEGIN DECLARE drop_clauses LONGTEXT; SELECT GROUP_CONCAT( CONCAT('DROP FOREIGN KEY `', constraint_name, '`') ORDER BY constraint_name SEPARATOR ', ' ) INTO drop_clauses FROM information_schema.table_constraints WHERE constraint_schema = DATABASE() AND table_name = table_name_value AND constraint_type = 'FOREIGN KEY'; IF drop_clauses IS NOT NULL AND drop_clauses <> '' THEN SET @drop_fk_sql = CONCAT( 'ALTER TABLE `', table_name_value, '` ', drop_clauses ); PREPARE drop_fk_stmt FROM @drop_fk_sql; EXECUTE drop_fk_stmt; DEALLOCATE PREPARE drop_fk_stmt; END IF; END$$ DELIMITER ; CALL drop_table_foreign_keys('im_usertags'); CALL drop_table_foreign_keys('im_devices'); CALL drop_table_foreign_keys('im_auth'); CALL drop_table_foreign_keys('im_topictags'); CALL drop_table_foreign_keys('im_subscriptions'); CALL drop_table_foreign_keys('im_messages'); CALL drop_table_foreign_keys('im_dellog'); CALL drop_table_foreign_keys('im_credentials'); CALL drop_table_foreign_keys('im_filemsglinks'); DROP PROCEDURE drop_table_foreign_keys; -- --------------------------------------------------------------------------- -- 6. Replace global unique indexes with tenant-scoped unique indexes. -- Parent identity indexes are also added for composite foreign keys. -- --------------------------------------------------------------------------- ALTER TABLE im_users ADD UNIQUE KEY uk_users_tenant_id_id (tenant_id, id), ADD KEY idx_users_tenant_state_stateat (tenant_id, state, stateat), ADD KEY idx_users_tenant_lastseen_updatedat (tenant_id, lastseen, updatedat); ALTER TABLE im_usertags ADD UNIQUE KEY uk_usertags_tenant_user_tag (tenant_id, userid, tag), ADD KEY idx_usertags_tenant_tag (tenant_id, tag), DROP INDEX usertags_userid_tag; ALTER TABLE im_devices ADD UNIQUE KEY uk_devices_tenant_hash (tenant_id, hash), ADD KEY idx_devices_tenant_user (tenant_id, userid), DROP INDEX devices_hash; ALTER TABLE im_auth ADD UNIQUE KEY uk_auth_tenant_user_scheme (tenant_id, userid, scheme), ADD UNIQUE KEY uk_auth_tenant_uname (tenant_id, uname), DROP INDEX auth_userid_scheme, DROP INDEX auth_uname; ALTER TABLE im_topics ADD UNIQUE KEY uk_topics_tenant_name (tenant_id, name), ADD KEY idx_topics_tenant_owner (tenant_id, owner), ADD KEY idx_topics_tenant_state_stateat (tenant_id, state, stateat), ADD KEY idx_topics_tenant_name_state_seqid (tenant_id, name, state, seqid), DROP INDEX topics_name; ALTER TABLE im_topictags ADD UNIQUE KEY uk_topictags_tenant_topic_tag (tenant_id, topic, tag), ADD KEY idx_topictags_tenant_tag (tenant_id, tag), DROP INDEX topictags_topic_tag; ALTER TABLE im_subscriptions ADD UNIQUE KEY uk_subscriptions_tenant_topic_user (tenant_id, topic, userid), ADD KEY idx_subscriptions_tenant_user_topic_deletedat (tenant_id, userid, topic, deletedat), ADD KEY idx_subscriptions_tenant_topic_deletedat (tenant_id, topic, deletedat), DROP INDEX subscriptions_topic_userid; ALTER TABLE im_messages ADD UNIQUE KEY uk_messages_tenant_id (tenant_id, id), ADD UNIQUE KEY uk_messages_tenant_topic_seqid (tenant_id, topic, seqid), DROP INDEX messages_topic_seqid; ALTER TABLE im_dellog ADD KEY idx_dellog_tenant_topic_delid_user (tenant_id, topic, delid, deletedfor), ADD KEY idx_dellog_tenant_topic_user_low_hi (tenant_id, topic, deletedfor, low, hi), ADD KEY idx_dellog_tenant_deletedfor (tenant_id, deletedfor); ALTER TABLE im_credentials ADD UNIQUE KEY uk_credentials_tenant_synthetic (tenant_id, synthetic), ADD KEY idx_credentials_tenant_user_method (tenant_id, userid, method, deletedat), DROP INDEX credentials_uniqueness; ALTER TABLE im_fileuploads ADD UNIQUE KEY uk_fileuploads_tenant_id (tenant_id, id), ADD KEY idx_fileuploads_tenant_status_updated (tenant_id, status, updatedat); ALTER TABLE im_filemsglinks ADD KEY idx_filemsglinks_tenant_file (tenant_id, fileid), ADD KEY idx_filemsglinks_tenant_message (tenant_id, msgid), ADD KEY idx_filemsglinks_tenant_topic (tenant_id, topic), ADD KEY idx_filemsglinks_tenant_user (tenant_id, userid); -- --------------------------------------------------------------------------- -- 7. Add tenant registry and tenant-consistent parent foreign keys. -- --------------------------------------------------------------------------- -- --------------------------------------------------------------------------- -- 8. Validate tenant consistency after constraints are installed. -- All result counts must be zero. -- --------------------------------------------------------------------------- SELECT 'user_tags_without_same_tenant_user' AS check_name, COUNT(*) AS invalid_count FROM im_usertags child LEFT JOIN im_users parent ON parent.tenant_id = child.tenant_id AND parent.id = child.userid WHERE parent.id IS NULL UNION ALL SELECT 'subscriptions_without_same_tenant_user', COUNT(*) FROM im_subscriptions child LEFT JOIN im_users parent ON parent.tenant_id = child.tenant_id AND parent.id = child.userid WHERE parent.id IS NULL UNION ALL SELECT 'subscriptions_without_same_tenant_topic', COUNT(*) FROM im_subscriptions child LEFT JOIN im_topics parent ON parent.tenant_id = child.tenant_id AND parent.name = child.topic WHERE parent.name IS NULL UNION ALL SELECT 'messages_without_same_tenant_topic', COUNT(*) FROM im_messages child LEFT JOIN im_topics parent ON parent.tenant_id = child.tenant_id AND parent.name = child.topic WHERE parent.name IS NULL UNION ALL SELECT 'file_links_without_same_tenant_file', COUNT(*) FROM im_filemsglinks child LEFT JOIN im_fileuploads parent ON parent.tenant_id = child.tenant_id AND parent.id = child.fileid WHERE parent.id IS NULL; -- --------------------------------------------------------------------------- -- 9. Update the schema version only after all validation succeeds. -- --------------------------------------------------------------------------- UPDATE im_kvmeta SET `value` = '119' WHERE `key` = 'version'; -- Intentionally unchanged: -- * im_tenant: it is the tenant registry itself. -- * im_kvmeta: it stores platform-level schema metadata. Tenant-scoped cache -- data must use a separate table or a mandatory tenant key namespace.