From ef98e54f35dcd31b0bce552c3aec67a43f03d54e Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Wed, 26 Jul 2023 07:29:15 +0100 Subject: [PATCH] storage: switch to native UUID type in StorageWrite --- go.mod | 1 + go.sum | 2 + server/api_storage.go | 7 +- server/console_storage.go | 4 +- server/console_storage_import.go | 10 +- server/core_storage.go | 14 +- server/core_storage_test.go | 137 +++-- server/runtime_go.go | 2 +- server/runtime_go_nakama.go | 20 +- server/runtime_javascript.go | 4 +- server/runtime_javascript_nakama.go | 4 +- server/runtime_lua.go | 4 +- server/runtime_lua_nakama.go | 8 +- server/storage_index.go | 18 +- server/storage_index_test.go | 49 +- vendor/golang.org/x/exp/LICENSE | 27 + vendor/golang.org/x/exp/PATENTS | 22 + .../x/exp/constraints/constraints.go | 50 ++ vendor/golang.org/x/exp/slices/cmp.go | 44 ++ vendor/golang.org/x/exp/slices/slices.go | 499 ++++++++++++++++++ vendor/golang.org/x/exp/slices/sort.go | 195 +++++++ .../golang.org/x/exp/slices/zsortanyfunc.go | 479 +++++++++++++++++ .../golang.org/x/exp/slices/zsortordered.go | 481 +++++++++++++++++ vendor/modules.txt | 4 + 24 files changed, 1945 insertions(+), 140 deletions(-) create mode 100644 vendor/golang.org/x/exp/LICENSE create mode 100644 vendor/golang.org/x/exp/PATENTS create mode 100644 vendor/golang.org/x/exp/constraints/constraints.go create mode 100644 vendor/golang.org/x/exp/slices/cmp.go create mode 100644 vendor/golang.org/x/exp/slices/slices.go create mode 100644 vendor/golang.org/x/exp/slices/sort.go create mode 100644 vendor/golang.org/x/exp/slices/zsortanyfunc.go create mode 100644 vendor/golang.org/x/exp/slices/zsortordered.go diff --git a/go.mod b/go.mod index 8e800b6193..af63d81416 100644 --- a/go.mod +++ b/go.mod @@ -74,6 +74,7 @@ require ( github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.10.1 // indirect go.uber.org/multierr v1.6.0 // indirect + golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect golang.org/x/net v0.12.0 // indirect golang.org/x/sys v0.10.0 // indirect golang.org/x/text v0.11.0 // indirect diff --git a/go.sum b/go.sum index e148cc13e5..11109fe830 100644 --- a/go.sum +++ b/go.sum @@ -610,6 +610,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY= +golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= diff --git a/server/api_storage.go b/server/api_storage.go index 5d49b6378b..6106b50b4e 100644 --- a/server/api_storage.go +++ b/server/api_storage.go @@ -153,12 +153,13 @@ func (s *ApiServer) ReadStorageObjects(ctx context.Context, in *api.ReadStorageO } func (s *ApiServer) WriteStorageObjects(ctx context.Context, in *api.WriteStorageObjectsRequest) (*api.StorageObjectAcks, error) { - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID).String() + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + userIDStr := userID.String() // Before hook. if fn := s.runtime.BeforeWriteStorageObjects(); fn != nil { beforeFn := func(clientIP, clientPort string) error { - result, err, code := fn(ctx, s.logger, userID, ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + result, err, code := fn(ctx, s.logger, userIDStr, ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) if err != nil { return status.Error(code, err.Error()) } @@ -225,7 +226,7 @@ func (s *ApiServer) WriteStorageObjects(ctx context.Context, in *api.WriteStorag // After hook. if fn := s.runtime.AfterWriteStorageObjects(); fn != nil { afterFn := func(clientIP, clientPort string) error { - return fn(ctx, s.logger, userID, ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, acks, in) + return fn(ctx, s.logger, userIDStr, ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, acks, in) } // Execute the after function lambda wrapped in a trace for stats measurement. diff --git a/server/console_storage.go b/server/console_storage.go index 190f1b3b93..711d92eea4 100644 --- a/server/console_storage.go +++ b/server/console_storage.go @@ -313,7 +313,7 @@ func (s *ConsoleServer) WriteStorageObject(ctx context.Context, in *console.Writ if in.Key == "" { return nil, status.Error(codes.InvalidArgument, "Requires a valid key.") } - _, err := uuid.FromString(in.UserId) + userID, err := uuid.FromString(in.UserId) if err != nil { return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.") } @@ -336,7 +336,7 @@ func (s *ConsoleServer) WriteStorageObject(ctx context.Context, in *console.Writ acks, code, err := StorageWriteObjects(ctx, s.logger, s.db, s.metrics, s.storageIndex, true, StorageOpWrites{ &StorageOpWrite{ - OwnerID: in.UserId, + OwnerID: userID, Object: &api.WriteStorageObject{ Collection: in.Collection, Key: in.Key, diff --git a/server/console_storage_import.go b/server/console_storage_import.go index 2c7473a37b..ed7186e747 100644 --- a/server/console_storage_import.go +++ b/server/console_storage_import.go @@ -155,7 +155,8 @@ func importStorageJSON(ctx context.Context, logger *zap.Logger, db *sql.DB, metr } for i, d := range importedData { - if _, err := uuid.FromString(d.UserID); err != nil { + userID, err := uuid.FromString(d.UserID) + if err != nil { return fmt.Errorf("invalid user ID on object #%d", i) } @@ -184,7 +185,7 @@ func importStorageJSON(ctx context.Context, logger *zap.Logger, db *sql.DB, metr } ops = append(ops, &StorageOpWrite{ - OwnerID: d.UserID, + OwnerID: userID, Object: &api.WriteStorageObject{ Collection: d.Collection, Key: d.Key, @@ -255,7 +256,8 @@ func importStorageCSV(ctx context.Context, logger *zap.Logger, db *sql.DB, metri } } else { user := record[columnIndexes["user_id"]] - if _, err := uuid.FromString(user); err != nil { + userID, err := uuid.FromString(user) + if err != nil { return fmt.Errorf("invalid user ID on row #%d", len(ops)+1) } collection := record[columnIndexes["collection"]] @@ -283,7 +285,7 @@ func importStorageCSV(ctx context.Context, logger *zap.Logger, db *sql.DB, metri } ops = append(ops, &StorageOpWrite{ - OwnerID: user, + OwnerID: userID, Object: &api.WriteStorageObject{ Collection: collection, Key: key, diff --git a/server/core_storage.go b/server/core_storage.go index 48b2069ad6..5b0b15edad 100644 --- a/server/core_storage.go +++ b/server/core_storage.go @@ -26,6 +26,8 @@ import ( "fmt" "sort" + "golang.org/x/exp/slices" + "github.com/gofrs/uuid/v5" "github.com/heroiclabs/nakama-common/api" "github.com/heroiclabs/nakama-common/runtime" @@ -47,7 +49,7 @@ type storageCursor struct { type StorageOpWrites []*StorageOpWrite type StorageOpWrite struct { - OwnerID string + OwnerID uuid.UUID Object *api.WriteStorageObject } @@ -87,7 +89,7 @@ func (s StorageOpWrites) Less(i, j int) bool { if s1.Object.Key != s2.Object.Key { return s1.Object.Key < s2.Object.Key } - return s1.OwnerID < s2.OwnerID + return slices.Compare(s1.OwnerID.Bytes(), s2.OwnerID.Bytes()) == -1 } // Internal representation for a batch of storage delete operations. @@ -598,7 +600,7 @@ func storageWriteObjects(ctx context.Context, logger *zap.Logger, metrics Metric Collection: object.Collection, Key: object.Key, Version: resultVersion, - UserId: op.OwnerID, + UserId: op.OwnerID.String(), } acks[indexedOps[op]] = ack } @@ -635,7 +637,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 +664,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 +685,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: diff --git a/server/core_storage_test.go b/server/core_storage_test.go index 7ea3d5b2f7..f71655c31e 100644 --- a/server/core_storage_test.go +++ b/server/core_storage_test.go @@ -36,7 +36,6 @@ func TestStorageWriteRuntimeGlobalSingle(t *testing.T) { key := GenerateString() ops := StorageOpWrites{&StorageOpWrite{ - OwnerID: uuid.Nil.String(), Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -86,7 +85,7 @@ func TestStorageWriteRuntimeUserMultiple(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: u0.String(), + OwnerID: u0, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: GenerateString(), @@ -96,7 +95,7 @@ func TestStorageWriteRuntimeUserMultiple(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: u1.String(), + OwnerID: u1, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: GenerateString(), @@ -106,7 +105,7 @@ func TestStorageWriteRuntimeUserMultiple(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: u2.String(), + OwnerID: u2, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: GenerateString(), @@ -137,7 +136,7 @@ func TestStorageWriteRuntimeGlobalSingleIfMatchNotExists(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uuid.Nil.String(), + OwnerID: uuid.Nil, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: GenerateString(), @@ -163,7 +162,7 @@ func TestStorageWriteRuntimeGlobalSingleIfMatchExists(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uuid.Nil.String(), + OwnerID: uuid.Nil, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: GenerateString(), @@ -187,7 +186,7 @@ func TestStorageWriteRuntimeGlobalSingleIfMatchExists(t *testing.T) { ops = StorageOpWrites{ &StorageOpWrite{ - OwnerID: uuid.Nil.String(), + OwnerID: uuid.Nil, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: ops[0].Object.Key, @@ -217,7 +216,7 @@ func TestStorageWriteRuntimeGlobalSingleIfMatchExistsFail(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uuid.Nil.String(), + OwnerID: uuid.Nil, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: GenerateString(), @@ -241,7 +240,7 @@ func TestStorageWriteRuntimeGlobalSingleIfMatchExistsFail(t *testing.T) { ops = StorageOpWrites{ &StorageOpWrite{ - OwnerID: uuid.Nil.String(), + OwnerID: uuid.Nil, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: ops[0].Object.Key, @@ -267,7 +266,7 @@ func TestStorageWriteRuntimeGlobalSingleIfNoneMatchNotExists(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uuid.Nil.String(), + OwnerID: uuid.Nil, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: GenerateString(), @@ -296,7 +295,7 @@ func TestStorageWriteRuntimeGlobalSingleIfNoneMatchExists(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uuid.Nil.String(), + OwnerID: uuid.Nil, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: GenerateString(), @@ -319,7 +318,7 @@ func TestStorageWriteRuntimeGlobalSingleIfNoneMatchExists(t *testing.T) { ops = StorageOpWrites{ &StorageOpWrite{ - OwnerID: uuid.Nil.String(), + OwnerID: uuid.Nil, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: ops[0].Object.Key, @@ -345,7 +344,7 @@ func TestStorageWriteRuntimeGlobalMultipleIfMatchNotExists(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uuid.Nil.String(), + OwnerID: uuid.Nil, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: GenerateString(), @@ -355,7 +354,7 @@ func TestStorageWriteRuntimeGlobalMultipleIfMatchNotExists(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: uuid.Nil.String(), + OwnerID: uuid.Nil, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: GenerateString(), @@ -384,7 +383,7 @@ func TestStorageWritePipelineUserSingle(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: GenerateString(), @@ -415,7 +414,7 @@ func TestStorageWritePipelineUserMultiple(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: GenerateString(), @@ -425,7 +424,7 @@ func TestStorageWritePipelineUserMultiple(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: GenerateString(), @@ -435,7 +434,7 @@ func TestStorageWritePipelineUserMultiple(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: GenerateString(), @@ -478,7 +477,7 @@ func TestStorageWriteRuntimeGlobalMultipleSameKey(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uuid.Nil.String(), + OwnerID: uuid.Nil, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -488,7 +487,7 @@ func TestStorageWriteRuntimeGlobalMultipleSameKey(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: uuid.Nil.String(), + OwnerID: uuid.Nil, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -498,7 +497,7 @@ func TestStorageWriteRuntimeGlobalMultipleSameKey(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: uuid.Nil.String(), + OwnerID: uuid.Nil, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -557,7 +556,7 @@ func TestStorageWritePipelineUserMultipleSameKey(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -567,7 +566,7 @@ func TestStorageWritePipelineUserMultipleSameKey(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -620,7 +619,7 @@ func TestStorageWritePipelineIfMatchNotExists(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: GenerateString(), @@ -649,7 +648,7 @@ func TestStorageWritePipelineIfMatchExistsFail(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: GenerateString(), @@ -672,7 +671,7 @@ func TestStorageWritePipelineIfMatchExistsFail(t *testing.T) { ops = StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: GenerateString(), @@ -702,7 +701,7 @@ func TestStorageWritePipelineIfMatchExists(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -725,7 +724,7 @@ func TestStorageWritePipelineIfMatchExists(t *testing.T) { ops = StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -757,7 +756,7 @@ func TestStorageWritePipelineIfNoneMatchNotExists(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: GenerateString(), @@ -790,7 +789,7 @@ func TestStorageWritePipelineIfNoneMatchExists(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -813,7 +812,7 @@ func TestStorageWritePipelineIfNoneMatchExists(t *testing.T) { ops = StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -843,7 +842,7 @@ func TestStorageWritePipelinePermissionFail(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -866,7 +865,7 @@ func TestStorageWritePipelinePermissionFail(t *testing.T) { ops = StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -893,7 +892,7 @@ func TestStorageFetchRuntimeGlobalPrivate(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uuid.Nil.String(), + OwnerID: uuid.Nil, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -941,7 +940,7 @@ func TestStorageFetchRuntimeMixed(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uuid.Nil.String(), + OwnerID: uuid.Nil, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -996,7 +995,7 @@ func TestStorageFetchRuntimeUserPrivate(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -1047,7 +1046,7 @@ func TestStorageFetchPipelineGlobalPrivate(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uuid.Nil.String(), + OwnerID: uuid.Nil, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -1092,7 +1091,7 @@ func TestStorageFetchPipelineUserPrivate(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -1138,7 +1137,7 @@ func TestStorageFetchPipelineUserRead(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -1191,7 +1190,7 @@ func TestStorageFetchPipelineUserPublic(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -1243,7 +1242,7 @@ func TestStorageFetchPipelineUserOtherRead(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -1288,7 +1287,7 @@ func TestStorageFetchPipelineUserOtherPublic(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -1342,7 +1341,7 @@ func TestStorageFetchPipelineUserOtherPublicMixed(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: record1, @@ -1352,7 +1351,7 @@ func TestStorageFetchPipelineUserOtherPublicMixed(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: record2, @@ -1418,7 +1417,7 @@ func TestStorageRemoveRuntimeGlobalPublic(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uuid.Nil.String(), + OwnerID: uuid.Nil, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -1463,7 +1462,7 @@ func TestStorageRemoveRuntimeGlobalPrivate(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uuid.Nil.String(), + OwnerID: uuid.Nil, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -1510,7 +1509,7 @@ func TestStorageRemoveRuntimeUserPublic(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -1557,7 +1556,7 @@ func TestStorageRemoveRuntimeUserPrivate(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -1614,7 +1613,7 @@ func TestStorageRemovePipelineUserWrite(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -1661,7 +1660,7 @@ func TestStorageRemovePipelineUserDenied(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -1727,7 +1726,7 @@ func TestStorageRemoveRuntimeGlobalIfMatchRejected(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uuid.Nil.String(), + OwnerID: uuid.Nil, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -1774,7 +1773,7 @@ func TestStorageRemoveRuntimeGlobalIfMatch(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uuid.Nil.String(), + OwnerID: uuid.Nil, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: key, @@ -1822,7 +1821,7 @@ func TestStorageListRuntimeUser(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: "b", @@ -1832,7 +1831,7 @@ func TestStorageListRuntimeUser(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: "a", @@ -1842,7 +1841,7 @@ func TestStorageListRuntimeUser(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: "testcollection", Key: "c", @@ -1879,7 +1878,7 @@ func TestStorageListPipelineUserSelf(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: collection, Key: "b", @@ -1889,7 +1888,7 @@ func TestStorageListPipelineUserSelf(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: collection, Key: "a", @@ -1899,7 +1898,7 @@ func TestStorageListPipelineUserSelf(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: collection, Key: "c", @@ -1938,7 +1937,7 @@ func TestStorageListPipelineUserOther(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: collection, Key: "b", @@ -1948,7 +1947,7 @@ func TestStorageListPipelineUserOther(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: collection, Key: "a", @@ -1958,7 +1957,7 @@ func TestStorageListPipelineUserOther(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: collection, Key: "c", @@ -1995,7 +1994,7 @@ func TestStorageListNoRepeats(t *testing.T) { ops := StorageOpWrites{ &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: collection, Key: "1", @@ -2005,7 +2004,7 @@ func TestStorageListNoRepeats(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: collection, Key: "2", @@ -2015,7 +2014,7 @@ func TestStorageListNoRepeats(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: collection, Key: "3", @@ -2025,7 +2024,7 @@ func TestStorageListNoRepeats(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: collection, Key: "4", @@ -2035,7 +2034,7 @@ func TestStorageListNoRepeats(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: collection, Key: "5", @@ -2045,7 +2044,7 @@ func TestStorageListNoRepeats(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: collection, Key: "6", @@ -2055,7 +2054,7 @@ func TestStorageListNoRepeats(t *testing.T) { }, }, &StorageOpWrite{ - OwnerID: uid.String(), + OwnerID: uid, Object: &api.WriteStorageObject{ Collection: collection, Key: "7", @@ -2212,7 +2211,7 @@ func testWrite(t *testing.T, newVal, prevVal string, permWrite int, authoritativ func writeObject(t *testing.T, db *sql.DB, collection, key string, owner uuid.UUID, writePerm int, newV, version string, authoritative bool) (*api.StorageObjectAcks, codes.Code, error) { t.Helper() ops := StorageOpWrites{&StorageOpWrite{ - OwnerID: owner.String(), + OwnerID: owner, Object: &api.WriteStorageObject{ Collection: collection, Key: key, diff --git a/server/runtime_go.go b/server/runtime_go.go index 73a06c0501..9a919165de 100644 --- a/server/runtime_go.go +++ b/server/runtime_go.go @@ -2580,7 +2580,7 @@ func (ri *RuntimeGoInitializer) RegisterStorageIndexFilter(indexName string, fn storageWrite := &runtime.StorageWrite{ Collection: write.Object.Collection, Key: write.Object.Key, - UserID: write.OwnerID, + UserID: write.OwnerID.String(), Value: write.Object.Value, Version: write.Object.Version, PermissionRead: int(write.Object.PermissionRead.GetValue()), diff --git a/server/runtime_go_nakama.go b/server/runtime_go_nakama.go index bb2aa34b67..b5086c1b82 100644 --- a/server/runtime_go_nakama.go +++ b/server/runtime_go_nakama.go @@ -1956,6 +1956,8 @@ func (n *RuntimeGoNakamaModule) StorageWrite(ctx context.Context, writes []*runt ops := make(StorageOpWrites, 0, size) for _, write := range writes { + var userID uuid.UUID + var err error if write.Collection == "" { return nil, errors.New("expects collection to be a non-empty string") } @@ -1963,7 +1965,7 @@ func (n *RuntimeGoNakamaModule) StorageWrite(ctx context.Context, writes []*runt return nil, errors.New("expects key to be a non-empty string") } if write.UserID != "" { - if _, err := uuid.FromString(write.UserID); err != nil { + if userID, err = uuid.FromString(write.UserID); err != nil { return nil, errors.New("expects an empty or valid user id") } } @@ -1972,6 +1974,7 @@ func (n *RuntimeGoNakamaModule) StorageWrite(ctx context.Context, writes []*runt } op := &StorageOpWrite{ + OwnerID: userID, Object: &api.WriteStorageObject{ Collection: write.Collection, Key: write.Key, @@ -1981,11 +1984,6 @@ func (n *RuntimeGoNakamaModule) StorageWrite(ctx context.Context, writes []*runt PermissionWrite: &wrapperspb.Int32Value{Value: int32(write.PermissionWrite)}, }, } - if write.UserID == "" { - op.OwnerID = uuid.Nil.String() - } else { - op.OwnerID = write.UserID - } ops = append(ops, op) } @@ -2127,6 +2125,8 @@ func (n *RuntimeGoNakamaModule) MultiUpdate(ctx context.Context, accountUpdates // Process storage write inputs. storageWriteOps := make(StorageOpWrites, 0, len(storageWrites)) for _, write := range storageWrites { + var userID uuid.UUID + var err error if write.Collection == "" { return nil, nil, errors.New("expects collection to be a non-empty string") } @@ -2134,7 +2134,7 @@ func (n *RuntimeGoNakamaModule) MultiUpdate(ctx context.Context, accountUpdates return nil, nil, errors.New("expects key to be a non-empty string") } if write.UserID != "" { - if _, err := uuid.FromString(write.UserID); err != nil { + if userID, err = uuid.FromString(write.UserID); err != nil { return nil, nil, errors.New("expects an empty or valid user id") } } @@ -2143,6 +2143,7 @@ func (n *RuntimeGoNakamaModule) MultiUpdate(ctx context.Context, accountUpdates } op := &StorageOpWrite{ + OwnerID: userID, Object: &api.WriteStorageObject{ Collection: write.Collection, Key: write.Key, @@ -2152,11 +2153,6 @@ func (n *RuntimeGoNakamaModule) MultiUpdate(ctx context.Context, accountUpdates PermissionWrite: &wrapperspb.Int32Value{Value: int32(write.PermissionWrite)}, }, } - if write.UserID == "" { - op.OwnerID = uuid.Nil.String() - } else { - op.OwnerID = write.UserID - } storageWriteOps = append(storageWriteOps, op) } diff --git a/server/runtime_javascript.go b/server/runtime_javascript.go index ab0bf67e05..2b05e3e454 100644 --- a/server/runtime_javascript.go +++ b/server/runtime_javascript.go @@ -2295,8 +2295,8 @@ func (rp *RuntimeProviderJS) StorageIndexFilter(ctx context.Context, indexName s objectMap := make(map[string]interface{}, 7) objectMap["key"] = storageWrite.Object.Key objectMap["collection"] = storageWrite.Object.Collection - if storageWrite.OwnerID != "" { - objectMap["userId"] = storageWrite.OwnerID + if !storageWrite.OwnerID.IsNil() { + objectMap["userId"] = storageWrite.OwnerID.String() } else { objectMap["userId"] = nil } diff --git a/server/runtime_javascript_nakama.go b/server/runtime_javascript_nakama.go index 2d1b9e63d2..12fce9cd5b 100644 --- a/server/runtime_javascript_nakama.go +++ b/server/runtime_javascript_nakama.go @@ -4539,7 +4539,7 @@ func jsArrayToStorageOpWrites(r *goja.Runtime, dataSlice []any) (StorageOpWrites } ops = append(ops, &StorageOpWrite{ - OwnerID: userID.String(), + OwnerID: userID, Object: writeOp, }) } @@ -4845,7 +4845,7 @@ func (n *runtimeJavascriptNakamaModule) multiUpdate(r *goja.Runtime) func(goja.F } storageWriteOps = append(storageWriteOps, &StorageOpWrite{ - OwnerID: userID.String(), + OwnerID: userID, Object: writeOp, }) } diff --git a/server/runtime_lua.go b/server/runtime_lua.go index e35299754d..a9dc50a0b8 100644 --- a/server/runtime_lua.go +++ b/server/runtime_lua.go @@ -2029,8 +2029,8 @@ func (rp *RuntimeProviderLua) StorageIndexFilter(ctx context.Context, indexName writeTable := r.vm.CreateTable(0, 7) writeTable.RawSetString("key", lua.LString(write.Object.Key)) writeTable.RawSetString("collection", lua.LString(write.Object.Collection)) - if write.OwnerID != "" { - writeTable.RawSetString("user_id", lua.LString(write.OwnerID)) + if !write.OwnerID.IsNil() { + writeTable.RawSetString("user_id", lua.LString(write.OwnerID.String())) } else { writeTable.RawSetString("user_id", lua.LNil) } diff --git a/server/runtime_lua_nakama.go b/server/runtime_lua_nakama.go index ca2d9b5cc1..56aa1691ee 100644 --- a/server/runtime_lua_nakama.go +++ b/server/runtime_lua_nakama.go @@ -5926,7 +5926,7 @@ func tableToStorageWrites(l *lua.LState, dataTable *lua.LTable) (StorageOpWrites } ops = append(ops, &StorageOpWrite{ - OwnerID: userID.String(), + OwnerID: userID, Object: d, }) }) @@ -5940,8 +5940,8 @@ func storageOpWritesToTable(l *lua.LState, ops StorageOpWrites) (*lua.LTable, er vt := l.CreateTable(0, 7) vt.RawSetString("key", lua.LString(v.Object.Key)) vt.RawSetString("collection", lua.LString(v.Object.Collection)) - if v.OwnerID != "" { - vt.RawSetString("user_id", lua.LString(v.OwnerID)) + if !v.OwnerID.IsNil() { + vt.RawSetString("user_id", lua.LString(v.OwnerID.String())) } else { vt.RawSetString("user_id", lua.LNil) } @@ -6338,7 +6338,7 @@ func (n *RuntimeLuaNakamaModule) multiUpdate(l *lua.LState) int { } storageWriteOps = append(storageWriteOps, &StorageOpWrite{ - OwnerID: userID.String(), + OwnerID: userID, Object: d, }) }) diff --git a/server/storage_index.go b/server/storage_index.go index faa3a819ac..01cb0eb308 100644 --- a/server/storage_index.go +++ b/server/storage_index.go @@ -98,7 +98,7 @@ func (si *LocalStorageIndex) Write(ctx context.Context, objects StorageOpWrites) if !insertWrite { // Delete existing document from index, if any. - docId := si.storageIndexDocumentId(so.Object.Collection, so.Object.Key, so.OwnerID) + docId := si.storageIndexDocumentId(so.Object.Collection, so.Object.Key, so.OwnerID.String()) batch.Delete(docId) deletes++ @@ -302,7 +302,7 @@ LIMIT $2` var rowsRead bool batch := bluge.NewBatch() - var dbUserID *uuid.UUID + var dbUserID uuid.UUID var dbKey string for rows.Next() { rowsRead = true @@ -318,7 +318,7 @@ LIMIT $2` if filterFn != nil { ok, err := filterFn(ctx, &StorageOpWrite{ - OwnerID: dbUserID.String(), + OwnerID: dbUserID, Object: &api.WriteStorageObject{ Collection: idx.Collection, Key: dbKey, @@ -336,7 +336,7 @@ LIMIT $2` } } - doc, err := si.mapIndexStorageFields(dbUserID.String(), idx.Collection, dbKey, dbVersion, dbValue, idx.Fields, dbUpdateTime.Time) + doc, err := si.mapIndexStorageFields(dbUserID, idx.Collection, dbKey, dbVersion, dbValue, idx.Fields, dbUpdateTime.Time) if err != nil { si.logger.Error("Failed to map storage object values to index", zap.Error(err)) return err @@ -385,8 +385,8 @@ LIMIT $2` return nil } -func (si *LocalStorageIndex) mapIndexStorageFields(userID, collection, key, version, value string, filters []string, updateTime time.Time) (*bluge.Document, error) { - if collection == "" || key == "" || userID == "" { +func (si *LocalStorageIndex) mapIndexStorageFields(userID uuid.UUID, collection, key, version, value string, filters []string, updateTime time.Time) (*bluge.Document, error) { + if collection == "" || key == "" { return nil, errors.New("insufficient fields to create index document id") } @@ -410,11 +410,11 @@ func (si *LocalStorageIndex) mapIndexStorageFields(userID, collection, key, vers return nil, nil } - rv := bluge.NewDocument(string(si.storageIndexDocumentId(collection, key, userID))) + rv := bluge.NewDocument(string(si.storageIndexDocumentId(collection, key, userID.String()))) rv.AddField(bluge.NewDateTimeField("update_time", updateTime).StoreValue().Sortable()) rv.AddField(bluge.NewKeywordField("collection", collection).StoreValue()) rv.AddField(bluge.NewKeywordField("key", key).StoreValue()) - rv.AddField(bluge.NewKeywordField("user_id", userID).StoreValue()) + rv.AddField(bluge.NewKeywordField("user_id", userID.String()).StoreValue()) rv.AddField(bluge.NewKeywordField("version", version).StoreValue()) BlugeWalkDocument(mapValue, []string{}, rv) @@ -553,7 +553,7 @@ func (si *LocalStorageIndex) RegisterFilters(runtime *Runtime) { } func (si *LocalStorageIndex) storageIndexDocumentId(collection, key, userID string) bluge.Identifier { - id := fmt.Sprintf("%s.%s.%s", collection, key, userID) + id := collection + `.` + key + `.` + userID return bluge.Identifier(id) } diff --git a/server/storage_index_test.go b/server/storage_index_test.go index 57c166b191..453e0480d0 100644 --- a/server/storage_index_test.go +++ b/server/storage_index_test.go @@ -3,11 +3,12 @@ package server import ( "context" "encoding/json" + "sync" + "testing" + "github.com/gofrs/uuid/v5" "github.com/heroiclabs/nakama-common/api" "github.com/stretchr/testify/assert" - "sync" - "testing" ) func TestLocalStorageIndex_Write(t *testing.T) { @@ -67,7 +68,7 @@ func TestLocalStorageIndex_Write(t *testing.T) { t.Run("indexes storage objects matching configured index collection key and fields to correct indices", func(t *testing.T) { so1 := &StorageOpWrite{ - OwnerID: nilUid.String(), + OwnerID: nilUid, Object: &api.WriteStorageObject{ Collection: collection1, Key: key, @@ -75,7 +76,7 @@ func TestLocalStorageIndex_Write(t *testing.T) { }, } so2 := &StorageOpWrite{ - OwnerID: u1.String(), + OwnerID: u1, Object: &api.WriteStorageObject{ Collection: collection1, Key: "key_no_match", @@ -83,7 +84,7 @@ func TestLocalStorageIndex_Write(t *testing.T) { }, } so3 := &StorageOpWrite{ - OwnerID: u2.String(), + OwnerID: u2, Object: &api.WriteStorageObject{ Collection: "collection_no_match", Key: key, @@ -91,7 +92,7 @@ func TestLocalStorageIndex_Write(t *testing.T) { }, } so4 := &StorageOpWrite{ - OwnerID: u3.String(), + OwnerID: u3, Object: &api.WriteStorageObject{ Collection: collection2, Key: key, @@ -99,7 +100,7 @@ func TestLocalStorageIndex_Write(t *testing.T) { }, } so5 := &StorageOpWrite{ - OwnerID: u4.String(), + OwnerID: u4, Object: &api.WriteStorageObject{ Collection: collection1, Key: key, @@ -107,7 +108,7 @@ func TestLocalStorageIndex_Write(t *testing.T) { }, } so6 := &StorageOpWrite{ - OwnerID: u5.String(), + OwnerID: u5, Object: &api.WriteStorageObject{ Collection: collection1, Key: "key2", @@ -136,7 +137,7 @@ func TestLocalStorageIndex_Write(t *testing.T) { delOps := make(StorageOpDeletes, 0, len(writeOps)) for _, op := range writeOps { delOps = append(delOps, &StorageOpDelete{ - OwnerID: op.OwnerID, + OwnerID: op.OwnerID.String(), ObjectID: &api.DeleteStorageObjectId{ Collection: op.Object.Collection, Key: op.Object.Key, @@ -156,7 +157,7 @@ func TestLocalStorageIndex_Write(t *testing.T) { value := string(valueBytes) so1 := &StorageOpWrite{ - OwnerID: nilUid.String(), + OwnerID: nilUid, Object: &api.WriteStorageObject{ Collection: collection1, Key: key, @@ -176,7 +177,7 @@ func TestLocalStorageIndex_Write(t *testing.T) { deletes := StorageOpDeletes{ &StorageOpDelete{ - OwnerID: so1.OwnerID, + OwnerID: so1.OwnerID.String(), ObjectID: &api.DeleteStorageObjectId{ Collection: so1.Object.Collection, Key: so1.Object.Key, @@ -190,7 +191,7 @@ func TestLocalStorageIndex_Write(t *testing.T) { t.Run("allows concurrent writes to index", func(t *testing.T) { so1 := &StorageOpWrite{ - OwnerID: nilUid.String(), + OwnerID: nilUid, Object: &api.WriteStorageObject{ Collection: collection1, Key: key, @@ -228,7 +229,7 @@ func TestLocalStorageIndex_Write(t *testing.T) { t.Run("evicts oldest entries after indexed count is 10% above index max entries", func(t *testing.T) { so1 := &StorageOpWrite{ - OwnerID: nilUid.String(), + OwnerID: nilUid, Object: &api.WriteStorageObject{ Collection: collection1, Key: key, @@ -236,7 +237,7 @@ func TestLocalStorageIndex_Write(t *testing.T) { }, } so2 := &StorageOpWrite{ - OwnerID: u1.String(), + OwnerID: u1, Object: &api.WriteStorageObject{ Collection: collection1, Key: collection1, @@ -244,7 +245,7 @@ func TestLocalStorageIndex_Write(t *testing.T) { }, } so3 := &StorageOpWrite{ - OwnerID: u2.String(), + OwnerID: u2, Object: &api.WriteStorageObject{ Collection: collection1, Key: key, @@ -252,7 +253,7 @@ func TestLocalStorageIndex_Write(t *testing.T) { }, } so4 := &StorageOpWrite{ - OwnerID: u3.String(), + OwnerID: u3, Object: &api.WriteStorageObject{ Collection: collection1, Key: key, @@ -279,7 +280,7 @@ func TestLocalStorageIndex_Write(t *testing.T) { delOps := make(StorageOpDeletes, 0, len(writeOps)) for _, op := range writeOps { delOps = append(delOps, &StorageOpDelete{ - OwnerID: op.OwnerID, + OwnerID: op.OwnerID.String(), ObjectID: &api.DeleteStorageObjectId{ Collection: op.Object.Collection, Key: op.Object.Key, @@ -332,7 +333,7 @@ func TestLocalStorageIndex_List(t *testing.T) { t.Run("returns all matching results for query", func(t *testing.T) { so1 := &StorageOpWrite{ - OwnerID: nilUid.String(), + OwnerID: nilUid, Object: &api.WriteStorageObject{ Collection: collection, Key: key, @@ -340,7 +341,7 @@ func TestLocalStorageIndex_List(t *testing.T) { }, } so2 := &StorageOpWrite{ - OwnerID: u1.String(), + OwnerID: u1, Object: &api.WriteStorageObject{ Collection: collection, Key: key, @@ -348,7 +349,7 @@ func TestLocalStorageIndex_List(t *testing.T) { }, } so3 := &StorageOpWrite{ - OwnerID: u1.String(), + OwnerID: u1, Object: &api.WriteStorageObject{ Collection: collection, Key: key, @@ -372,7 +373,7 @@ func TestLocalStorageIndex_List(t *testing.T) { delOps := make(StorageOpDeletes, 0, len(writeOps)) for _, op := range writeOps { delOps = append(delOps, &StorageOpDelete{ - OwnerID: op.OwnerID, + OwnerID: op.OwnerID.String(), ObjectID: &api.DeleteStorageObjectId{ Collection: op.Object.Collection, Key: op.Object.Key, @@ -414,7 +415,7 @@ func TestLocalStorageIndex_Delete(t *testing.T) { } so1 := &StorageOpWrite{ - OwnerID: nilUid.String(), + OwnerID: nilUid, Object: &api.WriteStorageObject{ Collection: collection, Key: "key1", @@ -422,7 +423,7 @@ func TestLocalStorageIndex_Delete(t *testing.T) { }, } so2 := &StorageOpWrite{ - OwnerID: u1.String(), + OwnerID: u1, Object: &api.WriteStorageObject{ Collection: collection, Key: "key2", @@ -463,7 +464,7 @@ func TestLocalStorageIndex_Delete(t *testing.T) { delOps := make(StorageOpDeletes, 0, len(writeOps)) for _, op := range writeOps { delOps = append(delOps, &StorageOpDelete{ - OwnerID: op.OwnerID, + OwnerID: op.OwnerID.String(), ObjectID: &api.DeleteStorageObjectId{ Collection: op.Object.Collection, Key: op.Object.Key, diff --git a/vendor/golang.org/x/exp/LICENSE b/vendor/golang.org/x/exp/LICENSE new file mode 100644 index 0000000000..6a66aea5ea --- /dev/null +++ b/vendor/golang.org/x/exp/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/golang.org/x/exp/PATENTS b/vendor/golang.org/x/exp/PATENTS new file mode 100644 index 0000000000..733099041f --- /dev/null +++ b/vendor/golang.org/x/exp/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/exp/constraints/constraints.go b/vendor/golang.org/x/exp/constraints/constraints.go new file mode 100644 index 0000000000..2c033dff47 --- /dev/null +++ b/vendor/golang.org/x/exp/constraints/constraints.go @@ -0,0 +1,50 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package constraints defines a set of useful constraints to be used +// with type parameters. +package constraints + +// Signed is a constraint that permits any signed integer type. +// If future releases of Go add new predeclared signed integer types, +// this constraint will be modified to include them. +type Signed interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 +} + +// Unsigned is a constraint that permits any unsigned integer type. +// If future releases of Go add new predeclared unsigned integer types, +// this constraint will be modified to include them. +type Unsigned interface { + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr +} + +// Integer is a constraint that permits any integer type. +// If future releases of Go add new predeclared integer types, +// this constraint will be modified to include them. +type Integer interface { + Signed | Unsigned +} + +// Float is a constraint that permits any floating-point type. +// If future releases of Go add new predeclared floating-point types, +// this constraint will be modified to include them. +type Float interface { + ~float32 | ~float64 +} + +// Complex is a constraint that permits any complex numeric type. +// If future releases of Go add new predeclared complex numeric types, +// this constraint will be modified to include them. +type Complex interface { + ~complex64 | ~complex128 +} + +// Ordered is a constraint that permits any ordered type: any type +// that supports the operators < <= >= >. +// If future releases of Go add new ordered types, +// this constraint will be modified to include them. +type Ordered interface { + Integer | Float | ~string +} diff --git a/vendor/golang.org/x/exp/slices/cmp.go b/vendor/golang.org/x/exp/slices/cmp.go new file mode 100644 index 0000000000..fbf1934a06 --- /dev/null +++ b/vendor/golang.org/x/exp/slices/cmp.go @@ -0,0 +1,44 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package slices + +import "golang.org/x/exp/constraints" + +// min is a version of the predeclared function from the Go 1.21 release. +func min[T constraints.Ordered](a, b T) T { + if a < b || isNaN(a) { + return a + } + return b +} + +// max is a version of the predeclared function from the Go 1.21 release. +func max[T constraints.Ordered](a, b T) T { + if a > b || isNaN(a) { + return a + } + return b +} + +// cmpLess is a copy of cmp.Less from the Go 1.21 release. +func cmpLess[T constraints.Ordered](x, y T) bool { + return (isNaN(x) && !isNaN(y)) || x < y +} + +// cmpCompare is a copy of cmp.Compare from the Go 1.21 release. +func cmpCompare[T constraints.Ordered](x, y T) int { + xNaN := isNaN(x) + yNaN := isNaN(y) + if xNaN && yNaN { + return 0 + } + if xNaN || x < y { + return -1 + } + if yNaN || x > y { + return +1 + } + return 0 +} diff --git a/vendor/golang.org/x/exp/slices/slices.go b/vendor/golang.org/x/exp/slices/slices.go new file mode 100644 index 0000000000..5e8158bba8 --- /dev/null +++ b/vendor/golang.org/x/exp/slices/slices.go @@ -0,0 +1,499 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package slices defines various functions useful with slices of any type. +package slices + +import ( + "unsafe" + + "golang.org/x/exp/constraints" +) + +// Equal reports whether two slices are equal: the same length and all +// elements equal. If the lengths are different, Equal returns false. +// Otherwise, the elements are compared in increasing index order, and the +// comparison stops at the first unequal pair. +// Floating point NaNs are not considered equal. +func Equal[S ~[]E, E comparable](s1, s2 S) bool { + if len(s1) != len(s2) { + return false + } + for i := range s1 { + if s1[i] != s2[i] { + return false + } + } + return true +} + +// EqualFunc reports whether two slices are equal using an equality +// function on each pair of elements. If the lengths are different, +// EqualFunc returns false. Otherwise, the elements are compared in +// increasing index order, and the comparison stops at the first index +// for which eq returns false. +func EqualFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, eq func(E1, E2) bool) bool { + if len(s1) != len(s2) { + return false + } + for i, v1 := range s1 { + v2 := s2[i] + if !eq(v1, v2) { + return false + } + } + return true +} + +// Compare compares the elements of s1 and s2, using [cmp.Compare] on each pair +// of elements. The elements are compared sequentially, starting at index 0, +// until one element is not equal to the other. +// The result of comparing the first non-matching elements is returned. +// If both slices are equal until one of them ends, the shorter slice is +// considered less than the longer one. +// The result is 0 if s1 == s2, -1 if s1 < s2, and +1 if s1 > s2. +func Compare[S ~[]E, E constraints.Ordered](s1, s2 S) int { + for i, v1 := range s1 { + if i >= len(s2) { + return +1 + } + v2 := s2[i] + if c := cmpCompare(v1, v2); c != 0 { + return c + } + } + if len(s1) < len(s2) { + return -1 + } + return 0 +} + +// CompareFunc is like [Compare] but uses a custom comparison function on each +// pair of elements. +// The result is the first non-zero result of cmp; if cmp always +// returns 0 the result is 0 if len(s1) == len(s2), -1 if len(s1) < len(s2), +// and +1 if len(s1) > len(s2). +func CompareFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, cmp func(E1, E2) int) int { + for i, v1 := range s1 { + if i >= len(s2) { + return +1 + } + v2 := s2[i] + if c := cmp(v1, v2); c != 0 { + return c + } + } + if len(s1) < len(s2) { + return -1 + } + return 0 +} + +// Index returns the index of the first occurrence of v in s, +// or -1 if not present. +func Index[S ~[]E, E comparable](s S, v E) int { + for i := range s { + if v == s[i] { + return i + } + } + return -1 +} + +// IndexFunc returns the first index i satisfying f(s[i]), +// or -1 if none do. +func IndexFunc[S ~[]E, E any](s S, f func(E) bool) int { + for i := range s { + if f(s[i]) { + return i + } + } + return -1 +} + +// Contains reports whether v is present in s. +func Contains[S ~[]E, E comparable](s S, v E) bool { + return Index(s, v) >= 0 +} + +// ContainsFunc reports whether at least one +// element e of s satisfies f(e). +func ContainsFunc[S ~[]E, E any](s S, f func(E) bool) bool { + return IndexFunc(s, f) >= 0 +} + +// Insert inserts the values v... into s at index i, +// returning the modified slice. +// The elements at s[i:] are shifted up to make room. +// In the returned slice r, r[i] == v[0], +// and r[i+len(v)] == value originally at r[i]. +// Insert panics if i is out of range. +// This function is O(len(s) + len(v)). +func Insert[S ~[]E, E any](s S, i int, v ...E) S { + m := len(v) + if m == 0 { + return s + } + n := len(s) + if i == n { + return append(s, v...) + } + if n+m > cap(s) { + // Use append rather than make so that we bump the size of + // the slice up to the next storage class. + // This is what Grow does but we don't call Grow because + // that might copy the values twice. + s2 := append(s[:i], make(S, n+m-i)...) + copy(s2[i:], v) + copy(s2[i+m:], s[i:]) + return s2 + } + s = s[:n+m] + + // before: + // s: aaaaaaaabbbbccccccccdddd + // ^ ^ ^ ^ + // i i+m n n+m + // after: + // s: aaaaaaaavvvvbbbbcccccccc + // ^ ^ ^ ^ + // i i+m n n+m + // + // a are the values that don't move in s. + // v are the values copied in from v. + // b and c are the values from s that are shifted up in index. + // d are the values that get overwritten, never to be seen again. + + if !overlaps(v, s[i+m:]) { + // Easy case - v does not overlap either the c or d regions. + // (It might be in some of a or b, or elsewhere entirely.) + // The data we copy up doesn't write to v at all, so just do it. + + copy(s[i+m:], s[i:]) + + // Now we have + // s: aaaaaaaabbbbbbbbcccccccc + // ^ ^ ^ ^ + // i i+m n n+m + // Note the b values are duplicated. + + copy(s[i:], v) + + // Now we have + // s: aaaaaaaavvvvbbbbcccccccc + // ^ ^ ^ ^ + // i i+m n n+m + // That's the result we want. + return s + } + + // The hard case - v overlaps c or d. We can't just shift up + // the data because we'd move or clobber the values we're trying + // to insert. + // So instead, write v on top of d, then rotate. + copy(s[n:], v) + + // Now we have + // s: aaaaaaaabbbbccccccccvvvv + // ^ ^ ^ ^ + // i i+m n n+m + + rotateRight(s[i:], m) + + // Now we have + // s: aaaaaaaavvvvbbbbcccccccc + // ^ ^ ^ ^ + // i i+m n n+m + // That's the result we want. + return s +} + +// Delete removes the elements s[i:j] from s, returning the modified slice. +// Delete panics if s[i:j] is not a valid slice of s. +// Delete is O(len(s)-j), so if many items must be deleted, it is better to +// make a single call deleting them all together than to delete one at a time. +// Delete might not modify the elements s[len(s)-(j-i):len(s)]. If those +// elements contain pointers you might consider zeroing those elements so that +// objects they reference can be garbage collected. +func Delete[S ~[]E, E any](s S, i, j int) S { + _ = s[i:j] // bounds check + + return append(s[:i], s[j:]...) +} + +// DeleteFunc removes any elements from s for which del returns true, +// returning the modified slice. +// When DeleteFunc removes m elements, it might not modify the elements +// s[len(s)-m:len(s)]. If those elements contain pointers you might consider +// zeroing those elements so that objects they reference can be garbage +// collected. +func DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S { + i := IndexFunc(s, del) + if i == -1 { + return s + } + // Don't start copying elements until we find one to delete. + for j := i + 1; j < len(s); j++ { + if v := s[j]; !del(v) { + s[i] = v + i++ + } + } + return s[:i] +} + +// Replace replaces the elements s[i:j] by the given v, and returns the +// modified slice. Replace panics if s[i:j] is not a valid slice of s. +func Replace[S ~[]E, E any](s S, i, j int, v ...E) S { + _ = s[i:j] // verify that i:j is a valid subslice + + if i == j { + return Insert(s, i, v...) + } + if j == len(s) { + return append(s[:i], v...) + } + + tot := len(s[:i]) + len(v) + len(s[j:]) + if tot > cap(s) { + // Too big to fit, allocate and copy over. + s2 := append(s[:i], make(S, tot-i)...) // See Insert + copy(s2[i:], v) + copy(s2[i+len(v):], s[j:]) + return s2 + } + + r := s[:tot] + + if i+len(v) <= j { + // Easy, as v fits in the deleted portion. + copy(r[i:], v) + if i+len(v) != j { + copy(r[i+len(v):], s[j:]) + } + return r + } + + // We are expanding (v is bigger than j-i). + // The situation is something like this: + // (example has i=4,j=8,len(s)=16,len(v)=6) + // s: aaaaxxxxbbbbbbbbyy + // ^ ^ ^ ^ + // i j len(s) tot + // a: prefix of s + // x: deleted range + // b: more of s + // y: area to expand into + + if !overlaps(r[i+len(v):], v) { + // Easy, as v is not clobbered by the first copy. + copy(r[i+len(v):], s[j:]) + copy(r[i:], v) + return r + } + + // This is a situation where we don't have a single place to which + // we can copy v. Parts of it need to go to two different places. + // We want to copy the prefix of v into y and the suffix into x, then + // rotate |y| spots to the right. + // + // v[2:] v[:2] + // | | + // s: aaaavvvvbbbbbbbbvv + // ^ ^ ^ ^ + // i j len(s) tot + // + // If either of those two destinations don't alias v, then we're good. + y := len(v) - (j - i) // length of y portion + + if !overlaps(r[i:j], v) { + copy(r[i:j], v[y:]) + copy(r[len(s):], v[:y]) + rotateRight(r[i:], y) + return r + } + if !overlaps(r[len(s):], v) { + copy(r[len(s):], v[:y]) + copy(r[i:j], v[y:]) + rotateRight(r[i:], y) + return r + } + + // Now we know that v overlaps both x and y. + // That means that the entirety of b is *inside* v. + // So we don't need to preserve b at all; instead we + // can copy v first, then copy the b part of v out of + // v to the right destination. + k := startIdx(v, s[j:]) + copy(r[i:], v) + copy(r[i+len(v):], r[i+k:]) + return r +} + +// Clone returns a copy of the slice. +// The elements are copied using assignment, so this is a shallow clone. +func Clone[S ~[]E, E any](s S) S { + // Preserve nil in case it matters. + if s == nil { + return nil + } + return append(S([]E{}), s...) +} + +// Compact replaces consecutive runs of equal elements with a single copy. +// This is like the uniq command found on Unix. +// Compact modifies the contents of the slice s and returns the modified slice, +// which may have a smaller length. +// When Compact discards m elements in total, it might not modify the elements +// s[len(s)-m:len(s)]. If those elements contain pointers you might consider +// zeroing those elements so that objects they reference can be garbage collected. +func Compact[S ~[]E, E comparable](s S) S { + if len(s) < 2 { + return s + } + i := 1 + for k := 1; k < len(s); k++ { + if s[k] != s[k-1] { + if i != k { + s[i] = s[k] + } + i++ + } + } + return s[:i] +} + +// CompactFunc is like [Compact] but uses an equality function to compare elements. +// For runs of elements that compare equal, CompactFunc keeps the first one. +func CompactFunc[S ~[]E, E any](s S, eq func(E, E) bool) S { + if len(s) < 2 { + return s + } + i := 1 + for k := 1; k < len(s); k++ { + if !eq(s[k], s[k-1]) { + if i != k { + s[i] = s[k] + } + i++ + } + } + return s[:i] +} + +// Grow increases the slice's capacity, if necessary, to guarantee space for +// another n elements. After Grow(n), at least n elements can be appended +// to the slice without another allocation. If n is negative or too large to +// allocate the memory, Grow panics. +func Grow[S ~[]E, E any](s S, n int) S { + if n < 0 { + panic("cannot be negative") + } + if n -= cap(s) - len(s); n > 0 { + // TODO(https://go.dev/issue/53888): Make using []E instead of S + // to workaround a compiler bug where the runtime.growslice optimization + // does not take effect. Revert when the compiler is fixed. + s = append([]E(s)[:cap(s)], make([]E, n)...)[:len(s)] + } + return s +} + +// Clip removes unused capacity from the slice, returning s[:len(s):len(s)]. +func Clip[S ~[]E, E any](s S) S { + return s[:len(s):len(s)] +} + +// Rotation algorithm explanation: +// +// rotate left by 2 +// start with +// 0123456789 +// split up like this +// 01 234567 89 +// swap first 2 and last 2 +// 89 234567 01 +// join first parts +// 89234567 01 +// recursively rotate first left part by 2 +// 23456789 01 +// join at the end +// 2345678901 +// +// rotate left by 8 +// start with +// 0123456789 +// split up like this +// 01 234567 89 +// swap first 2 and last 2 +// 89 234567 01 +// join last parts +// 89 23456701 +// recursively rotate second part left by 6 +// 89 01234567 +// join at the end +// 8901234567 + +// TODO: There are other rotate algorithms. +// This algorithm has the desirable property that it moves each element exactly twice. +// The triple-reverse algorithm is simpler and more cache friendly, but takes more writes. +// The follow-cycles algorithm can be 1-write but it is not very cache friendly. + +// rotateLeft rotates b left by n spaces. +// s_final[i] = s_orig[i+r], wrapping around. +func rotateLeft[E any](s []E, r int) { + for r != 0 && r != len(s) { + if r*2 <= len(s) { + swap(s[:r], s[len(s)-r:]) + s = s[:len(s)-r] + } else { + swap(s[:len(s)-r], s[r:]) + s, r = s[len(s)-r:], r*2-len(s) + } + } +} +func rotateRight[E any](s []E, r int) { + rotateLeft(s, len(s)-r) +} + +// swap swaps the contents of x and y. x and y must be equal length and disjoint. +func swap[E any](x, y []E) { + for i := 0; i < len(x); i++ { + x[i], y[i] = y[i], x[i] + } +} + +// overlaps reports whether the memory ranges a[0:len(a)] and b[0:len(b)] overlap. +func overlaps[E any](a, b []E) bool { + if len(a) == 0 || len(b) == 0 { + return false + } + elemSize := unsafe.Sizeof(a[0]) + if elemSize == 0 { + return false + } + // TODO: use a runtime/unsafe facility once one becomes available. See issue 12445. + // Also see crypto/internal/alias/alias.go:AnyOverlap + return uintptr(unsafe.Pointer(&a[0])) <= uintptr(unsafe.Pointer(&b[len(b)-1]))+(elemSize-1) && + uintptr(unsafe.Pointer(&b[0])) <= uintptr(unsafe.Pointer(&a[len(a)-1]))+(elemSize-1) +} + +// startIdx returns the index in haystack where the needle starts. +// prerequisite: the needle must be aliased entirely inside the haystack. +func startIdx[E any](haystack, needle []E) int { + p := &needle[0] + for i := range haystack { + if p == &haystack[i] { + return i + } + } + // TODO: what if the overlap is by a non-integral number of Es? + panic("needle not found") +} + +// Reverse reverses the elements of the slice in place. +func Reverse[S ~[]E, E any](s S) { + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + s[i], s[j] = s[j], s[i] + } +} diff --git a/vendor/golang.org/x/exp/slices/sort.go b/vendor/golang.org/x/exp/slices/sort.go new file mode 100644 index 0000000000..b67897f76b --- /dev/null +++ b/vendor/golang.org/x/exp/slices/sort.go @@ -0,0 +1,195 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:generate go run $GOROOT/src/sort/gen_sort_variants.go -exp + +package slices + +import ( + "math/bits" + + "golang.org/x/exp/constraints" +) + +// Sort sorts a slice of any ordered type in ascending order. +// When sorting floating-point numbers, NaNs are ordered before other values. +func Sort[S ~[]E, E constraints.Ordered](x S) { + n := len(x) + pdqsortOrdered(x, 0, n, bits.Len(uint(n))) +} + +// SortFunc sorts the slice x in ascending order as determined by the cmp +// function. This sort is not guaranteed to be stable. +// cmp(a, b) should return a negative number when a < b, a positive number when +// a > b and zero when a == b. +// +// SortFunc requires that cmp is a strict weak ordering. +// See https://en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings. +func SortFunc[S ~[]E, E any](x S, cmp func(a, b E) int) { + n := len(x) + pdqsortCmpFunc(x, 0, n, bits.Len(uint(n)), cmp) +} + +// SortStableFunc sorts the slice x while keeping the original order of equal +// elements, using cmp to compare elements in the same way as [SortFunc]. +func SortStableFunc[S ~[]E, E any](x S, cmp func(a, b E) int) { + stableCmpFunc(x, len(x), cmp) +} + +// IsSorted reports whether x is sorted in ascending order. +func IsSorted[S ~[]E, E constraints.Ordered](x S) bool { + for i := len(x) - 1; i > 0; i-- { + if cmpLess(x[i], x[i-1]) { + return false + } + } + return true +} + +// IsSortedFunc reports whether x is sorted in ascending order, with cmp as the +// comparison function as defined by [SortFunc]. +func IsSortedFunc[S ~[]E, E any](x S, cmp func(a, b E) int) bool { + for i := len(x) - 1; i > 0; i-- { + if cmp(x[i], x[i-1]) < 0 { + return false + } + } + return true +} + +// Min returns the minimal value in x. It panics if x is empty. +// For floating-point numbers, Min propagates NaNs (any NaN value in x +// forces the output to be NaN). +func Min[S ~[]E, E constraints.Ordered](x S) E { + if len(x) < 1 { + panic("slices.Min: empty list") + } + m := x[0] + for i := 1; i < len(x); i++ { + m = min(m, x[i]) + } + return m +} + +// MinFunc returns the minimal value in x, using cmp to compare elements. +// It panics if x is empty. If there is more than one minimal element +// according to the cmp function, MinFunc returns the first one. +func MinFunc[S ~[]E, E any](x S, cmp func(a, b E) int) E { + if len(x) < 1 { + panic("slices.MinFunc: empty list") + } + m := x[0] + for i := 1; i < len(x); i++ { + if cmp(x[i], m) < 0 { + m = x[i] + } + } + return m +} + +// Max returns the maximal value in x. It panics if x is empty. +// For floating-point E, Max propagates NaNs (any NaN value in x +// forces the output to be NaN). +func Max[S ~[]E, E constraints.Ordered](x S) E { + if len(x) < 1 { + panic("slices.Max: empty list") + } + m := x[0] + for i := 1; i < len(x); i++ { + m = max(m, x[i]) + } + return m +} + +// MaxFunc returns the maximal value in x, using cmp to compare elements. +// It panics if x is empty. If there is more than one maximal element +// according to the cmp function, MaxFunc returns the first one. +func MaxFunc[S ~[]E, E any](x S, cmp func(a, b E) int) E { + if len(x) < 1 { + panic("slices.MaxFunc: empty list") + } + m := x[0] + for i := 1; i < len(x); i++ { + if cmp(x[i], m) > 0 { + m = x[i] + } + } + return m +} + +// BinarySearch searches for target in a sorted slice and returns the position +// where target is found, or the position where target would appear in the +// sort order; it also returns a bool saying whether the target is really found +// in the slice. The slice must be sorted in increasing order. +func BinarySearch[S ~[]E, E constraints.Ordered](x S, target E) (int, bool) { + // Inlining is faster than calling BinarySearchFunc with a lambda. + n := len(x) + // Define x[-1] < target and x[n] >= target. + // Invariant: x[i-1] < target, x[j] >= target. + i, j := 0, n + for i < j { + h := int(uint(i+j) >> 1) // avoid overflow when computing h + // i ≤ h < j + if cmpLess(x[h], target) { + i = h + 1 // preserves x[i-1] < target + } else { + j = h // preserves x[j] >= target + } + } + // i == j, x[i-1] < target, and x[j] (= x[i]) >= target => answer is i. + return i, i < n && (x[i] == target || (isNaN(x[i]) && isNaN(target))) +} + +// BinarySearchFunc works like [BinarySearch], but uses a custom comparison +// function. The slice must be sorted in increasing order, where "increasing" +// is defined by cmp. cmp should return 0 if the slice element matches +// the target, a negative number if the slice element precedes the target, +// or a positive number if the slice element follows the target. +// cmp must implement the same ordering as the slice, such that if +// cmp(a, t) < 0 and cmp(b, t) >= 0, then a must precede b in the slice. +func BinarySearchFunc[S ~[]E, E, T any](x S, target T, cmp func(E, T) int) (int, bool) { + n := len(x) + // Define cmp(x[-1], target) < 0 and cmp(x[n], target) >= 0 . + // Invariant: cmp(x[i - 1], target) < 0, cmp(x[j], target) >= 0. + i, j := 0, n + for i < j { + h := int(uint(i+j) >> 1) // avoid overflow when computing h + // i ≤ h < j + if cmp(x[h], target) < 0 { + i = h + 1 // preserves cmp(x[i - 1], target) < 0 + } else { + j = h // preserves cmp(x[j], target) >= 0 + } + } + // i == j, cmp(x[i-1], target) < 0, and cmp(x[j], target) (= cmp(x[i], target)) >= 0 => answer is i. + return i, i < n && cmp(x[i], target) == 0 +} + +type sortedHint int // hint for pdqsort when choosing the pivot + +const ( + unknownHint sortedHint = iota + increasingHint + decreasingHint +) + +// xorshift paper: https://www.jstatsoft.org/article/view/v008i14/xorshift.pdf +type xorshift uint64 + +func (r *xorshift) Next() uint64 { + *r ^= *r << 13 + *r ^= *r >> 17 + *r ^= *r << 5 + return uint64(*r) +} + +func nextPowerOfTwo(length int) uint { + return 1 << bits.Len(uint(length)) +} + +// isNaN reports whether x is a NaN without requiring the math package. +// This will always return false if T is not floating-point. +func isNaN[T constraints.Ordered](x T) bool { + return x != x +} diff --git a/vendor/golang.org/x/exp/slices/zsortanyfunc.go b/vendor/golang.org/x/exp/slices/zsortanyfunc.go new file mode 100644 index 0000000000..06f2c7a248 --- /dev/null +++ b/vendor/golang.org/x/exp/slices/zsortanyfunc.go @@ -0,0 +1,479 @@ +// Code generated by gen_sort_variants.go; DO NOT EDIT. + +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package slices + +// insertionSortCmpFunc sorts data[a:b] using insertion sort. +func insertionSortCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) { + for i := a + 1; i < b; i++ { + for j := i; j > a && (cmp(data[j], data[j-1]) < 0); j-- { + data[j], data[j-1] = data[j-1], data[j] + } + } +} + +// siftDownCmpFunc implements the heap property on data[lo:hi]. +// first is an offset into the array where the root of the heap lies. +func siftDownCmpFunc[E any](data []E, lo, hi, first int, cmp func(a, b E) int) { + root := lo + for { + child := 2*root + 1 + if child >= hi { + break + } + if child+1 < hi && (cmp(data[first+child], data[first+child+1]) < 0) { + child++ + } + if !(cmp(data[first+root], data[first+child]) < 0) { + return + } + data[first+root], data[first+child] = data[first+child], data[first+root] + root = child + } +} + +func heapSortCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) { + first := a + lo := 0 + hi := b - a + + // Build heap with greatest element at top. + for i := (hi - 1) / 2; i >= 0; i-- { + siftDownCmpFunc(data, i, hi, first, cmp) + } + + // Pop elements, largest first, into end of data. + for i := hi - 1; i >= 0; i-- { + data[first], data[first+i] = data[first+i], data[first] + siftDownCmpFunc(data, lo, i, first, cmp) + } +} + +// pdqsortCmpFunc sorts data[a:b]. +// The algorithm based on pattern-defeating quicksort(pdqsort), but without the optimizations from BlockQuicksort. +// pdqsort paper: https://arxiv.org/pdf/2106.05123.pdf +// C++ implementation: https://github.com/orlp/pdqsort +// Rust implementation: https://docs.rs/pdqsort/latest/pdqsort/ +// limit is the number of allowed bad (very unbalanced) pivots before falling back to heapsort. +func pdqsortCmpFunc[E any](data []E, a, b, limit int, cmp func(a, b E) int) { + const maxInsertion = 12 + + var ( + wasBalanced = true // whether the last partitioning was reasonably balanced + wasPartitioned = true // whether the slice was already partitioned + ) + + for { + length := b - a + + if length <= maxInsertion { + insertionSortCmpFunc(data, a, b, cmp) + return + } + + // Fall back to heapsort if too many bad choices were made. + if limit == 0 { + heapSortCmpFunc(data, a, b, cmp) + return + } + + // If the last partitioning was imbalanced, we need to breaking patterns. + if !wasBalanced { + breakPatternsCmpFunc(data, a, b, cmp) + limit-- + } + + pivot, hint := choosePivotCmpFunc(data, a, b, cmp) + if hint == decreasingHint { + reverseRangeCmpFunc(data, a, b, cmp) + // The chosen pivot was pivot-a elements after the start of the array. + // After reversing it is pivot-a elements before the end of the array. + // The idea came from Rust's implementation. + pivot = (b - 1) - (pivot - a) + hint = increasingHint + } + + // The slice is likely already sorted. + if wasBalanced && wasPartitioned && hint == increasingHint { + if partialInsertionSortCmpFunc(data, a, b, cmp) { + return + } + } + + // Probably the slice contains many duplicate elements, partition the slice into + // elements equal to and elements greater than the pivot. + if a > 0 && !(cmp(data[a-1], data[pivot]) < 0) { + mid := partitionEqualCmpFunc(data, a, b, pivot, cmp) + a = mid + continue + } + + mid, alreadyPartitioned := partitionCmpFunc(data, a, b, pivot, cmp) + wasPartitioned = alreadyPartitioned + + leftLen, rightLen := mid-a, b-mid + balanceThreshold := length / 8 + if leftLen < rightLen { + wasBalanced = leftLen >= balanceThreshold + pdqsortCmpFunc(data, a, mid, limit, cmp) + a = mid + 1 + } else { + wasBalanced = rightLen >= balanceThreshold + pdqsortCmpFunc(data, mid+1, b, limit, cmp) + b = mid + } + } +} + +// partitionCmpFunc does one quicksort partition. +// Let p = data[pivot] +// Moves elements in data[a:b] around, so that data[i]

=p for inewpivot. +// On return, data[newpivot] = p +func partitionCmpFunc[E any](data []E, a, b, pivot int, cmp func(a, b E) int) (newpivot int, alreadyPartitioned bool) { + data[a], data[pivot] = data[pivot], data[a] + i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned + + for i <= j && (cmp(data[i], data[a]) < 0) { + i++ + } + for i <= j && !(cmp(data[j], data[a]) < 0) { + j-- + } + if i > j { + data[j], data[a] = data[a], data[j] + return j, true + } + data[i], data[j] = data[j], data[i] + i++ + j-- + + for { + for i <= j && (cmp(data[i], data[a]) < 0) { + i++ + } + for i <= j && !(cmp(data[j], data[a]) < 0) { + j-- + } + if i > j { + break + } + data[i], data[j] = data[j], data[i] + i++ + j-- + } + data[j], data[a] = data[a], data[j] + return j, false +} + +// partitionEqualCmpFunc partitions data[a:b] into elements equal to data[pivot] followed by elements greater than data[pivot]. +// It assumed that data[a:b] does not contain elements smaller than the data[pivot]. +func partitionEqualCmpFunc[E any](data []E, a, b, pivot int, cmp func(a, b E) int) (newpivot int) { + data[a], data[pivot] = data[pivot], data[a] + i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned + + for { + for i <= j && !(cmp(data[a], data[i]) < 0) { + i++ + } + for i <= j && (cmp(data[a], data[j]) < 0) { + j-- + } + if i > j { + break + } + data[i], data[j] = data[j], data[i] + i++ + j-- + } + return i +} + +// partialInsertionSortCmpFunc partially sorts a slice, returns true if the slice is sorted at the end. +func partialInsertionSortCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) bool { + const ( + maxSteps = 5 // maximum number of adjacent out-of-order pairs that will get shifted + shortestShifting = 50 // don't shift any elements on short arrays + ) + i := a + 1 + for j := 0; j < maxSteps; j++ { + for i < b && !(cmp(data[i], data[i-1]) < 0) { + i++ + } + + if i == b { + return true + } + + if b-a < shortestShifting { + return false + } + + data[i], data[i-1] = data[i-1], data[i] + + // Shift the smaller one to the left. + if i-a >= 2 { + for j := i - 1; j >= 1; j-- { + if !(cmp(data[j], data[j-1]) < 0) { + break + } + data[j], data[j-1] = data[j-1], data[j] + } + } + // Shift the greater one to the right. + if b-i >= 2 { + for j := i + 1; j < b; j++ { + if !(cmp(data[j], data[j-1]) < 0) { + break + } + data[j], data[j-1] = data[j-1], data[j] + } + } + } + return false +} + +// breakPatternsCmpFunc scatters some elements around in an attempt to break some patterns +// that might cause imbalanced partitions in quicksort. +func breakPatternsCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) { + length := b - a + if length >= 8 { + random := xorshift(length) + modulus := nextPowerOfTwo(length) + + for idx := a + (length/4)*2 - 1; idx <= a+(length/4)*2+1; idx++ { + other := int(uint(random.Next()) & (modulus - 1)) + if other >= length { + other -= length + } + data[idx], data[a+other] = data[a+other], data[idx] + } + } +} + +// choosePivotCmpFunc chooses a pivot in data[a:b]. +// +// [0,8): chooses a static pivot. +// [8,shortestNinther): uses the simple median-of-three method. +// [shortestNinther,∞): uses the Tukey ninther method. +func choosePivotCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) (pivot int, hint sortedHint) { + const ( + shortestNinther = 50 + maxSwaps = 4 * 3 + ) + + l := b - a + + var ( + swaps int + i = a + l/4*1 + j = a + l/4*2 + k = a + l/4*3 + ) + + if l >= 8 { + if l >= shortestNinther { + // Tukey ninther method, the idea came from Rust's implementation. + i = medianAdjacentCmpFunc(data, i, &swaps, cmp) + j = medianAdjacentCmpFunc(data, j, &swaps, cmp) + k = medianAdjacentCmpFunc(data, k, &swaps, cmp) + } + // Find the median among i, j, k and stores it into j. + j = medianCmpFunc(data, i, j, k, &swaps, cmp) + } + + switch swaps { + case 0: + return j, increasingHint + case maxSwaps: + return j, decreasingHint + default: + return j, unknownHint + } +} + +// order2CmpFunc returns x,y where data[x] <= data[y], where x,y=a,b or x,y=b,a. +func order2CmpFunc[E any](data []E, a, b int, swaps *int, cmp func(a, b E) int) (int, int) { + if cmp(data[b], data[a]) < 0 { + *swaps++ + return b, a + } + return a, b +} + +// medianCmpFunc returns x where data[x] is the median of data[a],data[b],data[c], where x is a, b, or c. +func medianCmpFunc[E any](data []E, a, b, c int, swaps *int, cmp func(a, b E) int) int { + a, b = order2CmpFunc(data, a, b, swaps, cmp) + b, c = order2CmpFunc(data, b, c, swaps, cmp) + a, b = order2CmpFunc(data, a, b, swaps, cmp) + return b +} + +// medianAdjacentCmpFunc finds the median of data[a - 1], data[a], data[a + 1] and stores the index into a. +func medianAdjacentCmpFunc[E any](data []E, a int, swaps *int, cmp func(a, b E) int) int { + return medianCmpFunc(data, a-1, a, a+1, swaps, cmp) +} + +func reverseRangeCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) { + i := a + j := b - 1 + for i < j { + data[i], data[j] = data[j], data[i] + i++ + j-- + } +} + +func swapRangeCmpFunc[E any](data []E, a, b, n int, cmp func(a, b E) int) { + for i := 0; i < n; i++ { + data[a+i], data[b+i] = data[b+i], data[a+i] + } +} + +func stableCmpFunc[E any](data []E, n int, cmp func(a, b E) int) { + blockSize := 20 // must be > 0 + a, b := 0, blockSize + for b <= n { + insertionSortCmpFunc(data, a, b, cmp) + a = b + b += blockSize + } + insertionSortCmpFunc(data, a, n, cmp) + + for blockSize < n { + a, b = 0, 2*blockSize + for b <= n { + symMergeCmpFunc(data, a, a+blockSize, b, cmp) + a = b + b += 2 * blockSize + } + if m := a + blockSize; m < n { + symMergeCmpFunc(data, a, m, n, cmp) + } + blockSize *= 2 + } +} + +// symMergeCmpFunc merges the two sorted subsequences data[a:m] and data[m:b] using +// the SymMerge algorithm from Pok-Son Kim and Arne Kutzner, "Stable Minimum +// Storage Merging by Symmetric Comparisons", in Susanne Albers and Tomasz +// Radzik, editors, Algorithms - ESA 2004, volume 3221 of Lecture Notes in +// Computer Science, pages 714-723. Springer, 2004. +// +// Let M = m-a and N = b-n. Wolog M < N. +// The recursion depth is bound by ceil(log(N+M)). +// The algorithm needs O(M*log(N/M + 1)) calls to data.Less. +// The algorithm needs O((M+N)*log(M)) calls to data.Swap. +// +// The paper gives O((M+N)*log(M)) as the number of assignments assuming a +// rotation algorithm which uses O(M+N+gcd(M+N)) assignments. The argumentation +// in the paper carries through for Swap operations, especially as the block +// swapping rotate uses only O(M+N) Swaps. +// +// symMerge assumes non-degenerate arguments: a < m && m < b. +// Having the caller check this condition eliminates many leaf recursion calls, +// which improves performance. +func symMergeCmpFunc[E any](data []E, a, m, b int, cmp func(a, b E) int) { + // Avoid unnecessary recursions of symMerge + // by direct insertion of data[a] into data[m:b] + // if data[a:m] only contains one element. + if m-a == 1 { + // Use binary search to find the lowest index i + // such that data[i] >= data[a] for m <= i < b. + // Exit the search loop with i == b in case no such index exists. + i := m + j := b + for i < j { + h := int(uint(i+j) >> 1) + if cmp(data[h], data[a]) < 0 { + i = h + 1 + } else { + j = h + } + } + // Swap values until data[a] reaches the position before i. + for k := a; k < i-1; k++ { + data[k], data[k+1] = data[k+1], data[k] + } + return + } + + // Avoid unnecessary recursions of symMerge + // by direct insertion of data[m] into data[a:m] + // if data[m:b] only contains one element. + if b-m == 1 { + // Use binary search to find the lowest index i + // such that data[i] > data[m] for a <= i < m. + // Exit the search loop with i == m in case no such index exists. + i := a + j := m + for i < j { + h := int(uint(i+j) >> 1) + if !(cmp(data[m], data[h]) < 0) { + i = h + 1 + } else { + j = h + } + } + // Swap values until data[m] reaches the position i. + for k := m; k > i; k-- { + data[k], data[k-1] = data[k-1], data[k] + } + return + } + + mid := int(uint(a+b) >> 1) + n := mid + m + var start, r int + if m > mid { + start = n - b + r = mid + } else { + start = a + r = m + } + p := n - 1 + + for start < r { + c := int(uint(start+r) >> 1) + if !(cmp(data[p-c], data[c]) < 0) { + start = c + 1 + } else { + r = c + } + } + + end := n - start + if start < m && m < end { + rotateCmpFunc(data, start, m, end, cmp) + } + if a < start && start < mid { + symMergeCmpFunc(data, a, start, mid, cmp) + } + if mid < end && end < b { + symMergeCmpFunc(data, mid, end, b, cmp) + } +} + +// rotateCmpFunc rotates two consecutive blocks u = data[a:m] and v = data[m:b] in data: +// Data of the form 'x u v y' is changed to 'x v u y'. +// rotate performs at most b-a many calls to data.Swap, +// and it assumes non-degenerate arguments: a < m && m < b. +func rotateCmpFunc[E any](data []E, a, m, b int, cmp func(a, b E) int) { + i := m - a + j := b - m + + for i != j { + if i > j { + swapRangeCmpFunc(data, m-i, m, j, cmp) + i -= j + } else { + swapRangeCmpFunc(data, m-i, m+j-i, i, cmp) + j -= i + } + } + // i == j + swapRangeCmpFunc(data, m-i, m, i, cmp) +} diff --git a/vendor/golang.org/x/exp/slices/zsortordered.go b/vendor/golang.org/x/exp/slices/zsortordered.go new file mode 100644 index 0000000000..99b47c3986 --- /dev/null +++ b/vendor/golang.org/x/exp/slices/zsortordered.go @@ -0,0 +1,481 @@ +// Code generated by gen_sort_variants.go; DO NOT EDIT. + +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package slices + +import "golang.org/x/exp/constraints" + +// insertionSortOrdered sorts data[a:b] using insertion sort. +func insertionSortOrdered[E constraints.Ordered](data []E, a, b int) { + for i := a + 1; i < b; i++ { + for j := i; j > a && cmpLess(data[j], data[j-1]); j-- { + data[j], data[j-1] = data[j-1], data[j] + } + } +} + +// siftDownOrdered implements the heap property on data[lo:hi]. +// first is an offset into the array where the root of the heap lies. +func siftDownOrdered[E constraints.Ordered](data []E, lo, hi, first int) { + root := lo + for { + child := 2*root + 1 + if child >= hi { + break + } + if child+1 < hi && cmpLess(data[first+child], data[first+child+1]) { + child++ + } + if !cmpLess(data[first+root], data[first+child]) { + return + } + data[first+root], data[first+child] = data[first+child], data[first+root] + root = child + } +} + +func heapSortOrdered[E constraints.Ordered](data []E, a, b int) { + first := a + lo := 0 + hi := b - a + + // Build heap with greatest element at top. + for i := (hi - 1) / 2; i >= 0; i-- { + siftDownOrdered(data, i, hi, first) + } + + // Pop elements, largest first, into end of data. + for i := hi - 1; i >= 0; i-- { + data[first], data[first+i] = data[first+i], data[first] + siftDownOrdered(data, lo, i, first) + } +} + +// pdqsortOrdered sorts data[a:b]. +// The algorithm based on pattern-defeating quicksort(pdqsort), but without the optimizations from BlockQuicksort. +// pdqsort paper: https://arxiv.org/pdf/2106.05123.pdf +// C++ implementation: https://github.com/orlp/pdqsort +// Rust implementation: https://docs.rs/pdqsort/latest/pdqsort/ +// limit is the number of allowed bad (very unbalanced) pivots before falling back to heapsort. +func pdqsortOrdered[E constraints.Ordered](data []E, a, b, limit int) { + const maxInsertion = 12 + + var ( + wasBalanced = true // whether the last partitioning was reasonably balanced + wasPartitioned = true // whether the slice was already partitioned + ) + + for { + length := b - a + + if length <= maxInsertion { + insertionSortOrdered(data, a, b) + return + } + + // Fall back to heapsort if too many bad choices were made. + if limit == 0 { + heapSortOrdered(data, a, b) + return + } + + // If the last partitioning was imbalanced, we need to breaking patterns. + if !wasBalanced { + breakPatternsOrdered(data, a, b) + limit-- + } + + pivot, hint := choosePivotOrdered(data, a, b) + if hint == decreasingHint { + reverseRangeOrdered(data, a, b) + // The chosen pivot was pivot-a elements after the start of the array. + // After reversing it is pivot-a elements before the end of the array. + // The idea came from Rust's implementation. + pivot = (b - 1) - (pivot - a) + hint = increasingHint + } + + // The slice is likely already sorted. + if wasBalanced && wasPartitioned && hint == increasingHint { + if partialInsertionSortOrdered(data, a, b) { + return + } + } + + // Probably the slice contains many duplicate elements, partition the slice into + // elements equal to and elements greater than the pivot. + if a > 0 && !cmpLess(data[a-1], data[pivot]) { + mid := partitionEqualOrdered(data, a, b, pivot) + a = mid + continue + } + + mid, alreadyPartitioned := partitionOrdered(data, a, b, pivot) + wasPartitioned = alreadyPartitioned + + leftLen, rightLen := mid-a, b-mid + balanceThreshold := length / 8 + if leftLen < rightLen { + wasBalanced = leftLen >= balanceThreshold + pdqsortOrdered(data, a, mid, limit) + a = mid + 1 + } else { + wasBalanced = rightLen >= balanceThreshold + pdqsortOrdered(data, mid+1, b, limit) + b = mid + } + } +} + +// partitionOrdered does one quicksort partition. +// Let p = data[pivot] +// Moves elements in data[a:b] around, so that data[i]

=p for inewpivot. +// On return, data[newpivot] = p +func partitionOrdered[E constraints.Ordered](data []E, a, b, pivot int) (newpivot int, alreadyPartitioned bool) { + data[a], data[pivot] = data[pivot], data[a] + i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned + + for i <= j && cmpLess(data[i], data[a]) { + i++ + } + for i <= j && !cmpLess(data[j], data[a]) { + j-- + } + if i > j { + data[j], data[a] = data[a], data[j] + return j, true + } + data[i], data[j] = data[j], data[i] + i++ + j-- + + for { + for i <= j && cmpLess(data[i], data[a]) { + i++ + } + for i <= j && !cmpLess(data[j], data[a]) { + j-- + } + if i > j { + break + } + data[i], data[j] = data[j], data[i] + i++ + j-- + } + data[j], data[a] = data[a], data[j] + return j, false +} + +// partitionEqualOrdered partitions data[a:b] into elements equal to data[pivot] followed by elements greater than data[pivot]. +// It assumed that data[a:b] does not contain elements smaller than the data[pivot]. +func partitionEqualOrdered[E constraints.Ordered](data []E, a, b, pivot int) (newpivot int) { + data[a], data[pivot] = data[pivot], data[a] + i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned + + for { + for i <= j && !cmpLess(data[a], data[i]) { + i++ + } + for i <= j && cmpLess(data[a], data[j]) { + j-- + } + if i > j { + break + } + data[i], data[j] = data[j], data[i] + i++ + j-- + } + return i +} + +// partialInsertionSortOrdered partially sorts a slice, returns true if the slice is sorted at the end. +func partialInsertionSortOrdered[E constraints.Ordered](data []E, a, b int) bool { + const ( + maxSteps = 5 // maximum number of adjacent out-of-order pairs that will get shifted + shortestShifting = 50 // don't shift any elements on short arrays + ) + i := a + 1 + for j := 0; j < maxSteps; j++ { + for i < b && !cmpLess(data[i], data[i-1]) { + i++ + } + + if i == b { + return true + } + + if b-a < shortestShifting { + return false + } + + data[i], data[i-1] = data[i-1], data[i] + + // Shift the smaller one to the left. + if i-a >= 2 { + for j := i - 1; j >= 1; j-- { + if !cmpLess(data[j], data[j-1]) { + break + } + data[j], data[j-1] = data[j-1], data[j] + } + } + // Shift the greater one to the right. + if b-i >= 2 { + for j := i + 1; j < b; j++ { + if !cmpLess(data[j], data[j-1]) { + break + } + data[j], data[j-1] = data[j-1], data[j] + } + } + } + return false +} + +// breakPatternsOrdered scatters some elements around in an attempt to break some patterns +// that might cause imbalanced partitions in quicksort. +func breakPatternsOrdered[E constraints.Ordered](data []E, a, b int) { + length := b - a + if length >= 8 { + random := xorshift(length) + modulus := nextPowerOfTwo(length) + + for idx := a + (length/4)*2 - 1; idx <= a+(length/4)*2+1; idx++ { + other := int(uint(random.Next()) & (modulus - 1)) + if other >= length { + other -= length + } + data[idx], data[a+other] = data[a+other], data[idx] + } + } +} + +// choosePivotOrdered chooses a pivot in data[a:b]. +// +// [0,8): chooses a static pivot. +// [8,shortestNinther): uses the simple median-of-three method. +// [shortestNinther,∞): uses the Tukey ninther method. +func choosePivotOrdered[E constraints.Ordered](data []E, a, b int) (pivot int, hint sortedHint) { + const ( + shortestNinther = 50 + maxSwaps = 4 * 3 + ) + + l := b - a + + var ( + swaps int + i = a + l/4*1 + j = a + l/4*2 + k = a + l/4*3 + ) + + if l >= 8 { + if l >= shortestNinther { + // Tukey ninther method, the idea came from Rust's implementation. + i = medianAdjacentOrdered(data, i, &swaps) + j = medianAdjacentOrdered(data, j, &swaps) + k = medianAdjacentOrdered(data, k, &swaps) + } + // Find the median among i, j, k and stores it into j. + j = medianOrdered(data, i, j, k, &swaps) + } + + switch swaps { + case 0: + return j, increasingHint + case maxSwaps: + return j, decreasingHint + default: + return j, unknownHint + } +} + +// order2Ordered returns x,y where data[x] <= data[y], where x,y=a,b or x,y=b,a. +func order2Ordered[E constraints.Ordered](data []E, a, b int, swaps *int) (int, int) { + if cmpLess(data[b], data[a]) { + *swaps++ + return b, a + } + return a, b +} + +// medianOrdered returns x where data[x] is the median of data[a],data[b],data[c], where x is a, b, or c. +func medianOrdered[E constraints.Ordered](data []E, a, b, c int, swaps *int) int { + a, b = order2Ordered(data, a, b, swaps) + b, c = order2Ordered(data, b, c, swaps) + a, b = order2Ordered(data, a, b, swaps) + return b +} + +// medianAdjacentOrdered finds the median of data[a - 1], data[a], data[a + 1] and stores the index into a. +func medianAdjacentOrdered[E constraints.Ordered](data []E, a int, swaps *int) int { + return medianOrdered(data, a-1, a, a+1, swaps) +} + +func reverseRangeOrdered[E constraints.Ordered](data []E, a, b int) { + i := a + j := b - 1 + for i < j { + data[i], data[j] = data[j], data[i] + i++ + j-- + } +} + +func swapRangeOrdered[E constraints.Ordered](data []E, a, b, n int) { + for i := 0; i < n; i++ { + data[a+i], data[b+i] = data[b+i], data[a+i] + } +} + +func stableOrdered[E constraints.Ordered](data []E, n int) { + blockSize := 20 // must be > 0 + a, b := 0, blockSize + for b <= n { + insertionSortOrdered(data, a, b) + a = b + b += blockSize + } + insertionSortOrdered(data, a, n) + + for blockSize < n { + a, b = 0, 2*blockSize + for b <= n { + symMergeOrdered(data, a, a+blockSize, b) + a = b + b += 2 * blockSize + } + if m := a + blockSize; m < n { + symMergeOrdered(data, a, m, n) + } + blockSize *= 2 + } +} + +// symMergeOrdered merges the two sorted subsequences data[a:m] and data[m:b] using +// the SymMerge algorithm from Pok-Son Kim and Arne Kutzner, "Stable Minimum +// Storage Merging by Symmetric Comparisons", in Susanne Albers and Tomasz +// Radzik, editors, Algorithms - ESA 2004, volume 3221 of Lecture Notes in +// Computer Science, pages 714-723. Springer, 2004. +// +// Let M = m-a and N = b-n. Wolog M < N. +// The recursion depth is bound by ceil(log(N+M)). +// The algorithm needs O(M*log(N/M + 1)) calls to data.Less. +// The algorithm needs O((M+N)*log(M)) calls to data.Swap. +// +// The paper gives O((M+N)*log(M)) as the number of assignments assuming a +// rotation algorithm which uses O(M+N+gcd(M+N)) assignments. The argumentation +// in the paper carries through for Swap operations, especially as the block +// swapping rotate uses only O(M+N) Swaps. +// +// symMerge assumes non-degenerate arguments: a < m && m < b. +// Having the caller check this condition eliminates many leaf recursion calls, +// which improves performance. +func symMergeOrdered[E constraints.Ordered](data []E, a, m, b int) { + // Avoid unnecessary recursions of symMerge + // by direct insertion of data[a] into data[m:b] + // if data[a:m] only contains one element. + if m-a == 1 { + // Use binary search to find the lowest index i + // such that data[i] >= data[a] for m <= i < b. + // Exit the search loop with i == b in case no such index exists. + i := m + j := b + for i < j { + h := int(uint(i+j) >> 1) + if cmpLess(data[h], data[a]) { + i = h + 1 + } else { + j = h + } + } + // Swap values until data[a] reaches the position before i. + for k := a; k < i-1; k++ { + data[k], data[k+1] = data[k+1], data[k] + } + return + } + + // Avoid unnecessary recursions of symMerge + // by direct insertion of data[m] into data[a:m] + // if data[m:b] only contains one element. + if b-m == 1 { + // Use binary search to find the lowest index i + // such that data[i] > data[m] for a <= i < m. + // Exit the search loop with i == m in case no such index exists. + i := a + j := m + for i < j { + h := int(uint(i+j) >> 1) + if !cmpLess(data[m], data[h]) { + i = h + 1 + } else { + j = h + } + } + // Swap values until data[m] reaches the position i. + for k := m; k > i; k-- { + data[k], data[k-1] = data[k-1], data[k] + } + return + } + + mid := int(uint(a+b) >> 1) + n := mid + m + var start, r int + if m > mid { + start = n - b + r = mid + } else { + start = a + r = m + } + p := n - 1 + + for start < r { + c := int(uint(start+r) >> 1) + if !cmpLess(data[p-c], data[c]) { + start = c + 1 + } else { + r = c + } + } + + end := n - start + if start < m && m < end { + rotateOrdered(data, start, m, end) + } + if a < start && start < mid { + symMergeOrdered(data, a, start, mid) + } + if mid < end && end < b { + symMergeOrdered(data, mid, end, b) + } +} + +// rotateOrdered rotates two consecutive blocks u = data[a:m] and v = data[m:b] in data: +// Data of the form 'x u v y' is changed to 'x v u y'. +// rotate performs at most b-a many calls to data.Swap, +// and it assumes non-degenerate arguments: a < m && m < b. +func rotateOrdered[E constraints.Ordered](data []E, a, m, b int) { + i := m - a + j := b - m + + for i != j { + if i > j { + swapRangeOrdered(data, m-i, m, j) + i -= j + } else { + swapRangeOrdered(data, m-i, m+j-i, i) + j -= i + } + } + // i == j + swapRangeOrdered(data, m-i, m, i) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 087da8b3a2..73aabb4e4f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -263,6 +263,10 @@ go.uber.org/zap/zaptest/observer golang.org/x/crypto/bcrypt golang.org/x/crypto/blowfish golang.org/x/crypto/pbkdf2 +# golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 +## explicit; go 1.20 +golang.org/x/exp/constraints +golang.org/x/exp/slices # golang.org/x/net v0.12.0 ## explicit; go 1.17 golang.org/x/net/context