summaryrefslogtreecommitdiff
path: root/internal/db/bundb/migrations
diff options
context:
space:
mode:
Diffstat (limited to 'internal/db/bundb/migrations')
-rw-r--r--internal/db/bundb/migrations/20250715095446_int_pols_forward_compat.go328
-rw-r--r--internal/db/bundb/migrations/20250715095446_int_pols_forward_compat/new/interactionrequest.go68
-rw-r--r--internal/db/bundb/migrations/20250715095446_int_pols_forward_compat/old/interactionrequest.go39
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
+)