Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[chore] replace statuses.updated_at column with statuses.edited_at #3636

Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/cache/size.go
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,7 @@ func sizeofStatus() uintptr {
MentionIDs: []string{},
EmojiIDs: []string{exampleID, exampleID, exampleID},
CreatedAt: exampleTime,
UpdatedAt: exampleTime,
EditedAt: exampleTime,
FetchedAt: exampleTime,
Local: func() *bool { ok := false; return &ok }(),
AccountURI: exampleURI,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// GoToSocial
// Copyright (C) GoToSocial Authors [email protected]
// 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"
"fmt"
"reflect"

oldmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20250106114512_replace_statuses_updatedat_with_editedat"
newmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
)

func init() {
up := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
var newStatus *newmodel.Status
newStatusType := reflect.TypeOf(newStatus)

// Generate new Status.EditedAt column definition from bun.
colDef, err := getBunColumnDef(tx, newStatusType, "EditedAt")
if err != nil {
return fmt.Errorf("error making column def: %w", err)
}

log.Info(ctx, "adding statuses.edited_at column...")
_, err = tx.NewAddColumn().Model(newStatus).
ColumnExpr(colDef).
Exec(ctx)
if err != nil {
return fmt.Errorf("error adding column: %w", err)
}

var whereSQL string
var whereArg []any

// Check for an empty length
// EditIDs JSON array, with different
// SQL depending on connected database.
switch tx.Dialect().Name() {
case dialect.SQLite:
whereSQL = "NOT (json_array_length(?) = 0 OR ? IS NULL)"
whereArg = []any{bun.Ident("edits"), bun.Ident("edits")}
case dialect.PG:
whereSQL = "NOT (CARDINALITY(?) = 0 OR ? IS NULL)"
whereArg = []any{bun.Ident("edits"), bun.Ident("edits")}
default:
panic("unsupported db type")
}

log.Info(ctx, "setting edited_at = updated_at where not empty(edits)...")
res, err := tx.NewUpdate().Model(newStatus).Where(whereSQL, whereArg...).
Set("? = ?",
bun.Ident("edited_at"),
bun.Ident("updated_at"),
).
Exec(ctx)
if err != nil {
return fmt.Errorf("error updating columns: %w", err)
}

count, _ := res.RowsAffected()
log.Infof(ctx, "updated %d statuses", count)

log.Info(ctx, "removing statuses.updated_at column...")
_, err = tx.NewDropColumn().Model((*oldmodel.Status)(nil)).
Column("updated_at").
Exec(ctx)
if err != nil {
return fmt.Errorf("error dropping column: %w", err)
}

return nil
})
}

down := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
return nil
})
}

