From 3f320da8840869dafc531082bb940591b2d6d3d3 Mon Sep 17 00:00:00 2001 From: or-else Date: Wed, 18 Jun 2025 16:02:45 +0300 Subject: [PATCH] minor cleanup in db adapters --- INSTALL.md | 4 ++-- server/db/common/common.go | 16 +++++++++++++++ server/db/mysql/adapter.go | 33 +++++++++++-------------------- server/db/postgres/adapter.go | 19 ++++-------------- server/store/types/types.go | 2 +- server/tinode.conf | 8 ++++---- server/utils.go | 2 +- server/validate/email/validate.go | 4 ++-- 8 files changed, 41 insertions(+), 47 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 07a91812..3614dd0f 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -6,7 +6,7 @@ The config file [`tinode.conf`](./server/tinode.conf) contains extensive instruc 1. Visit the [Releases page](https://github.com/tinode/chat/releases/), choose the latest or otherwise the most suitable release. From the list of binaries download the one for your database and platform. Once the binary is downloaded, unpack it to a directory of your choosing, `cd` to that directory. -2. Make sure your database is running. Make sure it's configured to accept connections from `localhost`. In case of MySQL, Tinode will try to connect as `root` without the password. In case of PostgreSQL, Tinode will try connect as `postgres` with the password `postgres`. See notes below (_Building from Source_, section 4) on how to configure Tinode to use a different user or a password. MySQL 5.7 or above is required. MySQL 5.6 or below **will not work**. PostgreSQL 13 or above is required. PostgreSQL 12 or below **will not work**. +2. Make sure your database is running. Make sure it's configured to accept connections from `localhost`. In case of MySQL, Tinode will try to connect as `root` without the password. In case of PostgreSQL, Tinode will try connect as `postgres` with the password `postgres`. See notes below (_Building from Source_, section 4) on how to configure Tinode to use a different user or a password. MySQL 5.7 or above is required (use InnoDB, not MyISAM storage engine). MySQL 5.6 or below **will not work**. PostgreSQL 13 or above is required. PostgreSQL 12 or below **will not work**. 3. Run the database initializer `init-db` (or `init-db.exe` on Windows): ``` @@ -33,7 +33,7 @@ See [instructions](./docker/README.md) 2. OPTIONAL only if you intend to modify the code: Install [protobuf](https://developers.google.com/protocol-buffers/) and [gRPC](https://grpc.io/docs/languages/go/quickstart/) including [code generator](https://developers.google.com/protocol-buffers/docs/reference/go-generated) for Go. 3. Make sure one of the following databases is installed and running: - * MySQL 5.7 or above. MySQL 5.6 or below **will not work**. + * MySQL 5.7 or above configured with `InnoDB` engine. MySQL 5.6 or below **will not work**. * PostgreSQL 13 or above. PostgreSQL 12 or below **will not work**. * MongoDB 4.4 or above. MongoDB 4.2 and below **will not work**. * RethinkDB (deprecated, support will be dropped in 2027). diff --git a/server/db/common/common.go b/server/db/common/common.go index 8cdf07b9..c9467b4d 100644 --- a/server/db/common/common.go +++ b/server/db/common/common.go @@ -4,9 +4,11 @@ package common import ( "encoding/json" "sort" + "strconv" "strings" "time" + "github.com/tinode/chat/server/store" t "github.com/tinode/chat/server/store/types" ) @@ -157,3 +159,17 @@ func ExtractTags(update map[string]any) []string { return []string(tags) } + +// EncodeUidString takes decoded string representation of int64, produce UID. +// UIDs are stored as decoded int64 values. +func EncodeUidString(str string) t.Uid { + unum, _ := strconv.ParseInt(str, 10, 64) + return store.EncodeUid(unum) +} + +// DecodeUidString takes UID as string, converts it to int64 representation. +// UIDs are stored as decoded int64 values. +func DecodeUidString(str string) int64 { + uid := t.ParseUid(str) + return store.DecodeUid(uid) +} diff --git a/server/db/mysql/adapter.go b/server/db/mysql/adapter.go index 098412c0..ee482b2d 100644 --- a/server/db/mysql/adapter.go +++ b/server/db/mysql/adapter.go @@ -315,7 +315,7 @@ func (a *adapter) CreateDb(reset bool) error { } } - if _, err = tx.Exec("CREATE DATABASE " + a.dbName + " CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); err != nil { + if _, err = tx.Exec("CREATE DATABASE " + a.dbName + " CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci"); err != nil { return err } @@ -1091,7 +1091,7 @@ func (a *adapter) UserGetAll(ids ...t.Uid) ([]t.User, error) { continue } - user.SetUid(encodeUidString(user.Id)) + user.SetUid(common.EncodeUidString(user.Id)) user.Public = common.FromJSON(user.Public) user.Trusted = common.FromJSON(user.Trusted) @@ -1595,7 +1595,7 @@ func (a *adapter) TopicGet(topic string) (*t.Topic, error) { return nil, err } - tt.Owner = encodeUidString(tt.Owner).String() + tt.Owner = common.EncodeUidString(tt.Owner).String() tt.Public = common.FromJSON(tt.Public) tt.Trusted = common.FromJSON(tt.Trusted) @@ -1798,7 +1798,7 @@ func (a *adapter) TopicsForUser(uid t.Uid, keepDeleted bool, opts *t.QueryOpt) ( break } - joinOn := uid.P2PName(encodeUidString(usr2.Id)) + joinOn := uid.P2PName(common.EncodeUidString(usr2.Id)) if sub, ok := join[joinOn]; ok { sub.UpdatedAt = common.SelectLatestTime(sub.UpdatedAt, usr2.UpdatedAt) sub.SetState(usr2.State) @@ -1897,7 +1897,7 @@ func (a *adapter) UsersForTopic(topic string, keepDeleted bool, opts *t.QueryOpt break } - sub.User = encodeUidString(sub.User).String() + sub.User = common.EncodeUidString(sub.User).String() sub.Private = common.FromJSON(sub.Private) sub.SetPublic(common.FromJSON(public)) sub.SetTrusted(common.FromJSON(trusted)) @@ -2238,7 +2238,7 @@ func (a *adapter) SubsForTopic(topic string, keepDeleted bool, opts *t.QueryOpt) break } - ss.User = encodeUidString(ss.User).String() + ss.User = common.EncodeUidString(ss.User).String() ss.Private = common.FromJSON(ss.Private) subs = append(subs, ss) } @@ -2599,7 +2599,7 @@ func (a *adapter) MessageGetAll(topic string, forUser t.Uid, opts *t.QueryOpt) ( if err = rows.StructScan(&msg); err != nil { break } - msg.From = encodeUidString(msg.From).String() + msg.From = common.EncodeUidString(msg.From).String() msg.Content = common.FromJSON(msg.Content) msgs = append(msgs, msg) } @@ -2771,7 +2771,7 @@ func messageDeleteList(tx *sqlx.Tx, topic string, toDel *t.DelMessage) error { return err } - forUser := decodeUidString(toDel.DeletedFor) + forUser := common.DecodeUidString(toDel.DeletedFor) for _, rng := range delRanges { if rng.Hi == 0 { // Dellog must contain valid Low and *Hi*. @@ -2971,7 +2971,7 @@ func (a *adapter) CredUpsert(cred *t.Credential) (bool, error) { }() now := t.TimeNow() - userId := decodeUidString(cred.User) + userId := common.DecodeUidString(cred.User) // Enforce uniqueness: if credential is confirmed, "method:value" must be unique. // if credential is not yet confirmed, "userid:method:value" is unique. @@ -3277,8 +3277,8 @@ func (a *adapter) FileGet(fid string) (*t.FileDef, error) { return nil, err } - fd.Id = encodeUidString(fd.Id).String() - fd.User = encodeUidString(fd.User).String() + fd.Id = common.EncodeUidString(fd.Id).String() + fd.User = common.EncodeUidString(fd.User).String() return &fd, nil @@ -3522,17 +3522,6 @@ func isMissingDb(err error) bool { return ok && myerr.Number == 1049 } -// UIDs are stored as decoded int64 values. Take decoded string representation of int64, produce UID. -func encodeUidString(str string) t.Uid { - unum, _ := strconv.ParseInt(str, 10, 64) - return store.EncodeUid(unum) -} - -func decodeUidString(str string) int64 { - uid := t.ParseUid(str) - return store.DecodeUid(uid) -} - func init() { store.RegisterAdapter(&adapter{}) } diff --git a/server/db/postgres/adapter.go b/server/db/postgres/adapter.go index 0159f01d..f034bb9e 100644 --- a/server/db/postgres/adapter.go +++ b/server/db/postgres/adapter.go @@ -2278,7 +2278,7 @@ func (a *adapter) SubsDelForUser(user t.Uid, hard bool) error { } -// Find returns a list of users and group topics which match teh given tags, such as "email:jdoe@example.com" or "tel:+18003287448". +// Find returns a list of users and group topics which match the given tags, such as "email:jdoe@example.com" or "tel:+18003287448". func (a *adapter) Find(caller, promoPrefix string, req [][]string, opt []string, activeOnly bool) ([]t.Subscription, error) { index := make(map[string]struct{}) var args []any @@ -2693,7 +2693,7 @@ func messageDeleteList(ctx context.Context, tx pgx.Tx, topic string, toDel *t.De // Now make log entries. Needed for both hard- and soft-deleting. // Prepare statement is not needed because the driver prepares the statement on first use then caches it. - forUser := decodeUidString(toDel.DeletedFor) + forUser := common.DecodeUidString(toDel.DeletedFor) for _, rng := range toDel.SeqIdRanges { if rng.Hi == 0 { // Dellog must contain valid Low and *Hi*. @@ -2895,7 +2895,7 @@ func (a *adapter) CredUpsert(cred *t.Credential) (bool, error) { }() now := t.TimeNow() - userId := decodeUidString(cred.User) + userId := common.DecodeUidString(cred.User) // Enforce uniqueness: if credential is confirmed, "method:value" must be unique. // if credential is not yet confirmed, "userid:method:value" is unique. @@ -3466,18 +3466,7 @@ func isMissingDb(err error) bool { return strings.Contains(msg, "SQLSTATE 3D000") } -// UIDs are stored as decoded int64 values. Take decoded string representation of int64, produce UID. -func encodeUidString(str string) t.Uid { - unum, _ := strconv.ParseInt(str, 10, 64) - return store.EncodeUid(unum) -} - -func decodeUidString(str string) int64 { - uid := t.ParseUid(str) - return store.DecodeUid(uid) -} - -// Converting a structure with data to enter a connection string +// setConnStr converts a config structure to a DSN connection string. func setConnStr(c configType) (string, error) { // Default to disable SSL mode. sslMode := "disable" diff --git a/server/store/types/types.go b/server/store/types/types.go index 57a74751..03d56165 100644 --- a/server/store/types/types.go +++ b/server/store/types/types.go @@ -8,10 +8,10 @@ import ( "encoding/binary" "encoding/json" "errors" + "slices" "sort" "strings" "time" - "slices" ) // StoreError satisfies Error interface but allows constant values for diff --git a/server/tinode.conf b/server/tinode.conf index c0fb477f..c49addda 100644 --- a/server/tinode.conf +++ b/server/tinode.conf @@ -289,16 +289,16 @@ "Net": "tcp", "Addr": "localhost", "DBName": "tinode", - // The 'collation=utf8mb4_unicode_ci' is optional but highly recommended for - // emoji and certain CJK characters. - "Collation": "utf8mb4_unicode_ci", + // The 'collation=utf8mb4_0900_ai_ci' is default in MySQL 8.0 and above. It is optional but highly + // recommended for emoji and certain CJK characters in earlier versions of MySQL. + "Collation": "utf8mb4_0900_ai_ci", // Parse time values to time.Time. Required. "ParseTime": true, // DSN: alternative way of specifying database configuration, passed unchanged // to MySQL driver. See https://github.com/go-sql-driver/mysql#dsn-data-source-name for syntax. // DSN may optionally start with mysql:// - // "dsn": "root@tcp(localhost)/tinode?parseTime=true&collation=utf8mb4_unicode_ci", + // "dsn": "root@tcp(localhost)/tinode?parseTime=true&collation=utf8mb4_0900_ai_ci", // MySQL connection pool settings. // Maximum number of open connections to the database. Default: 0 (unlimited). diff --git a/server/utils.go b/server/utils.go index b22849df..dfdabd42 100644 --- a/server/utils.go +++ b/server/utils.go @@ -502,7 +502,7 @@ func validateTag(tag string) (string, string) { // hasDuplicateNamespaceTags checks for duplication of unique NS tags. // Each namespace can have only one tag. This does not prevent tags from -// being duplicate accross requests, just saves an extra DB call. +// being duplicate across requests, just saves an extra DB call. func hasDuplicateNamespaceTags(src []string, uniqueNS string) bool { found := map[string]bool{} for _, tag := range src { diff --git a/server/validate/email/validate.go b/server/validate/email/validate.go index 6988a62a..49edf321 100644 --- a/server/validate/email/validate.go +++ b/server/validate/email/validate.go @@ -275,9 +275,9 @@ func (v *validator) Request(user t.Uid, email, lang, resp string, tmpToken []byt var template *textt.Template if v.langMatcher != nil { // Find the template for the requested language. - // Make sure the language tag is standartized. Matcher is a bit dumber than Parse(). + // Make sure the language tag is standardized. Matcher is a bit dumber than Parse(). normalized, _ := i18n.Parse(lang) - // The matched tag is usualy not in the list of available languages (e.g. es_ES -> es-u-rg-eszzzz). + // The matched tag is usually not in the list of available languages (e.g. es_ES -> es-u-rg-eszzzz). // Use index to find the template instead of tag. _, idx := i18n.MatchStrings(v.langMatcher, normalized.String()) template = v.validationTempl[idx]