From bf9ce2188b5dcd6ae495ead3e99cf42bc24748f1 Mon Sep 17 00:00:00 2001 From: OpenIM-Gordon <46924906+FGadvancer@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:29:09 +0800 Subject: [PATCH] fix(db): add conversation order index --- pkg/db/conversation_index_test.go | 60 +++++++++++++++++++++++++++++++ pkg/db/conversation_model.go | 7 ++-- pkg/db/db_init.go | 25 ++++++++++++- 3 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 pkg/db/conversation_index_test.go diff --git a/pkg/db/conversation_index_test.go b/pkg/db/conversation_index_test.go new file mode 100644 index 000000000..e2f8e7d36 --- /dev/null +++ b/pkg/db/conversation_index_test.go @@ -0,0 +1,60 @@ +// Copyright © 2026 OpenIM SDK. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !js +// +build !js + +package db + +import ( + "context" + "strings" + "testing" +) + +func TestConversationListSplitUsesVisibleOrderIndex(t *testing.T) { + ctx := context.Background() + database, err := NewDataBase(ctx, "conversation_index_user", t.TempDir(), 0) + if err != nil { + t.Fatalf("NewDataBase failed: %v", err) + } + t.Cleanup(func() { + if err := database.Close(ctx); err != nil { + t.Errorf("Close failed: %v", err) + } + }) + + type explainRow struct { + Detail string + } + var rows []explainRow + err = database.conn.WithContext(ctx). + Raw("EXPLAIN QUERY PLAN SELECT * FROM local_conversations WHERE latest_msg_send_time > 0 ORDER BY "+conversationOrder+" LIMIT ?", 50). + Scan(&rows).Error + if err != nil { + t.Fatalf("EXPLAIN QUERY PLAN failed: %v", err) + } + + var details []string + for _, row := range rows { + details = append(details, row.Detail) + } + plan := strings.Join(details, "\n") + if !strings.Contains(plan, "idx_local_conversations_visible_order") { + t.Fatalf("query plan does not use visible order index:\n%s", plan) + } + if strings.Contains(plan, "USE TEMP B-TREE FOR ORDER BY") { + t.Fatalf("query plan still sorts with a temp b-tree:\n%s", plan) + } +} diff --git a/pkg/db/conversation_model.go b/pkg/db/conversation_model.go index 8f47ba3c6..430108e17 100644 --- a/pkg/db/conversation_model.go +++ b/pkg/db/conversation_model.go @@ -33,7 +33,8 @@ import ( ) const ( - batchSize = 200 + batchSize = 200 + conversationOrder = "is_pinned DESC, MAX(latest_msg_send_time,draft_text_time) DESC, conversation_id ASC" ) func (d *DataBase) GetConversationByUserID(ctx context.Context, userID string) (*model_struct.LocalConversation, error) { @@ -46,7 +47,7 @@ func (d *DataBase) GetAllConversationListDB(ctx context.Context) ([]*model_struc d.mRWMutex.Lock() defer d.mRWMutex.Unlock() var conversationList []*model_struct.LocalConversation - err := errs.WrapMsg(d.conn.WithContext(ctx).Where("latest_msg_send_time > ?", 0).Order("case when is_pinned=1 then 0 else 1 end,max(latest_msg_send_time,draft_text_time) DESC").Find(&conversationList).Error, + err := errs.WrapMsg(d.conn.WithContext(ctx).Where("latest_msg_send_time > 0").Order(conversationOrder).Find(&conversationList).Error, "GetAllConversationList failed") if err != nil { return nil, err @@ -87,7 +88,7 @@ func (d *DataBase) GetConversationListSplitDB(ctx context.Context, offset, count d.mRWMutex.Lock() defer d.mRWMutex.Unlock() var conversationList []*model_struct.LocalConversation - return conversationList, errs.WrapMsg(d.conn.WithContext(ctx).Where("latest_msg_send_time > ?", 0).Order("case when is_pinned=1 then 0 else 1 end,max(latest_msg_send_time,draft_text_time) DESC").Offset(offset).Limit(count).Find(&conversationList).Error, + return conversationList, errs.WrapMsg(d.conn.WithContext(ctx).Where("latest_msg_send_time > 0").Order(conversationOrder).Offset(offset).Limit(count).Find(&conversationList).Error, "GetFriendList failed") } func (d *DataBase) BatchInsertConversationList(ctx context.Context, conversationList []*model_struct.LocalConversation) error { diff --git a/pkg/db/db_init.go b/pkg/db/db_init.go index ac7f984c1..d36769f04 100644 --- a/pkg/db/db_init.go +++ b/pkg/db/db_init.go @@ -215,6 +215,9 @@ func (d *DataBase) versionDataMigrate(ctx context.Context) error { if err != nil { return err } + if err = d.ensureConversationIndexes(ctx); err != nil { + return err + } err = d.SetAppSDKVersion(ctx, &model_struct.LocalAppSDKVersion{Version: version.Version}) if err != nil { return err @@ -227,13 +230,33 @@ func (d *DataBase) versionDataMigrate(ctx context.Context) error { if verModel.Version != version.Version { switch version.Version { case "3.8.0": - d.conn.AutoMigrate(&model_struct.LocalAppSDKVersion{}) + if err = d.conn.AutoMigrate(&model_struct.LocalAppSDKVersion{}); err != nil { + return err + } + } + if err = d.ensureConversationIndexes(ctx); err != nil { + return err } err = d.SetAppSDKVersion(ctx, &model_struct.LocalAppSDKVersion{Version: version.Version}) if err != nil { return err } + return nil } return nil } + +func (d *DataBase) ensureConversationIndexes(ctx context.Context) error { + statements := []string{ + "CREATE INDEX IF NOT EXISTS idx_local_conversations_visible_order ON " + + "local_conversations (is_pinned DESC, MAX(latest_msg_send_time,draft_text_time) " + + "DESC, conversation_id ASC) WHERE latest_msg_send_time > 0", + } + for _, statement := range statements { + if err := d.conn.WithContext(ctx).Exec(statement).Error; err != nil { + return errs.WrapMsg(err, "ensure conversation indexes failed", "sql", statement) + } + } + return nil +}