if err := Migrations.Register(up, down); err != nil {
panic(err)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// GoToSocial
// Copyright (C) GoToSocial Authors [email protected]
// 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

// A policy URI is GoToSocial's internal representation of
// one ActivityPub URI for an Actor or a Collection of Actors,
// specific to the domain of enforcing interaction policies.
//
// A PolicyValue can be stored in the database either as one
// of the Value constants defined below (to save space), OR as
// a full-fledged ActivityPub URI.
//
// A PolicyValue should be translated to the canonical string
// value of the represented URI when federating an item, or
// from the canonical string value of the URI when receiving
// or retrieving an item.
//
// For example, if the PolicyValue `followers` was being
// federated outwards in an interaction policy attached to an
// item created by the actor `https://example.org/users/someone`,
// then it should be translated to their followers URI when sent,
// eg., `https://example.org/users/someone/followers`.
//
// Likewise, if GoToSocial receives an item with an interaction
// policy containing `https://example.org/users/someone/followers`,
// and the item was created by `https://example.org/users/someone`,
// then the followers URI would be converted to `followers`
// for internal storage.
type PolicyValue string

const (
// Stand-in for ActivityPub magic public URI,
// which encompasses every possible Actor URI.
PolicyValuePublic PolicyValue = "public"
// Stand-in for the Followers Collection of
// the item owner's Actor.
PolicyValueFollowers PolicyValue = "followers"
// Stand-in for the Following Collection of
// the item owner's Actor.
PolicyValueFollowing PolicyValue = "following"
// Stand-in for the Mutuals Collection of
// the item owner's Actor.
//
// (TODO: Reserved, currently unused).
PolicyValueMutuals PolicyValue = "mutuals"
// Stand-in for Actor URIs tagged in the item.
PolicyValueMentioned PolicyValue = "mentioned"
// Stand-in for the Actor URI of the item owner.
PolicyValueAuthor PolicyValue = "author"
)

type PolicyValues []PolicyValue

// An InteractionPolicy determines which
// interactions will be accepted for an
// item, and according to what rules.
type InteractionPolicy struct {
// Conditions in which a Like
// interaction will be accepted
// for an item with this policy.
CanLike PolicyRules
// Conditions in which a Reply
// interaction will be accepted
// for an item with this policy.
CanReply PolicyRules
// Conditions in which an Announce
// interaction will be accepted
// for an item with this policy.
CanAnnounce PolicyRules
}

// PolicyRules represents the rules according
// to which a certain interaction is permitted
// to various Actor and Actor Collection URIs.
type PolicyRules struct {
// Always is for PolicyValues who are
// permitted to do an interaction
// without requiring approval.
Always PolicyValues
// WithApproval is for PolicyValues who
// are conditionally permitted to do
// an interaction, pending approval.
WithApproval PolicyValues
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// GoToSocial
// Copyright (C) GoToSocial Authors [email protected]
// 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"
)

// Status represents a user-created 'post' or
// 'status' in the database, either remote or local
type Status struct {
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
FetchedAt time.Time `bun:"type:timestamptz,nullzero"` // when was item (remote) last fetched.
PinnedAt time.Time `bun:"type:timestamptz,nullzero"` // Status was pinned by owning account at this time.
URI string `bun:",unique,nullzero,notnull"` // activitypub URI of this status
URL string `bun:",nullzero"` // web url for viewing this status
Content string `bun:""` // content of this status; likely html-formatted but not guaranteed
AttachmentIDs []string `bun:"attachments,array"` // Database IDs of any media attachments associated with this status
TagIDs []string `bun:"tags,array"` // Database IDs of any tags used in this status
MentionIDs []string `bun:"mentions,array"` // Database IDs of any mentions in this status
EmojiIDs []string `bun:"emojis,array"` // Database IDs of any emojis used in this status
Local *bool `bun:",nullzero,notnull,default:false"` // is this status from a local account?
AccountID string `bun:"type:CHAR(26),nullzero,notnull"` // which account posted this status?
AccountURI string `bun:",nullzero,notnull"` // activitypub uri of the owner of this status
InReplyToID string `bun:"type:CHAR(26),nullzero"` // id of the status this status replies to
InReplyToURI string `bun:",nullzero"` // activitypub uri of the status this status is a reply to
InReplyToAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that this status replies to
InReplyTo *Status `bun:"-"` // status corresponding to inReplyToID
BoostOfID string `bun:"type:CHAR(26),nullzero"` // id of the status this status is a boost of
BoostOfURI string `bun:"-"` // URI of the status this status is a boost of; field not inserted in the db, just for dereferencing purposes.
BoostOfAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that owns the boosted status
BoostOf *Status `bun:"-"` // status that corresponds to boostOfID
ThreadID string `bun:"type:CHAR(26),nullzero"` // id of the thread to which this status belongs; only set for remote statuses if a local account is involved at some point in the thread, otherwise null
EditIDs []string `bun:"edits,array"` //
PollID string `bun:"type:CHAR(26),nullzero"` //
ContentWarning string `bun:",nullzero"` // cw string for this status
Visibility Visibility `bun:",nullzero,notnull"` // visibility entry for this status
Sensitive *bool `bun:",nullzero,notnull,default:false"` // mark the status as sensitive?
Language string `bun:",nullzero"` // what language is this status written in?
CreatedWithApplicationID string `bun:"type:CHAR(26),nullzero"` // Which application was used to create this status?
ActivityStreamsType string `bun:",nullzero,notnull"` // What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types. Will probably almost always be Note but who knows!.
Text string `bun:""` // Original text of the status without formatting
Federated *bool `bun:",notnull"` // This status will be federated beyond the local timeline(s)
InteractionPolicy *InteractionPolicy `bun:""` // InteractionPolicy for this status. If null then the default InteractionPolicy should be assumed for this status's Visibility. Always null for boost wrappers.
PendingApproval *bool `bun:",nullzero,notnull,default:false"` // If true then status is a reply or boost wrapper that must be Approved by the reply-ee or boost-ee before being fully distributed.
PreApproved bool `bun:"-"` // If true, then status is a reply to or boost wrapper of a status on our instance, has permission to do the interaction, and an Accept should be sent out for it immediately. Field not stored in the DB.
ApprovedByURI string `bun:",nullzero"` // URI of an Accept Activity that approves the Announce or Create Activity that this status was/will be attached to.
}

// GetID implements timeline.Timelineable{}.
func (s *Status) GetID() string {
return s.ID
}

// IsLocal returns true if this is a local
// status (ie., originating from this instance).
func (s *Status) IsLocal() bool {
return s.Local != nil && *s.Local
}

// enumType is the type we (at least, should) use
// for database enum types. it is the largest size
// supported by a PostgreSQL SMALLINT, since an
// SQLite SMALLINT is actually variable in size.
type enumType int16

// Visibility represents the
// visibility granularity of a status.
type Visibility enumType

const (
// VisibilityNone means nobody can see this.
// It's only used for web status visibility.
VisibilityNone Visibility = 1

// VisibilityPublic means this status will
// be visible to everyone on all timelines.
VisibilityPublic Visibility = 2

// VisibilityUnlocked means this status will be visible to everyone,
// but will only show on home timeline to followers, and in lists.
VisibilityUnlocked Visibility = 3

// VisibilityFollowersOnly means this status is viewable to followers only.
VisibilityFollowersOnly Visibility = 4

// VisibilityMutualsOnly means this status
// is visible to mutual followers only.
VisibilityMutualsOnly Visibility = 5

// VisibilityDirect means this status is
// visible only to mentioned recipients.
VisibilityDirect Visibility = 6

// VisibilityDefault is used when no other setting can be found.
VisibilityDefault Visibility = VisibilityUnlocked
)
12 changes: 0 additions & 12 deletions internal/db/bundb/poll.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"context"
"errors"
"slices"
"time"

"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
Expand Down Expand Up @@ -158,17 +157,6 @@ func (p *pollDB) UpdatePoll(ctx context.Context, poll *gtsmodel.Poll, cols ...st

return p.state.Caches.DB.Poll.Store(poll, func() error {
return p.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
// Update the status' "updated_at" field.
if _, err := tx.NewUpdate().
Table("statuses").
Where("? = ?", bun.Ident("id"), poll.StatusID).
SetColumn("updated_at", "?", time.Now()).
Exec(ctx); err != nil {
return err
}

// Finally, update poll
// columns in database.
_, err := tx.NewUpdate().
Model(poll).
Column(cols...).
Expand Down
1 change: 0 additions & 1 deletion internal/db/bundb/timeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ func getFutureStatus() *gtsmodel.Status {
MentionIDs: []string{},
EmojiIDs: []string{},
CreatedAt: theDistantFuture,
UpdatedAt: theDistantFuture,
Local: util.Ptr(true),
AccountURI: "http://localhost:8080/users/admin",
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
Expand Down
1 change: 0 additions & 1 deletion internal/db/test/conversation.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ func (f *ConversationFactory) NewTestStatus(localAccount *gtsmodel.Account, thre
status := &gtsmodel.Status{
ID: statusID,
CreatedAt: createdAt,
UpdatedAt: createdAt,
URI: "http://localhost:8080/users/" + localAccount.Username + "/statuses/" + statusID,
AccountID: localAccount.ID,
AccountURI: localAccount.URI,
Expand Down
Loading