diff options
Diffstat (limited to 'internal/db/bundb/migrations')
3 files changed, 435 insertions, 0 deletions
diff --git a/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat.go b/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat.go new file mode 100644 index 000000000..0df515082 --- /dev/null +++ b/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat.go @@ -0,0 +1,328 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package migrations + +import ( + "context" + "database/sql" + "errors" + "net/url" + "strings" + + "code.superseriousbusiness.org/gotosocial/internal/config" + "code.superseriousbusiness.org/gotosocial/internal/gtserror" + "code.superseriousbusiness.org/gotosocial/internal/id" + "code.superseriousbusiness.org/gotosocial/internal/log" + "code.superseriousbusiness.org/gotosocial/internal/util" + "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect" + + new_gtsmodel "code.superseriousbusiness.org/gotosocial/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat/new" + old_gtsmodel "code.superseriousbusiness.org/gotosocial/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat/old" +) + +func init() { + up := func(ctx context.Context, db *bun.DB) error { + const tmpTableName = "new_interaction_requests" + const tableName = "interaction_requests" + var host = config.GetHost() + var accountDomain = config.GetAccountDomain() + + // Count number of interaction + // requests we need to update. + total, err := db.NewSelect(). + Table(tableName). + Count(ctx) + if err != nil { + return gtserror.Newf("error geting interaction requests table count: %w", err) + } + + // Create new interaction_requests table and convert all existing into it. + if err := db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + + log.Info(ctx, "creating new interaction_requests table") + if _, err := tx.NewCreateTable(). + ModelTableExpr(tmpTableName). + Model((*new_gtsmodel.InteractionRequest)(nil)). + Exec(ctx); err != nil { + return gtserror.Newf("error creating new interaction requests table: %w", err) + } + + // Conversion batch size. + const batchsz = 1000 + + var maxID string + var count int + + // Start at largest + // possible ULID value. + maxID = id.Highest + + // Preallocate interaction request slices to maximum possible size. + oldRequests := make([]*old_gtsmodel.InteractionRequest, 0, batchsz) + newRequests := make([]*new_gtsmodel.InteractionRequest, 0, batchsz) + + log.Info(ctx, "migrating interaction requests to new table, this may take some time!") + outer: + for { + // Reset slices slices. + clear(oldRequests) + clear(newRequests) + oldRequests = oldRequests[:0] + newRequests = newRequests[:0] + + // Select next batch of + // interaction requests. + if err := tx.NewSelect(). + Model(&oldRequests). + Where("? < ?", bun.Ident("id"), maxID). + OrderExpr("? DESC", bun.Ident("id")). + Limit(batchsz). + Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) { + return gtserror.Newf("error selecting interaction requests: %w", err) + } + + // Reached end of requests. + if len(oldRequests) == 0 { + break outer + } + + // Set next maxID value from old requests. + maxID = oldRequests[len(oldRequests)-1].ID + + inner: + // Convert old request models to new. + for _, oldRequest := range oldRequests { + newRequest := &new_gtsmodel.InteractionRequest{ + ID: oldRequest.ID, + TargetStatusID: oldRequest.StatusID, + TargetAccountID: oldRequest.TargetAccountID, + InteractingAccountID: oldRequest.InteractingAccountID, + InteractionURI: oldRequest.InteractionURI, + InteractionType: int16(oldRequest.InteractionType), // #nosec G115 + Polite: util.Ptr(false), // old requests were always impolite + AcceptedAt: oldRequest.AcceptedAt, + RejectedAt: oldRequest.RejectedAt, + ResponseURI: oldRequest.URI, + } + + // Append new request to slice, + // though we continue operating on + // its ptr in the rest of this loop. + newRequests = append(newRequests, + newRequest) + + // Re-use the original interaction URI to create + // a mock interaction request URI on the new model. + switch oldRequest.InteractionType { + case old_gtsmodel.InteractionLike: + newRequest.InteractionRequestURI = oldRequest.InteractionURI + new_gtsmodel.LikeRequestSuffix + case old_gtsmodel.InteractionReply: + newRequest.InteractionRequestURI = oldRequest.InteractionURI + new_gtsmodel.ReplyRequestSuffix + case old_gtsmodel.InteractionAnnounce: + newRequest.InteractionRequestURI = oldRequest.InteractionURI + new_gtsmodel.AnnounceRequestSuffix + } + + // If the request was accepted by us, then generate an authorization + // URI for it, in order to be able to serve an Authorization if necessary. + if oldRequest.AcceptedAt.IsZero() || oldRequest.URI == "" { + + // Wasn't accepted, + // nothing else to do. + continue inner + } + + // Parse URI details of accept URI string. + acceptURI, err := url.Parse(oldRequest.URI) + if err != nil { + log.Warnf(ctx, "could not parse oldRequest.URI for interaction request %s,"+ + " skipping forward-compat hack (don't worry, this is not a big deal): %v", + oldRequest.ID, err) + continue inner + } + + // Check whether accept URI originated from this instance. + if !(acceptURI.Host == host || acceptURI.Host == accountDomain) { + + // Not an accept from + // us, leave it alone. + continue inner + } + + // Reuse the Accept URI to create an Authorization URI. + // Creates `https://example.org/users/aaa/authorizations/[ID]` + // from `https://example.org/users/aaa/accepts/[ID]`. + authorizationURI := strings.ReplaceAll( + oldRequest.URI, + "/accepts/"+oldRequest.ID, + "/authorizations/"+oldRequest.ID, + ) + newRequest.AuthorizationURI = authorizationURI + + var updateTableName string + + // Determine which table will have corresponding approved_by_uri. + if oldRequest.InteractionType == old_gtsmodel.InteractionLike { + updateTableName = "status_faves" + } else { + updateTableName = "statuses" + } + + // Update the corresponding interaction + // with generated authorization URI. + if _, err := tx.NewUpdate(). + Table(updateTableName). + Set("? = ?", bun.Ident("approved_by_uri"), authorizationURI). + Where("? = ?", bun.Ident("uri"), oldRequest.InteractionURI). + Exec(ctx); err != nil { + return gtserror.Newf("error updating approved_by_uri: %w", err) + } + } + + // Insert converted interaction + // request models to new table. + if _, err := tx. + NewInsert(). + Model(&newRequests). + Exec(ctx); err != nil { + return gtserror.Newf("error inserting interaction requests: %w", err) + } + + // Increment insert count. + count += len(newRequests) + + log.Infof(ctx, "[%d of %d] converting interaction requests", count, total) + } + + return nil + }); err != nil { + return err + } + + // Ensure that the above transaction + // has gone ahead without issues. + // + // Also placing this here might make + // breaking this into piecemeal steps + // easier if turns out necessary. + newTotal, err := db.NewSelect(). + Table(tmpTableName). + Count(ctx) + if err != nil { + return gtserror.Newf("error geting new interaction requests table count: %w", err) + } else if total != newTotal { + return gtserror.Newf("new interaction requests table contains unexpected count %d, want %d", newTotal, total) + } + + // Attempt to merge any sqlite write-ahead-log. + if err := doWALCheckpoint(ctx, db); err != nil { + return err + } + + // Drop the old interaction requests table and rename new one to replace it. + if err := db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + + log.Info(ctx, "dropping old interaction_requests table") + if _, err := tx.NewDropTable(). + Table(tableName). + Exec(ctx); err != nil { + return gtserror.Newf("error dropping old interaction requests table: %w", err) + } + + log.Info(ctx, "renaming new interaction_requests table to old") + if _, err := tx.NewRaw("ALTER TABLE ? RENAME TO ?", + bun.Ident(tmpTableName), + bun.Ident(tableName), + ).Exec(ctx); err != nil { + return gtserror.Newf("error renaming interaction requests table: %w", err) + } + + // Create necessary indices on the new table. + for index, columns := range map[string][]string{ + "interaction_requests_target_status_id_idx": {"target_status_id"}, + "interaction_requests_interacting_account_id_idx": {"interacting_account_id"}, + "interaction_requests_target_account_id_idx": {"target_account_id"}, + "interaction_requests_accepted_at_idx": {"accepted_at"}, + "interaction_requests_rejected_at_idx": {"rejected_at"}, + } { + log.Infof(ctx, "recreating %s index", index) + if _, err := tx.NewCreateIndex(). + Table(tableName). + Index(index). + Column(columns...). + Exec(ctx); err != nil { + return err + } + } + + if tx.Dialect().Name() == dialect.PG { + // Rename postgres uniqueness constraints: + // "new_interaction_requests_*" -> "interaction_requests_*" + log.Info(ctx, "renaming interaction_requests constraints on new table") + for _, spec := range []struct { + old string + new string + }{ + { + old: "new_interaction_requests_pkey", + new: "interaction_requests_pkey", + }, + { + old: "new_interaction_requests_interaction_request_uri_key", + new: "interaction_requests_interaction_request_uri_key", + }, + { + old: "new_interaction_requests_interaction_uri_key", + new: "interaction_requests_interaction_uri_key", + }, + { + old: "new_interaction_requests_response_uri_key", + new: "interaction_requests_response_uri_key", + }, + { + old: "new_interaction_requests_authorization_uri_key", + new: "interaction_requests_authorization_uri_key", + }, + } { + if _, err := tx.NewRaw("ALTER TABLE ? RENAME CONSTRAINT ? TO ?", + bun.Ident(tableName), + bun.Safe(spec.old), + bun.Safe(spec.new), + ).Exec(ctx); err != nil { + return gtserror.Newf("error renaming postgres interaction requests constraint %s: %w", spec.new, err) + } + } + } + + return nil + }); err != nil { + return err + } + + // Final sqlite write-ahead-log merge. + return doWALCheckpoint(ctx, db) + } + + down := func(ctx context.Context, db *bun.DB) error { + return nil + } + + if err := Migrations.Register(up, down); err != nil { + panic(err) + } +} diff --git a/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat/new/interactionrequest.go b/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat/new/interactionrequest.go new file mode 100644 index 000000000..bdd2d5811 --- /dev/null +++ b/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat/new/interactionrequest.go @@ -0,0 +1,68 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package gtsmodel + +import ( + "time" + + "github.com/uptrace/bun" +) + +type InteractionRequest struct { + // Used only for migration. + bun.BaseModel `bun:"table:new_interaction_requests"` + + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` + + // Removed in new model. + // CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` + + // Renamed from "StatusID" to "TargetStatusID" in new model. + TargetStatusID string `bun:"type:CHAR(26),nullzero,notnull"` + + TargetAccountID string `bun:"type:CHAR(26),nullzero,notnull"` + + InteractingAccountID string `bun:"type:CHAR(26),nullzero,notnull"` + + // Added in new model. + InteractionRequestURI string `bun:",nullzero,notnull,unique"` + + InteractionURI string `bun:",nullzero,notnull,unique"` + + // Changed type from int to int16 in new model. + InteractionType int16 `bun:",notnull"` + + // Added in new model. + Polite *bool `bun:",nullzero,notnull,default:false"` + + AcceptedAt time.Time `bun:"type:timestamptz,nullzero"` + + RejectedAt time.Time `bun:"type:timestamptz,nullzero"` + + // Renamed from "URI" to "ResponseURI" in new model. + ResponseURI string `bun:",nullzero,unique"` + + // Added in new model. + AuthorizationURI string `bun:",nullzero,unique"` +} + +const ( + LikeRequestSuffix = "#LikeRequest" + ReplyRequestSuffix = "#ReplyRequest" + AnnounceRequestSuffix = "#AnnounceRequest" +) diff --git a/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat/old/interactionrequest.go b/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat/old/interactionrequest.go new file mode 100644 index 000000000..a341f4d5b --- /dev/null +++ b/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat/old/interactionrequest.go @@ -0,0 +1,39 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package gtsmodel + +import "time" + +type InteractionRequest struct { + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` + CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` + StatusID string `bun:"type:CHAR(26),nullzero,notnull"` + TargetAccountID string `bun:"type:CHAR(26),nullzero,notnull"` + InteractingAccountID string `bun:"type:CHAR(26),nullzero,notnull"` + InteractionURI string `bun:",nullzero,notnull,unique"` + InteractionType int `bun:",notnull"` + AcceptedAt time.Time `bun:"type:timestamptz,nullzero"` + RejectedAt time.Time `bun:"type:timestamptz,nullzero"` + URI string `bun:",nullzero,unique"` +} + +const ( + InteractionLike int = 0 + InteractionReply int = 1 + InteractionAnnounce int = 2 +) |
