diff options
Diffstat (limited to 'internal/db/bundb/migrations')
8 files changed, 526 insertions, 103 deletions
diff --git a/internal/db/bundb/migrations/20240809134448_interaction_requests_client_api.go b/internal/db/bundb/migrations/20240809134448_interaction_requests_client_api.go index 82c2b4016..a3fb8675e 100644 --- a/internal/db/bundb/migrations/20240809134448_interaction_requests_client_api.go +++ b/internal/db/bundb/migrations/20240809134448_interaction_requests_client_api.go @@ -93,11 +93,7 @@ func init() { // For each currently pending status, check whether it's a reply or // a boost, and insert a corresponding interaction request into the db. for _, pendingStatus := range pendingStatuses { - req, err := typeutils.StatusToInteractionRequest(ctx, pendingStatus) - if err != nil { - return err - } - + req := typeutils.StatusToInteractionRequest(pendingStatus) if _, err := tx. NewInsert(). Model(req). @@ -125,10 +121,7 @@ func init() { } for _, pendingFave := range pendingFaves { - req, err := typeutils.StatusFaveToInteractionRequest(ctx, pendingFave) - if err != nil { - return err - } + req := typeutils.StatusFaveToInteractionRequest(pendingFave) if _, err := tx. NewInsert(). diff --git a/internal/db/bundb/migrations/20241113151042_remove_mention_updated_at.go b/internal/db/bundb/migrations/20241113151042_remove_mention_updated_at.go new file mode 100644 index 000000000..bd72dc109 --- /dev/null +++ b/internal/db/bundb/migrations/20241113151042_remove_mention_updated_at.go @@ -0,0 +1,57 @@ +// 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" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/uptrace/bun" +) + +func init() { + up := func(ctx context.Context, db *bun.DB) error { + return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + + // Check for 'updated_at' column on mentions table, else return. + exists, err := doesColumnExist(ctx, tx, "mentions", "updated_at") + if err != nil { + return err + } else if !exists { + return nil + } + + // Remove 'updated_at' column. + _, err = tx.NewDropColumn(). + Model((*gtsmodel.Mention)(nil)). + Column("updated_at"). + Exec(ctx) + return err + }) + } + + 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) + } +} diff --git a/internal/db/bundb/migrations/20241113152126_add_status_edits.go b/internal/db/bundb/migrations/20241113152126_add_status_edits.go new file mode 100644 index 000000000..aa0b0d4b9 --- /dev/null +++ b/internal/db/bundb/migrations/20241113152126_add_status_edits.go @@ -0,0 +1,67 @@ +// 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" + "reflect" + + gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20241113152126_add_status_edits" + + "github.com/uptrace/bun" +) + +func init() { + up := func(ctx context.Context, db *bun.DB) error { + return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + statusType := reflect.TypeOf((*gtsmodel.Status)(nil)) + + // Generate new Status.EditIDs column definition from bun. + colDef, err := getBunColumnDef(tx, statusType, "EditIDs") + if err != nil { + return err + } + + // Add EditIDs column to Status table. + _, err = tx.NewAddColumn(). + Model((*gtsmodel.Status)(nil)). + ColumnExpr(colDef). + Exec(ctx) + if err != nil { + return err + } + + // Create the main StatusEdits table. + _, err = tx.NewCreateTable(). + IfNotExists(). + Model((*gtsmodel.StatusEdit)(nil)). + Exec(ctx) + return err + }) + } + + 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) + } +} diff --git a/internal/db/bundb/migrations/20241113152126_add_status_edits/status.go b/internal/db/bundb/migrations/20241113152126_add_status_edits/status.go new file mode 100644 index 000000000..1b7d93f70 --- /dev/null +++ b/internal/db/bundb/migrations/20241113152126_add_status_edits/status.go @@ -0,0 +1,97 @@ +// 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/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// 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 + Attachments []*gtsmodel.MediaAttachment `bun:"attached_media,rel:has-many"` // Attachments corresponding to attachmentIDs + TagIDs []string `bun:"tags,array"` // Database IDs of any tags used in this status + Tags []*gtsmodel.Tag `bun:"attached_tags,m2m:status_to_tags"` // Tags corresponding to tagIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation + MentionIDs []string `bun:"mentions,array"` // Database IDs of any mentions in this status + Mentions []*gtsmodel.Mention `bun:"attached_mentions,rel:has-many"` // Mentions corresponding to mentionIDs + EmojiIDs []string `bun:"emojis,array"` // Database IDs of any emojis used in this status + Emojis []*gtsmodel.Emoji `bun:"attached_emojis,m2m:status_to_emojis"` // Emojis corresponding to emojiIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation + 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? + Account *gtsmodel.Account `bun:"rel:belongs-to"` // account corresponding to accountID + 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 + InReplyToAccount *gtsmodel.Account `bun:"rel:belongs-to"` // account corresponding to inReplyToAccountID + 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 + BoostOfAccount *gtsmodel.Account `bun:"rel:belongs-to"` // account that corresponds to boostOfAccountID + 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"` // + Edits []*StatusEdit `bun:"-"` // + PollID string `bun:"type:CHAR(26),nullzero"` // + Poll *gtsmodel.Poll `bun:"-"` // + 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? + CreatedWithApplication *gtsmodel.Application `bun:"rel:belongs-to"` // application corresponding to createdWithApplicationID + 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 *gtsmodel.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. +} + +// Visibility represents the visibility granularity of a status. +type Visibility string + +const ( + // VisibilityNone means nobody can see this. + // It's only used for web status visibility. + VisibilityNone Visibility = "none" + // VisibilityPublic means this status will be visible to everyone on all timelines. + VisibilityPublic Visibility = "public" + // VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists. + VisibilityUnlocked Visibility = "unlocked" + // VisibilityFollowersOnly means this status is viewable to followers only. + VisibilityFollowersOnly Visibility = "followers_only" + // VisibilityMutualsOnly means this status is visible to mutual followers only. + VisibilityMutualsOnly Visibility = "mutuals_only" + // VisibilityDirect means this status is visible only to mentioned recipients. + VisibilityDirect Visibility = "direct" + // VisibilityDefault is used when no other setting can be found. + VisibilityDefault Visibility = VisibilityUnlocked +) diff --git a/internal/db/bundb/migrations/20241113152126_add_status_edits/statusedit.go b/internal/db/bundb/migrations/20241113152126_add_status_edits/statusedit.go new file mode 100644 index 000000000..b27c3b343 --- /dev/null +++ b/internal/db/bundb/migrations/20241113152126_add_status_edits/statusedit.go @@ -0,0 +1,48 @@ +// 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" +) + +// StatusEdit represents a **historical** view of a Status +// after a received edit. The Status itself will always +// contain the latest up-to-date information. +// +// Note that stored status edits may not exactly match that +// of the origin server, they are a best-effort by receiver +// to store version history. There is no AP history endpoint. +type StatusEdit struct { + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database. + Content string `bun:""` // Content of status at time of edit; likely html-formatted but not guaranteed. + ContentWarning string `bun:",nullzero"` // Content warning of status at time of edit. + Text string `bun:""` // Original status text, without formatting, at time of edit. + Language string `bun:",nullzero"` // Status language at time of edit. + Sensitive *bool `bun:",nullzero,notnull,default:false"` // Status sensitive flag at time of edit. + AttachmentIDs []string `bun:"attachments,array"` // Database IDs of media attachments associated with status at time of edit. + AttachmentDescriptions []string `bun:",array"` // Previous media descriptions of media attachments associated with status at time of edit. + PollOptions []string `bun:",array"` // Poll options of status at time of edit, only set if status contains a poll. + PollVotes []int `bun:",array"` // Poll vote count at time of status edit, only set if poll votes were reset. + StatusID string `bun:"type:CHAR(26),nullzero,notnull"` // The originating status ID this is a historical edit of. + CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // The creation time of this version of the status content (according to receiving server). + + // We don't bother having a *gtsmodel.Status model here + // as the StatusEdit is always just attached to a Status, + // so it doesn't need a self-reference back to it. +} diff --git a/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints.go b/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints.go index 10ae95c17..7621ddc6c 100644 --- a/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints.go +++ b/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints.go @@ -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{ diff --git a/internal/db/bundb/migrations/20241203124608_remove_media_attachment_updated_at.go b/internal/db/bundb/migrations/20241203124608_remove_media_attachment_updated_at.go new file mode 100644 index 000000000..344168b38 --- /dev/null +++ b/internal/db/bundb/migrations/20241203124608_remove_media_attachment_updated_at.go @@ -0,0 +1,57 @@ +// 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" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/uptrace/bun" +) + +func init() { + up := func(ctx context.Context, db *bun.DB) error { + return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + + // Check for 'updated_at' column on media attachments table, else return. + exists, err := doesColumnExist(ctx, tx, "media_attachments", "updated_at") + if err != nil { + return err + } else if !exists { + return nil + } + + // Remove 'updated_at' column. + _, err = tx.NewDropColumn(). + Model((*gtsmodel.MediaAttachment)(nil)). + Column("updated_at"). + Exec(ctx) + return err + }) + } + + 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) + } +} diff --git a/internal/db/bundb/migrations/util.go b/internal/db/bundb/migrations/util.go index 47de09e23..edf7c1d05 100644 --- a/internal/db/bundb/migrations/util.go +++ b/internal/db/bundb/migrations/util.go @@ -19,11 +19,209 @@ 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" + "github.com/uptrace/bun/dialect/sqltype" + "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. +// +// NOTE: this function must stay in sync with (*bun.CreateTableQuery{}).AppendQuery(), +// specifically where it loops over table fields appending each column definition. +func getBunColumnDef(db bun.IDB, rtype reflect.Type, fieldName string) (string, error) { + d := db.Dialect() + f := d.Features() + + // Get bun schema definitions for Go type and its field. + field, table, err := getModelField(db, rtype, fieldName) + if err != nil { + return "", err + } + + // Start with reasonable buf. + buf := make([]byte, 0, 64) + + // Start with the SQL column name. + buf = append(buf, field.SQLName...) + buf = append(buf, " "...) + + // Append the SQL + // type information. + switch { + + // Most of the time these two will match, but for the cases where DiscoveredSQLType is dialect-specific, + // e.g. pgdialect would change sqltype.SmallInt to pgTypeSmallSerial for columns that have `bun:",autoincrement"` + case !strings.EqualFold(field.CreateTableSQLType, field.DiscoveredSQLType): + buf = append(buf, field.CreateTableSQLType...) + + // For all common SQL types except VARCHAR, both UserDefinedSQLType and DiscoveredSQLType specify the correct type, + // and we needn't modify it. For VARCHAR columns, we will stop to check if a valid length has been set in .Varchar(int). + case !strings.EqualFold(field.CreateTableSQLType, sqltype.VarChar): + buf = append(buf, field.CreateTableSQLType...) + + // All else falls back + // to a default varchar. + default: + if d.Name() == dialect.Oracle { + buf = append(buf, "VARCHAR2"...) + } else { + buf = append(buf, sqltype.VarChar...) + } + buf = append(buf, "("...) + buf = strconv.AppendInt(buf, int64(d.DefaultVarcharLen()), 10) + buf = append(buf, ")"...) + } + + // Append not null definition if field requires. + if field.NotNull && d.Name() != dialect.Oracle { + buf = append(buf, " NOT NULL"...) + } + + // Append autoincrement definition if field requires. + if field.Identity && f.Has(feature.GeneratedIdentity) || + (field.AutoIncrement && (f.Has(feature.AutoIncrement) || f.Has(feature.Identity))) { + buf = d.AppendSequence(buf, table, field) + } + + // Append any default value. + if field.SQLDefault != "" { + buf = append(buf, " DEFAULT "...) + buf = append(buf, field.SQLDefault...) + } + + return byteutil.B2S(buf), nil +} + +// getModelField returns the uptrace/bun schema details for given Go type and field name. +func getModelField(db bun.IDB, rtype reflect.Type, fieldName string) (*schema.Field, *schema.Table, error) { + + // Get the associated table for Go type. + table := db.Dialect().Tables().Get(rtype) + if table == nil { + return nil, nil, fmt.Errorf("no table found for type: %s", rtype) + } + + var field *schema.Field + + // Look for field matching Go name. + for i := range table.Fields { + if table.Fields[i].GoName == fieldName { + field = table.Fields[i] + break + } + } + + if field == nil { + return nil, nil, fmt.Errorf("no bun field found on %s with name: %s", rtype, fieldName) + } + + return field, table, nil +} + // doesColumnExist safely checks whether given column exists on table, handling both SQLite and PostgreSQL appropriately. func doesColumnExist(ctx context.Context, tx bun.Tx, table, col string) (bool, error) { var n int |