add StatusEdit{} test models

This commit is contained in:
kim 2024-11-27 14:44:01 +00:00
parent b10bd9b782
commit 63ddeb0879
3 changed files with 316 additions and 95 deletions

View file

@ -19,12 +19,9 @@ package migrations
import (
"context"
"errors"
old_gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
new_gtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/uptrace/bun"
@ -128,97 +125,6 @@ func init() {
}
}
// convertEnums performs a transaction that converts
// a table's column of our old-style enums (strings) to
// more performant and space-saving integer types.
func convertEnums[OldType ~string, NewType ~int16](
ctx context.Context,
tx bun.Tx,
table string,
column string,
mapping map[OldType]NewType,
defaultValue *NewType,
) error {
if len(mapping) == 0 {
return errors.New("empty mapping")
}
// Generate new column name.
newColumn := column + "_new"
log.Infof(ctx, "converting %s.%s enums; "+
"this may take a while, please don't interrupt!",
table, column,
)
// Ensure a default value.
if defaultValue == nil {
var zero NewType
defaultValue = &zero
}
// Add new column to database.
if _, err := tx.NewAddColumn().
Table(table).
ColumnExpr("? SMALLINT NOT NULL DEFAULT ?",
bun.Ident(newColumn),
*defaultValue).
Exec(ctx); err != nil {
return gtserror.Newf("error adding new column: %w", err)
}
// Get a count of all in table.
total, err := tx.NewSelect().
Table(table).
Count(ctx)
if err != nil {
return gtserror.Newf("error selecting total count: %w", err)
}
var updated int
for old, new := range mapping {
// Update old to new values.
res, err := tx.NewUpdate().
Table(table).
Where("? = ?", bun.Ident(column), old).
Set("? = ?", bun.Ident(newColumn), new).
Exec(ctx)
if err != nil {
return gtserror.Newf("error updating old column values: %w", err)
}
// Count number items updated.
n, _ := res.RowsAffected()
updated += int(n)
}
// Check total updated.
if total != updated {
log.Warnf(ctx, "total=%d does not match updated=%d", total, updated)
}
// Drop the old column from table.
if _, err := tx.NewDropColumn().
Table(table).
ColumnExpr("?", bun.Ident(column)).
Exec(ctx); err != nil {
return gtserror.Newf("error dropping old column: %w", err)
}
// Rename new to old name.
if _, err := tx.NewRaw(
"ALTER TABLE ? RENAME COLUMN ? TO ?",
bun.Ident(table),
bun.Ident(newColumn),
bun.Ident(column),
).Exec(ctx); err != nil {
return gtserror.Newf("error renaming new column: %w", err)
}
return nil
}
// visibilityEnumMapping maps old Visibility enum values to their newer integer type.
func visibilityEnumMapping[T ~string]() map[T]new_gtsmodel.Visibility {
return map[T]new_gtsmodel.Visibility{

View file

@ -19,12 +19,15 @@ package migrations
import (
"context"
"errors"
"fmt"
"reflect"
"strconv"
"strings"
"codeberg.org/gruf/go-byteutil"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
"github.com/uptrace/bun/dialect/feature"
@ -32,6 +35,97 @@ import (
"github.com/uptrace/bun/schema"
)
// convertEnums performs a transaction that converts
// a table's column of our old-style enums (strings) to
// more performant and space-saving integer types.
func convertEnums[OldType ~string, NewType ~int16](
ctx context.Context,
tx bun.Tx,
table string,
column string,
mapping map[OldType]NewType,
defaultValue *NewType,
) error {
if len(mapping) == 0 {
return errors.New("empty mapping")
}
// Generate new column name.
newColumn := column + "_new"
log.Infof(ctx, "converting %s.%s enums; "+
"this may take a while, please don't interrupt!",
table, column,
)
// Ensure a default value.
if defaultValue == nil {
var zero NewType
defaultValue = &zero
}
// Add new column to database.
if _, err := tx.NewAddColumn().
Table(table).
ColumnExpr("? SMALLINT NOT NULL DEFAULT ?",
bun.Ident(newColumn),
*defaultValue).
Exec(ctx); err != nil {
return gtserror.Newf("error adding new column: %w", err)
}
// Get a count of all in table.
total, err := tx.NewSelect().
Table(table).
Count(ctx)
if err != nil {
return gtserror.Newf("error selecting total count: %w", err)
}
var updated int
for old, new := range mapping {
// Update old to new values.
res, err := tx.NewUpdate().
Table(table).
Where("? = ?", bun.Ident(column), old).
Set("? = ?", bun.Ident(newColumn), new).
Exec(ctx)
if err != nil {
return gtserror.Newf("error updating old column values: %w", err)
}
// Count number items updated.
n, _ := res.RowsAffected()
updated += int(n)
}
// Check total updated.
if total != updated {
log.Warnf(ctx, "total=%d does not match updated=%d", total, updated)
}
// Drop the old column from table.
if _, err := tx.NewDropColumn().
Table(table).
ColumnExpr("?", bun.Ident(column)).
Exec(ctx); err != nil {
return gtserror.Newf("error dropping old column: %w", err)
}
// Rename new to old name.
if _, err := tx.NewRaw(
"ALTER TABLE ? RENAME COLUMN ? TO ?",
bun.Ident(table),
bun.Ident(newColumn),
bun.Ident(column),
).Exec(ctx); err != nil {
return gtserror.Newf("error renaming new column: %w", err)
}
return nil
}
// getBunColumnDef generates a column definition string for the SQL table represented by
// Go type, with the SQL column represented by the given Go field name. This ensures when
// adding a new column for table by migration that it will end up as bun would create it.

View file

@ -1043,6 +1043,25 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
Header: util.Ptr(false),
Cached: util.Ptr(true),
},
"local_account_2_status_9_attachment_1": {
ID: "01JDQ164HM08SGJ7ZEK9003Z4B",
StatusID: "01JDPZEZ77X1NX0TY9M10BK1HM",
URL: "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE88YG74PVAB81PX2XA9F3FG.mp3",
RemoteURL: "http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE892Y8ZS68TQCNPX7J888P3.mp3",
CreatedAt: TimeMustParse("2024-11-01T10:01:00+02:00"),
UpdatedAt: TimeMustParse("2024-11-01T10:01:00+02:00"),
Type: gtsmodel.FileTypeUnknown,
FileMeta: gtsmodel.FileMeta{},
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
Description: "Jolly salsa song, public domain.",
Blurhash: "",
Processing: gtsmodel.ProcessingStatusProcessed,
File: gtsmodel.File{},
Thumbnail: gtsmodel.Thumbnail{RemoteURL: ""},
Avatar: util.Ptr(false),
Header: util.Ptr(false),
Cached: util.Ptr(false),
},
"remote_account_1_status_1_attachment_1": {
ID: "01FVW7RXPQ8YJHTEXYPE7Q8ZY0",
StatusID: "01FVW7JHQFSFK166WWKR8CBA6M",
@ -1739,6 +1758,32 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Federated: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
},
"local_account_1_status_9": {
ID: "01JDPZC707CKDN8N4QVWM4Z1NR",
URI: "http://localhost:8080/users/the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR",
URL: "http://localhost:8080/@the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR",
Content: "<p>this is the latest revision of the status, with a content-warning</p>",
Text: "this is the latest revision of the status, with a content-warning",
ContentWarning: "edited status",
AttachmentIDs: nil,
CreatedAt: TimeMustParse("2024-11-01T11:00:00+02:00"),
UpdatedAt: TimeMustParse("2024-11-01T11:02:00+02:00"),
Local: util.Ptr(true),
AccountURI: "http://localhost:8080/users/the_mighty_zork",
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
InReplyToID: "",
InReplyToAccountID: "",
InReplyToURI: "",
BoostOfID: "",
ThreadID: "",
EditIDs: []string{"01JDPZCZ2Y9KSGZW0R7ZG8T8Y2", "01JDPZDADMD1T9HKF94RECF7PP"},
Visibility: gtsmodel.VisibilityPublic,
Sensitive: util.Ptr(false),
Language: "en",
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
Federated: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
},
"local_account_2_status_1": {
ID: "01F8MHBQCBTDKN6X5VHGMMN4MA",
URI: "http://localhost:8080/users/1happyturtle/statuses/01F8MHBQCBTDKN6X5VHGMMN4MA",
@ -1967,6 +2012,32 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
PollID: "01HEN2QB5NR4NCEHGYC3HN84K6",
PendingApproval: util.Ptr(false),
},
"local_account_2_status_9": {
ID: "01JDPZEZ77X1NX0TY9M10BK1HM",
URI: "http://localhost:8080/users/1happyturtle/statuses/01JDPZEZ77X1NX0TY9M10BK1HM",
URL: "http://localhost:8080/@1happyturtle/statuses/01JDPZEZ77X1NX0TY9M10BK1HM",
Content: "<p>now edited to bring back the previous edit's media!</p>",
Text: "now edited to bring back the previous edit's media!",
ContentWarning: "edit with media attachments",
AttachmentIDs: []string{"01JDQ164HM08SGJ7ZEK9003Z4B"},
CreatedAt: TimeMustParse("2024-11-01T10:00:00+02:00"),
UpdatedAt: TimeMustParse("2024-11-01T10:03:00+02:00"),
Local: util.Ptr(true),
AccountURI: "http://localhost:8080/users/the_mighty_zork",
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
InReplyToID: "",
InReplyToAccountID: "",
InReplyToURI: "",
BoostOfID: "",
ThreadID: "",
EditIDs: []string{"01JDPZPBXAX0M02YSEPB21KX4R", "01JDPZPJHKP7E3M0YQXEXPS1YT", "01JDPZPY3F85Y7B78ETRXEMWD9"},
Visibility: gtsmodel.VisibilityPublic,
Sensitive: util.Ptr(false),
Language: "en",
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
Federated: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
},
"remote_account_1_status_1": {
ID: "01FVW7JHQFSFK166WWKR8CBA6M",
URI: "http://fossbros-anonymous.io/users/foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M",
@ -2042,6 +2113,33 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
PollID: "01HEWV1GW2D49R919NPEDXPTZ5",
PendingApproval: util.Ptr(false),
},
"remote_account_1_status_4": {
ID: "01JDQ07JZTX9CMDJP67CNA71YD",
URI: "http://fossbros-anonymous.io/users/foss_satan/statuses/______",
URL: "http://fossbros-anonymous.io/@foss_satan/statuses/______",
Content: "<p>this is the latest status edit without poll change</p>",
Text: "this is the latest status edit without poll change",
ContentWarning: "",
AttachmentIDs: nil,
CreatedAt: TimeMustParse("2024-11-01T09:00:00+02:00"),
UpdatedAt: TimeMustParse("2024-11-01T09:02:00+02:00"),
Local: util.Ptr(false),
AccountURI: "http://fossbros-anonymous.io/users/foss_satan",
AccountID: "01F8MH5ZK5VRH73AKHQM6Y9VNX",
InReplyToID: "",
InReplyToAccountID: "",
InReplyToURI: "",
BoostOfID: "",
ThreadID: "",
EditIDs: []string{"01JDQ07ZZ4FGP13YN8TF63P5A6", "01JDQ08AYQC0G6413VAHA51CV9"},
PollID: "01JDQ0EZ5HM9T4WXRQ5WSVD40J",
Visibility: gtsmodel.VisibilityPublic,
Sensitive: util.Ptr(false),
Language: "en",
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
Federated: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
},
"remote_account_2_status_1": {
ID: "01HE7XJ1CG84TBKH5V9XKBVGF5",
URI: "http://example.org/users/Some_User/statuses/01HE7XJ1CG84TBKH5V9XKBVGF5",
@ -2125,6 +2223,19 @@ func NewTestPolls() map[string]*gtsmodel.Poll {
ClosedAt: time.Time{},
Closing: false,
},
"remote_account_1_status_4_poll": {
ID: "01JDQ0EZ5HM9T4WXRQ5WSVD40J",
Multiple: util.Ptr(false),
HideCounts: util.Ptr(false),
Options: []string{"yes", "no", "maybe", "i don't know", "can you repeat the question"},
Votes: []int{0, 0, 0, 0, 2},
Voters: util.Ptr(2),
StatusID: "01JDQ07JZTX9CMDJP67CNA71YD",
// empty expiry AND closed date, i.e. no end
ExpiresAt: time.Time{},
ClosedAt: time.Time{},
Closing: false,
},
}
}
@ -2184,6 +2295,24 @@ func NewTestPollVotes() map[string]*gtsmodel.PollVote {
Poll: nil,
CreatedAt: TimeMustParse("2021-09-11T11:47:37+02:00"),
},
"remote_account_1_status_4_poll_vote_local_account_1": {
ID: "01JDQ0SX9QVVFHS7P8M1PA3SVG",
Choices: []int{4},
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
Account: nil,
PollID: "01JDQ0EZ5HM9T4WXRQ5WSVD40J",
Poll: nil,
CreatedAt: TimeMustParse("2024-11-01T09:01:30+02:00"),
},
"remote_account_1_status_4_poll_vote_local_account_2": {
ID: "01JDQ0T3EEDN7SAVBQMQP4PR12",
Choices: []int{4},
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
Account: nil,
PollID: "01JDQ0EZ5HM9T4WXRQ5WSVD40J",
Poll: nil,
CreatedAt: TimeMustParse("2024-11-01T09:02:30+02:00"),
},
}
}
@ -3485,7 +3614,99 @@ func NewTestInteractionRequests() map[string]*gtsmodel.InteractionRequest {
}
func NewTestStatusEdits() map[string]*gtsmodel.StatusEdit {
panic("TODO")
return map[string]*gtsmodel.StatusEdit{
"local_account_1_status_9_edit_1": {
ID: "01JDPZCZ2Y9KSGZW0R7ZG8T8Y2",
Content: "<p>this is the original status</p>",
ContentWarning: "",
Text: "this is the original status",
Language: "en",
Sensitive: util.Ptr(false),
AttachmentIDs: nil,
PollOptions: nil,
PollVotes: nil,
StatusID: "01JDPZC707CKDN8N4QVWM4Z1NR",
CreatedAt: TimeMustParse("2024-11-01T11:00:00+02:00"),
},
"local_account_1_status_9_edit_2": {
ID: "01JDPZDADMD1T9HKF94RECF7PP",
Content: "<p>this is the first status edit! now with content-warning</p>",
ContentWarning: "edited status",
Text: "this is the first status edit! now with content-warning",
Language: "en",
Sensitive: util.Ptr(false),
AttachmentIDs: nil,
PollOptions: nil,
PollVotes: nil,
StatusID: "01JDPZC707CKDN8N4QVWM4Z1NR",
CreatedAt: TimeMustParse("2024-11-01T11:01:00+02:00"),
},
"local_account_2_status_9_edit_1": {
ID: "01JDPZPBXAX0M02YSEPB21KX4R",
Content: "<p>this is the original status</p>",
ContentWarning: "",
Text: "this is the original status",
Language: "en",
Sensitive: util.Ptr(false),
AttachmentIDs: nil,
PollOptions: nil,
PollVotes: nil,
StatusID: "01JDPZEZ77X1NX0TY9M10BK1HM",
CreatedAt: TimeMustParse("2024-11-01T10:00:00+02:00"),
},
"local_account_2_status_9_edit_2": {
ID: "01JDPZPJHKP7E3M0YQXEXPS1YT",
Content: "<p>now edited to have some media!</p>",
ContentWarning: "edit with media attachments",
Text: "now edited to have some media!",
Language: "en",
Sensitive: util.Ptr(true),
AttachmentIDs: []string{"01JDQ164HM08SGJ7ZEK9003Z4B"},
PollOptions: nil,
PollVotes: nil,
StatusID: "01JDPZEZ77X1NX0TY9M10BK1HM",
CreatedAt: TimeMustParse("2024-11-01T10:01:00+02:00"),
},
"local_account_2_status_9_edit_3": {
ID: "01JDPZPY3F85Y7B78ETRXEMWD9",
Content: "<p>now edited to remove the media</p>",
ContentWarning: "edit missing previous media attachments",
Text: "now edited to remove the media",
Language: "en",
Sensitive: util.Ptr(false),
AttachmentIDs: nil,
PollOptions: nil,
PollVotes: nil,
StatusID: "01JDPZEZ77X1NX0TY9M10BK1HM",
CreatedAt: TimeMustParse("2024-11-01T10:02:00+02:00"),
},
"remote_account_1_status_4_edit_1": {
ID: "01JDQ07ZZ4FGP13YN8TF63P5A6",
Content: "<p>this is the original status, with a poll!</p>",
ContentWarning: "",
Text: "this is the original status, with a poll!",
Language: "en",
Sensitive: util.Ptr(false),
AttachmentIDs: nil,
PollOptions: []string{"yes", "no", "spiderman"},
PollVotes: []int{42, 42, 69},
StatusID: "01JDQ07JZTX9CMDJP67CNA71YD",
CreatedAt: TimeMustParse("2024-11-01T09:00:00+02:00"),
},
"remote_account_1_status_4_edit_2": {
ID: "01JDQ08AYQC0G6413VAHA51CV9",
Content: "<p>this is the first status edit! now with a different poll!</p>",
ContentWarning: "edited status",
Text: "this is the first status edit! now with a different poll!",
Language: "en",
Sensitive: util.Ptr(false),
AttachmentIDs: nil,
PollOptions: []string{"yes", "no", "maybe", "i don't know", "can you repeat the question"},
PollVotes: []int{0, 0, 0, 0, 1},
StatusID: "01JDQ07JZTX9CMDJP67CNA71YD",
CreatedAt: TimeMustParse("2024-11-01T09:01:00+02:00"),
},
}
}
// GetSignatureForActivity prepares a mock HTTP request as if it were going to deliver activity to destination signed for privkey and pubKeyID, signs the request and returns the header values.