diff --git a/server/core_storage.go b/server/core_storage.go index 48b2069ad6..9414fae027 100644 --- a/server/core_storage.go +++ b/server/core_storage.go @@ -25,6 +25,7 @@ import ( "errors" "fmt" "sort" + "strings" "github.com/gofrs/uuid/v5" "github.com/heroiclabs/nakama-common/api" @@ -416,10 +417,6 @@ func storageListObjects(rows *sql.Rows, limit int) (*api.StorageObjectList, erro } func StorageReadObjects(ctx context.Context, logger *zap.Logger, db *sql.DB, caller uuid.UUID, objectIDs []*api.ReadStorageObjectId) (*api.StorageObjects, error) { - collectionParam := make([]string, 0, len(objectIDs)) - keyParam := make([]string, 0, len(objectIDs)) - userIdParam := make([]uuid.UUID, 0, len(objectIDs)) - // When selecting variable number of object we'd like to keep number of // SQL query arguments constant, otherwise query statistics explode, because // from PostgreSQL perspective query with different number of arguments is a distinct query @@ -451,9 +448,24 @@ func StorageReadObjects(ctx context.Context, logger *zap.Logger, db *sql.DB, cal ` } - for _, id := range objectIDs { - collectionParam = append(collectionParam, id.Collection) - keyParam = append(keyParam, id.Key) + collectionParamStr := &strings.Builder{} + collectionParamStr.Grow(2 + len(objectIDs)*128) + collectionParamStr.WriteString("{") + keyParamStr := &strings.Builder{} + keyParamStr.Grow(2 + len(objectIDs)*128) + keyParamStr.WriteString("{") + userIdParamStr := &strings.Builder{} + userIdParamStr.Grow(2 + len(objectIDs)*36) + userIdParamStr.WriteString("{") + + for idx, id := range objectIDs { + if idx > 0 { + collectionParamStr.WriteString(",") + keyParamStr.WriteString(",") + userIdParamStr.WriteString(",") + } + collectionParamStr.WriteString(id.Collection) + keyParamStr.WriteString(id.Key) var reqUid uuid.UUID if uid := id.GetUserId(); uid != "" { if uid, err := uuid.FromString(uid); err == nil { @@ -463,10 +475,14 @@ func StorageReadObjects(ctx context.Context, logger *zap.Logger, db *sql.DB, cal return nil, err } } - userIdParam = append(userIdParam, reqUid) + userIdParamStr.WriteString(reqUid.String()) } - params := []interface{}{collectionParam, keyParam, userIdParam} + collectionParamStr.WriteString("}") + keyParamStr.WriteString("}") + userIdParamStr.WriteString("}") + + params := []interface{}{collectionParamStr.String(), keyParamStr.String(), userIdParamStr.String()} if caller != uuid.Nil { params = append(params, caller) } @@ -635,7 +651,7 @@ func storagePrepBatch(batch *pgx.Batch, authoritativeWrite bool, op *StorageOpWr query = ` WITH upd AS ( UPDATE storage SET value = $4, version = $5, read = $6, write = $7, update_time = now() - WHERE collection = $1 AND key = $2 AND user_id = $3::UUID AND version = $8 + WHERE collection = $1 AND key = $2 AND user_id = $3 AND version = $8 ` + writeCheck + ` AND NOT (storage.version = $5 AND storage.read = $6 AND storage.write = $7) -- micro optimization: don't update row unnecessary RETURNING read, write, version @@ -662,7 +678,7 @@ func storagePrepBatch(batch *pgx.Batch, authoritativeWrite bool, op *StorageOpWr query = ` WITH upd AS ( INSERT INTO storage (collection, key, user_id, value, version, read, write, create_time, update_time) - VALUES ($1, $2, $3::UUID, $4, $5, $6, $7, now(), now()) + VALUES ($1, $2, $3, $4, $5, $6, $7, now(), now()) ON CONFLICT (collection, key, user_id) DO UPDATE SET value = $4, version = $5, read = $6, write = $7, update_time = now() WHERE TRUE` + writeCheck + ` @@ -683,7 +699,7 @@ func storagePrepBatch(batch *pgx.Batch, authoritativeWrite bool, op *StorageOpWr // Existing permission checks are not applicable for new storage objects. query = ` INSERT INTO storage (collection, key, user_id, value, version, read, write, create_time, update_time) - VALUES ($1, $2, $3::UUID, $4, $5, $6, $7, now(), now()) + VALUES ($1, $2, $3, $4, $5, $6, $7, now(), now()) RETURNING read, write, version` // Outcomes: