From 2e0612acbbba2966d1e123c9e64ddd13740f53ee Mon Sep 17 00:00:00 2001 From: richjohnrambo Date: Tue, 22 Sep 2026 14:40:15 +0800 Subject: [PATCH] fix(mysql): adapt tenant shared database fields --- server/db/mysql/adapter.go | 314 ++++++- server/db/mysql/group_message_shards.go | 80 +- server/db/mysql/group_schema.go | 30 +- server/db/mysql/group_schema_test.go | 30 +- server/db/mysql/registration_invites.go | 17 +- .../db/mysql/report_group_regression_test.go | 2 +- server/db/mysql/tenant_adapter.go | 827 ++++++++++-------- server/db/mysql/tenant_infrastructure.go | 33 +- server/db/mysql/tenant_security_audit.go | 7 +- server/db/mysql/tenant_tables.go | 37 + server/db/mysql/tests/mysql_test.go | 12 +- 11 files changed, 861 insertions(+), 528 deletions(-) create mode 100644 server/db/mysql/tenant_tables.go diff --git a/server/db/mysql/adapter.go b/server/db/mysql/adapter.go index 67743f0..7f91efb 100644 --- a/server/db/mysql/adapter.go +++ b/server/db/mysql/adapter.go @@ -45,6 +45,7 @@ type adapter struct { baseConfig *configType tenantDBRouting bool tenantScopedDB bool + tenantID t.TenantID tenantRegistryDB string tenantAdapters map[t.TenantID]*adapter tenantMu sync.Mutex @@ -250,6 +251,7 @@ func (a *adapter) Open(jsonconfig json.RawMessage) error { config.Config.DBName = a.dbName a.baseConfig = &config a.tenantDBRouting = config.TenantDBRouting + a.tenantID = defaultTenantID a.tenantRegistryDB = strings.TrimSpace(config.TenantRegistryDB) a.tenantAdapters = make(map[t.TenantID]*adapter) @@ -296,7 +298,7 @@ func (a *adapter) Close() error { var err error a.tenantMu.Lock() for tenantID, tenantAdp := range a.tenantAdapters { - if tenantAdp != nil && tenantAdp.db != nil { + if tenantAdp != nil && tenantAdp.db != nil && tenantAdp.db != a.db { if closeErr := tenantAdp.db.Close(); err == nil { err = closeErr } @@ -471,8 +473,27 @@ func (a *adapter) TenantListActive(limit int) ([]t.Tenant, error) { func (a *adapter) TenantAdapterFor(tenantID t.TenantID) (dbadapter.TenantBusinessAdapter, dbadapter.TenantAuthAdapter, error) { if !a.tenantDBRouting { + if a.tenantID != defaultTenantID && a.tenantID != tenantID { + return nil, nil, t.ErrMalformed + } + if a.registryDB != nil { + tenant, err := a.tenantByID(tenantID) + if err != nil { + return nil, nil, err + } + if tenant.State != t.TenantStateActive { + return nil, nil, t.ErrPermissionDenied + } + } return a, a, nil } + tenant, err := a.tenantByID(tenantID) + if err != nil { + return nil, nil, err + } + if tenant.State != t.TenantStateActive { + return nil, nil, t.ErrPermissionDenied + } a.tenantMu.Lock() if tenantAdp := a.tenantAdapters[tenantID]; tenantAdp != nil { a.tenantMu.Unlock() @@ -480,36 +501,15 @@ func (a *adapter) TenantAdapterFor(tenantID t.TenantID) (dbadapter.TenantBusines } a.tenantMu.Unlock() - tenant, err := a.tenantByID(tenantID) - if err != nil { - return nil, nil, err - } - dbName := strings.TrimSpace(tenant.DBName) - dsn := strings.TrimSpace(tenant.DBDSN) - if a.baseConfig == nil { - return nil, nil, t.ErrFailed - } - db, resolvedDBName, err := openMySQLDB(*a.baseConfig, mysqlDefaultString(dsn, a.dsn), dbName) - if err != nil { - return nil, nil, err - } - if err = db.Ping(); err != nil { - db.Close() - return nil, nil, err - } - if err = ensureUserPrivacySchema(db); err != nil { - db.Close() - return nil, nil, errors.New("mysql adapter failed to ensure user privacy schema for tenant: " + err.Error()) - } registryDB := a.registryDB if registryDB == nil { registryDB = a.db } tenantAdp := &adapter{ - db: db, + db: a.db, registryDB: registryDB, - dsn: mysqlDefaultString(dsn, a.dsn), - dbName: resolvedDBName, + dsn: a.dsn, + dbName: a.dbName, maxResults: a.maxResults, maxMessageResults: a.maxMessageResults, version: a.version, @@ -517,12 +517,14 @@ func (a *adapter) TenantAdapterFor(tenantID t.TenantID) (dbadapter.TenantBusines txTimeout: a.txTimeout, baseConfig: a.baseConfig, tenantDBRouting: false, - tenantScopedDB: true, + tenantID: tenantID, + // A dedicated database is still tenant-owned. Keep tenant predicates and + // explicit tenant_id writes enabled even though databases are not shared. + tenantScopedDB: false, } a.tenantMu.Lock() if existing := a.tenantAdapters[tenantID]; existing != nil { a.tenantMu.Unlock() - db.Close() return existing, existing, nil } a.tenantAdapters[tenantID] = tenantAdp @@ -570,7 +572,7 @@ func createTenantRegistry(tx *sql.Tx, registryDB, businessDB string) error { } if _, err := tx.Exec( `CREATE TABLE IF NOT EXISTS im_tenant( - id BIGINT NOT NULL AUTO_INCREMENT, + id BIGINT NOT NULL, code VARCHAR(64) NOT NULL, name VARCHAR(128) NOT NULL, tenant_desc VARCHAR(256), @@ -1429,18 +1431,35 @@ func (a *adapter) TenantAuthAddRecord(tenantID t.TenantID, uid t.Uid, if tenantAdp != a { return tenantAdp.TenantAuthAddRecord(tenantID, uid, authLvl, secret, expires) } + var exp *time.Time + if !expires.IsZero() { + exp = &expires + } var username string ctx, cancel := a.getContext() if cancel != nil { defer cancel() } - if err = a.db.GetContext(ctx, &username, "SELECT username FROM im_users WHERE id=?", store.DecodeUid(uid)); err != nil { + if err = a.db.GetContext(ctx, &username, "SELECT username FROM im_users WHERE tenant_id=? AND id=?", tenantID, store.DecodeUid(uid)); err != nil { if err == sql.ErrNoRows { return t.ErrNotFound } return err } - return a.AuthAddRecord(uid, "basic", username, authLvl, secret, expires) + authID, err := nextSnowflakeID() + if err != nil { + return err + } + _, err = a.db.ExecContext(ctx, "INSERT INTO im_auth(tenant_id,id,userid,authLvl,secret,expires) VALUES(?,?,?,?,?,?)", + tenantID, authID, store.DecodeUid(uid), authLvl, secret, exp) + if isIDOutOfRange(err) { + _, err = a.db.ExecContext(ctx, "INSERT INTO im_auth(tenant_id,userid,authLvl,secret,expires) VALUES(?,?,?,?,?)", + tenantID, store.DecodeUid(uid), authLvl, secret, exp) + } + if isDupe(err) { + return t.ErrDuplicate + } + return err } // TenantAuthDeleteRecord deletes the Basic authentication record. @@ -1452,7 +1471,12 @@ func (a *adapter) TenantAuthDeleteRecord(tenantID t.TenantID, uid t.Uid) error { if tenantAdp != a { return tenantAdp.TenantAuthDeleteRecord(tenantID, uid) } - return a.AuthDelScheme(uid, "basic") + ctx, cancel := a.getContext() + if cancel != nil { + defer cancel() + } + _, err = a.db.ExecContext(ctx, "DELETE FROM im_auth WHERE tenant_id=? AND userid=?", tenantID, store.DecodeUid(uid)) + return err } // TenantAuthUpdateRecord updates the Basic authentication record and optional username. @@ -1465,7 +1489,43 @@ func (a *adapter) TenantAuthUpdateRecord(tenantID t.TenantID, uid t.Uid, usernam if tenantAdp != a { return tenantAdp.TenantAuthUpdateRecord(tenantID, uid, username, authLvl, secret, expires) } - return a.AuthUpdRecord(uid, "basic", username, authLvl, secret, expires) + params := []string{"authLvl=?"} + args := []any{authLvl} + if len(secret) > 0 { + params = append(params, "secret=?") + args = append(args, secret) + } + if !expires.IsZero() { + params = append(params, "expires=?") + args = append(args, expires) + } + decodedUID := store.DecodeUid(uid) + ctx, cancel := a.getContextForTx() + if cancel != nil { + defer cancel() + } + tx, err := a.db.BeginTxx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + if username != "" { + if _, err = tx.ExecContext(ctx, "UPDATE im_users SET username=?,updatedat=? WHERE tenant_id=? AND id=?", username, t.TimeNow(), tenantID, decodedUID); err != nil { + if isDupe(err) { + return t.ErrDuplicate + } + return err + } + } + args = append(args, tenantID, decodedUID) + resp, err := tx.ExecContext(ctx, "UPDATE im_auth SET "+strings.Join(params, ",")+" WHERE tenant_id=? AND userid=?", args...) + if err != nil { + return err + } + if count, _ := resp.RowsAffected(); count <= 0 { + return t.ErrNotFound + } + return tx.Commit() } // TenantAuthGetRecord retrieves a user's Basic authentication record. @@ -1479,7 +1539,29 @@ func (a *adapter) TenantAuthGetRecord(tenantID t.TenantID, uid t.Uid) (string, if tenantAdp != a { return tenantAdp.TenantAuthGetRecord(tenantID, uid) } - return a.AuthGetRecord(uid, "basic") + var record struct { + Username string + Authlvl auth.Level + Secret []byte + Expires *time.Time + } + ctx, cancel := a.getContext() + if cancel != nil { + defer cancel() + } + err = a.db.GetContext(ctx, &record, `SELECT u.username,a.secret,a.expires,a.authlvl + FROM im_auth AS a JOIN im_users AS u ON u.tenant_id=a.tenant_id AND u.id=a.userid + WHERE a.tenant_id=? AND a.userid=?`, tenantID, store.DecodeUid(uid)) + if err == sql.ErrNoRows { + return "", auth.LevelNone, nil, expires, t.ErrNotFound + } + if err != nil { + return "", auth.LevelNone, nil, expires, err + } + if record.Expires != nil { + expires = *record.Expires + } + return record.Username, record.Authlvl, record.Secret, expires, nil } // TenantAuthGetByLogin retrieves a Basic authentication record by tenant-local login. @@ -1493,7 +1575,34 @@ func (a *adapter) TenantAuthGetByLogin(tenantID t.TenantID, login string) (t.Uid if tenantAdp != a { return tenantAdp.TenantAuthGetByLogin(tenantID, login) } - return a.AuthGetUniqueRecord(login) + var record struct { + Userid int64 + Authlvl auth.Level + Secret []byte + Expires *time.Time + } + lookup := login + if len(login) == 11 && asciiDigits(login) { + lookup = "+86" + login + } + ctx, cancel := a.getContext() + if cancel != nil { + defer cancel() + } + err = a.db.GetContext(ctx, &record, `SELECT a.userid,a.secret,a.expires,a.authlvl + FROM im_users AS u JOIN im_auth AS a ON a.tenant_id=u.tenant_id AND a.userid=u.id + WHERE u.tenant_id=? AND (u.username=? OR (u.phone IS NOT NULL AND u.phone=?)) + ORDER BY (u.username=?) DESC LIMIT 1`, tenantID, login, lookup, login) + if err == sql.ErrNoRows { + return t.ZeroUid, auth.LevelNone, nil, expires, nil + } + if err != nil { + return t.ZeroUid, auth.LevelNone, nil, expires, err + } + if record.Expires != nil { + expires = *record.Expires + } + return store.EncodeUid(record.Userid), record.Authlvl, record.Secret, expires, nil } // Add user's authentication record @@ -2011,9 +2120,9 @@ func (a *adapter) TenantUserGetByCred(tenantID t.TenantID, method, value string) return t.ZeroUid, t.ErrMalformed } if tenantAdp != a { - return tenantAdp.UserGetByCred(method, value) + return tenantAdp.TenantUserGetByCred(tenantID, method, value) } - if !a.tenantScopedDB && method == "tel" { + if method == "tel" { ctx, cancel := a.getContext() if cancel != nil { defer cancel() @@ -2029,7 +2138,20 @@ func (a *adapter) TenantUserGetByCred(tenantID t.TenantID, method, value string) } return store.EncodeUid(decodedUID), nil } - return a.UserGetByCred(method, value) + ctx, cancel := a.getContext() + if cancel != nil { + defer cancel() + } + var decodedUID int64 + err = a.db.GetContext(ctx, &decodedUID, + "SELECT userid FROM im_credentials WHERE tenant_id=? AND synthetic=?", tenantID, method+":"+value) + if err == sql.ErrNoRows { + return t.ZeroUid, nil + } + if err != nil { + return t.ZeroUid, err + } + return store.EncodeUid(decodedUID), nil } // UserUnreadCount returns the total number of unread messages in all topics with @@ -3855,9 +3977,73 @@ func (a *adapter) TenantCredUpsert(tenantID t.TenantID, cred *t.Credential) (boo return false, t.ErrMalformed } if tenantAdp != a { - return tenantAdp.CredUpsert(cred) + return tenantAdp.TenantCredUpsert(tenantID, cred) } - return a.CredUpsert(cred) + if cred.Method == "tel" { + cred.Value, err = normalizeUserPhone(cred.Value) + if err != nil { + return false, err + } + } + ctx, cancel := a.getContextForTx() + if cancel != nil { + defer cancel() + } + tx, err := a.db.BeginTxx(ctx, nil) + if err != nil { + return false, err + } + defer tx.Rollback() + now := t.TimeNow() + userID := common.DecodeUidString(cred.User) + synth := cred.Method + ":" + cred.Value + if !cred.Done { + var done bool + err = tx.Get(&done, "SELECT done FROM im_credentials WHERE tenant_id=? AND synthetic=?", tenantID, synth) + if err == nil { + return false, t.ErrDuplicate + } + if err != sql.ErrNoRows { + return false, err + } + synth = cred.User + ":" + synth + if _, err = tx.Exec("UPDATE im_credentials SET deletedat=? WHERE tenant_id=? AND userid=? AND method=? AND done=FALSE", + now, tenantID, userID, cred.Method); err != nil { + return false, err + } + res, updateErr := tx.Exec("UPDATE im_credentials SET updatedat=?,deletedat=NULL,resp=?,done=FALSE WHERE tenant_id=? AND synthetic=?", + cred.UpdatedAt, cred.Resp, tenantID, synth) + if updateErr != nil { + return false, updateErr + } + if rows, _ := res.RowsAffected(); rows > 0 { + return false, tx.Commit() + } + } else if _, err = tx.Exec("DELETE FROM im_credentials WHERE tenant_id=? AND synthetic=?", tenantID, cred.User+":"+synth); err != nil { + return false, err + } + credID, err := nextSnowflakeID() + if err != nil { + return false, err + } + _, err = tx.Exec(`INSERT INTO im_credentials(tenant_id,id,createdat,updatedat,method,value,synthetic,userid,resp,done) + VALUES(?,?,?,?,?,?,?,?,?,?)`, tenantID, credID, cred.CreatedAt, cred.UpdatedAt, cred.Method, cred.Value, synth, userID, cred.Resp, cred.Done) + if isIDOutOfRange(err) { + _, err = tx.Exec(`INSERT INTO im_credentials(tenant_id,createdat,updatedat,method,value,synthetic,userid,resp,done) + VALUES(?,?,?,?,?,?,?,?,?)`, tenantID, cred.CreatedAt, cred.UpdatedAt, cred.Method, cred.Value, synth, userID, cred.Resp, cred.Done) + } + if err != nil { + if isDupe(err) { + return true, t.ErrDuplicate + } + return true, err + } + if cred.Done && cred.Method == "tel" { + if _, err = tx.Exec("UPDATE im_users SET phone=? WHERE tenant_id=? AND id=? AND (phone IS NULL OR phone='')", cred.Value, tenantID, userID); err != nil { + return true, err + } + } + return true, tx.Commit() } func tenantCredDel(tx *sqlx.Tx, tenantID t.TenantID, uid t.Uid, method, value string) error { @@ -3989,7 +4175,7 @@ func (a *adapter) TenantCredDel(tenantID t.TenantID, uid t.Uid, method, value st return t.ErrMalformed } if tenantAdp != a { - return tenantAdp.CredDel(uid, method, value) + return tenantAdp.TenantCredDel(tenantID, uid, method, value) } ctx, cancel := a.getContextForTx() if cancel != nil { @@ -4036,7 +4222,7 @@ func (a *adapter) TenantCredConfirm(tenantID t.TenantID, uid t.Uid, method strin return t.ErrMalformed } if tenantAdp != a { - return tenantAdp.CredConfirm(uid, method) + return tenantAdp.TenantCredConfirm(tenantID, uid, method) } ctx, cancel := a.getContext() if cancel != nil { @@ -4076,7 +4262,7 @@ func (a *adapter) TenantCredFail(tenantID t.TenantID, uid t.Uid, method string) return t.ErrMalformed } if tenantAdp != a { - return tenantAdp.CredFail(uid, method) + return tenantAdp.TenantCredFail(tenantID, uid, method) } ctx, cancel := a.getContext() if cancel != nil { @@ -4116,9 +4302,24 @@ func (a *adapter) TenantCredGetActive(tenantID t.TenantID, uid t.Uid, method str return nil, t.ErrMalformed } if tenantAdp != a { - return tenantAdp.CredGetActive(uid, method) + return tenantAdp.TenantCredGetActive(tenantID, uid, method) } - return a.CredGetActive(uid, method) + ctx, cancel := a.getContext() + if cancel != nil { + defer cancel() + } + var cred t.Credential + err = a.db.GetContext(ctx, &cred, `SELECT createdat,updatedat,method,value,resp,done,retries + FROM im_credentials WHERE tenant_id=? AND userid=? AND deletedat IS NULL AND method=? AND done=FALSE`, + tenantID, store.DecodeUid(uid), method) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + cred.User = uid.String() + return &cred, nil } // CredGetAll returns credential records for the given user and method, all or validated only. @@ -4159,9 +4360,30 @@ func (a *adapter) TenantCredGetAll(tenantID t.TenantID, uid t.Uid, method string return nil, t.ErrMalformed } if tenantAdp != a { - return tenantAdp.CredGetAll(uid, method, validatedOnly) + return tenantAdp.TenantCredGetAll(tenantID, uid, method, validatedOnly) } - return a.CredGetAll(uid, method, validatedOnly) + query := `SELECT createdat,updatedat,method,value,resp,done,retries + FROM im_credentials WHERE tenant_id=? AND userid=? AND deletedat IS NULL` + args := []any{tenantID, store.DecodeUid(uid)} + if method != "" { + query += " AND method=?" + args = append(args, method) + } + if validatedOnly { + query += " AND done=TRUE" + } + ctx, cancel := a.getContext() + if cancel != nil { + defer cancel() + } + var credentials []t.Credential + if err = a.db.SelectContext(ctx, &credentials, query, args...); err != nil { + return nil, err + } + for i := range credentials { + credentials[i].User = uid.String() + } + return credentials, nil } // FileUploads diff --git a/server/db/mysql/group_message_shards.go b/server/db/mysql/group_message_shards.go index c201fd4..dd7dcdd 100644 --- a/server/db/mysql/group_message_shards.go +++ b/server/db/mysql/group_message_shards.go @@ -13,37 +13,35 @@ import ( t "github.com/tinode/chat/server/store/types" ) -const legacyGroupMessageTable = "im_group_messages" - -func ensureGroupMessageShardInfrastructure(exec sqlExecutor) error { +func ensureGroupMessageShardInfrastructure(exec sqlExecutor, tenantID t.TenantID) error { statements := []string{ `CREATE TABLE IF NOT EXISTS im_group_message_shards ( - shard_month CHAR(6) NOT NULL, table_name VARCHAR(64) NOT NULL, + tenant_id BIGINT NOT NULL DEFAULT 0, shard_month CHAR(6) NOT NULL, table_name VARCHAR(64) NOT NULL, shard_start DATETIME(3) NOT NULL, shard_end DATETIME(3) NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'prepared', row_count BIGINT NOT NULL DEFAULT 0, min_created_at DATETIME(3), max_created_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(shard_month), UNIQUE INDEX uk_im_group_message_shards_table(table_name), - INDEX idx_im_group_message_shards_status(status, shard_month) + PRIMARY KEY(tenant_id, shard_month), UNIQUE INDEX uk_im_group_message_shards_table(tenant_id, table_name), + INDEX idx_im_group_message_shards_status(tenant_id, status, shard_month) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, `CREATE TABLE IF NOT EXISTS im_group_message_topic_shards ( - topic VARCHAR(64) NOT NULL, shard_month CHAR(6) NOT NULL, + tenant_id BIGINT NOT NULL DEFAULT 0, topic VARCHAR(64) NOT NULL, shard_month CHAR(6) NOT NULL, min_seq BIGINT NOT NULL, max_seq BIGINT NOT NULL, message_count BIGINT NOT NULL DEFAULT 0, min_created_at DATETIME(3) NOT NULL, max_created_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(topic, shard_month), INDEX idx_im_group_message_topic_shards_seq(topic, max_seq), - INDEX idx_im_group_message_topic_shards_month(shard_month, topic) + PRIMARY KEY(tenant_id, topic, shard_month), INDEX idx_im_group_message_topic_shards_seq(tenant_id, topic, max_seq), + INDEX idx_im_group_message_topic_shards_month(tenant_id, shard_month, topic) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, `CREATE TABLE IF NOT EXISTS im_group_message_dedup ( - topic VARCHAR(64) NOT NULL, from_user BIGINT NOT NULL, client_msg_id VARCHAR(64) NOT NULL, + tenant_id BIGINT NOT NULL DEFAULT 0, topic VARCHAR(64) NOT NULL, from_user BIGINT NOT NULL, client_msg_id VARCHAR(64) NOT NULL, seqid BIGINT NOT NULL, shard_month CHAR(6) NOT NULL, message_id BIGINT, status VARCHAR(16) NOT NULL DEFAULT 'pending', expires_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(topic, from_user, client_msg_id), INDEX idx_im_group_message_dedup_expires(expires_at), - INDEX idx_im_group_message_dedup_topic_seq(topic, seqid) + PRIMARY KEY(tenant_id, topic, from_user, client_msg_id), INDEX idx_im_group_message_dedup_expires(expires_at), + INDEX idx_im_group_message_dedup_topic_seq(tenant_id, topic, seqid) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, } for _, statement := range statements { @@ -52,10 +50,10 @@ func ensureGroupMessageShardInfrastructure(exec sqlExecutor) error { } } - return ensureGroupMessageShardWindow(exec, time.Now().UTC()) + return ensureGroupMessageShardWindow(exec, tenantID, time.Now().UTC()) } -func ensureGroupMessageShardWindow(exec sqlExecutor, now time.Time) error { +func ensureGroupMessageShardWindow(exec sqlExecutor, tenantID t.TenantID, now time.Time) error { first := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC) for _, offset := range []int{-1, 0, 1, 2} { start := first.AddDate(0, offset, 0) @@ -65,11 +63,11 @@ func ensureGroupMessageShardWindow(exec sqlExecutor, now time.Time) error { } else if offset == 0 { status = "writable" } - if err := ensureGroupMessageShard(exec, start, status); err != nil { + if err := ensureGroupMessageShard(exec, tenantID, start, status); err != nil { return err } } - _, err := exec.Exec("UPDATE im_group_message_shards SET status='sealed' WHERE status='sealing' AND shard_end '9' { - return "", false - } - } - parsed, err := time.Parse("200601", month) - if err != nil || parsed.Format("200601") != month { - return "", false - } - return "im_group_messages_" + month, true +func groupMessageShardTable(tenantID t.TenantID, month string) (string, bool) { + return tenantGroupMessagesTable(tenantID, month) } func groupMessageTablesForTopic(q sqlx.QueryerContext, ctx context.Context, tenantID t.TenantID, topic string) ([]string, error) { @@ -151,7 +137,7 @@ func groupMessageTablesForTopic(q sqlx.QueryerContext, ctx context.Context, tena routes = nil if err = sqlx.SelectContext(ctx, q, &routes, `SELECT shard_month AS month,min_seq,max_seq,message_count AS count - FROM im_group_message_topic_shards WHERE topic=? ORDER BY shard_month DESC`, topic); err != nil { + FROM im_group_message_topic_shards WHERE tenant_id=? AND topic=? ORDER BY shard_month DESC`, tenantID, topic); err != nil { return nil, err } if redisAvailable { @@ -162,11 +148,11 @@ func groupMessageTablesForTopic(q sqlx.QueryerContext, ctx context.Context, tena } tables := make([]string, 0, len(routes)+1) for _, route := range routes { - if table, ok := groupMessageShardTable(route.Month); ok { + if table, ok := groupMessageShardTable(tenantID, route.Month); ok { tables = append(tables, table) } } - return append(tables, legacyGroupMessageTable), nil + return tables, nil } func groupMessageTablesForSeq(q sqlx.QueryerContext, ctx context.Context, tenantID t.TenantID, topic string, seq int64) ([]string, error) { @@ -179,8 +165,8 @@ func groupMessageTablesForSeq(q sqlx.QueryerContext, ctx context.Context, tenant routes = nil if err = sqlx.SelectContext(ctx, q, &routes, `SELECT shard_month AS month,min_seq,max_seq,message_count AS count - FROM im_group_message_topic_shards WHERE topic=? AND min_seq<=? AND max_seq>=? - ORDER BY shard_month DESC`, topic, seq, seq); err != nil { + FROM im_group_message_topic_shards WHERE tenant_id=? AND topic=? AND min_seq<=? AND max_seq>=? + ORDER BY shard_month DESC`, tenantID, topic, seq, seq); err != nil { return nil, err } if redisAvailable { @@ -192,18 +178,18 @@ func groupMessageTablesForSeq(q sqlx.QueryerContext, ctx context.Context, tenant tables := make([]string, 0, 2) for _, route := range routes { if route.MinSeq <= seq && route.MaxSeq >= seq { - if table, ok := groupMessageShardTable(route.Month); ok { + if table, ok := groupMessageShardTable(tenantID, route.Month); ok { tables = append(tables, table) } } } - return append(tables, legacyGroupMessageTable), nil + return tables, nil } func groupMessageMonthFromTable(table string) string { - const prefix = "im_group_messages_" - if len(table) == len(prefix)+6 && table[:len(prefix)] == prefix { - return table[len(prefix):] + const suffixLength = 6 + if len(table) > suffixLength && validShardMonth(table[len(table)-suffixLength:]) { + return table[len(table)-suffixLength:] } return "" } diff --git a/server/db/mysql/group_schema.go b/server/db/mysql/group_schema.go index 0c5b8eb..6ec412c 100644 --- a/server/db/mysql/group_schema.go +++ b/server/db/mysql/group_schema.go @@ -4,12 +4,12 @@ package mysql // ensureIMGroupBusinessTables installs the product group-chat schema in the -// currently selected tenant database. Tenant databases intentionally do not -// carry tenant_id columns. +// currently selected tenant database. Rows still carry tenant_id explicitly; +// database routing remains one tenant per database. func ensureIMGroupBusinessTables(exec sqlExecutor) error { statements := []string{ `CREATE TABLE IF NOT EXISTS im_group_settings ( - id BIGINT NOT NULL, topic VARCHAR(32) NOT NULL, owner_user_id BIGINT NOT NULL, + tenant_id BIGINT NOT NULL DEFAULT 0, id BIGINT NOT NULL, topic VARCHAR(32) NOT NULL, owner_user_id BIGINT NOT NULL, name VARCHAR(128) NOT NULL DEFAULT '', avatar_file_id VARCHAR(64) NOT NULL DEFAULT '', description VARCHAR(512) NOT NULL DEFAULT '', group_status VARCHAR(16) NOT NULL DEFAULT 'active', join_approval TINYINT NOT NULL DEFAULT 0, mute_all TINYINT NOT NULL DEFAULT 0, @@ -24,7 +24,7 @@ func ensureIMGroupBusinessTables(exec sqlExecutor) error { INDEX idx_im_group_settings_status_updated(group_status, updated_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, `CREATE TABLE IF NOT EXISTS im_group_messages ( - id BIGINT NOT NULL AUTO_INCREMENT, topic VARCHAR(64) NOT NULL, group_id BIGINT NOT NULL, + tenant_id BIGINT NOT NULL DEFAULT 0, id BIGINT NOT NULL AUTO_INCREMENT, topic VARCHAR(64) NOT NULL, group_id BIGINT NOT NULL, seqid BIGINT NOT NULL, from_user BIGINT NOT NULL, client_msg_id VARCHAR(64), msg_type VARCHAR(32) NOT NULL DEFAULT 'text', status VARCHAR(32) NOT NULL DEFAULT 'normal', content JSON, content_nonce VARBINARY(12), key_version INT NOT NULL DEFAULT 0, @@ -45,7 +45,7 @@ func ensureIMGroupBusinessTables(exec sqlExecutor) error { INDEX idx_im_group_messages_expires(expires_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, `CREATE TABLE IF NOT EXISTS im_group_members ( - id BIGINT NOT NULL AUTO_INCREMENT, topic VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, + tenant_id BIGINT NOT NULL DEFAULT 0, id BIGINT NOT NULL AUTO_INCREMENT, topic VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, role VARCHAR(16) NOT NULL DEFAULT 'member', member_status VARCHAR(16) NOT NULL DEFAULT 'active', nickname VARCHAR(64) NOT NULL DEFAULT '', pinned TINYINT NOT NULL DEFAULT 0, mute_notifications TINYINT NOT NULL DEFAULT 0, join_source VARCHAR(32) NOT NULL DEFAULT 'manual', @@ -59,7 +59,7 @@ func ensureIMGroupBusinessTables(exec sqlExecutor) error { INDEX idx_im_group_members_topic_status(topic, member_status, id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, `CREATE TABLE IF NOT EXISTS im_group_invitations ( - id BIGINT NOT NULL AUTO_INCREMENT, invitation_id VARCHAR(64) NOT NULL, topic VARCHAR(32) NOT NULL, + tenant_id BIGINT NOT NULL DEFAULT 0, id BIGINT NOT NULL AUTO_INCREMENT, invitation_id VARCHAR(64) NOT NULL, topic VARCHAR(32) NOT NULL, inviter_user_id BIGINT NOT NULL, target_user_id BIGINT NOT NULL, message VARCHAR(256) NOT NULL DEFAULT '', status VARCHAR(16) NOT NULL DEFAULT 'pending', result VARCHAR(32) NOT NULL DEFAULT '', join_request_id VARCHAR(64) NOT NULL DEFAULT '', idempotency_key VARCHAR(128) NOT NULL DEFAULT '', @@ -72,7 +72,7 @@ func ensureIMGroupBusinessTables(exec sqlExecutor) error { INDEX idx_im_group_invitations_topic_status(topic, status, id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, `CREATE TABLE IF NOT EXISTS im_group_join_requests ( - id BIGINT NOT NULL AUTO_INCREMENT, request_id VARCHAR(64) NOT NULL, topic VARCHAR(32) NOT NULL, + tenant_id BIGINT NOT NULL DEFAULT 0, id BIGINT NOT NULL AUTO_INCREMENT, request_id VARCHAR(64) NOT NULL, topic VARCHAR(32) NOT NULL, applicant_user_id BIGINT NOT NULL, inviter_user_id BIGINT, source VARCHAR(32) NOT NULL DEFAULT 'manual', qr_token_hash CHAR(64) NOT NULL DEFAULT '', message VARCHAR(256) NOT NULL DEFAULT '', status VARCHAR(16) NOT NULL DEFAULT 'pending', reject_reason VARCHAR(256) NOT NULL DEFAULT '', @@ -87,7 +87,7 @@ func ensureIMGroupBusinessTables(exec sqlExecutor) error { INDEX idx_im_group_join_requests_expire(status, expires_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, `CREATE TABLE IF NOT EXISTS im_group_announcements ( - id BIGINT NOT NULL AUTO_INCREMENT, announcement_id VARCHAR(64) NOT NULL, topic VARCHAR(32) NOT NULL, + tenant_id BIGINT NOT NULL DEFAULT 0, id BIGINT NOT NULL AUTO_INCREMENT, announcement_id VARCHAR(64) NOT NULL, topic VARCHAR(32) NOT NULL, author_user_id BIGINT NOT NULL, content TEXT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'current', version INT NOT NULL DEFAULT 1, published_at DATETIME(3) NOT NULL, deleted_by BIGINT, deleted_at DATETIME(3), idempotency_key VARCHAR(128) NOT NULL DEFAULT '', @@ -99,14 +99,14 @@ func ensureIMGroupBusinessTables(exec sqlExecutor) error { INDEX idx_im_group_announcements_topic_time(topic, published_at DESC, id DESC) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, `CREATE TABLE IF NOT EXISTS im_group_announcement_reads ( - id BIGINT NOT NULL, announcement_id VARCHAR(64) NOT NULL, topic VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, + tenant_id BIGINT NOT NULL DEFAULT 0, id BIGINT NOT NULL, announcement_id VARCHAR(64) NOT NULL, topic VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, read_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_group_announcement_reads_announcement_user(announcement_id, user_id), INDEX idx_im_group_announcement_reads_topic_user(topic, user_id, read_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, `CREATE TABLE IF NOT EXISTS im_group_mutes ( - id BIGINT NOT NULL, topic VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, muted_by BIGINT NOT NULL, + tenant_id BIGINT NOT NULL DEFAULT 0, id BIGINT NOT NULL, topic VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, muted_by BIGINT NOT NULL, muted_at DATETIME(3) NOT NULL, expires_at DATETIME(3), reason VARCHAR(256) NOT NULL DEFAULT '', status VARCHAR(16) NOT NULL DEFAULT 'active', revoked_by BIGINT, revoked_at DATETIME(3), version INT NOT NULL DEFAULT 1, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), @@ -116,7 +116,7 @@ func ensureIMGroupBusinessTables(exec sqlExecutor) error { INDEX idx_im_group_mutes_user_status(user_id, status, expires_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, `CREATE TABLE IF NOT EXISTS im_group_qrcodes ( - id BIGINT NOT NULL AUTO_INCREMENT, token_id VARCHAR(64) NOT NULL, token_hash CHAR(64) NOT NULL, + tenant_id BIGINT NOT NULL DEFAULT 0, id BIGINT NOT NULL AUTO_INCREMENT, token_id VARCHAR(64) NOT NULL, token_hash CHAR(64) NOT NULL, topic VARCHAR(32) NOT NULL, created_by BIGINT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'active', expires_at DATETIME(3) NOT NULL, revoked_by BIGINT, revoked_at DATETIME(3), created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), @@ -125,7 +125,7 @@ func ensureIMGroupBusinessTables(exec sqlExecutor) error { UNIQUE INDEX uk_im_group_qrcodes_hash(token_hash), INDEX idx_im_group_qrcodes_topic_status(topic, status, expires_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, `CREATE TABLE IF NOT EXISTS im_group_files ( - id BIGINT NOT NULL AUTO_INCREMENT, group_file_id VARCHAR(64) NOT NULL, topic VARCHAR(32) NOT NULL, + tenant_id BIGINT NOT NULL DEFAULT 0, id BIGINT NOT NULL AUTO_INCREMENT, group_file_id VARCHAR(64) NOT NULL, topic VARCHAR(32) NOT NULL, file_id VARCHAR(64) NOT NULL, source_topic VARCHAR(32) NOT NULL DEFAULT '', source_seq BIGINT NOT NULL DEFAULT 0, uploader_id BIGINT NOT NULL, file_name VARCHAR(255) NOT NULL DEFAULT '', mime_type VARCHAR(128) NOT NULL DEFAULT '', file_type VARCHAR(32) NOT NULL DEFAULT 'file', file_size BIGINT NOT NULL DEFAULT 0, @@ -140,7 +140,7 @@ func ensureIMGroupBusinessTables(exec sqlExecutor) error { INDEX idx_im_group_files_topic_name(topic, status, file_name), INDEX idx_im_group_files_uploader(uploader_id, id DESC) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, `CREATE TABLE IF NOT EXISTS im_group_admin_logs ( - id BIGINT NOT NULL AUTO_INCREMENT, log_id VARCHAR(64) NOT NULL, topic VARCHAR(32) NOT NULL, + tenant_id BIGINT NOT NULL DEFAULT 0, id BIGINT NOT NULL AUTO_INCREMENT, log_id VARCHAR(64) NOT NULL, topic VARCHAR(32) NOT NULL, operator_id BIGINT NOT NULL, target_user_id BIGINT, action VARCHAR(64) NOT NULL, object_type VARCHAR(32) NOT NULL DEFAULT '', object_id VARCHAR(64) NOT NULL DEFAULT '', before_data JSON, after_data JSON, request_id VARCHAR(128) NOT NULL DEFAULT '', @@ -150,7 +150,7 @@ func ensureIMGroupBusinessTables(exec sqlExecutor) error { INDEX idx_im_group_admin_logs_operator(operator_id, id DESC), INDEX idx_im_group_admin_logs_object(object_type, object_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, `CREATE TABLE IF NOT EXISTS im_group_idempotency ( - id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, method VARCHAR(8) NOT NULL, + tenant_id BIGINT NOT NULL DEFAULT 0, id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, method VARCHAR(8) NOT NULL, route VARCHAR(256) NOT NULL, idempotency_key VARCHAR(128) NOT NULL, body_hash CHAR(64) NOT NULL, result_code INT NOT NULL DEFAULT 0, result_body JSON, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), @@ -168,5 +168,5 @@ func ensureIMGroupBusinessTables(exec sqlExecutor) error { return err } } - return ensureGroupMessageShardInfrastructure(exec) + return ensureGroupMessageShardInfrastructure(exec, defaultTenantID) } diff --git a/server/db/mysql/group_schema_test.go b/server/db/mysql/group_schema_test.go index 77de2ae..9be7468 100644 --- a/server/db/mysql/group_schema_test.go +++ b/server/db/mysql/group_schema_test.go @@ -6,6 +6,8 @@ import ( "database/sql" "strings" "testing" + + types "github.com/tinode/chat/server/store/types" ) type recordingGroupSchemaExecutor struct { @@ -54,12 +56,36 @@ func TestEnsureIMGroupBusinessTablesUsesSelectedTenantDatabase(t *testing.T) { } func TestGroupMessageShardTableRejectsUnsafeMonth(t *testing.T) { - if table, ok := groupMessageShardTable("202609"); !ok || table != "im_group_messages_202609" { + if table, ok := groupMessageShardTable(1, "202609"); !ok || table != "im_group_messages_202609" { t.Fatalf("unexpected valid shard: table=%q ok=%v", table, ok) } for _, month := range []string{"", "20269", "202613", "2026_09", "202609;DROP TABLE im_users"} { - if table, ok := groupMessageShardTable(month); ok || table != "" { + if table, ok := groupMessageShardTable(1, month); ok || table != "" { t.Fatalf("unsafe shard month accepted: month=%q table=%q", month, table) } } } + +func TestTenantPartitionTableNames(t *testing.T) { + tests := []struct { + tenantID types.TenantID + friends string + messages string + group string + }{ + {tenantID: 1, friends: "im_friendships", messages: "im_messages", group: "im_group_messages_202609"}, + {tenantID: 10, friends: "im_friendships", messages: "im_messages", group: "im_group_messages_202609"}, + {tenantID: 1000, friends: "im_friendships", messages: "im_messages", group: "im_group_messages_202609"}, + } + for _, tc := range tests { + if got := tenantFriendshipsTable(tc.tenantID); got != tc.friends { + t.Fatalf("tenant %d friendship table: got %q, want %q", tc.tenantID, got, tc.friends) + } + if got := tenantMessagesTable(tc.tenantID); got != tc.messages { + t.Fatalf("tenant %d message table: got %q, want %q", tc.tenantID, got, tc.messages) + } + if got, ok := tenantGroupMessagesTable(tc.tenantID, "202609"); !ok || got != tc.group { + t.Fatalf("tenant %d group message table: got %q, ok=%v, want %q", tc.tenantID, got, ok, tc.group) + } + } +} diff --git a/server/db/mysql/registration_invites.go b/server/db/mysql/registration_invites.go index 92e9131..d01466a 100644 --- a/server/db/mysql/registration_invites.go +++ b/server/db/mysql/registration_invites.go @@ -5,6 +5,7 @@ package mysql import ( "database/sql" + "fmt" "strconv" "strings" "time" @@ -390,7 +391,7 @@ func (a *adapter) TenantRegistrationInviteRecordUse(id t.TenantID, value string, if row.MaxUses > 0 { var usedCount int if err = tx.GetContext(txCtx, &usedCount, - "SELECT COUNT(*) FROM im_registration_invite_uses WHERE invite_code_id=?", row.ID); err != nil { + "SELECT COUNT(*) FROM im_registration_invite_uses WHERE tenant_id=? AND invite_code_id=?", id, row.ID); err != nil { return err } if usedCount >= row.MaxUses { @@ -402,12 +403,12 @@ func (a *adapter) TenantRegistrationInviteRecordUse(id t.TenantID, value string, return err } if _, err = tx.ExecContext(txCtx, `INSERT INTO im_registration_invite_uses - (id,invite_code_id,user_id,request_id,used_at) VALUES(?,?,?,?,?)`, useID, row.ID, + (tenant_id,id,invite_code_id,user_id,request_id,used_at) VALUES(?,?,?,?,?,?)`, id, useID, row.ID, store.DecodeUid(uid), requestID, now); err != nil { if isDupe(err) { var existingCodeID int64 if lookupErr := tx.GetContext(txCtx, &existingCodeID, - "SELECT invite_code_id FROM im_registration_invite_uses WHERE user_id=?", store.DecodeUid(uid)); lookupErr == nil && existingCodeID == row.ID { + "SELECT invite_code_id FROM im_registration_invite_uses WHERE tenant_id=? AND user_id=?", id, store.DecodeUid(uid)); lookupErr == nil && existingCodeID == row.ID { err = nil } else { return t.ErrDuplicate @@ -419,7 +420,7 @@ func (a *adapter) TenantRegistrationInviteRecordUse(id t.TenantID, value string, if row.BoundUserID != nil && *row.BoundUserID > 0 { boundUID := t.ParseUserId(strconv.FormatInt(*row.BoundUserID, 10)) if !boundUID.IsZero() && boundUID != uid { - if err = upsertFriendshipPairTx(tx, uid, boundUID, uid.P2PName(boundUID), now); err != nil { + if err = upsertFriendshipPairTx(tx, id, uid, boundUID, uid.P2PName(boundUID), now); err != nil { return err } } @@ -430,7 +431,7 @@ func (a *adapter) TenantRegistrationInviteRecordUse(id t.TenantID, value string, return a.TenantRegistrationInviteSyncUsage(id, row.ID) } -func upsertFriendshipPairTx(tx *sqlx.Tx, uid1, uid2 t.Uid, topic string, now time.Time) error { +func upsertFriendshipPairTx(tx *sqlx.Tx, tenantID t.TenantID, uid1, uid2 t.Uid, topic string, now time.Time) error { if uid1.IsZero() || uid2.IsZero() || uid1 == uid2 { return t.ErrMalformed } @@ -443,9 +444,9 @@ func upsertFriendshipPairTx(tx *sqlx.Tx, uid1, uid2 t.Uid, topic string, now tim return err } if _, err = tx.Exec( - "INSERT INTO im_friendships(id,user_id,peer_id,status,topic,created_at,updated_at) VALUES(?,?,?,?,?,?,?) "+ + fmt.Sprintf("INSERT INTO %s(tenant_id,id,user_id,peer_id,status,topic,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?) ", tenantFriendshipsTable(tenantID))+ "ON DUPLICATE KEY UPDATE status=VALUES(status),topic=VALUES(topic),updated_at=VALUES(updated_at)", - friendshipID, decodeUID(pair[0]), decodeUID(pair[1]), t.RelationshipFriend, topic, now, now); err != nil { + tenantID, friendshipID, decodeUID(pair[0]), decodeUID(pair[1]), t.RelationshipFriend, topic, now, now); err != nil { return err } } @@ -474,7 +475,7 @@ func (a *adapter) TenantRegistrationInviteSyncUsage(id t.TenantID, codeID int64) LastUsedAt *time.Time `db:"last_used_at"` } if err = a.db.GetContext(ctx, &aggregate, `SELECT COUNT(*) AS usage_count,MAX(used_at) AS last_used_at - FROM im_registration_invite_uses WHERE invite_code_id=?`, codeID); err != nil { + FROM im_registration_invite_uses WHERE tenant_id=? AND invite_code_id=?`, id, codeID); err != nil { return err } res, err := a.registrationInviteControlDB().ExecContext(ctx, `UPDATE im_registration_invite_codes diff --git a/server/db/mysql/report_group_regression_test.go b/server/db/mysql/report_group_regression_test.go index bc01ac9..b2f009b 100644 --- a/server/db/mysql/report_group_regression_test.go +++ b/server/db/mysql/report_group_regression_test.go @@ -158,7 +158,7 @@ func TestTenantFileAccessIncludesAvatarAudienceRules(t *testing.T) { } body := string(source) for _, want := range []string{ - "INNER JOIN im_friendships AS fs", + "INNER JOIN %s AS fs", "fs.user_id=? AND fs.peer_id=fml.userid AND fs.status=?", "INNER JOIN im_group_members AS gm", "gm.topic=fml.topic AND gm.user_id=? AND gm.member_status=?", diff --git a/server/db/mysql/tenant_adapter.go b/server/db/mysql/tenant_adapter.go index 683f6b0..a66e235 100644 --- a/server/db/mysql/tenant_adapter.go +++ b/server/db/mysql/tenant_adapter.go @@ -377,10 +377,98 @@ func (a *adapter) TenantUserCreate(id t.TenantID, user *t.User) error { if err != nil || user == nil || user.TenantID != id { return t.ErrMalformed } - if tenantAdp.tenantScopedDB { - return tenantAdp.UserCreate(user) + if tenantAdp != a { + return tenantAdp.TenantUserCreate(id, user) } - return tenantAdp.UserCreate(user) + ctx, cancel := a.getContextForTx() + if cancel != nil { + defer cancel() + } + tx, err := a.db.BeginTxx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + decodedUID := decodeUID(user.Uid()) + securityID, err := nextSnowflakeID() + if err != nil { + return err + } + profileID, err := nextSnowflakeID() + if err != nil { + return err + } + var phone any + if strings.TrimSpace(user.Phone) != "" { + canonicalPhone, normalizeErr := normalizeUserPhone(user.Phone) + if normalizeErr != nil { + return normalizeErr + } + user.Phone = canonicalPhone + phone = canonicalPhone + } + accountNo, hasAccountNo, err := nextTenantUserAccountNo(ctx, tx, id) + if err != nil { + return err + } + nickname, avatarFileID := profileSeedFromPublic(user.Public) + insert := "INSERT INTO im_users(tenant_id,id,username,createdat,updatedat,state,access,phone,trusted) VALUES(?,?,?,?,?,?,?,?,?)" + args := []any{id, decodedUID, user.Username, user.CreatedAt, user.UpdatedAt, user.State, user.Access, phone, common.ToJSON(user.Trusted)} + if hasAccountNo { + insert = "INSERT INTO im_users(tenant_id,id,account_no,username,createdat,updatedat,state,access,phone,trusted) VALUES(?,?,?,?,?,?,?,?,?,?)" + args = []any{id, decodedUID, accountNo, user.Username, user.CreatedAt, user.UpdatedAt, user.State, user.Access, phone, common.ToJSON(user.Trusted)} + } + if _, err = tx.Exec(insert, args...); err != nil { + if isDupe(err) { + return t.ErrDuplicate + } + return err + } + if _, err = tx.Exec("INSERT INTO im_user_security(tenant_id,id,user_id,auth_version,created_at,updated_at) VALUES(?,?,?,?,?,?)", + id, securityID, decodedUID, 1, user.CreatedAt, user.UpdatedAt); err != nil { + return err + } + if _, err = tx.Exec(`INSERT IGNORE INTO im_user_profiles(tenant_id,id,user_id,avatar_file_id,nickname,public_account,created_at,updated_at) + VALUES(?,?,?,?,?,?,?,?)`, id, profileID, decodedUID, avatarFileID, nickname, user.Uid().UserId(), user.CreatedAt, user.UpdatedAt); err != nil { + return err + } + user.Tags = nil + return tx.Commit() +} + +func nextTenantUserAccountNo(ctx context.Context, tx *sqlx.Tx, tenantID t.TenantID) (uint32, bool, error) { + var columns int + if err := tx.GetContext(ctx, &columns, `SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='im_users' AND COLUMN_NAME='account_no'`); err != nil { + return 0, false, err + } + if columns == 0 { + return 0, false, nil + } + if _, err := tx.ExecContext(ctx, `UPDATE im_user_account_sequences + SET next_value=GREATEST(next_value,(SELECT GREATEST(COALESCE(MAX(account_no),999),999) FROM im_users WHERE tenant_id=? AND account_no>0)) + WHERE tenant_id=? AND id=1`, tenantID, tenantID); err != nil { + return 0, true, err + } + result, err := tx.ExecContext(ctx, `UPDATE im_user_account_sequences + SET next_value=LAST_INSERT_ID(next_value+1) WHERE tenant_id=? AND id=1`, tenantID) + if err != nil { + return 0, true, err + } + if affected, affectedErr := result.RowsAffected(); affectedErr != nil { + return 0, true, affectedErr + } else if affected != 1 { + return 0, true, fmt.Errorf("mysql: user account sequence is not initialized for tenant %d", tenantID) + } + var next uint64 + if err = tx.GetContext(ctx, &next, "SELECT LAST_INSERT_ID()"); err != nil { + return 0, true, err + } + if next > uint64(^uint32(0)) { + return 0, true, fmt.Errorf("mysql: user account number exhausted for tenant %d", tenantID) + } + return uint32(next), true, nil } func (a *adapter) TenantUserGet(id t.TenantID, uid t.Uid) (*t.User, error) { @@ -389,8 +477,7 @@ func (a *adapter) TenantUserGet(id t.TenantID, uid t.Uid) (*t.User, error) { return nil, t.ErrMalformed } if tenantAdp != a { - user, err := tenantAdp.UserGet(uid) - return stampTenantUser(id, user), err + return tenantAdp.TenantUserGet(id, uid) } if a.tenantScopedDB { user, err := a.UserGet(uid) @@ -401,12 +488,12 @@ func (a *adapter) TenantUserGet(id t.TenantID, uid t.Uid) (*t.User, error) { defer cancel() } var row profileUserRow - err = a.db.GetContext(ctx, &row, `SELECT ? AS tenant_id,u.id,u.username,u.createdat,u.updatedat,u.state,u.stateat,u.access,u.lastseen,u.useragent, + err = a.db.GetContext(ctx, &row, `SELECT u.tenant_id,u.id,u.username,u.createdat,u.updatedat,u.state,u.stateat,u.access,u.lastseen,u.useragent, COALESCE(u.phone,'') AS phone,u.trusted, p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature, p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account,p.updated_at AS profile_updated_at - FROM im_users AS u LEFT JOIN im_user_profiles AS p ON p.user_id=u.id - WHERE u.id=? AND u.state!=?`, + FROM im_users AS u LEFT JOIN im_user_profiles AS p ON p.tenant_id=u.tenant_id AND p.user_id=u.id + WHERE u.tenant_id=? AND u.id=? AND u.state!=?`, id, decodeUID(uid), t.StateDeleted) if err == sql.ErrNoRows { return nil, nil @@ -423,21 +510,8 @@ func (a *adapter) TenantUserGetByPhone(id t.TenantID, phone string) (t.Uid, erro if err != nil || phone == "" { return t.ZeroUid, t.ErrMalformed } - if tenantAdp != a || a.tenantScopedDB { - ctx, cancel := tenantAdp.getContext() - if cancel != nil { - defer cancel() - } - var decoded int64 - err = tenantAdp.db.GetContext(ctx, &decoded, - "SELECT id FROM im_users WHERE phone=? AND state!=?", phone, t.StateDeleted) - if err == sql.ErrNoRows { - return t.ZeroUid, nil - } - if err != nil { - return t.ZeroUid, err - } - return store.EncodeUid(decoded), nil + if tenantAdp != a { + return tenantAdp.TenantUserGetByPhone(id, phone) } ctx, cancel := a.getContext() if cancel != nil { @@ -461,8 +535,7 @@ func (a *adapter) TenantUserGetAll(id t.TenantID, uids ...t.Uid) ([]t.User, erro return nil, err } if tenantAdp != a { - users, err := tenantAdp.UserGetAll(uids...) - return stampTenantUsers(id, users), err + return tenantAdp.TenantUserGetAll(id, uids...) } if a.tenantScopedDB { users, err := a.UserGetAll(uids...) @@ -491,12 +564,12 @@ func (a *adapter) tenantUserGetAllTx(ctx context.Context, tx *sqlx.Tx, id t.Tena var q string var params []any if a.tenantScopedDB { - q, params, _ = sqlx.In(`SELECT id,username,createdat,updatedat,state,stateat,access,lastseen,useragent, - COALESCE(phone,'') AS phone,trusted, + q, params, _ = sqlx.In(`SELECT u.id AS id,u.username,u.createdat,u.updatedat,u.state,u.stateat,u.access,u.lastseen,u.useragent, + COALESCE(u.phone,'') AS phone,u.trusted, p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature, p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account,p.updated_at AS profile_updated_at - FROM im_users AS u LEFT JOIN im_user_profiles AS p ON p.user_id=u.id - WHERE u.id IN (?) AND u.state!=?`, args, t.StateDeleted) + FROM im_users AS u LEFT JOIN im_user_profiles AS p ON p.tenant_id=u.tenant_id AND p.user_id=u.id + WHERE u.tenant_id=? AND u.id IN (?) AND u.state!=?`, id, args, t.StateDeleted) } else { q, params, _ = sqlx.In(`SELECT u.tenant_id,u.id,u.username,u.createdat,u.updatedat,u.state,u.stateat,u.access,u.lastseen,u.useragent, COALESCE(u.phone,'') AS phone,u.trusted, @@ -535,7 +608,7 @@ func (a *adapter) TenantUserDelete(id t.TenantID, uid t.Uid, hard bool) error { return t.ErrMalformed } if tenantAdp != a { - return tenantAdp.UserDelete(uid, hard) + return tenantAdp.TenantUserDelete(id, uid, hard) } if a.tenantScopedDB { return a.UserDelete(uid, hard) @@ -588,7 +661,7 @@ func (a *adapter) TenantUserDelete(id t.TenantID, uid t.Uid, hard bool) error { if _, err = tx.Exec("DELETE dl FROM im_dellog AS dl JOIN im_topics AS tp ON tp.tenant_id=dl.tenant_id AND tp.name=dl.topic WHERE tp.tenant_id=? AND tp.owner=?", id, decoded); err != nil { return err } - if _, err = tx.Exec("DELETE m FROM im_messages AS m JOIN im_topics AS tp ON tp.tenant_id=m.tenant_id AND tp.name=m.topic WHERE tp.tenant_id=? AND tp.owner=?", id, decoded); err != nil { + if _, err = tx.Exec(fmt.Sprintf("DELETE m FROM %s AS m JOIN im_topics AS tp ON tp.tenant_id=m.tenant_id AND tp.name=m.topic WHERE tp.tenant_id=? AND tp.owner=?", tenantMessagesTable(id)), id, decoded); err != nil { return err } sqlq, params, _ := sqlx.In("DELETE FROM im_subscriptions WHERE tenant_id=? AND topic IN (?)", id, ownAll) @@ -602,10 +675,10 @@ func (a *adapter) TenantUserDelete(id t.TenantID, uid t.Uid, hard bool) error { return err } } - if _, err = tx.Exec("DELETE FROM im_auth WHERE userid=?", decoded); err != nil { + if _, err = tx.Exec("DELETE FROM im_auth WHERE tenant_id=? AND userid=?", id, decoded); err != nil { return err } - if _, err = tx.Exec("DELETE FROM im_credentials WHERE userid=?", decoded); err != nil { + if _, err = tx.Exec("DELETE FROM im_credentials WHERE tenant_id=? AND userid=?", id, decoded); err != nil { return err } if _, err = tx.Exec("DELETE FROM im_users WHERE tenant_id=? AND id=?", id, decoded); err != nil { @@ -667,7 +740,7 @@ func (a *adapter) TenantUserUpdate(id t.TenantID, uid t.Uid, update map[string]a return t.ErrMalformed } if tenantAdp != a { - return tenantAdp.UserUpdate(uid, update) + return tenantAdp.TenantUserUpdate(id, uid, update) } if a.tenantScopedDB { return a.UserUpdate(uid, update) @@ -714,7 +787,7 @@ func (a *adapter) TenantUserUpdateTags(id t.TenantID, uid t.Uid, add, remove, re return nil, t.ErrMalformed } if tenantAdp != a { - return tenantAdp.UserUpdateTags(uid, add, remove, reset) + return tenantAdp.TenantUserUpdateTags(id, uid, add, remove, reset) } if a.tenantScopedDB { return a.UserUpdateTags(uid, add, remove, reset) @@ -728,7 +801,7 @@ func (a *adapter) TenantUserUnreadCount(id t.TenantID, uids ...t.Uid) (map[t.Uid return nil, err } if tenantAdp != a { - return tenantAdp.UserUnreadCount(uids...) + return tenantAdp.TenantUserUnreadCount(id, uids...) } if a.tenantScopedDB { return a.UserUnreadCount(uids...) @@ -774,7 +847,7 @@ func (a *adapter) TenantUserGetUnvalidated(id t.TenantID, before time.Time, limi return nil, err } if tenantAdp != a { - return tenantAdp.UserGetUnvalidated(before, limit) + return tenantAdp.TenantUserGetUnvalidated(id, before, limit) } if a.tenantScopedDB { return a.UserGetUnvalidated(before, limit) @@ -785,9 +858,9 @@ func (a *adapter) TenantUserGetUnvalidated(id t.TenantID, before time.Time, limi } rows, err := a.db.QueryxContext(ctx, "SELECT u.id,IFNULL(SUM(c.done),0) AS total FROM im_users AS u "+ - "LEFT JOIN im_credentials AS c ON u.id=c.userid "+ - "WHERE u.lastseen IS NULL AND u.updatedat 0 { - q = `SELECT u.tenant_id,u.id,u.updatedat,u.state,u.access,u.lastseen,u.useragent,u.trusted, + q = fmt.Sprintf(`SELECT u.tenant_id,u.id,u.updatedat,u.state,u.access,u.lastseen,u.useragent,u.trusted, p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature, p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account,p.updated_at AS profile_updated_at, fs.remark AS friendship_remark FROM im_users AS u - LEFT JOIN im_user_profiles AS p ON p.user_id=u.id - LEFT JOIN im_friendships AS fs ON fs.user_id=? AND fs.peer_id=u.id AND fs.status=? - WHERE u.tenant_id=? AND u.id IN (?)` + LEFT JOIN im_user_profiles AS p ON p.tenant_id=u.tenant_id AND p.user_id=u.id + LEFT JOIN %s AS fs ON fs.tenant_id=u.tenant_id AND fs.user_id=? AND fs.peer_id=u.id AND fs.status=? + WHERE u.tenant_id=? AND u.id IN (?)`, tenantFriendshipsTable(id)) q, args, _ = sqlx.In(q, decodeUID(uid), t.RelationshipFriend, id, usrq) if !keepDeleted { q += " AND u.state!=?" @@ -1144,8 +1215,7 @@ func (a *adapter) TenantUsersForTopic(id t.TenantID, topic string, keepDeleted b return nil, t.ErrMalformed } if tenantAdp != a { - subs, err := tenantAdp.UsersForTopic(topic, keepDeleted, opts) - return stampTenantSubs(id, subs), err + return tenantAdp.TenantUsersForTopic(id, topic, keepDeleted, opts) } if a.tenantScopedDB { subs, err := a.UsersForTopic(topic, keepDeleted, opts) @@ -1274,7 +1344,7 @@ func (a *adapter) TenantOwnTopics(id t.TenantID, uid t.Uid) ([]string, error) { return nil, t.ErrMalformed } if tenantAdp != a { - return tenantAdp.OwnTopics(uid) + return tenantAdp.TenantOwnTopics(id, uid) } if a.tenantScopedDB { return a.OwnTopics(uid) @@ -1289,7 +1359,7 @@ func (a *adapter) TenantChannelsForUser(id t.TenantID, uid t.Uid) ([]string, err return nil, t.ErrMalformed } if tenantAdp != a { - return tenantAdp.ChannelsForUser(uid) + return tenantAdp.TenantChannelsForUser(id, uid) } if a.tenantScopedDB { return a.ChannelsForUser(uid) @@ -1309,7 +1379,7 @@ func (a *adapter) TenantTopicShare(id t.TenantID, topic string, subs []*t.Subscr } } if tenantAdp != a { - return tenantAdp.TopicShare(topic, subs) + return tenantAdp.TenantTopicShare(id, topic, subs) } if a.tenantScopedDB { return a.TopicShare(topic, subs) @@ -1341,11 +1411,12 @@ func (a *adapter) TenantTopicShare(id t.TenantID, topic string, subs []*t.Subscr } func tenantMessageDeleteList(tx *sqlx.Tx, tenantID t.TenantID, topic string, toDel *t.DelMessage) error { + messageTable := tenantMessagesTable(tenantID) if toDel == nil { if _, err := tx.Exec("DELETE FROM im_dellog WHERE tenant_id=? AND topic=?", tenantID, topic); err != nil { return err } - _, err := tx.Exec("DELETE FROM im_messages WHERE tenant_id=? AND topic=?", tenantID, topic) + _, err := tx.Exec(fmt.Sprintf("DELETE FROM %s WHERE tenant_id=? AND topic=?", messageTable), tenantID, topic) return err } delRanges := toDel.SeqIdRanges @@ -1363,7 +1434,7 @@ func tenantMessageDeleteList(tx *sqlx.Tx, tenantID t.TenantID, topic string, toD args = append(args, newerThan) } var seqIDs []int64 - if err := tx.Select(&seqIDs, "SELECT seqid FROM im_messages AS m WHERE "+where, args...); err != nil { + if err := tx.Select(&seqIDs, fmt.Sprintf("SELECT seqid FROM %s AS m WHERE ", messageTable)+where, args...); err != nil { return err } if len(seqIDs) == 0 { @@ -1374,10 +1445,10 @@ func tenantMessageDeleteList(tx *sqlx.Tx, tenantID t.TenantID, topic string, toD rSQL, rArgs := common.RangesToSql(delRanges) where = "m.tenant_id=? AND m.topic=? AND m.seqid " + rSQL args = append([]any{tenantID, topic}, rArgs...) - if _, err := tx.Exec("DELETE fml.* FROM im_filemsglinks AS fml INNER JOIN im_messages AS m ON m.tenant_id=fml.tenant_id AND m.id=fml.msgid WHERE "+where, args...); err != nil { + if _, err := tx.Exec(fmt.Sprintf("DELETE fml.* FROM im_filemsglinks AS fml INNER JOIN %s AS m ON m.tenant_id=fml.tenant_id AND m.id=fml.msgid WHERE ", messageTable)+where, args...); err != nil { return err } - if _, err := tx.Exec("UPDATE im_messages AS m SET m.deletedat=?,m.delid=?,m.`from`=0,m.head=NULL,m.content=NULL WHERE "+ + if _, err := tx.Exec(fmt.Sprintf("UPDATE %s AS m SET m.deletedat=?,m.delid=?,m.`from`=0,m.head=NULL,m.content=NULL WHERE ", messageTable)+ where, append([]any{t.TimeNow(), toDel.DelId}, args...)...); err != nil { return err } @@ -1407,26 +1478,26 @@ func groupMessageDeleteList(tx *sqlx.Tx, tenantID t.TenantID, topic string, toDe return err } if toDel == nil { - if _, err = tx.Exec("DELETE FROM im_dellog WHERE topic=?", topic); err != nil { + if _, err = tx.Exec("DELETE FROM im_dellog WHERE tenant_id=? AND topic=?", tenantID, topic); err != nil { return err } for _, table := range tables { - if _, err = tx.Exec("DELETE FROM "+table+" WHERE topic=?", topic); err != nil { + if _, err = tx.Exec("DELETE FROM "+table+" WHERE tenant_id=? AND topic=?", tenantID, topic); err != nil { return err } } - if _, err = tx.Exec("DELETE FROM im_group_message_topic_shards WHERE topic=?", topic); err != nil { + if _, err = tx.Exec("DELETE FROM im_group_message_topic_shards WHERE tenant_id=? AND topic=?", tenantID, topic); err != nil { return err } - _, err = tx.Exec("DELETE FROM im_group_message_dedup WHERE topic=?", topic) + _, err = tx.Exec("DELETE FROM im_group_message_dedup WHERE tenant_id=? AND topic=?", tenantID, topic) return err } delRanges := toDel.SeqIdRanges if toDel.DeletedFor == "" { seqIDs := make([]int64, 0) for _, table := range tables { - where := "m.topic=?" - args := []any{topic} + where := "m.tenant_id=? AND m.topic=?" + args := []any{tenantID, topic} if len(delRanges) > 0 { rSQL, rArgs := common.RangesToSql(delRanges) where += " AND m.seqid " + rSQL @@ -1455,8 +1526,8 @@ func groupMessageDeleteList(tx *sqlx.Tx, tenantID t.TenantID, topic string, toDe } delRanges = t.SliceToRanges(uniqueSeqIDs) rSQL, rArgs := common.RangesToSql(delRanges) - where := "m.topic=? AND m.seqid " + rSQL - args := append([]any{topic}, rArgs...) + where := "m.tenant_id=? AND m.topic=? AND m.seqid " + rSQL + args := append([]any{tenantID, topic}, rArgs...) now := t.TimeNow() for _, table := range tables { if _, err = tx.Exec("UPDATE "+table+" AS m SET m.updated_at=?,m.deleted_at=?,m.status='deleted',m.from_user=0,m.head=NULL,m.content=NULL,m.content_size=0 WHERE "+ @@ -1465,7 +1536,7 @@ func groupMessageDeleteList(tx *sqlx.Tx, tenantID t.TenantID, topic string, toDe } } } - stmt, err := tx.Prepare("INSERT INTO im_dellog(topic,deletedfor,delid,low,hi) VALUES(?,?,?,?,?)") + stmt, err := tx.Prepare("INSERT INTO im_dellog(tenant_id,topic,deletedfor,delid,low,hi) VALUES(?,?,?,?,?,?)") if err != nil { return err } @@ -1475,7 +1546,7 @@ func groupMessageDeleteList(tx *sqlx.Tx, tenantID t.TenantID, topic string, toDe if rng.Hi == 0 { rng.Hi = rng.Low + 1 } - if _, err = stmt.Exec(topic, forUser, toDel.DelId, rng.Low, rng.Hi); err != nil { + if _, err = stmt.Exec(tenantID, topic, forUser, toDel.DelId, rng.Low, rng.Hi); err != nil { return err } } @@ -1488,7 +1559,7 @@ func (a *adapter) TenantTopicDelete(id t.TenantID, topic string, isChan, hard bo return t.ErrMalformed } if tenantAdp != a { - return tenantAdp.TopicDelete(topic, isChan, hard) + return tenantAdp.TenantTopicDelete(id, topic, isChan, hard) } if a.tenantScopedDB { return a.TopicDelete(topic, isChan, hard) @@ -1549,7 +1620,7 @@ func (a *adapter) TenantTopicUpdateOnMessage(id t.TenantID, topic string, msg *t return t.ErrMalformed } if tenantAdp != a { - return tenantAdp.TopicUpdateOnMessage(topic, msg) + return tenantAdp.TenantTopicUpdateOnMessage(id, topic, msg) } if a.tenantScopedDB { return a.TopicUpdateOnMessage(topic, msg) @@ -1569,7 +1640,7 @@ func (a *adapter) TenantTopicUpdateSubCnt(id t.TenantID, topic string) error { return t.ErrMalformed } if tenantAdp != a { - return tenantAdp.TopicUpdateSubCnt(topic) + return tenantAdp.TenantTopicUpdateSubCnt(id, topic) } if a.tenantScopedDB { return a.TopicUpdateSubCnt(topic) @@ -1590,7 +1661,7 @@ func (a *adapter) TenantTopicUpdate(id t.TenantID, topic string, update map[stri return t.ErrMalformed } if tenantAdp != a { - return tenantAdp.TopicUpdate(topic, update) + return tenantAdp.TenantTopicUpdate(id, topic, update) } if a.tenantScopedDB { return a.TopicUpdate(topic, update) @@ -1680,7 +1751,7 @@ func (a *adapter) TenantTopicOwnerChange(id t.TenantID, topic string, owner t.Ui return t.ErrMalformed } if tenantAdp != a { - return tenantAdp.TopicOwnerChange(topic, owner) + return tenantAdp.TenantTopicOwnerChange(id, topic, owner) } if a.tenantScopedDB { return a.TopicOwnerChange(topic, owner) @@ -1699,11 +1770,7 @@ func (a *adapter) TenantSubscriptionGet(id t.TenantID, topic string, uid t.Uid, return nil, t.ErrMalformed } if tenantAdp != a { - sub, err := tenantAdp.SubscriptionGet(topic, uid, keepDeleted) - if sub != nil { - sub.TenantID = id - } - return sub, err + return tenantAdp.TenantSubscriptionGet(id, topic, uid, keepDeleted) } if a.tenantScopedDB { sub, err := a.SubscriptionGet(topic, uid, keepDeleted) @@ -1740,8 +1807,7 @@ func (a *adapter) TenantSubsForUser(id t.TenantID, uid t.Uid) ([]t.Subscription, return nil, t.ErrMalformed } if tenantAdp != a { - subs, err := tenantAdp.SubsForUser(uid) - return stampTenantSubs(id, subs), err + return tenantAdp.TenantSubsForUser(id, uid) } if a.tenantScopedDB { subs, err := a.SubsForUser(uid) @@ -1772,8 +1838,7 @@ func (a *adapter) TenantSubsForTopic(id t.TenantID, topic string, keepDeleted bo return nil, t.ErrMalformed } if tenantAdp != a { - subs, err := tenantAdp.SubsForTopic(topic, keepDeleted, opts) - return stampTenantSubs(id, subs), err + return tenantAdp.TenantSubsForTopic(id, topic, keepDeleted, opts) } if a.tenantScopedDB { subs, err := a.SubsForTopic(topic, keepDeleted, opts) @@ -1828,7 +1893,7 @@ func (a *adapter) TenantSubsUpdate(id t.TenantID, topic string, uid t.Uid, updat return t.ErrMalformed } if tenantAdp != a { - return tenantAdp.SubsUpdate(topic, uid, update) + return tenantAdp.TenantSubsUpdate(id, topic, uid, update) } if a.tenantScopedDB { return a.SubsUpdate(topic, uid, update) @@ -1854,7 +1919,7 @@ func (a *adapter) TenantSubsDelete(id t.TenantID, topic string, uid t.Uid) error return t.ErrMalformed } if tenantAdp != a { - return tenantAdp.SubsDelete(topic, uid) + return tenantAdp.TenantSubsDelete(id, topic, uid) } if a.tenantScopedDB { return a.SubsDelete(topic, uid) @@ -1939,8 +2004,7 @@ func (a *adapter) TenantFind(id t.TenantID, caller, promoPrefix string, req [][] return nil, err } if tenantAdp != a { - subs, err := tenantAdp.Find(caller, promoPrefix, req, opt, activeOnly) - return stampTenantSubs(id, subs), err + return tenantAdp.TenantFind(id, caller, promoPrefix, req, opt, activeOnly) } if a.tenantScopedDB { subs, err := a.Find(caller, promoPrefix, req, opt, activeOnly) @@ -2026,7 +2090,7 @@ func (a *adapter) TenantFindOne(id t.TenantID, tag string) (string, error) { return "", t.ErrMalformed } if tenantAdp != a { - return tenantAdp.FindOne(tag) + return tenantAdp.TenantFindOne(id, tag) } if a.tenantScopedDB { return a.FindOne(tag) @@ -2071,7 +2135,7 @@ func (a *adapter) TenantMessageSave(id t.TenantID, msg *t.Message) error { } clientMsgID := sql.NullString{String: msg.ClientMsgID, Valid: msg.ClientMsgID != ""} res, err := a.db.ExecContext(ctx, - "INSERT INTO im_messages(tenant_id,createdat,updatedat,seqid,topic,`from`,client_msg_id,version,head,content,content_nonce,key_version) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)", + fmt.Sprintf("INSERT INTO %s(tenant_id,createdat,updatedat,seqid,topic,`from`,client_msg_id,version,head,content,content_nonce,key_version) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)", tenantMessagesTable(id)), id, msg.CreatedAt, msg.UpdatedAt, msg.SeqId, msg.Topic, decodeUID(t.ParseUid(msg.From)), clientMsgID, messageVersion(msg.Version), msg.Head, common.ToJSON(msg.Content), msg.ContentNonce, msg.KeyVersion) if isDupe(err) { return t.ErrDuplicate @@ -2098,24 +2162,24 @@ func (a *adapter) TenantGroupMessageSave(id t.TenantID, msg *t.Message) error { if cancel != nil { defer cancel() } - groupID, err := a.groupIDByTopic(ctx, msg.Topic) + groupID, err := a.groupIDByTopic(ctx, id, msg.Topic) if err != nil { return err } if msg.ShardMonth == "" { msg.ShardMonth = msg.CreatedAt.UTC().Format("200601") } - table, ok := groupMessageShardTable(msg.ShardMonth) + table, ok := groupMessageShardTable(id, msg.ShardMonth) if !ok || msg.Uid().IsZero() { return t.ErrMalformed } if msg.ShardMonth == time.Now().UTC().Format("200601") { - if _, err = a.db.ExecContext(ctx, "UPDATE im_group_message_shards SET status='writable' WHERE shard_month=? AND status='prepared'", msg.ShardMonth); err != nil { + if _, err = a.db.ExecContext(ctx, "UPDATE im_group_message_shards SET status='writable' WHERE tenant_id=? AND shard_month=? AND status='prepared'", id, msg.ShardMonth); err != nil { return err } } var shardStatus string - if err = a.db.QueryRowContext(ctx, "SELECT status FROM im_group_message_shards WHERE shard_month=? AND table_name=?", msg.ShardMonth, table).Scan(&shardStatus); err != nil { + if err = a.db.QueryRowContext(ctx, "SELECT status FROM im_group_message_shards WHERE tenant_id=? AND shard_month=? AND table_name=?", id, msg.ShardMonth, table).Scan(&shardStatus); err != nil { if err == sql.ErrNoRows { return fmt.Errorf("mysql: group message shard %s is not registered", msg.ShardMonth) } @@ -2134,8 +2198,8 @@ func (a *adapter) TenantGroupMessageSave(id t.TenantID, msg *t.Message) error { messageID := store.DecodeUid(msg.Uid()) if clientMsgID.Valid { if _, err = tx.ExecContext(ctx, `INSERT INTO im_group_message_dedup - (topic,from_user,client_msg_id,seqid,shard_month,message_id,status,expires_at) - VALUES(?,?,?,?,?,?,'done',?)`, msg.Topic, decodeUID(t.ParseUid(msg.From)), clientMsgID, + (tenant_id,topic,from_user,client_msg_id,seqid,shard_month,message_id,status,expires_at) + VALUES(?,?,?,?,?,?,?,'done',?)`, id, msg.Topic, decodeUID(t.ParseUid(msg.From)), clientMsgID, msg.SeqId, msg.ShardMonth, messageID, time.Now().UTC().Add(30*24*time.Hour)); err != nil { if isDupe(err) { return t.ErrDuplicate @@ -2144,11 +2208,11 @@ func (a *adapter) TenantGroupMessageSave(id t.TenantID, msg *t.Message) error { } } query := fmt.Sprintf(`INSERT INTO %s - (message_id,topic,group_id,seqid,from_user,client_msg_id,msg_type,status,content,head,mentioned_user_ids,mention_all, + (tenant_id,message_id,topic,group_id,seqid,from_user,client_msg_id,msg_type,status,content,head,mentioned_user_ids,mention_all, file_count,content_size,version,created_at,updated_at,content_nonce,key_version) - VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, table) + VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, table) _, err = tx.ExecContext(ctx, query, - messageID, msg.Topic, groupID, msg.SeqId, decodeUID(t.ParseUid(msg.From)), clientMsgID, messageHeadType(msg.Head), "normal", + id, messageID, msg.Topic, groupID, msg.SeqId, decodeUID(t.ParseUid(msg.From)), clientMsgID, messageHeadType(msg.Head), "normal", contentJSON, msg.Head, messageHeadJSON(msg.Head, "mentioned_user_ids"), messageHeadBool(msg.Head, "mention_all"), 0, len(contentJSON), messageVersion(msg.Version), msg.CreatedAt, msg.UpdatedAt, msg.ContentNonce, msg.KeyVersion) if isDupe(err) { @@ -2158,18 +2222,18 @@ func (a *adapter) TenantGroupMessageSave(id t.TenantID, msg *t.Message) error { return err } if _, err = tx.ExecContext(ctx, `INSERT INTO im_group_message_topic_shards - (topic,shard_month,min_seq,max_seq,message_count,min_created_at,max_created_at) - VALUES(?,?,?,?,1,?,?) ON DUPLICATE KEY UPDATE + (tenant_id,topic,shard_month,min_seq,max_seq,message_count,min_created_at,max_created_at) + VALUES(?,?,?,?,?,1,?,?) ON DUPLICATE KEY UPDATE min_seq=LEAST(min_seq,VALUES(min_seq)),max_seq=GREATEST(max_seq,VALUES(max_seq)), message_count=message_count+1,min_created_at=LEAST(min_created_at,VALUES(min_created_at)), max_created_at=GREATEST(max_created_at,VALUES(max_created_at))`, - msg.Topic, msg.ShardMonth, msg.SeqId, msg.SeqId, msg.CreatedAt, msg.CreatedAt); err != nil { + id, msg.Topic, msg.ShardMonth, msg.SeqId, msg.SeqId, msg.CreatedAt, msg.CreatedAt); err != nil { return err } if _, err = tx.ExecContext(ctx, `UPDATE im_group_message_shards SET row_count=row_count+1, min_created_at=IF(min_created_at IS NULL,?,LEAST(min_created_at,?)), - max_created_at=IF(max_created_at IS NULL,?,GREATEST(max_created_at,?)) WHERE shard_month=?`, - msg.CreatedAt, msg.CreatedAt, msg.CreatedAt, msg.CreatedAt, msg.ShardMonth); err != nil { + max_created_at=IF(max_created_at IS NULL,?,GREATEST(max_created_at,?)) WHERE tenant_id=? AND shard_month=?`, + msg.CreatedAt, msg.CreatedAt, msg.CreatedAt, msg.CreatedAt, id, msg.ShardMonth); err != nil { return err } return tx.Commit() @@ -2198,8 +2262,8 @@ func (a *adapter) TenantMessageGetByClientID(id t.TenantID, from t.Uid, clientMs }{} err := a.db.QueryRowxContext(ctx, "SELECT id,createdat,updatedat,deletedat,delid,seqid,topic,`from`,IFNULL(client_msg_id,'') AS client_msg_id,version,recalled_at,recalled_by,head,content,content_nonce,key_version "+ - "FROM im_messages WHERE `from`=? AND client_msg_id=?", - decodeUID(from), clientMsgID).StructScan(&row) + fmt.Sprintf("FROM %s WHERE tenant_id=? AND `from`=? AND client_msg_id=?", tenantMessagesTable(id)), + id, decodeUID(from), clientMsgID).StructScan(&row) if err == sql.ErrNoRows { return nil, nil } @@ -2227,7 +2291,7 @@ func (a *adapter) TenantGroupMessageGetByClientID(id t.TenantID, topic string, f } var seq int64 if err = a.db.QueryRowContext(ctx, `SELECT seqid FROM im_group_message_dedup - WHERE topic=? AND from_user=? AND client_msg_id=?`, topic, decodeUID(from), clientMsgID).Scan(&seq); err == nil { + WHERE tenant_id=? AND topic=? AND from_user=? AND client_msg_id=?`, id, topic, decodeUID(from), clientMsgID).Scan(&seq); err == nil { msg, getErr := tenantGroupMessageGetOne(a.db, ctx, id, topic, seq, "") if getErr == nil { return msg, nil @@ -2252,8 +2316,8 @@ func (a *adapter) TenantGroupMessageGetByClientID(id t.TenantID, topic string, f t.Message RecalledBy sql.NullInt64 `db:"recalled_by"` }{} - query := fmt.Sprintf("SELECT %s AS id,created_at AS createdat,updated_at AS updatedat,deleted_at AS deletedat,0 AS delid,seqid,topic,from_user AS `from`,IFNULL(client_msg_id,'') AS client_msg_id,version,recalled_at,recalled_by,head,content,content_nonce,key_version,'%s' AS shard_month FROM %s WHERE topic=? AND from_user=? AND client_msg_id=?", idColumn, month, table) - err = a.db.QueryRowxContext(ctx, query, topic, decodeUID(from), clientMsgID).StructScan(&row) + query := fmt.Sprintf("SELECT %s AS id,created_at AS createdat,updated_at AS updatedat,deleted_at AS deletedat,0 AS delid,seqid,topic,from_user AS `from`,IFNULL(client_msg_id,'') AS client_msg_id,version,recalled_at,recalled_by,head,content,content_nonce,key_version,'%s' AS shard_month FROM %s WHERE tenant_id=? AND topic=? AND from_user=? AND client_msg_id=?", idColumn, month, table) + err = a.db.QueryRowxContext(ctx, query, id, topic, decodeUID(from), clientMsgID).StructScan(&row) if err == nil { msg := normalizeGroupMessage(id, row.Message, row.RecalledBy) return &msg, nil @@ -2348,8 +2412,8 @@ func (a *adapter) TenantMessageRecall(id t.TenantID, topic string, seq int64, ac return nil, t.ErrDuplicate } _, err = tx.ExecContext(ctx, - "UPDATE im_messages SET updatedat=?,version=version+1,recalled_at=?,recalled_by=?,head=NULL,content=NULL WHERE topic=? AND seqid=? AND version=?", - recalledAt, recalledAt, decodeUID(actor), topic, seq, expectedVersion) + fmt.Sprintf("UPDATE %s SET updatedat=?,version=version+1,recalled_at=?,recalled_by=?,head=NULL,content=NULL WHERE tenant_id=? AND topic=? AND seqid=? AND version=?", tenantMessagesTable(id)), + recalledAt, recalledAt, decodeUID(actor), id, topic, seq, expectedVersion) if err != nil { return nil, err } @@ -2397,8 +2461,8 @@ func (a *adapter) TenantGroupMessageRecall(id t.TenantID, topic string, seq int6 return nil, t.ErrDuplicate } _, err = tx.ExecContext(ctx, fmt.Sprintf( - "UPDATE %s SET updated_at=?,version=version+1,status='recalled',recalled_at=?,recalled_by=?,head=NULL,content=NULL WHERE topic=? AND seqid=? AND version=?", messageTable), - recalledAt, recalledAt, decodeUID(actor), topic, seq, expectedVersion) + "UPDATE %s SET updated_at=?,version=version+1,status='recalled',recalled_at=?,recalled_by=?,head=NULL,content=NULL WHERE tenant_id=? AND topic=? AND seqid=? AND version=?", messageTable), + recalledAt, recalledAt, decodeUID(actor), id, topic, seq, expectedVersion) if err != nil { return nil, err } @@ -2448,14 +2512,14 @@ func (a *adapter) TenantMessageEdit(id t.TenantID, topic string, seq int64, acto return nil, t.ErrDuplicate } if _, err = tx.ExecContext(ctx, - "INSERT INTO im_message_revisions(createdat,updatedat,topic,seqid,version,actor,head,content,content_nonce,key_version,reason) VALUES(?,?,?,?,?,?,?,?,?,?,?)", - editedAt, editedAt, topic, seq, msg.Version, decodeUID(actor), msg.Head, common.ToJSON(msg.Content), msg.ContentNonce, msg.KeyVersion, strings.TrimSpace(reason)); err != nil { + "INSERT INTO im_message_revisions(tenant_id,createdat,updatedat,topic,seqid,version,actor,head,content,content_nonce,key_version,reason) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)", + id, editedAt, editedAt, topic, seq, msg.Version, decodeUID(actor), msg.Head, common.ToJSON(msg.Content), msg.ContentNonce, msg.KeyVersion, strings.TrimSpace(reason)); err != nil { return nil, err } storedContent, nonce, keyVersion := messageStoredContent(content) _, err = tx.ExecContext(ctx, - "UPDATE im_messages SET updatedat=?,version=version+1,head=?,content=?,content_nonce=?,key_version=? WHERE topic=? AND seqid=? AND version=?", - editedAt, head, common.ToJSON(storedContent), nonce, keyVersion, topic, seq, expectedVersion) + fmt.Sprintf("UPDATE %s SET updatedat=?,version=version+1,head=?,content=?,content_nonce=?,key_version=? WHERE tenant_id=? AND topic=? AND seqid=? AND version=?", tenantMessagesTable(id)), + editedAt, head, common.ToJSON(storedContent), nonce, keyVersion, id, topic, seq, expectedVersion) if err != nil { return nil, err } @@ -2503,15 +2567,15 @@ func (a *adapter) TenantGroupMessageEdit(id t.TenantID, topic string, seq int64, return nil, t.ErrDuplicate } if _, err = tx.ExecContext(ctx, - "INSERT INTO im_message_revisions(createdat,updatedat,topic,seqid,version,actor,head,content,content_nonce,key_version,reason) VALUES(?,?,?,?,?,?,?,?,?,?,?)", - editedAt, editedAt, topic, seq, msg.Version, decodeUID(actor), msg.Head, common.ToJSON(msg.Content), msg.ContentNonce, msg.KeyVersion, strings.TrimSpace(reason)); err != nil { + "INSERT INTO im_message_revisions(tenant_id,createdat,updatedat,topic,seqid,version,actor,head,content,content_nonce,key_version,reason) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)", + id, editedAt, editedAt, topic, seq, msg.Version, decodeUID(actor), msg.Head, common.ToJSON(msg.Content), msg.ContentNonce, msg.KeyVersion, strings.TrimSpace(reason)); err != nil { return nil, err } storedContent, nonce, keyVersion := messageStoredContent(content) contentJSON := common.ToJSON(storedContent) _, err = tx.ExecContext(ctx, fmt.Sprintf( - "UPDATE %s SET updated_at=?,version=version+1,msg_type=?,head=?,content=?,content_nonce=?,key_version=?,content_size=? WHERE topic=? AND seqid=? AND version=?", messageTable), - editedAt, messageHeadType(head), head, contentJSON, nonce, keyVersion, len(contentJSON), topic, seq, expectedVersion) + "UPDATE %s SET updated_at=?,version=version+1,msg_type=?,head=?,content=?,content_nonce=?,key_version=?,content_size=? WHERE tenant_id=? AND topic=? AND seqid=? AND version=?", messageTable), + editedAt, messageHeadType(head), head, contentJSON, nonce, keyVersion, len(contentJSON), id, topic, seq, expectedVersion) if err != nil { return nil, err } @@ -2530,8 +2594,7 @@ func (a *adapter) TenantMessageGetAll(id t.TenantID, topic string, uid t.Uid, op return nil, t.ErrMalformed } if tenantAdp != a { - msgs, err := tenantAdp.MessageGetAll(topic, uid, opts) - return stampTenantMessages(id, msgs), err + return tenantAdp.TenantMessageGetAll(id, topic, uid, opts) } if a.tenantScopedDB { msgs, err := a.MessageGetAll(topic, uid, opts) @@ -2570,7 +2633,7 @@ func (a *adapter) TenantMessageGetAll(id t.TenantID, topic string, uid t.Uid, op } rows, err := a.db.QueryxContext(ctx, "SELECT m.tenant_id,m.createdat,m.updatedat,m.deletedat,m.delid,m.seqid,m.topic,m.`from`,IFNULL(m.client_msg_id,'') AS client_msg_id,m.version,m.recalled_at,m.recalled_by,m.head,m.content,m.content_nonce,m.key_version "+ - "FROM im_messages AS m LEFT JOIN im_dellog AS d ON d.tenant_id=m.tenant_id AND d.topic=m.topic "+ + fmt.Sprintf("FROM %s AS m LEFT JOIN im_dellog AS d ON d.tenant_id=m.tenant_id AND d.topic=m.topic ", tenantMessagesTable(id))+ "AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=? WHERE m.tenant_id=? AND m.delid=0 AND m.topic=? "+ seq+" AND d.deletedfor IS NULL ORDER BY m.seqid DESC LIMIT ?", append([]any{decodeUID(uid), id, topic}, args[4:]...)...) @@ -2645,10 +2708,10 @@ func (a *adapter) TenantGroupMessageGetAll(id t.TenantID, topic string, uid t.Ui if month != "" { idColumn = "message_id" } - args := []any{store.DecodeUid(uid), topic} + args := []any{id, store.DecodeUid(uid), id, topic} args = append(args, seqArgs...) args = append(args, limit-len(msgs)) - query := fmt.Sprintf("SELECT m.%s AS id,m.created_at AS createdat,m.updated_at AS updatedat,m.deleted_at AS deletedat,0 AS delid,m.seqid,m.topic,m.from_user AS `from`,IFNULL(m.client_msg_id,'') AS client_msg_id,m.version,m.recalled_at,m.recalled_by,m.head,m.content,m.content_nonce,m.key_version,'%s' AS shard_month FROM %s AS m LEFT JOIN im_dellog AS d ON d.topic COLLATE utf8mb4_unicode_ci=m.topic AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=? WHERE m.status!='deleted' AND m.topic=? %s AND d.deletedfor IS NULL ORDER BY m.seqid DESC LIMIT ?", idColumn, month, table, seqClause) + query := fmt.Sprintf("SELECT m.%s AS id,m.created_at AS createdat,m.updated_at AS updatedat,m.deleted_at AS deletedat,0 AS delid,m.seqid,m.topic,m.from_user AS `from`,IFNULL(m.client_msg_id,'') AS client_msg_id,m.version,m.recalled_at,m.recalled_by,m.head,m.content,m.content_nonce,m.key_version,'%s' AS shard_month FROM %s AS m LEFT JOIN im_dellog AS d ON d.tenant_id=? AND d.topic COLLATE utf8mb4_unicode_ci=m.topic AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=? WHERE m.tenant_id=? AND m.status!='deleted' AND m.topic=? %s AND d.deletedfor IS NULL ORDER BY m.seqid DESC LIMIT ?", idColumn, month, table, seqClause) rows, queryErr := a.db.QueryxContext(ctx, query, args...) if queryErr != nil { return nil, queryErr @@ -2700,8 +2763,8 @@ func (a *adapter) TenantMessageSearch(id t.TenantID, topic string, uid t.Uid, qu if t.GetTopicCat(topic) == t.TopicCatGrp { return a.tenantGroupMessageSearch(id, topic, uid, queryText, messageType, cursor, limit) } - where := []string{"m.topic=?", "m.delid=0"} - args := []any{topic} + where := []string{"m.tenant_id=?", "m.topic=?", "m.delid=0"} + args := []any{id, topic} if cursor != "" { parsed, err := strconv.Atoi(cursor) if err != nil || parsed <= 0 { @@ -2720,11 +2783,11 @@ func (a *adapter) TenantMessageSearch(id t.TenantID, topic string, uid t.Uid, qu defer cancel() } scanLimit := messageSearchScanLimit(limit, queryText) - queryArgs := append([]any{decodeUID(uid)}, args...) + queryArgs := append([]any{id, decodeUID(uid)}, args...) queryArgs = append(queryArgs, scanLimit) rows, err := a.db.QueryxContext(ctx, "SELECT m.createdat,m.updatedat,m.deletedat,m.delid,m.seqid,m.topic,m.`from`,IFNULL(m.client_msg_id,'') AS client_msg_id,m.version,m.recalled_at,m.recalled_by,m.head,m.content,m.content_nonce,m.key_version "+ - "FROM im_messages AS m LEFT JOIN im_dellog AS d ON d.topic=m.topic "+ + fmt.Sprintf("FROM %s AS m LEFT JOIN im_dellog AS d ON d.tenant_id=? AND d.topic=m.topic ", tenantMessagesTable(id))+ "AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=? WHERE "+strings.Join(where, " AND ")+" AND d.deletedfor IS NULL ORDER BY m.seqid DESC LIMIT ?", queryArgs...) if err != nil { @@ -2766,8 +2829,8 @@ func (a *adapter) TenantMessageSearch(id t.TenantID, topic string, uid t.Uid, qu } func (a *adapter) tenantGroupMessageSearch(id t.TenantID, topic string, uid t.Uid, queryText, messageType, cursor string, limit int) ([]t.Message, string, error) { - where := []string{"m.status!='deleted'", "m.topic=?"} - args := []any{topic} + where := []string{"m.tenant_id=?", "m.status!='deleted'", "m.topic=?"} + args := []any{id, topic} if cursor != "" { parsed, err := strconv.ParseInt(cursor, 10, 64) if err != nil || parsed <= 0 { @@ -2802,9 +2865,9 @@ func (a *adapter) tenantGroupMessageSearch(id t.TenantID, topic string, uid t.Ui if month != "" { idColumn = "message_id" } - queryArgs := append([]any{store.DecodeUid(uid)}, args...) + queryArgs := append([]any{id, store.DecodeUid(uid)}, args...) queryArgs = append(queryArgs, scanLimit-len(seen)) - query := fmt.Sprintf("SELECT m.%s AS id,m.created_at AS createdat,m.updated_at AS updatedat,m.deleted_at AS deletedat,0 AS delid,m.seqid,m.topic,m.from_user AS `from`,IFNULL(m.client_msg_id,'') AS client_msg_id,m.version,m.recalled_at,m.recalled_by,m.head,m.content,m.content_nonce,m.key_version,'%s' AS shard_month FROM %s AS m LEFT JOIN im_dellog AS d ON d.topic COLLATE utf8mb4_unicode_ci=m.topic AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=? WHERE %s AND d.deletedfor IS NULL ORDER BY m.seqid DESC LIMIT ?", idColumn, month, table, strings.Join(where, " AND ")) + query := fmt.Sprintf("SELECT m.%s AS id,m.created_at AS createdat,m.updated_at AS updatedat,m.deleted_at AS deletedat,0 AS delid,m.seqid,m.topic,m.from_user AS `from`,IFNULL(m.client_msg_id,'') AS client_msg_id,m.version,m.recalled_at,m.recalled_by,m.head,m.content,m.content_nonce,m.key_version,'%s' AS shard_month FROM %s AS m LEFT JOIN im_dellog AS d ON d.tenant_id=? AND d.topic COLLATE utf8mb4_unicode_ci=m.topic AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=? WHERE %s AND d.deletedfor IS NULL ORDER BY m.seqid DESC LIMIT ?", idColumn, month, table, strings.Join(where, " AND ")) rows, queryErr := a.db.QueryxContext(ctx, query, queryArgs...) if queryErr != nil { return nil, "", queryErr @@ -2997,12 +3060,12 @@ func (a *adapter) TenantMessageMutations(id t.TenantID, topic string, uid t.Uid, func (a *adapter) tenantP2PMessageMutations(ctx context.Context, id t.TenantID, topic string, uid t.Uid, limit int, currentVersion int64) ([]t.MessageMutation, string, bool, int64, error) { rows, err := a.db.QueryxContext(ctx, "SELECT m.seqid,m.version,m.updatedat AS mutated_at,m.recalled_at,m.recalled_by,rev.actor "+ - "FROM im_messages AS m "+ - "LEFT JOIN im_dellog AS d ON d.topic=m.topic AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=? "+ - "LEFT JOIN im_message_revisions AS rev ON rev.topic=m.topic AND rev.seqid=m.seqid AND rev.version=m.version-1 "+ - "WHERE m.topic=? AND d.deletedfor IS NULL AND (m.version>1 OR m.recalled_at IS NOT NULL OR m.deletedat IS NOT NULL) "+ + fmt.Sprintf("FROM %s AS m ", tenantMessagesTable(id))+ + "LEFT JOIN im_dellog AS d ON d.tenant_id=m.tenant_id AND d.topic=m.topic AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=? "+ + "LEFT JOIN im_message_revisions AS rev ON rev.tenant_id=m.tenant_id AND rev.topic=m.topic AND rev.seqid=m.seqid AND rev.version=m.version-1 "+ + "WHERE m.tenant_id=? AND m.topic=? AND d.deletedfor IS NULL AND (m.version>1 OR m.recalled_at IS NOT NULL OR m.deletedat IS NOT NULL) "+ "ORDER BY m.updatedat DESC,m.seqid DESC LIMIT ?", - decodeUID(uid), topic, limit+1) + decodeUID(uid), id, topic, limit+1) if err != nil { return nil, "", false, 0, err } @@ -3011,14 +3074,22 @@ func (a *adapter) tenantP2PMessageMutations(ctx context.Context, id t.TenantID, } func (a *adapter) tenantGroupMessageMutations(ctx context.Context, id t.TenantID, topic string, uid t.Uid, limit int, currentVersion int64) ([]t.MessageMutation, string, bool, int64, error) { - rows, err := a.db.QueryxContext(ctx, - "SELECT m.seqid,m.version,m.updated_at AS mutated_at,m.recalled_at,m.recalled_by,rev.actor "+ - "FROM im_group_messages AS m "+ - "LEFT JOIN im_dellog AS d ON d.topic COLLATE utf8mb4_unicode_ci=m.topic AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=? "+ - "LEFT JOIN im_message_revisions AS rev ON rev.topic=m.topic AND rev.seqid=m.seqid AND rev.version=m.version-1 "+ - "WHERE m.topic=? AND d.deletedfor IS NULL AND (m.version>1 OR m.recalled_at IS NOT NULL OR m.deleted_at IS NOT NULL OR m.status='recalled') "+ - "ORDER BY m.updated_at DESC,m.seqid DESC LIMIT ?", - decodeUID(uid), topic, limit+1) + tables, err := groupMessageTablesForTopic(a.db, ctx, id, topic) + if err != nil || len(tables) == 0 { + return nil, "", false, 0, err + } + parts := make([]string, 0, len(tables)) + args := make([]any, 0, len(tables)*3+1) + for _, table := range tables { + parts = append(parts, "SELECT m.seqid,m.version,m.updated_at AS mutated_at,m.recalled_at,m.recalled_by,rev.actor "+ + fmt.Sprintf("FROM %s AS m ", table)+ + "LEFT JOIN im_dellog AS d ON d.tenant_id=m.tenant_id AND d.topic COLLATE utf8mb4_unicode_ci=m.topic AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=? "+ + "LEFT JOIN im_message_revisions AS rev ON rev.tenant_id=m.tenant_id AND rev.topic=m.topic AND rev.seqid=m.seqid AND rev.version=m.version-1 "+ + "WHERE m.tenant_id=? AND m.topic=? AND d.deletedfor IS NULL AND (m.version>1 OR m.recalled_at IS NOT NULL OR m.deleted_at IS NOT NULL OR m.status='recalled')") + args = append(args, decodeUID(uid), id, topic) + } + args = append(args, limit+1) + rows, err := a.db.QueryxContext(ctx, "SELECT * FROM ("+strings.Join(parts, " UNION ALL ")+") AS mutations ORDER BY mutated_at DESC,seqid DESC LIMIT ?", args...) if err != nil { return nil, "", false, 0, err } @@ -3296,25 +3367,25 @@ func (a *adapter) TenantUserProfileGet(id t.TenantID, uid t.Uid) (*t.UserProfile if cancel != nil { defer cancel() } - profile, err := a.userProfileByUID(ctx, uid) + profile, err := a.userProfileByUID(ctx, id, uid) if err == nil { return profile, nil } if err != sql.ErrNoRows { return nil, err } - if err := a.ensureUserProfile(ctx, uid); err != nil { + if err := a.ensureUserProfile(ctx, id, uid); err != nil { return nil, err } - return a.userProfileByUID(ctx, uid) + return a.userProfileByUID(ctx, id, uid) } -func (a *adapter) ensureUserProfile(ctx context.Context, uid t.Uid) error { +func (a *adapter) ensureUserProfile(ctx context.Context, tenantID t.TenantID, uid t.Uid) error { profileID, err := nextSnowflakeID() if err != nil { return err } - if _, err := a.db.ExecContext(ctx, `INSERT IGNORE INTO im_user_profiles(id,user_id,public_account,friend_add_enabled,friend_add_policy_version) VALUES(?,?,?,?,?)`, profileID, decodeUID(uid), uid.UserId(), true, 1); err != nil { + if _, err := a.db.ExecContext(ctx, `INSERT IGNORE INTO im_user_profiles(tenant_id,id,user_id,public_account,friend_add_enabled,friend_add_policy_version) VALUES(?,?,?,?,?,?)`, tenantID, profileID, decodeUID(uid), uid.UserId(), true, 1); err != nil { return err } return nil @@ -3337,7 +3408,7 @@ func (a *adapter) TenantUserProfileUpdate(id t.TenantID, uid t.Uid, patch t.User if cancel != nil { defer cancel() } - if err := a.ensureUserProfile(ctx, uid); err != nil { + if err := a.ensureUserProfile(ctx, id, uid); err != nil { return nil, err } sets := []string{"version=version+1", "updated_at=CURRENT_TIMESTAMP(3)"} @@ -3375,8 +3446,8 @@ func (a *adapter) TenantUserProfileUpdate(id t.TenantID, uid t.Uid, patch t.User sets = append(sets, "email=?") args = append(args, strings.TrimSpace(*patch.Email)) } - args = append(args, decodeUID(uid), patch.Version) - res, err := a.db.ExecContext(ctx, `UPDATE im_user_profiles SET `+strings.Join(sets, ",")+` WHERE user_id=? AND version=?`, args...) + args = append(args, id, decodeUID(uid), patch.Version) + res, err := a.db.ExecContext(ctx, `UPDATE im_user_profiles SET `+strings.Join(sets, ",")+` WHERE tenant_id=? AND user_id=? AND version=?`, args...) if err != nil { return nil, err } @@ -3384,7 +3455,7 @@ func (a *adapter) TenantUserProfileUpdate(id t.TenantID, uid t.Uid, patch t.User if affected == 0 { return nil, t.ErrInvalidResponse } - return a.userProfileByUID(ctx, uid) + return a.userProfileByUID(ctx, id, uid) } func (a *adapter) TenantUserPrivacyGet(id t.TenantID, uid t.Uid) (*t.UserPrivacySettings, error) { @@ -3404,25 +3475,25 @@ func (a *adapter) TenantUserPrivacyGet(id t.TenantID, uid t.Uid) (*t.UserPrivacy if cancel != nil { defer cancel() } - privacy, err := a.userPrivacyByUID(ctx, uid) + privacy, err := a.userPrivacyByUID(ctx, id, uid) if err == nil { return privacy, nil } if err != sql.ErrNoRows { return nil, err } - if err := a.ensureUserPrivacy(ctx, uid); err != nil { + if err := a.ensureUserPrivacy(ctx, id, uid); err != nil { return nil, err } - return a.userPrivacyByUID(ctx, uid) + return a.userPrivacyByUID(ctx, id, uid) } -func (a *adapter) ensureUserPrivacy(ctx context.Context, uid t.Uid) error { +func (a *adapter) ensureUserPrivacy(ctx context.Context, tenantID t.TenantID, uid t.Uid) error { privacyID, err := nextSnowflakeID() if err != nil { return err } - if _, err := a.db.ExecContext(ctx, `INSERT IGNORE INTO im_user_privacy_settings(id,user_id,add_me_methods) VALUES(?,?,'{"account":true,"phone":true,"qr":true,"group":true}')`, privacyID, decodeUID(uid)); err != nil { + if _, err := a.db.ExecContext(ctx, `INSERT IGNORE INTO im_user_privacy_settings(tenant_id,id,user_id,add_me_methods) VALUES(?,?,?,'{"account":true,"phone":true,"qr":true,"group":true}')`, tenantID, privacyID, decodeUID(uid)); err != nil { return err } return nil @@ -3445,7 +3516,7 @@ func (a *adapter) TenantUserPrivacyUpdate(id t.TenantID, uid t.Uid, patch t.User if cancel != nil { defer cancel() } - if err := a.ensureUserPrivacy(ctx, uid); err != nil { + if err := a.ensureUserPrivacy(ctx, id, uid); err != nil { return nil, err } sets := []string{"version=version+1", "updated_at=CURRENT_TIMESTAMP(3)"} @@ -3478,8 +3549,8 @@ func (a *adapter) TenantUserPrivacyUpdate(id t.TenantID, uid t.Uid, patch t.User sets = append(sets, "group_invite_verification=?") args = append(args, *patch.GroupInviteVerification) } - args = append(args, decodeUID(uid), patch.Version) - res, err := a.db.ExecContext(ctx, `UPDATE im_user_privacy_settings SET `+strings.Join(sets, ",")+` WHERE user_id=? AND version=?`, args...) + args = append(args, id, decodeUID(uid), patch.Version) + res, err := a.db.ExecContext(ctx, `UPDATE im_user_privacy_settings SET `+strings.Join(sets, ",")+` WHERE tenant_id=? AND user_id=? AND version=?`, args...) if err != nil { return nil, err } @@ -3487,7 +3558,7 @@ func (a *adapter) TenantUserPrivacyUpdate(id t.TenantID, uid t.Uid, patch t.User if affected == 0 { return nil, t.ErrInvalidResponse } - return a.userPrivacyByUID(ctx, uid) + return a.userPrivacyByUID(ctx, id, uid) } func (a *adapter) TenantDevicePushTokenUpsert(id t.TenantID, token *t.DevicePushToken) (*t.DevicePushToken, error) { @@ -3522,12 +3593,12 @@ func (a *adapter) TenantDevicePushTokenUpsert(id t.TenantID, token *t.DevicePush return nil, err } _, err = a.db.ExecContext(ctx, - `INSERT INTO im_device_push_tokens(id,device_id,user_id,provider,token,app_version,platform,sdk_version,push_env,notification_permission,created_at,updated_at) - VALUES(?,?,?,?,?,?,?,?,?,?,?,?) + `INSERT INTO im_device_push_tokens(tenant_id,id,device_id,user_id,provider,token,app_version,platform,sdk_version,push_env,notification_permission,created_at,updated_at) + VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE device_id=VALUES(device_id),user_id=VALUES(user_id),provider=VALUES(provider),token=VALUES(token), app_version=VALUES(app_version),platform=VALUES(platform),sdk_version=VALUES(sdk_version),push_env=VALUES(push_env), notification_permission=VALUES(notification_permission),updated_at=VALUES(updated_at)`, - tokenID, token.DeviceID, decodeUID(token.UserID), token.Provider, token.Token, token.AppVersion, token.Platform, + id, tokenID, token.DeviceID, decodeUID(token.UserID), token.Provider, token.Token, token.AppVersion, token.Platform, token.SDKVersion, token.PushEnv, token.NotificationPermission, now, now) if err != nil { return nil, err @@ -3578,7 +3649,7 @@ func (a *adapter) tenantDevicePushTokenGetAllByProviders(id t.TenantID, provider return map[t.Uid][]t.DeviceDef{}, 0, nil } query, args, err := sqlx.In(`SELECT user_id,token,platform,updated_at FROM im_device_push_tokens - WHERE provider IN (?) AND user_id IN (?)`, normalizedProviders, decoded) + WHERE tenant_id=? AND provider IN (?) AND user_id IN (?)`, id, normalizedProviders, decoded) if err != nil { return nil, 0, err } @@ -3638,15 +3709,15 @@ func (a *adapter) TenantRiskChallengeCreate(id t.TenantID, challenge *t.RiskChal challenge.Level = strings.ToLower(strings.TrimSpace(challenge.Level)) challenge.NoticeCode = strings.TrimSpace(challenge.NoticeCode) _, err = a.db.ExecContext(ctx, - `INSERT INTO im_risk_challenges(challenge_id,user_id,action,level,notice_code,notice_params,state,expires_at,created_at,updated_at) - VALUES(?,?,?,?,?,?,?,?,?,?) + `INSERT INTO im_risk_challenges(tenant_id,challenge_id,user_id,action,level,notice_code,notice_params,state,expires_at,created_at,updated_at) + VALUES(?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE updated_at=updated_at`, - strings.TrimSpace(challenge.ChallengeID), decodeUID(challenge.UserID), strings.TrimSpace(challenge.Action), + id, strings.TrimSpace(challenge.ChallengeID), decodeUID(challenge.UserID), strings.TrimSpace(challenge.Action), challenge.Level, challenge.NoticeCode, string(params), challenge.State, challenge.ExpiresAt, now, now) if err != nil { return nil, err } - return a.riskChallengeByID(ctx, challenge.ChallengeID) + return a.riskChallengeByID(ctx, id, challenge.ChallengeID) } func (a *adapter) TenantRiskChallengeConfirm(id t.TenantID, uid t.Uid, challengeID, decision, actionTokenID string, now time.Time) (*t.RiskChallenge, error) { @@ -3680,7 +3751,7 @@ func (a *adapter) TenantRiskChallengeConfirm(id t.TenantID, uid t.Uid, challenge _ = tx.Rollback() } }() - challenge, err := a.riskChallengeByIDTx(ctx, tx, challengeID, true) + challenge, err := a.riskChallengeByIDTx(ctx, tx, id, challengeID, true) if err != nil { return nil, err } @@ -3693,8 +3764,8 @@ func (a *adapter) TenantRiskChallengeConfirm(id t.TenantID, uid t.Uid, challenge } _, err = tx.ExecContext(ctx, `UPDATE im_risk_challenges SET state=?,decision=?,action_token_id=?,updated_at=? - WHERE challenge_id=? AND state=?`, - nextState, decision, strings.TrimSpace(actionTokenID), now, challengeID, t.RiskChallengeStatePending) + WHERE tenant_id=? AND challenge_id=? AND state=?`, + nextState, decision, strings.TrimSpace(actionTokenID), now, id, challengeID, t.RiskChallengeStatePending) if err != nil { return nil, err } @@ -3702,7 +3773,7 @@ func (a *adapter) TenantRiskChallengeConfirm(id t.TenantID, uid t.Uid, challenge return nil, err } tx = nil - return a.riskChallengeByID(ctx, challengeID) + return a.riskChallengeByID(ctx, id, challengeID) } func (a *adapter) TenantAbuseReasons(id t.TenantID, targetType string) ([]t.AbuseReason, error) { @@ -3759,16 +3830,16 @@ func (a *adapter) TenantAbuseReportCreate(id t.TenantID, report *t.AbuseReport, now := t.TimeNow() report.Status = t.AbuseReportStatusReceived _, err = a.db.ExecContext(ctx, - `INSERT INTO im_abuse_reports(report_id,reporter_id,target_type,target_user_id,target_topic,target_seq,reason_code,description,evidence_file_ids,snapshot,idempotency_key,status,created_at,updated_at) - VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?) + `INSERT INTO im_abuse_reports(tenant_id,report_id,reporter_id,target_type,target_user_id,target_topic,target_seq,reason_code,description,evidence_file_ids,snapshot,idempotency_key,status,created_at,updated_at) + VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE updated_at=updated_at`, - report.ReportID, decodeUID(report.ReporterID), report.TargetType, decodeUID(report.TargetUserID), report.TargetTopic, + id, report.ReportID, decodeUID(report.ReporterID), report.TargetType, decodeUID(report.TargetUserID), report.TargetTopic, report.TargetSeq, report.ReasonCode, report.Description, string(evidence), string(snapshot), strings.TrimSpace(idempotencyKey), report.Status, now, now) if err != nil { return nil, err } - return a.abuseReportByIdempotency(ctx, report.ReporterID, idempotencyKey) + return a.abuseReportByIdempotency(ctx, id, report.ReporterID, idempotencyKey) } func (a *adapter) TenantFeedbackConfigGet(id t.TenantID, uid t.Uid) (*t.FeedbackConfig, error) { @@ -3821,23 +3892,23 @@ func (a *adapter) TenantFeedbackCreate(id t.TenantID, feedback *t.FeedbackSubmis now := t.TimeNow() feedback.Status = t.FeedbackStatusReceived _, err = a.db.ExecContext(ctx, - `INSERT INTO im_feedback_submissions(feedback_id,user_id,category,content,contact,attachment_file_ids,client_context,include_diagnostics,idempotency_key,status,created_at,updated_at) - VALUES(?,?,?,?,?,?,?,?,?,?,?,?) + `INSERT INTO im_feedback_submissions(tenant_id,feedback_id,user_id,category,content,contact,attachment_file_ids,client_context,include_diagnostics,idempotency_key,status,created_at,updated_at) + VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE updated_at=updated_at`, - feedback.FeedbackID, decodeUID(feedback.UserID), feedback.Category, feedback.Content, feedback.Contact, + id, feedback.FeedbackID, decodeUID(feedback.UserID), feedback.Category, feedback.Content, feedback.Contact, string(attachments), string(clientContext), feedback.IncludeDiagnostics, strings.TrimSpace(idempotencyKey), feedback.Status, now, now) if err != nil { return nil, err } - return a.feedbackByIdempotency(ctx, feedback.UserID, idempotencyKey) + return a.feedbackByIdempotency(ctx, id, feedback.UserID, idempotencyKey) } -func (a *adapter) riskChallengeByID(ctx context.Context, challengeID string) (*t.RiskChallenge, error) { - return a.riskChallengeByIDTx(ctx, a.db, challengeID, false) +func (a *adapter) riskChallengeByID(ctx context.Context, tenantID t.TenantID, challengeID string) (*t.RiskChallenge, error) { + return a.riskChallengeByIDTx(ctx, a.db, tenantID, challengeID, false) } -func (a *adapter) riskChallengeByIDTx(ctx context.Context, q sqlx.QueryerContext, challengeID string, forUpdate bool) (*t.RiskChallenge, error) { +func (a *adapter) riskChallengeByIDTx(ctx context.Context, q sqlx.QueryerContext, tenantID t.TenantID, challengeID string, forUpdate bool) (*t.RiskChallenge, error) { row := struct { ID int64 `db:"id"` ChallengeID string `db:"challenge_id"` @@ -3854,11 +3925,11 @@ func (a *adapter) riskChallengeByIDTx(ctx context.Context, q sqlx.QueryerContext UpdatedAt time.Time `db:"updated_at"` }{} query := `SELECT id,challenge_id,user_id,action,level,notice_code,notice_params,state,expires_at,decision,action_token_id,created_at,updated_at - FROM im_risk_challenges WHERE challenge_id=?` + FROM im_risk_challenges WHERE tenant_id=? AND challenge_id=?` if forUpdate { query += " FOR UPDATE" } - if err := sqlx.GetContext(ctx, q, &row, query, challengeID); err != nil { + if err := sqlx.GetContext(ctx, q, &row, query, tenantID, challengeID); err != nil { if err == sql.ErrNoRows { return nil, t.ErrNotFound } @@ -3883,7 +3954,7 @@ func (a *adapter) riskChallengeByIDTx(ctx context.Context, q sqlx.QueryerContext return challenge, nil } -func (a *adapter) abuseReportByIdempotency(ctx context.Context, uid t.Uid, idempotencyKey string) (*t.AbuseReport, error) { +func (a *adapter) abuseReportByIdempotency(ctx context.Context, tenantID t.TenantID, uid t.Uid, idempotencyKey string) (*t.AbuseReport, error) { row := struct { ID int64 `db:"id"` ReportID string `db:"report_id"` @@ -3902,8 +3973,8 @@ func (a *adapter) abuseReportByIdempotency(ctx context.Context, uid t.Uid, idemp }{} if err := sqlx.GetContext(ctx, a.db, &row, `SELECT id,report_id,reporter_id,target_type,target_user_id,target_topic,target_seq,reason_code,description,evidence_file_ids,snapshot,status,created_at,updated_at - FROM im_abuse_reports WHERE reporter_id=? AND idempotency_key=?`, - decodeUID(uid), strings.TrimSpace(idempotencyKey)); err != nil { + FROM im_abuse_reports WHERE tenant_id=? AND reporter_id=? AND idempotency_key=?`, + tenantID, decodeUID(uid), strings.TrimSpace(idempotencyKey)); err != nil { return nil, err } report := &t.AbuseReport{ @@ -3965,7 +4036,7 @@ func (a *adapter) feedbackConfig(ctx context.Context) (*t.FeedbackConfig, error) return config, nil } -func (a *adapter) feedbackByIdempotency(ctx context.Context, uid t.Uid, idempotencyKey string) (*t.FeedbackSubmission, error) { +func (a *adapter) feedbackByIdempotency(ctx context.Context, tenantID t.TenantID, uid t.Uid, idempotencyKey string) (*t.FeedbackSubmission, error) { row := struct { ID int64 `db:"id"` FeedbackID string `db:"feedback_id"` @@ -3982,8 +4053,8 @@ func (a *adapter) feedbackByIdempotency(ctx context.Context, uid t.Uid, idempote }{} if err := sqlx.GetContext(ctx, a.db, &row, `SELECT id,feedback_id,user_id,category,content,contact,attachment_file_ids,client_context,include_diagnostics,status,created_at,updated_at - FROM im_feedback_submissions WHERE user_id=? AND idempotency_key=?`, - decodeUID(uid), strings.TrimSpace(idempotencyKey)); err != nil { + FROM im_feedback_submissions WHERE tenant_id=? AND user_id=? AND idempotency_key=?`, + tenantID, decodeUID(uid), strings.TrimSpace(idempotencyKey)); err != nil { return nil, err } feedback := &t.FeedbackSubmission{ @@ -4005,7 +4076,7 @@ func (a *adapter) feedbackByIdempotency(ctx context.Context, uid t.Uid, idempote return feedback, nil } -func (a *adapter) userProfileByUID(ctx context.Context, uid t.Uid) (*t.UserProfile, error) { +func (a *adapter) userProfileByUID(ctx context.Context, tenantID t.TenantID, uid t.Uid) (*t.UserProfile, error) { row := struct { UserID int64 `db:"user_id"` AvatarFileID string `db:"avatar_file_id"` @@ -4024,7 +4095,7 @@ func (a *adapter) userProfileByUID(ctx context.Context, uid t.Uid) (*t.UserProfi UpdatedAt time.Time `db:"updated_at"` }{} if err := sqlx.GetContext(ctx, a.db, &row, `SELECT user_id,avatar_file_id,nickname,birth_date,signature,gender,region,email, - public_account,friend_add_enabled,friend_add_disabled_reason,friend_add_policy_version,version,created_at,updated_at FROM im_user_profiles WHERE user_id=?`, decodeUID(uid)); err != nil { + public_account,friend_add_enabled,friend_add_disabled_reason,friend_add_policy_version,version,created_at,updated_at FROM im_user_profiles WHERE tenant_id=? AND user_id=?`, tenantID, decodeUID(uid)); err != nil { return nil, err } profile := &t.UserProfile{ @@ -4049,7 +4120,7 @@ func (a *adapter) userProfileByUID(ctx context.Context, uid t.Uid) (*t.UserProfi return profile, nil } -func (a *adapter) userPrivacyByUID(ctx context.Context, uid t.Uid) (*t.UserPrivacySettings, error) { +func (a *adapter) userPrivacyByUID(ctx context.Context, tenantID t.TenantID, uid t.Uid) (*t.UserPrivacySettings, error) { row := struct { UserID int64 `db:"user_id"` AddMeMethods []byte `db:"add_me_methods"` @@ -4063,7 +4134,7 @@ func (a *adapter) userPrivacyByUID(ctx context.Context, uid t.Uid) (*t.UserPriva UpdatedAt time.Time `db:"updated_at"` }{} if err := sqlx.GetContext(ctx, a.db, &row, `SELECT user_id,add_me_methods,profile_visibility,last_seen_visibility, - read_receipts,friend_request_verification,group_invite_verification,version,created_at,updated_at FROM im_user_privacy_settings WHERE user_id=?`, decodeUID(uid)); err != nil { + read_receipts,friend_request_verification,group_invite_verification,version,created_at,updated_at FROM im_user_privacy_settings WHERE tenant_id=? AND user_id=?`, tenantID, decodeUID(uid)); err != nil { return nil, err } settings := &t.UserPrivacySettings{ @@ -4089,8 +4160,8 @@ func tenantMessageGetOne(q sqlx.QueryerContext, ctx context.Context, tenantID t. }{} err := sqlx.GetContext(ctx, q, &row, "SELECT id,createdat,updatedat,deletedat,delid,seqid,topic,`from`,IFNULL(client_msg_id,'') AS client_msg_id,version,recalled_at,recalled_by,head,content,content_nonce,key_version "+ - "FROM im_messages WHERE topic=? AND seqid=?"+suffix, - topic, seq) + fmt.Sprintf("FROM %s WHERE tenant_id=? AND topic=? AND seqid=?", tenantMessagesTable(tenantID))+suffix, + tenantID, topic, seq) if err != nil { return nil, err } @@ -4104,9 +4175,9 @@ func tenantMessageGetOne(q sqlx.QueryerContext, ctx context.Context, tenantID t. return &msg, nil } -func (a *adapter) groupIDByTopic(ctx context.Context, topic string) (int64, error) { +func (a *adapter) groupIDByTopic(ctx context.Context, tenantID t.TenantID, topic string) (int64, error) { var groupID int64 - err := a.db.QueryRowContext(ctx, "SELECT id FROM im_group_settings WHERE topic=? LIMIT 1", topic).Scan(&groupID) + err := a.db.QueryRowContext(ctx, "SELECT id FROM im_group_settings WHERE tenant_id=? AND topic=? LIMIT 1", tenantID, topic).Scan(&groupID) if err == sql.ErrNoRows { return 0, t.ErrNotFound } @@ -4133,8 +4204,8 @@ func tenantGroupMessageGetOneWithTable(q sqlx.QueryerContext, ctx context.Contex t.Message RecalledBy sql.NullInt64 `db:"recalled_by"` }{} - query := fmt.Sprintf("SELECT %s AS id,created_at AS createdat,updated_at AS updatedat,deleted_at AS deletedat,0 AS delid,seqid,topic,from_user AS `from`,IFNULL(client_msg_id,'') AS client_msg_id,version,recalled_at,recalled_by,head,content,content_nonce,key_version,'%s' AS shard_month FROM %s WHERE topic=? AND seqid=?%s", idColumn, month, table, suffix) - err = sqlx.GetContext(ctx, q, &row, query, topic, seq) + query := fmt.Sprintf("SELECT %s AS id,created_at AS createdat,updated_at AS updatedat,deleted_at AS deletedat,0 AS delid,seqid,topic,from_user AS `from`,IFNULL(client_msg_id,'') AS client_msg_id,version,recalled_at,recalled_by,head,content,content_nonce,key_version,'%s' AS shard_month FROM %s WHERE tenant_id=? AND topic=? AND seqid=?%s", idColumn, month, table, suffix) + err = sqlx.GetContext(ctx, q, &row, query, tenantID, topic, seq) if err == nil { msg := normalizeGroupMessage(tenantID, row.Message, row.RecalledBy) return &msg, table, nil @@ -4188,7 +4259,7 @@ func (a *adapter) TenantConversationSettingsGet(id t.TenantID, uid t.Uid, topic if cancel != nil { defer cancel() } - settings, err := a.conversationSettingsByKey(ctx, uid, strings.TrimSpace(topic)) + settings, err := a.conversationSettingsByKey(ctx, id, uid, strings.TrimSpace(topic)) if err == sql.ErrNoRows { settingsID, idErr := nextSnowflakeID() if idErr != nil { @@ -4196,13 +4267,13 @@ func (a *adapter) TenantConversationSettingsGet(id t.TenantID, uid t.Uid, topic } now := t.TimeNow() _, err = a.db.ExecContext(ctx, `INSERT IGNORE INTO im_conversation_settings - (id,user_id,topic,pinned,muted,remark,message_retention,version,created_at,updated_at) - VALUES(?,?,?,?,?,?,?,?,?,?)`, - settingsID, decodeUID(uid), strings.TrimSpace(topic), false, false, "", "default", 1, now, now) + (tenant_id,id,user_id,topic,pinned,muted,remark,message_retention,version,created_at,updated_at) + VALUES(?,?,?,?,?,?,?,?,?,?,?)`, + id, settingsID, decodeUID(uid), strings.TrimSpace(topic), false, false, "", "default", 1, now, now) if err != nil { return nil, err } - return a.conversationSettingsByKey(ctx, uid, strings.TrimSpace(topic)) + return a.conversationSettingsByKey(ctx, id, uid, strings.TrimSpace(topic)) } if err != nil { return nil, err @@ -4248,13 +4319,13 @@ func (a *adapter) TenantConversationSettingsUpdate(id t.TenantID, uid t.Uid, top return a.TenantConversationSettingsGet(id, uid, topic) } sets = append(sets, "version=version+1", "updated_at=CURRENT_TIMESTAMP(3)") - args = append(args, decodeUID(uid), strings.TrimSpace(topic), patch.Version) + args = append(args, id, decodeUID(uid), strings.TrimSpace(topic), patch.Version) ctx, cancel := a.getContext() if cancel != nil { defer cancel() } res, err := a.db.ExecContext(ctx, `UPDATE im_conversation_settings SET `+strings.Join(sets, ",")+` - WHERE user_id=? AND topic=? AND version=?`, args...) + WHERE tenant_id=? AND user_id=? AND topic=? AND version=?`, args...) if err != nil { return nil, err } @@ -4262,7 +4333,7 @@ func (a *adapter) TenantConversationSettingsUpdate(id t.TenantID, uid t.Uid, top if affected == 0 { return nil, t.ErrInvalidResponse } - return a.conversationSettingsByKey(ctx, uid, strings.TrimSpace(topic)) + return a.conversationSettingsByKey(ctx, id, uid, strings.TrimSpace(topic)) } func (a *adapter) TenantTopicDraftGet(id t.TenantID, uid t.Uid, topic string) (*t.TopicDraft, error) { @@ -4282,7 +4353,7 @@ func (a *adapter) TenantTopicDraftGet(id t.TenantID, uid t.Uid, topic string) (* if cancel != nil { defer cancel() } - draft, err := a.topicDraftByKey(ctx, uid, strings.TrimSpace(topic)) + draft, err := a.topicDraftByKey(ctx, id, uid, strings.TrimSpace(topic)) if err == sql.ErrNoRows { return nil, t.ErrNotFound } @@ -4313,22 +4384,22 @@ func (a *adapter) TenantTopicDraftPut(id t.TenantID, uid t.Uid, topic, content s } now := t.TimeNow() res, err := a.db.ExecContext(ctx, `INSERT INTO im_topic_drafts - (id,user_id,topic,content,entities_json,version,created_at,updated_at) - VALUES(?,?,?,?,?,?,?,?) + (tenant_id,id,user_id,topic,content,entities_json,version,created_at,updated_at) + VALUES(?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE id=id`, - draftID, decodeUID(uid), strings.TrimSpace(topic), content, entities, 1, now, now) + id, draftID, decodeUID(uid), strings.TrimSpace(topic), content, entities, 1, now, now) if err != nil { return nil, err } if affected, _ := res.RowsAffected(); affected == 0 { return nil, t.ErrInvalidResponse } - return a.topicDraftByKey(ctx, uid, strings.TrimSpace(topic)) + return a.topicDraftByKey(ctx, id, uid, strings.TrimSpace(topic)) } res, err := a.db.ExecContext(ctx, `UPDATE im_topic_drafts SET content=?,entities_json=?,version=version+1,updated_at=CURRENT_TIMESTAMP(3) - WHERE user_id=? AND topic=? AND version=?`, - content, entities, decodeUID(uid), strings.TrimSpace(topic), version) + WHERE tenant_id=? AND user_id=? AND topic=? AND version=?`, + content, entities, id, decodeUID(uid), strings.TrimSpace(topic), version) if err != nil { return nil, err } @@ -4336,10 +4407,10 @@ func (a *adapter) TenantTopicDraftPut(id t.TenantID, uid t.Uid, topic, content s if affected == 0 { return nil, t.ErrInvalidResponse } - return a.topicDraftByKey(ctx, uid, strings.TrimSpace(topic)) + return a.topicDraftByKey(ctx, id, uid, strings.TrimSpace(topic)) } -func (a *adapter) conversationSettingsByKey(ctx context.Context, uid t.Uid, topic string) (*t.ConversationSettings, error) { +func (a *adapter) conversationSettingsByKey(ctx context.Context, tenantID t.TenantID, uid t.Uid, topic string) (*t.ConversationSettings, error) { row := struct { ID int64 UserID int64 `db:"user_id"` @@ -4353,7 +4424,7 @@ func (a *adapter) conversationSettingsByKey(ctx context.Context, uid t.Uid, topi UpdatedAt time.Time `db:"updated_at"` }{} if err := sqlx.GetContext(ctx, a.db, &row, `SELECT id,user_id,topic,pinned,muted,remark,message_retention,version,created_at,updated_at - FROM im_conversation_settings WHERE user_id=? AND topic=?`, decodeUID(uid), topic); err != nil { + FROM im_conversation_settings WHERE tenant_id=? AND user_id=? AND topic=?`, tenantID, decodeUID(uid), topic); err != nil { return nil, err } return &t.ConversationSettings{ID: row.ID, UserID: store.EncodeUid(row.UserID), UserIDRaw: row.UserID, Topic: row.Topic, @@ -4361,7 +4432,7 @@ func (a *adapter) conversationSettingsByKey(ctx context.Context, uid t.Uid, topi Version: row.Version, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}, nil } -func (a *adapter) topicDraftByKey(ctx context.Context, uid t.Uid, topic string) (*t.TopicDraft, error) { +func (a *adapter) topicDraftByKey(ctx context.Context, tenantID t.TenantID, uid t.Uid, topic string) (*t.TopicDraft, error) { row := struct { ID int64 UserID int64 `db:"user_id"` @@ -4373,7 +4444,7 @@ func (a *adapter) topicDraftByKey(ctx context.Context, uid t.Uid, topic string) UpdatedAt time.Time `db:"updated_at"` }{} if err := sqlx.GetContext(ctx, a.db, &row, `SELECT id,user_id,topic,content,entities_json,version,created_at,updated_at - FROM im_topic_drafts WHERE user_id=? AND topic=?`, decodeUID(uid), topic); err != nil { + FROM im_topic_drafts WHERE tenant_id=? AND user_id=? AND topic=?`, tenantID, decodeUID(uid), topic); err != nil { return nil, err } return &t.TopicDraft{ID: row.ID, UserID: store.EncodeUid(row.UserID), UserIDRaw: row.UserID, Topic: row.Topic, @@ -4398,10 +4469,10 @@ func (a *adapter) TenantFavoriteCreate(id t.TenantID, fav *t.FavoriteItem) (*t.F defer cancel() } _, err := a.db.ExecContext(ctx, - `INSERT INTO im_favorites(favorite_id,user_id,item_type,source_topic,source_seq,snapshot,attachment_refs,availability,idempotency_key,collected_at,created_at,updated_at) - VALUES(?,?,?,?,?,?,?,?,?,?,?,?) + `INSERT INTO im_favorites(tenant_id,favorite_id,user_id,item_type,source_topic,source_seq,snapshot,attachment_refs,availability,idempotency_key,collected_at,created_at,updated_at) + VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE updated_at=updated_at`, - fav.FavoriteID, decodeUID(fav.UserID), fav.ItemType, fav.SourceTopic, fav.SourceSeq, common.ToJSON(fav.Snapshot), common.ToJSON(fav.AttachmentRefs), fav.Availability, fav.IdempotencyKey, fav.CollectedAt, fav.CreatedAt, fav.UpdatedAt) + id, fav.FavoriteID, decodeUID(fav.UserID), fav.ItemType, fav.SourceTopic, fav.SourceSeq, common.ToJSON(fav.Snapshot), common.ToJSON(fav.AttachmentRefs), fav.Availability, fav.IdempotencyKey, fav.CollectedAt, fav.CreatedAt, fav.UpdatedAt) if err != nil { return nil, err } @@ -4422,8 +4493,8 @@ func (a *adapter) tenantFavoriteFindExisting(ctx context.Context, id t.TenantID, where = append(where, "idempotency_key=?") args = append(args, fav.IdempotencyKey) } - args = append([]any{decodeUID(fav.UserID)}, args...) - rows, err := a.db.QueryxContext(ctx, "SELECT * FROM im_favorites WHERE user_id=? AND ("+strings.Join(where, " OR ")+") ORDER BY id DESC LIMIT 1", args...) + args = append([]any{id, decodeUID(fav.UserID)}, args...) + rows, err := a.db.QueryxContext(ctx, "SELECT * FROM im_favorites WHERE tenant_id=? AND user_id=? AND ("+strings.Join(where, " OR ")+") ORDER BY id DESC LIMIT 1", args...) if err != nil { return nil, err } @@ -4455,8 +4526,8 @@ func (a *adapter) TenantFavoriteList(id t.TenantID, uid t.Uid, itemType, queryTe if limit <= 0 || limit > 100 { limit = 50 } - where := []string{"user_id=?"} - args := []any{decodeUID(uid)} + where := []string{"tenant_id=?", "user_id=?"} + args := []any{id, decodeUID(uid)} if cursor != "" { parsed, err := strconv.ParseInt(cursor, 10, 64) if err != nil || parsed <= 0 { @@ -4522,7 +4593,7 @@ func (a *adapter) TenantFavoriteGet(id t.TenantID, uid t.Uid, favoriteID string) if cancel != nil { defer cancel() } - rows, err := a.db.QueryxContext(ctx, "SELECT * FROM im_favorites WHERE user_id=? AND favorite_id=?", decodeUID(uid), favoriteID) + rows, err := a.db.QueryxContext(ctx, "SELECT * FROM im_favorites WHERE tenant_id=? AND user_id=? AND favorite_id=?", id, decodeUID(uid), favoriteID) if err != nil { return nil, err } @@ -4555,7 +4626,7 @@ func (a *adapter) TenantFavoriteDelete(id t.TenantID, uid t.Uid, favoriteID stri if cancel != nil { defer cancel() } - res, err := a.db.ExecContext(ctx, "DELETE FROM im_favorites WHERE user_id=? AND favorite_id=?", decodeUID(uid), favoriteID) + res, err := a.db.ExecContext(ctx, "DELETE FROM im_favorites WHERE tenant_id=? AND user_id=? AND favorite_id=?", id, decodeUID(uid), favoriteID) if err != nil { return err } @@ -4586,7 +4657,7 @@ func (a *adapter) TenantMessageDeleteList(id t.TenantID, topic string, del *t.De return t.ErrMalformed } if tenantAdp != a { - return tenantAdp.MessageDeleteList(topic, del) + return tenantAdp.TenantMessageDeleteList(id, topic, del) } if a.tenantScopedDB { return a.MessageDeleteList(topic, del) @@ -4643,11 +4714,7 @@ func (a *adapter) TenantMessageGetDeleted(id t.TenantID, topic string, uid t.Uid return nil, t.ErrMalformed } if tenantAdp != a { - deleted, err := tenantAdp.MessageGetDeleted(topic, uid, opts) - for i := range deleted { - deleted[i].TenantID = id - } - return deleted, err + return tenantAdp.TenantMessageGetDeleted(id, topic, uid, opts) } if a.tenantScopedDB { deleted, err := a.MessageGetDeleted(topic, uid, opts) @@ -4724,7 +4791,7 @@ func (a *adapter) TenantDeviceUpsert(id t.TenantID, uid t.Uid, dev *t.DeviceDef) return t.ErrMalformed } if tenantAdp != a { - return tenantAdp.DeviceUpsert(uid, dev) + return tenantAdp.TenantDeviceUpsert(id, uid, dev) } if a.tenantScopedDB { return a.DeviceUpsert(uid, dev) @@ -4759,13 +4826,7 @@ func (a *adapter) TenantDeviceGetAll(id t.TenantID, uids ...t.Uid) (map[t.Uid][] return nil, 0, err } if tenantAdp != a { - devs, count, err := tenantAdp.DeviceGetAll(uids...) - for uid := range devs { - for i := range devs[uid] { - devs[uid][i].TenantID = id - } - } - return devs, count, err + return tenantAdp.TenantDeviceGetAll(id, uids...) } if a.tenantScopedDB { devs, count, err := a.DeviceGetAll(uids...) @@ -4824,7 +4885,7 @@ func (a *adapter) TenantDeviceDelete(id t.TenantID, uid t.Uid, deviceID string) return t.ErrMalformed } if tenantAdp != a { - return tenantAdp.DeviceDelete(uid, deviceID) + return tenantAdp.TenantDeviceDelete(id, uid, deviceID) } if a.tenantScopedDB { return a.DeviceDelete(uid, deviceID) @@ -4855,7 +4916,7 @@ func (a *adapter) TenantFileStartUpload(id t.TenantID, fd *t.FileDef) error { return t.ErrMalformed } if tenantAdp != a { - return tenantAdp.FileStartUpload(fd) + return tenantAdp.TenantFileStartUpload(id, fd) } if a.tenantScopedDB { return a.FileStartUpload(fd) @@ -4880,8 +4941,7 @@ func (a *adapter) TenantFileFinishUpload(id t.TenantID, fd *t.FileDef, success b return nil, t.ErrMalformed } if tenantAdp != a { - file, err := tenantAdp.FileFinishUpload(fd, success, size) - return stampTenantFile(id, file), err + return tenantAdp.TenantFileFinishUpload(id, fd, success, size) } if a.tenantScopedDB { file, err := a.FileFinishUpload(fd, success, size) @@ -4928,8 +4988,7 @@ func (a *adapter) TenantFileGet(id t.TenantID, fid string) (*t.FileDef, error) { return nil, err } if tenantAdp != a { - file, err := tenantAdp.FileGet(fid) - return stampTenantFile(id, file), err + return tenantAdp.TenantFileGet(id, fid) } if a.tenantScopedDB { file, err := a.FileGet(fid) @@ -4971,7 +5030,7 @@ func (a *adapter) TenantFileDeleteUnused(id t.TenantID, olderThan time.Time, lim return nil, err } if tenantAdp != a { - return tenantAdp.FileDeleteUnused(olderThan, limit) + return tenantAdp.TenantFileDeleteUnused(id, olderThan, limit) } if a.tenantScopedDB { return a.FileDeleteUnused(olderThan, limit) @@ -5037,7 +5096,7 @@ func (a *adapter) TenantFileLinkAttachments(id t.TenantID, topic string, userID, return t.ErrMalformed } if tenantAdp != a { - return tenantAdp.FileLinkAttachments(topic, userID, msgID, fids) + return tenantAdp.TenantFileLinkAttachments(id, topic, userID, msgID, fids) } if a.tenantScopedDB { return a.FileLinkAttachments(topic, userID, msgID, fids) @@ -5111,7 +5170,7 @@ func (a *adapter) TenantFileUserHasAccess(id t.TenantID, fid string, uid t.Uid) return false, t.ErrMalformed } if tenantAdp != a { - return tenantAdp.FileUserHasAccess(fid, uid) + return tenantAdp.TenantFileUserHasAccess(id, fid, uid) } if a.tenantScopedDB { return a.FileUserHasAccess(fid, uid) @@ -5125,7 +5184,7 @@ func (a *adapter) TenantFileUserHasAccess(id t.TenantID, fid string, uid t.Uid) defer cancel() } var allowed int - err = a.db.GetContext(ctx, &allowed, ` + err = a.db.GetContext(ctx, &allowed, fmt.Sprintf(` SELECT COUNT(*) FROM im_fileuploads AS fu WHERE fu.tenant_id=? AND (fu.id=? OR fu.location=? OR fu.location LIKE ?) AND fu.status=? AND ( fu.userid=? @@ -5135,7 +5194,7 @@ func (a *adapter) TenantFileUserHasAccess(id t.TenantID, fid string, uid t.Uid) ) OR EXISTS ( SELECT 1 FROM im_filemsglinks AS fml - INNER JOIN im_friendships AS fs + INNER JOIN %s AS fs ON fs.user_id=? AND fs.peer_id=fml.userid AND fs.status=? WHERE fml.tenant_id=fu.tenant_id AND fml.fileid=fu.id AND fml.userid IS NOT NULL ) @@ -5154,7 +5213,7 @@ func (a *adapter) TenantFileUserHasAccess(id t.TenantID, fid string, uid t.Uid) ) OR EXISTS ( SELECT 1 FROM im_filemsglinks AS fml - INNER JOIN im_messages AS m + INNER JOIN %s AS m ON m.tenant_id=fml.tenant_id AND m.id=fml.msgid INNER JOIN im_subscriptions AS s ON s.tenant_id=m.tenant_id AND s.topic=m.topic AND s.userid=? @@ -5171,10 +5230,10 @@ func (a *adapter) TenantFileUserHasAccess(id t.TenantID, fid string, uid t.Uid) SELECT 1 FROM im_group_files AS gf INNER JOIN im_subscriptions AS s ON s.tenant_id=fu.tenant_id AND s.topic=gf.topic AND s.userid=? - WHERE (gf.file_id=? OR gf.file_id LIKE CONCAT(?,'.%')) AND gf.status='active' + WHERE (gf.file_id=? OR gf.file_id LIKE CONCAT(?,'.%%')) AND gf.status='active' AND s.deletedat IS NULL AND INSTR(s.modewant,'R')>0 AND INSTR(s.modegiven,'R')>0 ) - ) LIMIT 1`, + ) LIMIT 1`, tenantFriendshipsTable(id), tenantMessagesTable(id)), id, decodeUID(fileID), fileID.String32(), "%/"+fileID.String32(), t.UploadCompleted, decodeUID(uid), decodeUID(uid), decodeUID(uid), t.RelationshipFriend, decodeUID(uid), decodeUID(uid), t.GroupMemberStatusActive, decodeUID(uid), decodeUID(uid), decodeUID(uid), fid, fid) @@ -5261,8 +5320,8 @@ func (a *adapter) TenantVerificationCreate(id t.TenantID, vc *t.VerificationCode _, err = tx.ExecContext(ctx, `UPDATE im_verification_codes SET state=?,updated_at=? - WHERE scene=? AND phone_hash=? AND state=?`, - t.VerificationStateExpired, now, vc.Scene, vc.PhoneHash, t.VerificationStatePending) + WHERE tenant_id=? AND scene=? AND phone_hash=? AND state=?`, + t.VerificationStateExpired, now, id, vc.Scene, vc.PhoneHash, t.VerificationStatePending) if err != nil { return err } @@ -5271,10 +5330,10 @@ func (a *adapter) TenantVerificationCreate(id t.TenantID, vc *t.VerificationCode vc.UpdatedAt = now _, err = tx.ExecContext(ctx, `INSERT INTO im_verification_codes - (challenge_id,scene,action,phone_hash,phone_mask,target_user,code_hash,state,attempts,max_attempts, + (tenant_id,challenge_id,scene,action,phone_hash,phone_mask,target_user,code_hash,state,attempts,max_attempts, expires_at,resend_after,created_at,updated_at) - VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, - vc.ChallengeID, vc.Scene, vc.Action, vc.PhoneHash, vc.PhoneMask, nullableUID(vc.TargetUser), vc.CodeHash, vc.State, vc.Attempts, + VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, + id, vc.ChallengeID, vc.Scene, vc.Action, vc.PhoneHash, vc.PhoneMask, nullableUID(vc.TargetUser), vc.CodeHash, vc.State, vc.Attempts, vc.MaxAttempts, vc.ExpiresAt, vc.ResendAfter, vc.CreatedAt, vc.UpdatedAt) if isDupe(err) { return t.ErrDuplicate @@ -5307,7 +5366,7 @@ func (a *adapter) TenantVerificationGet(id t.TenantID, challengeID string) (*t.V `SELECT id,challenge_id,scene,action,phone_hash,phone_mask,IFNULL(target_user,0) AS target_user,code_hash,verification_token_hash, state,attempts,max_attempts,expires_at,resend_after,verified_at,consumed_at,created_at,updated_at FROM im_verification_codes - WHERE challenge_id=? LIMIT 1`, challengeID) + WHERE tenant_id=? AND challenge_id=? LIMIT 1`, id, challengeID) if err == sql.ErrNoRows { return nil, t.ErrNotFound } @@ -5339,8 +5398,8 @@ func (a *adapter) TenantVerificationGetActive(id t.TenantID, scene, phoneHash st `SELECT id,challenge_id,scene,action,phone_hash,phone_mask,IFNULL(target_user,0) AS target_user,code_hash,verification_token_hash, state,attempts,max_attempts,expires_at,resend_after,verified_at,consumed_at,created_at,updated_at FROM im_verification_codes - WHERE scene=? AND phone_hash=? AND state=? AND expires_at>? - ORDER BY created_at DESC LIMIT 1`, scene, phoneHash, t.VerificationStatePending, now) + WHERE tenant_id=? AND scene=? AND phone_hash=? AND state=? AND expires_at>? + ORDER BY created_at DESC LIMIT 1`, id, scene, phoneHash, t.VerificationStatePending, now) if err == sql.ErrNoRows { return nil, t.ErrNotFound } @@ -5419,8 +5478,8 @@ func (a *adapter) TenantVerificationFail(id t.TenantID, challengeID string, stat _, err := a.db.ExecContext(ctx, `UPDATE im_verification_codes SET attempts=attempts+1,state=?,updated_at=? - WHERE challenge_id=?`, - state, now, challengeID) + WHERE tenant_id=? AND challenge_id=?`, + state, now, id, challengeID) return err } @@ -5444,8 +5503,8 @@ func (a *adapter) TenantVerificationMarkVerified(id t.TenantID, challengeID, tok _, err := a.db.ExecContext(ctx, `UPDATE im_verification_codes SET state=?,verification_token_hash=?,verified_at=?,updated_at=? - WHERE challenge_id=? AND state=?`, - t.VerificationStateVerified, tokenHash, verifiedAt, verifiedAt, challengeID, t.VerificationStatePending) + WHERE tenant_id=? AND challenge_id=? AND state=?`, + t.VerificationStateVerified, tokenHash, verifiedAt, verifiedAt, id, challengeID, t.VerificationStatePending) return err } @@ -5469,8 +5528,8 @@ func (a *adapter) TenantVerificationConsume(id t.TenantID, challengeID, tokenHas res, err := a.db.ExecContext(ctx, `UPDATE im_verification_codes SET state=?,consumed_at=?,updated_at=? - WHERE challenge_id=? AND verification_token_hash=? AND state=? AND expires_at>?`, - t.VerificationStateConsumed, consumedAt, consumedAt, challengeID, tokenHash, + WHERE tenant_id=? AND challenge_id=? AND verification_token_hash=? AND state=? AND expires_at>?`, + t.VerificationStateConsumed, consumedAt, consumedAt, id, challengeID, tokenHash, t.VerificationStateVerified, consumedAt) if err != nil { return err @@ -5501,8 +5560,8 @@ func (a *adapter) TenantUserSecurityGetAuthVersion(id t.TenantID, uid t.Uid) (in decoded := decodeUID(uid) var version int err := a.db.GetContext(ctx, &version, - `SELECT auth_version FROM im_user_security WHERE user_id=?`, - decoded) + `SELECT auth_version FROM im_user_security WHERE tenant_id=? AND user_id=?`, + id, decoded) if err == nil { return version, nil } @@ -5516,16 +5575,16 @@ func (a *adapter) TenantUserSecurityGetAuthVersion(id t.TenantID, uid t.Uid) (in return 0, err } _, err = a.db.ExecContext(ctx, - `INSERT INTO im_user_security(id,user_id,auth_version,created_at,updated_at) - VALUES(?,?,?,?,?) + `INSERT INTO im_user_security(tenant_id,id,user_id,auth_version,created_at,updated_at) + VALUES(?,?,?,?,?,?) ON DUPLICATE KEY UPDATE auth_version=auth_version`, - securityID, decoded, 1, now, now) + id, securityID, decoded, 1, now, now) if err != nil { return 0, err } err = a.db.GetContext(ctx, &version, - `SELECT auth_version FROM im_user_security WHERE user_id=?`, - decoded) + `SELECT auth_version FROM im_user_security WHERE tenant_id=? AND user_id=?`, + id, decoded) if err == sql.ErrNoRows { return 0, t.ErrNotFound } @@ -5570,19 +5629,19 @@ func (a *adapter) TenantUserSecurityBumpAuthVersion(id t.TenantID, uid t.Uid, up return 0, err } if _, err = tx.ExecContext(ctx, - `INSERT INTO im_user_security(id,user_id,auth_version,password_updated_at,created_at,updated_at) - VALUES(?,?,?,?,?,?) + `INSERT INTO im_user_security(tenant_id,id,user_id,auth_version,password_updated_at,created_at,updated_at) + VALUES(?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE auth_version=auth_version+1, password_updated_at=VALUES(password_updated_at), updated_at=VALUES(updated_at)`, - securityID, decoded, 2, updatedAt, updatedAt, updatedAt); err != nil { + id, securityID, decoded, 2, updatedAt, updatedAt, updatedAt); err != nil { return 0, err } var version int if err = tx.GetContext(ctx, &version, - `SELECT auth_version FROM im_user_security WHERE user_id=?`, - decoded); err != nil { + `SELECT auth_version FROM im_user_security WHERE tenant_id=? AND user_id=?`, + id, decoded); err != nil { return 0, err } if err = tx.Commit(); err != nil { @@ -5613,9 +5672,9 @@ func (a *adapter) TenantRealNameGet(id t.TenantID, uid t.Uid) (*t.RealNameVerifi `SELECT id,user_id,submission_id,status,name_cipher,id_type,id_number_cipher,id_number_hash, masked_name,masked_id_number,review_reason,verified_at,created_at,updated_at FROM im_realname_verifications - WHERE user_id=? + WHERE tenant_id=? AND user_id=? ORDER BY created_at DESC,id DESC LIMIT 1`, - decodeUID(uid)) + id, decodeUID(uid)) if err == sql.ErrNoRows { return nil, t.ErrNotFound } @@ -5650,10 +5709,10 @@ func (a *adapter) TenantRealNameCreate(id t.TenantID, rv *t.RealNameVerification rv.UpdatedAt = now _, err := a.db.ExecContext(ctx, `INSERT INTO im_realname_verifications - (user_id,submission_id,status,name_cipher,id_type,id_number_cipher,id_number_hash, + (tenant_id,user_id,submission_id,status,name_cipher,id_type,id_number_cipher,id_number_hash, masked_name,masked_id_number,review_reason,verified_at,created_at,updated_at) - VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)`, - decodeUID(rv.UserID), rv.SubmissionID, rv.Status, rv.NameCipher, rv.IDType, rv.IDNumberCipher, rv.IDNumberHash, + VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, + id, decodeUID(rv.UserID), rv.SubmissionID, rv.Status, rv.NameCipher, rv.IDType, rv.IDNumberCipher, rv.IDNumberHash, rv.MaskedName, rv.MaskedIDNumber, rv.ReviewReason, rv.VerifiedAt, rv.CreatedAt, rv.UpdatedAt) if isDupe(err) { return t.ErrDuplicate @@ -5691,9 +5750,9 @@ func (a *adapter) TenantRealNameAppealCreate(id t.TenantID, appeal *t.RealNameAp defer cancel() } _, err = a.db.ExecContext(ctx, `INSERT INTO im_realname_appeals - (id,appeal_id,user_id,reason,attachments_json,status,review_comment,created_at,updated_at) - VALUES(?,?,?,?,?,?,?,?,?)`, - appeal.ID, appeal.AppealID, decodeUID(appeal.UserID), appeal.Reason, appeal.AttachmentsJSON, + (tenant_id,id,appeal_id,user_id,reason,attachments_json,status,review_comment,created_at,updated_at) + VALUES(?,?,?,?,?,?,?,?,?,?)`, + id, appeal.ID, appeal.AppealID, decodeUID(appeal.UserID), appeal.Reason, appeal.AttachmentsJSON, appeal.Status, appeal.ReviewComment, appeal.CreatedAt, appeal.UpdatedAt) if isDupe(err) { return nil, t.ErrDuplicate @@ -5727,9 +5786,9 @@ func (a *adapter) TenantActionTokenCreate(id t.TenantID, token *t.ActionToken) e token.UpdatedAt = now _, err := a.db.ExecContext(ctx, `INSERT INTO im_action_tokens - (user_id,token_id,action,target,token_hash,state,expires_at,created_at,updated_at) - VALUES(?,?,?,?,?,?,?,?,?)`, - decodeUID(token.UserID), token.TokenID, token.Action, token.Target, token.TokenHash, token.State, + (tenant_id,user_id,token_id,action,target,token_hash,state,expires_at,created_at,updated_at) + VALUES(?,?,?,?,?,?,?,?,?,?)`, + id, decodeUID(token.UserID), token.TokenID, token.Action, token.Target, token.TokenHash, token.State, token.ExpiresAt, token.CreatedAt, token.UpdatedAt) if isDupe(err) { return t.ErrDuplicate @@ -5775,8 +5834,8 @@ func (a *adapter) TenantActionTokenConsume(id t.TenantID, uid t.Uid, token, acti err = tx.GetContext(ctx, &at, `SELECT id,user_id,token_id,action,target,token_hash,state,expires_at,consumed_at,created_at,updated_at FROM im_action_tokens - WHERE user_id=? AND token_id=? AND action=? AND target=? LIMIT 1`, - decodeUID(uid), tokenID, action, target) + WHERE tenant_id=? AND user_id=? AND token_id=? AND action=? AND target=? LIMIT 1`, + id, decodeUID(uid), tokenID, action, target) if err == sql.ErrNoRows { return nil, t.ErrNotFound } @@ -5788,8 +5847,8 @@ func (a *adapter) TenantActionTokenConsume(id t.TenantID, uid t.Uid, token, acti } res, err := tx.ExecContext(ctx, `UPDATE im_action_tokens SET state=1,consumed_at=?,updated_at=? - WHERE user_id=? AND token_id=? AND action=? AND target=? AND state=0 AND expires_at>?`, - now, now, decodeUID(uid), tokenID, action, target, now) + WHERE tenant_id=? AND user_id=? AND token_id=? AND action=? AND target=? AND state=0 AND expires_at>?`, + now, now, id, decodeUID(uid), tokenID, action, target, now) if err != nil { return nil, err } @@ -5824,8 +5883,8 @@ func (a *adapter) TenantIndependentPasswordGet(id t.TenantID, uid t.Uid, purpose err := a.db.GetContext(ctx, &pass, `SELECT user_id,purpose,password_hash,failed_attempts,locked_until,created_at,updated_at FROM im_youth_mode_settings - WHERE user_id=? AND purpose=?`, - decodeUID(uid), purpose) + WHERE tenant_id=? AND user_id=? AND purpose=?`, + id, decodeUID(uid), purpose) if err == sql.ErrNoRows { return nil, t.ErrNotFound } @@ -5863,10 +5922,10 @@ func (a *adapter) TenantIndependentPasswordUpsert(id t.TenantID, pass *t.Indepen } _, err = a.db.ExecContext(ctx, `INSERT INTO im_youth_mode_settings - (id,user_id,purpose,password_hash,failed_attempts,locked_until,created_at,updated_at) - VALUES(?,?,?,?,0,NULL,?,?) + (tenant_id,id,user_id,purpose,password_hash,failed_attempts,locked_until,created_at,updated_at) + VALUES(?,?,?,?,?,0,NULL,?,?) ON DUPLICATE KEY UPDATE password_hash=VALUES(password_hash),failed_attempts=0,locked_until=NULL,updated_at=VALUES(updated_at)`, - settingsID, decodeUID(pass.UserID), pass.Purpose, pass.PasswordHash, pass.CreatedAt, pass.UpdatedAt) + id, settingsID, decodeUID(pass.UserID), pass.Purpose, pass.PasswordHash, pass.CreatedAt, pass.UpdatedAt) return err } @@ -5892,8 +5951,8 @@ func (a *adapter) TenantIndependentPasswordUpdateAttempts(id t.TenantID, uid t.U } _, err := a.db.ExecContext(ctx, `UPDATE im_youth_mode_settings SET failed_attempts=?,locked_until=?,updated_at=? - WHERE user_id=? AND purpose=?`, - attempts, lockedUntil, updatedAt, decodeUID(uid), purpose) + WHERE tenant_id=? AND user_id=? AND purpose=?`, + attempts, lockedUntil, updatedAt, id, decodeUID(uid), purpose) return err } @@ -5914,9 +5973,9 @@ func (a *adapter) TenantYouthModeGet(id t.TenantID, uid t.Uid) (*t.YouthModeSett if cancel != nil { defer cancel() } - settings, err := a.youthModeByUID(ctx, uid) + settings, err := a.youthModeByUID(ctx, id, uid) if err == sql.ErrNoRows { - return defaultYouthModeSettings(uid), nil + return defaultYouthModeSettings(id, uid), nil } return settings, err } @@ -5948,8 +6007,8 @@ func (a *adapter) TenantYouthModeSet(id t.TenantID, uid t.Uid, enabled bool, pol res, err := a.db.ExecContext(ctx, `UPDATE im_youth_mode_settings SET enabled=?,policy_version=?,enabled_at=?,restrictions=COALESCE(restrictions,'["night_quiet","content_filter","payment_guard"]'),updated_at=? - WHERE user_id=? AND purpose=? AND LENGTH(password_hash)>0`, - enabled, policyVersion, enabledAt, updatedAt, decodeUID(uid), "youth_mode") + WHERE tenant_id=? AND user_id=? AND purpose=? AND LENGTH(password_hash)>0`, + enabled, policyVersion, enabledAt, updatedAt, id, decodeUID(uid), "youth_mode") if err != nil { return nil, err } @@ -5957,10 +6016,10 @@ func (a *adapter) TenantYouthModeSet(id t.TenantID, uid t.Uid, enabled bool, pol if affected == 0 { return nil, t.ErrMalformed } - return a.youthModeByUID(ctx, uid) + return a.youthModeByUID(ctx, id, uid) } -func (a *adapter) youthModeByUID(ctx context.Context, uid t.Uid) (*t.YouthModeSettings, error) { +func (a *adapter) youthModeByUID(ctx context.Context, tenantID t.TenantID, uid t.Uid) (*t.YouthModeSettings, error) { row := struct { UserID int64 `db:"user_id"` PasswordHash []byte `db:"password_hash"` @@ -5973,7 +6032,7 @@ func (a *adapter) youthModeByUID(ctx context.Context, uid t.Uid) (*t.YouthModeSe }{} if err := sqlx.GetContext(ctx, a.db, &row, `SELECT user_id,password_hash,enabled,policy_version,restrictions,enabled_at,created_at,updated_at - FROM im_youth_mode_settings WHERE user_id=? AND purpose=?`, decodeUID(uid), "youth_mode"); err != nil { + FROM im_youth_mode_settings WHERE tenant_id=? AND user_id=? AND purpose=?`, tenantID, decodeUID(uid), "youth_mode"); err != nil { return nil, err } settings := &t.YouthModeSettings{ @@ -5993,7 +6052,7 @@ func (a *adapter) youthModeByUID(ctx context.Context, uid t.Uid) (*t.YouthModeSe return settings, nil } -func defaultYouthModeSettings(uid t.Uid) *t.YouthModeSettings { +func defaultYouthModeSettings(tenantID t.TenantID, uid t.Uid) *t.YouthModeSettings { now := t.TimeNow() return &t.YouthModeSettings{ UserID: uid, @@ -6031,15 +6090,15 @@ func (a *adapter) TenantSystemNotificationCreate(id t.TenantID, notification *t. notification.UpdatedAt = now _, err := a.db.ExecContext(ctx, `INSERT INTO im_system_notifications - (user_id,event_id,event_type,title,summary,action,read_at,created_at,updated_at) - VALUES(?,?,?,?,?,?,?,?,?) + (tenant_id,user_id,event_id,event_type,title,summary,action,read_at,created_at,updated_at) + VALUES(?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE event_type=VALUES(event_type), title=VALUES(title), summary=VALUES(summary), action=VALUES(action), updated_at=VALUES(updated_at)`, - decodeUID(notification.UserID), notification.EventID, notification.EventType, notification.Title, + id, decodeUID(notification.UserID), notification.EventID, notification.EventType, notification.Title, notification.Summary, common.ToJSON(notification.Action), notification.ReadAt, notification.CreatedAt, notification.UpdatedAt) return err } @@ -6070,8 +6129,8 @@ func (a *adapter) TenantSystemNotificationList(id t.TenantID, uid t.Uid, cursor } query := `SELECT id,user_id,event_id,event_type,title,summary,action,read_at,created_at,updated_at FROM im_system_notifications - WHERE user_id=?` - args := []any{decodeUID(uid)} + WHERE tenant_id=? AND user_id=?` + args := []any{id, decodeUID(uid)} if lastID > 0 { query += " AND id 100 { limit = 20 } - where := []string{} - args := []any{} + where := []string{"fr.tenant_id=?"} + args := []any{id} decodedUID := decodeUID(uid) switch direction { case "", "inbound": @@ -6290,8 +6349,8 @@ func (a *adapter) TenantFriendRequestList(id t.TenantID, uid t.Uid, direction, s p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature, p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account FROM im_friend_requests AS fr - JOIN im_users AS u ON u.id=IF(fr.applicant_id=?,fr.target_id,fr.applicant_id) - LEFT JOIN im_user_profiles AS p ON p.user_id=u.id + JOIN im_users AS u ON u.tenant_id=fr.tenant_id AND u.id=IF(fr.applicant_id=?,fr.target_id,fr.applicant_id) + LEFT JOIN im_user_profiles AS p ON p.tenant_id=u.tenant_id AND p.user_id=u.id WHERE ` + strings.Join(where, " AND ") + ` ORDER BY fr.id DESC LIMIT ?` args = append([]any{decodedUID}, args...) @@ -6395,8 +6454,8 @@ func (a *adapter) TenantFriendRequestPendingCount(id t.TenantID, uid t.Uid) (int } var count int if err := a.db.GetContext(ctx, &count, - `SELECT COUNT(*) FROM im_friend_requests WHERE target_id=? AND status=? AND expires_at>?`, - decodeUID(uid), t.FriendRequestPending, t.TimeNow()); err != nil { + `SELECT COUNT(*) FROM im_friend_requests WHERE tenant_id=? AND target_id=? AND status=? AND expires_at>?`, + id, decodeUID(uid), t.FriendRequestPending, t.TimeNow()); err != nil { return 0, err } return count, nil @@ -6421,8 +6480,8 @@ func (a *adapter) TenantFriendRequestLatestBetween(id t.TenantID, uid1, uid2 t.U } return a.friendRequestByQuery(ctx, id, - "SELECT * FROM im_friend_requests WHERE ((applicant_id=? AND target_id=?) OR (applicant_id=? AND target_id=?)) ORDER BY created_at DESC,id DESC LIMIT 1", - decodeUID(uid1), decodeUID(uid2), decodeUID(uid2), decodeUID(uid1)) + "SELECT * FROM im_friend_requests WHERE tenant_id=? AND ((applicant_id=? AND target_id=?) OR (applicant_id=? AND target_id=?)) ORDER BY created_at DESC,id DESC LIMIT 1", + id, decodeUID(uid1), decodeUID(uid2), decodeUID(uid2), decodeUID(uid1)) } func (a *adapter) TenantFriendRequestAccept(id t.TenantID, requestID string, actor t.Uid, handledAt time.Time) (*t.FriendRequest, error) { @@ -6466,7 +6525,7 @@ func (a *adapter) TenantFriendRequestAccept(id t.TenantID, requestID string, act CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` }{} - err = tx.GetContext(ctx, &row, "SELECT * FROM im_friend_requests WHERE request_id=? FOR UPDATE", requestID) + err = tx.GetContext(ctx, &row, "SELECT * FROM im_friend_requests WHERE tenant_id=? AND request_id=? FOR UPDATE", id, requestID) if err == sql.ErrNoRows { return nil, t.ErrNotFound } @@ -6502,8 +6561,8 @@ func (a *adapter) TenantFriendRequestAccept(id t.TenantID, requestID string, act } if row.Status == t.FriendRequestPending { if _, err = tx.ExecContext(ctx, - "UPDATE im_friend_requests SET status=?,handled_at=?,updated_at=? WHERE request_id=? AND status=?", - t.FriendRequestAccepted, handledAt, handledAt, requestID, t.FriendRequestPending); err != nil { + "UPDATE im_friend_requests SET status=?,handled_at=?,updated_at=? WHERE tenant_id=? AND request_id=? AND status=?", + t.FriendRequestAccepted, handledAt, handledAt, id, requestID, t.FriendRequestPending); err != nil { return nil, err } row.Status = t.FriendRequestAccepted @@ -6511,9 +6570,9 @@ func (a *adapter) TenantFriendRequestAccept(id t.TenantID, requestID string, act row.UpdatedAt = handledAt } if _, err = tx.ExecContext(ctx, `UPDATE im_friend_requests SET status=?,handled_at=?,updated_at=? - WHERE request_id<>? AND status=? AND + WHERE tenant_id=? AND request_id<>? AND status=? AND ((applicant_id=? AND target_id=?) OR (applicant_id=? AND target_id=?))`, - t.FriendRequestExpired, handledAt, handledAt, requestID, t.FriendRequestPending, + t.FriendRequestExpired, handledAt, handledAt, id, requestID, t.FriendRequestPending, decodeUID(applicant), decodeUID(target), decodeUID(target), decodeUID(applicant)); err != nil { return nil, err } @@ -6523,9 +6582,9 @@ func (a *adapter) TenantFriendRequestAccept(id t.TenantID, requestID string, act return nil, idErr } if _, err = tx.ExecContext(ctx, - "INSERT INTO im_friendships(id,user_id,peer_id,status,topic,created_at,updated_at) VALUES(?,?,?,?,?,?,?) "+ + fmt.Sprintf("INSERT INTO %s(tenant_id,id,user_id,peer_id,status,topic,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?) ", tenantFriendshipsTable(id))+ "ON DUPLICATE KEY UPDATE status=VALUES(status),topic=VALUES(topic),updated_at=VALUES(updated_at)", - friendshipID, decodeUID(pair[0]), decodeUID(pair[1]), t.RelationshipFriend, topic, handledAt, handledAt); err != nil { + id, friendshipID, decodeUID(pair[0]), decodeUID(pair[1]), t.RelationshipFriend, topic, handledAt, handledAt); err != nil { return nil, err } } @@ -6583,14 +6642,14 @@ func (a *adapter) TenantFriendRequestAccept(id t.TenantID, requestID string, act if !isDupe(err) { return nil, err } - if _, err = tx.ExecContext(ctx, "UPDATE im_topics SET updatedat=?,touchedat=?,state=? WHERE name=?", - handledAt, handledAt, t.StateOK, topic); err != nil { + if _, err = tx.ExecContext(ctx, "UPDATE im_topics SET updatedat=?,touchedat=?,state=? WHERE tenant_id=? AND name=?", + handledAt, handledAt, t.StateOK, id, topic); err != nil { return nil, err } } eventType := "friend.request." + t.FriendRequestAccepted applicantEventID := t.StableEventID("relation", row.RequestID+":"+t.FriendRequestAccepted, eventType, applicant.UserId()) - if err = insertSystemNotificationTx(tx, applicant, t.StableEventID(applicantEventID, "notification"), eventType, + if err = insertSystemNotificationTx(tx, id, applicant, t.StableEventID(applicantEventID, "notification"), eventType, "好友申请已处理", t.FriendRequestAccepted, map[string]any{"type": "friend_request", "request_id": row.RequestID, "user_id": actor.UserId(), "topic": topic}); err != nil { return nil, err } @@ -6633,13 +6692,13 @@ func (a *adapter) TenantFriendRequestResolve(id t.TenantID, requestID string, ac } defer tx.Rollback() if _, err = tx.ExecContext(ctx, - "UPDATE im_friend_requests SET status=?,handled_at=?,updated_at=? WHERE request_id=? AND status=?", - status, handledAt, handledAt, requestID, t.FriendRequestPending); err != nil { + "UPDATE im_friend_requests SET status=?,handled_at=?,updated_at=? WHERE tenant_id=? AND request_id=? AND status=?", + status, handledAt, handledAt, id, requestID, t.FriendRequestPending); err != nil { return nil, err } eventType := "friend.request." + status eventID := t.StableEventID("relation", req.RequestID+":"+status, eventType, req.ApplicantID.UserId()) - if err = insertSystemNotificationTx(tx, req.ApplicantID, t.StableEventID(eventID, "notification"), eventType, + if err = insertSystemNotificationTx(tx, id, req.ApplicantID, t.StableEventID(eventID, "notification"), eventType, "好友申请已处理", status, map[string]any{"type": "friend_request", "request_id": req.RequestID, "user_id": actor.UserId()}); err != nil { return nil, err } @@ -6675,9 +6734,9 @@ func (a *adapter) TenantFriendshipUpsertPair(id t.TenantID, uid1, uid2 t.Uid, to return err } if _, err := a.db.ExecContext(ctx, - "INSERT INTO im_friendships(id,user_id,peer_id,status,topic,created_at,updated_at) VALUES(?,?,?,?,?,?,?) "+ + fmt.Sprintf("INSERT INTO %s(tenant_id,id,user_id,peer_id,status,topic,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?) ", tenantFriendshipsTable(id))+ "ON DUPLICATE KEY UPDATE status=VALUES(status),topic=VALUES(topic),updated_at=VALUES(updated_at)", - friendshipID, decodeUID(pair[0]), decodeUID(pair[1]), t.RelationshipFriend, topic, now, now); err != nil { + id, friendshipID, decodeUID(pair[0]), decodeUID(pair[1]), t.RelationshipFriend, topic, now, now); err != nil { return err } } @@ -6701,8 +6760,8 @@ func (a *adapter) TenantFriendshipList(id t.TenantID, uid t.Uid, queryText, init limit = 50 } uidID := decodeUID(uid) - where := []string{"fs.user_id=?", "fs.status=?", "fs.peer_id<>?"} - args := []any{uidID, t.RelationshipFriend, uidID} + where := []string{"fs.tenant_id=?", "fs.user_id=?", "fs.status=?", "fs.peer_id<>?"} + args := []any{id, uidID, t.RelationshipFriend, uidID} if cursor != "" { parsed, err := strconv.ParseInt(cursor, 10, 64) if err != nil || parsed <= 0 { @@ -6726,14 +6785,14 @@ func (a *adapter) TenantFriendshipList(id t.TenantID, uid t.Uid, queryText, init if cancel != nil { defer cancel() } - rows, err := a.db.QueryxContext(ctx, `SELECT fs.*,u.trusted, + rows, err := a.db.QueryxContext(ctx, fmt.Sprintf(`SELECT fs.*,u.trusted, p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature, p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account,p.updated_at AS profile_updated_at - FROM im_friendships AS fs - JOIN im_users AS u ON u.id=fs.peer_id - LEFT JOIN im_user_profiles AS p ON p.user_id=u.id + FROM %s AS fs + JOIN im_users AS u ON u.tenant_id=fs.tenant_id AND u.id=fs.peer_id + LEFT JOIN im_user_profiles AS p ON p.tenant_id=u.tenant_id AND p.user_id=u.id WHERE `+strings.Join(where, " AND ")+` - ORDER BY fs.peer_id ASC LIMIT ?`, args...) + ORDER BY fs.peer_id ASC LIMIT ?`, tenantFriendshipsTable(id)), args...) if err != nil { return nil, "", err } @@ -6818,9 +6877,9 @@ func (a *adapter) TenantFriendshipGet(id t.TenantID, userID, peerID t.Uid) (*t.F CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` }{} - err := a.db.GetContext(ctx, &row, `SELECT user_id,peer_id,status,remark,topic,created_at,updated_at - FROM im_friendships WHERE user_id=? AND peer_id=?`, - decodeUID(userID), decodeUID(peerID)) + err := a.db.GetContext(ctx, &row, fmt.Sprintf(`SELECT user_id,peer_id,status,remark,topic,created_at,updated_at + FROM %s WHERE tenant_id=? AND user_id=? AND peer_id=?`, tenantFriendshipsTable(id)), + id, decodeUID(userID), decodeUID(peerID)) if err == sql.ErrNoRows { return nil, nil } @@ -6857,8 +6916,8 @@ func (a *adapter) TenantFriendshipUpdateRemark(id t.TenantID, userID, peerID t.U defer cancel() } res, err := a.db.ExecContext(ctx, - "UPDATE im_friendships SET remark=?,updated_at=? WHERE user_id=? AND peer_id=? AND status=?", - remark, now, decodeUID(userID), decodeUID(peerID), t.RelationshipFriend) + fmt.Sprintf("UPDATE %s SET remark=?,updated_at=? WHERE tenant_id=? AND user_id=? AND peer_id=? AND status=?", tenantFriendshipsTable(id)), + remark, now, id, decodeUID(userID), decodeUID(peerID), t.RelationshipFriend) if err != nil { return nil, err } @@ -6886,8 +6945,8 @@ func (a *adapter) TenantFriendshipDeletePair(id t.TenantID, uid1, uid2 t.Uid, no defer cancel() } res, err := a.db.ExecContext(ctx, - "UPDATE im_friendships SET status=?,updated_at=? WHERE (user_id=? AND peer_id=?) OR (user_id=? AND peer_id=?)", - t.RelationshipNone, now, decodeUID(uid1), decodeUID(uid2), decodeUID(uid2), decodeUID(uid1)) + fmt.Sprintf("UPDATE %s SET status=?,updated_at=? WHERE tenant_id=? AND ((user_id=? AND peer_id=?) OR (user_id=? AND peer_id=?))", tenantFriendshipsTable(id)), + t.RelationshipNone, now, id, decodeUID(uid1), decodeUID(uid2), decodeUID(uid2), decodeUID(uid1)) if err != nil { return err } @@ -6921,8 +6980,8 @@ func (a *adapter) TenantUserBlockGet(id t.TenantID, blockerID, blockedID t.Uid) UpdatedAt time.Time `db:"updated_at"` }{} err := a.db.GetContext(ctx, &row, `SELECT blocker_id,blocked_id,created_at,updated_at - FROM im_user_blocks WHERE blocker_id=? AND blocked_id=?`, - decodeUID(blockerID), decodeUID(blockedID)) + FROM im_user_blocks WHERE tenant_id=? AND blocker_id=? AND blocked_id=?`, + id, decodeUID(blockerID), decodeUID(blockedID)) if err == sql.ErrNoRows { return nil, nil } @@ -6954,8 +7013,8 @@ func (a *adapter) TenantUserBlockList(id t.TenantID, blockerID t.Uid, cursor str if limit <= 0 || limit > 100 { limit = 50 } - where := []string{"b.blocker_id=?"} - args := []any{decodeUID(blockerID)} + where := []string{"b.tenant_id=?", "b.blocker_id=?"} + args := []any{id, decodeUID(blockerID)} if cursor != "" { parsed, err := strconv.ParseInt(cursor, 10, 64) if err != nil || parsed <= 0 { @@ -6973,8 +7032,8 @@ func (a *adapter) TenantUserBlockList(id t.TenantID, blockerID t.Uid, cursor str p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature, p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account FROM im_user_blocks AS b - LEFT JOIN im_users AS u ON u.id=b.blocked_id - LEFT JOIN im_user_profiles AS p ON p.user_id=b.blocked_id + LEFT JOIN im_users AS u ON u.tenant_id=b.tenant_id AND u.id=b.blocked_id + LEFT JOIN im_user_profiles AS p ON p.tenant_id=b.tenant_id AND p.user_id=b.blocked_id WHERE `+strings.Join(where, " AND ")+` ORDER BY b.blocked_id DESC LIMIT ?`, args...) if err != nil { @@ -7049,9 +7108,9 @@ func (a *adapter) TenantUserBlockUpsert(id t.TenantID, blockerID, blockedID t.Ui return nil, err } _, err = a.db.ExecContext(ctx, - "INSERT INTO im_user_blocks(id,blocker_id,blocked_id,created_at,updated_at) VALUES(?,?,?,?,?) "+ + "INSERT INTO im_user_blocks(tenant_id,id,blocker_id,blocked_id,created_at,updated_at) VALUES(?,?,?,?,?,?) "+ "ON DUPLICATE KEY UPDATE updated_at=VALUES(updated_at)", - blockID, decodeUID(blockerID), decodeUID(blockedID), now, now) + id, blockID, decodeUID(blockerID), decodeUID(blockedID), now, now) if err != nil { return nil, err } @@ -7075,8 +7134,8 @@ func (a *adapter) TenantUserBlockDelete(id t.TenantID, blockerID, blockedID t.Ui if cancel != nil { defer cancel() } - _, err := a.db.ExecContext(ctx, "DELETE FROM im_user_blocks WHERE blocker_id=? AND blocked_id=?", - decodeUID(blockerID), decodeUID(blockedID)) + _, err := a.db.ExecContext(ctx, "DELETE FROM im_user_blocks WHERE tenant_id=? AND blocker_id=? AND blocked_id=?", + id, decodeUID(blockerID), decodeUID(blockedID)) return err } @@ -7134,10 +7193,10 @@ func (a *adapter) TenantUserSearch(id t.TenantID, viewer t.Uid, queryText, searc default: return nil, "", t.ErrMalformed } - where = append(where, `(EXISTS ( - SELECT 1 FROM im_friendships AS privacy_friend + where = append(where, fmt.Sprintf(`(EXISTS ( + SELECT 1 FROM %s AS privacy_friend WHERE privacy_friend.user_id=? AND privacy_friend.peer_id=u.id AND privacy_friend.status=? - ) OR COALESCE(JSON_UNQUOTE(JSON_EXTRACT(privacy.add_me_methods, ?)), 'true')='true')`) + ) OR COALESCE(JSON_UNQUOTE(JSON_EXTRACT(privacy.add_me_methods, ?)), 'true')='true')`, tenantFriendshipsTable(id))) args = append(args, decodeUID(viewer), t.RelationshipFriend, "$."+addMethod) args = append([]any{id}, args...) args = append(args, limit+1) @@ -7183,11 +7242,11 @@ func userIDNumericArg(value string) int64 { } func (a *adapter) friendRequestByRequestID(ctx context.Context, tenantID t.TenantID, requestID string) (*t.FriendRequest, error) { - return a.friendRequestByQuery(ctx, tenantID, "SELECT * FROM im_friend_requests WHERE request_id=?", requestID) + return a.friendRequestByQuery(ctx, tenantID, "SELECT * FROM im_friend_requests WHERE tenant_id=? AND request_id=?", tenantID, requestID) } func (a *adapter) friendRequestByIdempotency(ctx context.Context, tenantID t.TenantID, applicantID t.Uid, key string) (*t.FriendRequest, error) { - return a.friendRequestByQuery(ctx, tenantID, "SELECT * FROM im_friend_requests WHERE applicant_id=? AND idempotency_key=?", decodeUID(applicantID), key) + return a.friendRequestByQuery(ctx, tenantID, "SELECT * FROM im_friend_requests WHERE tenant_id=? AND applicant_id=? AND idempotency_key=?", tenantID, decodeUID(applicantID), key) } func (a *adapter) friendRequestByQuery(ctx context.Context, tenantID t.TenantID, query string, args ...any) (*t.FriendRequest, error) { diff --git a/server/db/mysql/tenant_infrastructure.go b/server/db/mysql/tenant_infrastructure.go index fdb1083..58f1cb7 100644 --- a/server/db/mysql/tenant_infrastructure.go +++ b/server/db/mysql/tenant_infrastructure.go @@ -42,8 +42,8 @@ func (a *adapter) TenantHTTPIdempotencyBegin(id t.TenantID, record *t.HTTPIdempo record.ExpiresAt = now.Add(24 * time.Hour) } _, err = a.db.Exec(`INSERT INTO im_http_idempotency - (user_id,method,route,idempotency_key,body_hash,state,response_status,response_body,expires_at,created_at,updated_at) - VALUES(?,?,?,?,?,?,?,?,?,?,?)`, decodeUID(record.UserID), record.Method, record.Route, record.Key, + (tenant_id,user_id,method,route,idempotency_key,body_hash,state,response_status,response_body,expires_at,created_at,updated_at) + VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)`, id, decodeUID(record.UserID), record.Method, record.Route, record.Key, record.BodyHash, t.HTTPIdempotencyProcessing, 0, nil, record.ExpiresAt, now, now) if err == nil { record.State = t.HTTPIdempotencyProcessing @@ -54,18 +54,18 @@ func (a *adapter) TenantHTTPIdempotencyBegin(id t.TenantID, record *t.HTTPIdempo if !isDupe(err) { return nil, false, err } - existing, err := a.httpIdempotencyGet(record.UserID, record.Method, record.Route, record.Key) + existing, err := a.httpIdempotencyGet(id, record.UserID, record.Method, record.Route, record.Key) if err == nil && !existing.ExpiresAt.After(now) { res, deleteErr := a.db.Exec(`DELETE FROM im_http_idempotency - WHERE user_id=? AND method=? AND route=? AND idempotency_key=? AND expires_at<=?`, - decodeUID(record.UserID), record.Method, record.Route, record.Key, now) + WHERE tenant_id=? AND user_id=? AND method=? AND route=? AND idempotency_key=? AND expires_at<=?`, + id, decodeUID(record.UserID), record.Method, record.Route, record.Key, now) if deleteErr != nil { return nil, false, deleteErr } if deleted, _ := res.RowsAffected(); deleted > 0 { return a.TenantHTTPIdempotencyBegin(id, record) } - current, getErr := a.httpIdempotencyGet(record.UserID, record.Method, record.Route, record.Key) + current, getErr := a.httpIdempotencyGet(id, record.UserID, record.Method, record.Route, record.Key) return current, false, getErr } return existing, false, err @@ -84,8 +84,8 @@ func (a *adapter) TenantHTTPIdempotencyComplete(id t.TenantID, record *t.HTTPIde } now := t.TimeNow() res, err := a.db.Exec(`UPDATE im_http_idempotency SET state=?,response_status=?,response_body=?,updated_at=? - WHERE user_id=? AND method=? AND route=? AND idempotency_key=? AND body_hash=? AND state=?`, - t.HTTPIdempotencyCompleted, record.ResponseStatus, record.ResponseBody, now, decodeUID(record.UserID), + WHERE tenant_id=? AND user_id=? AND method=? AND route=? AND idempotency_key=? AND body_hash=? AND state=?`, + t.HTTPIdempotencyCompleted, record.ResponseStatus, record.ResponseBody, now, id, decodeUID(record.UserID), record.Method, record.Route, record.Key, record.BodyHash, t.HTTPIdempotencyProcessing) if err != nil { return err @@ -108,12 +108,12 @@ func (a *adapter) TenantHTTPIdempotencyAbort(id t.TenantID, record *t.HTTPIdempo return tenantAdp.TenantHTTPIdempotencyAbort(id, record) } _, err = a.db.Exec(`DELETE FROM im_http_idempotency - WHERE user_id=? AND method=? AND route=? AND idempotency_key=? AND body_hash=? AND state=?`, - decodeUID(record.UserID), record.Method, record.Route, record.Key, record.BodyHash, t.HTTPIdempotencyProcessing) + WHERE tenant_id=? AND user_id=? AND method=? AND route=? AND idempotency_key=? AND body_hash=? AND state=?`, + id, decodeUID(record.UserID), record.Method, record.Route, record.Key, record.BodyHash, t.HTTPIdempotencyProcessing) return err } -func (a *adapter) httpIdempotencyGet(uid t.Uid, method, route, key string) (*t.HTTPIdempotencyRecord, error) { +func (a *adapter) httpIdempotencyGet(tenantID t.TenantID, uid t.Uid, method, route, key string) (*t.HTTPIdempotencyRecord, error) { row := struct { UserID int64 `db:"user_id"` Method string @@ -128,7 +128,8 @@ func (a *adapter) httpIdempotencyGet(uid t.Uid, method, route, key string) (*t.H ExpiresAt time.Time `db:"expires_at"` }{} err := a.db.Get(&row, `SELECT user_id,method,route,idempotency_key,body_hash,state,response_status,response_body,created_at,updated_at,expires_at - FROM im_http_idempotency WHERE user_id=? AND method=? AND route=? AND idempotency_key=?`, decodeUID(uid), method, route, key) + FROM im_http_idempotency WHERE tenant_id=? AND user_id=? AND method=? AND route=? AND idempotency_key=?`, + tenantID, decodeUID(uid), method, route, key) if err == sql.ErrNoRows { return nil, t.ErrNotFound } @@ -512,16 +513,16 @@ func truncateKafkaString(value string, limit int) string { return string(runes[:limit]) } -func insertSystemNotificationTx(tx *sqlx.Tx, uid t.Uid, eventID, eventType, title, summary string, action any) error { +func insertSystemNotificationTx(tx *sqlx.Tx, tenantID t.TenantID, uid t.Uid, eventID, eventType, title, summary string, action any) error { encoded, err := json.Marshal(action) if err != nil { return err } now := t.TimeNow() _, err = tx.Exec(`INSERT INTO im_system_notifications - (user_id,event_id,event_type,title,summary,action,created_at,updated_at) - VALUES(?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE event_type=VALUES(event_type),title=VALUES(title), - summary=VALUES(summary),action=VALUES(action),updated_at=VALUES(updated_at)`, decodeUID(uid), eventID, + (tenant_id,user_id,event_id,event_type,title,summary,action,created_at,updated_at) + VALUES(?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE event_type=VALUES(event_type),title=VALUES(title), + summary=VALUES(summary),action=VALUES(action),updated_at=VALUES(updated_at)`, tenantID, decodeUID(uid), eventID, eventType, title, summary, encoded, now, now) return err } diff --git a/server/db/mysql/tenant_security_audit.go b/server/db/mysql/tenant_security_audit.go index de6e74b..ae46f9a 100644 --- a/server/db/mysql/tenant_security_audit.go +++ b/server/db/mysql/tenant_security_audit.go @@ -134,7 +134,7 @@ func (a *adapter) TenantMessageAdminApply(id t.TenantID, action *t.MessageAdminA if action.ConversationType == "group" { _, err = tx.ExecContext(ctx, "UPDATE "+messageTable+" SET status=?,deleted_at=?,updated_at=? WHERE topic=? AND seqid=?", afterStatus, action.ProcessedAt, action.ProcessedAt, action.Topic, action.SeqID) } else { - _, err = tx.ExecContext(ctx, "UPDATE im_messages SET deletedat=?,updatedat=? WHERE topic=? AND seqid=?", action.ProcessedAt, action.ProcessedAt, action.Topic, action.SeqID) + _, err = tx.ExecContext(ctx, "UPDATE "+messageTable+" SET deletedat=?,updatedat=? WHERE topic=? AND seqid=?", action.ProcessedAt, action.ProcessedAt, action.Topic, action.SeqID) } if err != nil { return err @@ -177,7 +177,8 @@ func lockAdminMessage(ctx context.Context, tx *sqlx.Tx, tenantID t.TenantID, con DeletedAt sql.NullTime `db:"deletedat"` RecalledAt sql.NullTime `db:"recalled_at"` } - err := tx.GetContext(ctx, &row, "SELECT `from`,deletedat,recalled_at FROM im_messages WHERE topic=? AND seqid=? FOR UPDATE", topic, seqID) + messageTable := tenantMessagesTable(tenantID) + err := tx.GetContext(ctx, &row, "SELECT `from`,deletedat,recalled_at FROM "+messageTable+" WHERE topic=? AND seqid=? FOR UPDATE", topic, seqID) if errors.Is(err, sql.ErrNoRows) { return 0, "", "", t.ErrNotFound } @@ -188,7 +189,7 @@ func lockAdminMessage(ctx context.Context, tx *sqlx.Tx, tenantID t.TenantID, con if row.DeletedAt.Valid { status = "deleted" } - return row.Sender, status, "im_messages", err + return row.Sender, status, messageTable, err } func nullableJSON(raw []byte) any { diff --git a/server/db/mysql/tenant_tables.go b/server/db/mysql/tenant_tables.go new file mode 100644 index 0000000..00547c3 --- /dev/null +++ b/server/db/mysql/tenant_tables.go @@ -0,0 +1,37 @@ +//go:build mysql +// +build mysql + +package mysql + +import ( + "fmt" + + t "github.com/tinode/chat/server/store/types" +) + +func tenantFriendshipsTable(t.TenantID) string { + return "im_friendships" +} + +func tenantMessagesTable(t.TenantID) string { + return "im_messages" +} + +func tenantGroupMessagesTable(_ t.TenantID, month string) (string, bool) { + if !validShardMonth(month) { + return "", false + } + return fmt.Sprintf("im_group_messages_%s", month), true +} + +func validShardMonth(month string) bool { + if len(month) != 6 { + return false + } + for i := range month { + if month[i] < '0' || month[i] > '9' { + return false + } + } + return month[4:] >= "01" && month[4:] <= "12" +} diff --git a/server/db/mysql/tests/mysql_test.go b/server/db/mysql/tests/mysql_test.go index 4538af3..c0707a7 100644 --- a/server/db/mysql/tests/mysql_test.go +++ b/server/db/mysql/tests/mysql_test.go @@ -84,8 +84,8 @@ func TestGroupBusinessTablesCreated(t *testing.T) { if err := db.Get(&count, query, args...); err != nil { t.Fatal(err) } - if count != 0 { - t.Fatalf("tenant business tables contain %d tenant_id columns", count) + if count != len(tables) { + t.Fatalf("tenant business tables with tenant_id: got %d, want %d", count, len(tables)) } } @@ -264,12 +264,12 @@ func TestTenantAuthIsolation(t *testing.T) { } } createTenant := func(code, dbName string) types.TenantID { - res, err := db.Exec("INSERT INTO im_tenant(code,name,db_name,state,created_by,updated_by) VALUES(?,?,?,?,?,?)", - code+suffix, code, dbName, types.TenantStateActive, 0, 0) - if err != nil { + var id int64 + if err := db.Get(&id, "SELECT COALESCE(MAX(id),0)+1 FROM im_tenant"); err != nil { t.Fatal(err) } - id, err := res.LastInsertId() + _, err := db.Exec("INSERT INTO im_tenant(id,code,name,db_name,state,created_by,updated_by) VALUES(?,?,?,?,?,?,?)", + id, code+suffix, code, dbName, types.TenantStateActive, 0, 0) if err != nil { t.Fatal(err) }