gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

20230405130021_status_fave_unique_constraints.go (4374B)


      1 // GoToSocial
      2 // Copyright (C) GoToSocial Authors admin@gotosocial.org
      3 // SPDX-License-Identifier: AGPL-3.0-or-later
      4 //
      5 // This program is free software: you can redistribute it and/or modify
      6 // it under the terms of the GNU Affero General Public License as published by
      7 // the Free Software Foundation, either version 3 of the License, or
      8 // (at your option) any later version.
      9 //
     10 // This program is distributed in the hope that it will be useful,
     11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 // GNU Affero General Public License for more details.
     14 //
     15 // You should have received a copy of the GNU Affero General Public License
     16 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
     17 
     18 package migrations
     19 
     20 import (
     21 	"context"
     22 
     23 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
     24 	"github.com/uptrace/bun"
     25 )
     26 
     27 func init() {
     28 	up := func(ctx context.Context, db *bun.DB) error {
     29 		// To update fave constraints, we need to migrate faves into a new table.
     30 		// See section 7 here: https://www.sqlite.org/lang_altertable.html
     31 
     32 		return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
     33 			// Remove any duplicate faves that were created before constraints.
     34 			// We need to ensure that we don't just delete all faves that are
     35 			// duplicates--we should keep the original, non-duplicate fave.
     36 			// So, produced query will look something like this:
     37 			//
     38 			//	DELETE FROM "status_faves"
     39 			//	WHERE id IN (
     40 			//		WITH cte AS (
     41 			//			SELECT
     42 			//				"id",
     43 			//				ROW_NUMBER() OVER(
     44 			//					PARTITION BY "account_id", "status_id"
     45 			//					ORDER BY "account_id", "status_id"
     46 			//				) AS "row_number"
     47 			//			FROM status_faves
     48 			//		)
     49 			//		SELECT "id" FROM cte
     50 			//		WHERE "row_number" > 1
     51 			//	)
     52 			//
     53 			// The above query only deletes status_faves with ids that are
     54 			// in the subquery. The subquery selects the IDs of all duplicate
     55 			// status faves past the first one, where 'duplicate' means 'has
     56 			// the same account id and status id'.
     57 			overQ := tx.NewRaw(
     58 				"PARTITION BY ?, ? ORDER BY ?, ?",
     59 				bun.Ident("account_id"),
     60 				bun.Ident("status_id"),
     61 				bun.Ident("account_id"),
     62 				bun.Ident("status_id"),
     63 			)
     64 
     65 			rowNumberQ := tx.NewRaw(
     66 				"SELECT ?, ROW_NUMBER() OVER(?) AS ? FROM status_faves",
     67 				bun.Ident("id"),
     68 				overQ,
     69 				bun.Ident("row_number"),
     70 			)
     71 
     72 			inQ := tx.NewRaw(
     73 				"WITH cte AS (?) SELECT ? FROM cte WHERE ? > 1",
     74 				rowNumberQ,
     75 				bun.Ident("id"),
     76 				bun.Ident("row_number"),
     77 			)
     78 
     79 			if _, err := tx.
     80 				NewDelete().
     81 				Table("status_faves").
     82 				Where("id IN (?)", inQ).
     83 				Exec(ctx); err != nil {
     84 				return err
     85 			}
     86 
     87 			// Create the new faves table.
     88 			if _, err := tx.
     89 				NewCreateTable().
     90 				ModelTableExpr("new_status_faves").
     91 				Model(&gtsmodel.StatusFave{}).
     92 				Exec(ctx); err != nil {
     93 				return err
     94 			}
     95 
     96 			// Specify columns explicitly to
     97 			// avoid any Postgres shenanigans.
     98 			columns := []string{
     99 				"id",
    100 				"created_at",
    101 				"updated_at",
    102 				"account_id",
    103 				"target_account_id",
    104 				"status_id",
    105 				"uri",
    106 			}
    107 
    108 			// Copy remaining faves to the new table.
    109 			if _, err := tx.
    110 				NewInsert().
    111 				Table("new_status_faves").
    112 				Table("status_faves").
    113 				Column(columns...).
    114 				Exec(ctx); err != nil {
    115 				return err
    116 			}
    117 
    118 			// Drop the old table.
    119 			if _, err := tx.
    120 				NewDropTable().
    121 				Table("status_faves").
    122 				Exec(ctx); err != nil {
    123 				return err
    124 			}
    125 
    126 			// Rename new table to old table.
    127 			if _, err := tx.
    128 				ExecContext(
    129 					ctx,
    130 					"ALTER TABLE ? RENAME TO ?",
    131 					bun.Ident("new_status_faves"),
    132 					bun.Ident("status_faves"),
    133 				); err != nil {
    134 				return err
    135 			}
    136 
    137 			// Add indexes to the new table.
    138 			for index, columns := range map[string][]string{
    139 				"status_faves_id_idx":         {"id"},
    140 				"status_faves_account_id_idx": {"account_id"},
    141 				"status_faves_status_id_idx":  {"status_id"},
    142 			} {
    143 				if _, err := tx.
    144 					NewCreateIndex().
    145 					Table("status_faves").
    146 					Index(index).
    147 					Column(columns...).
    148 					Exec(ctx); err != nil {
    149 					return err
    150 				}
    151 			}
    152 
    153 			return nil
    154 		})
    155 	}
    156 
    157 	down := func(ctx context.Context, db *bun.DB) error {
    158 		return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
    159 			return nil
    160 		})
    161 	}
    162 
    163 	if err := Migrations.Register(up, down); err != nil {
    164 		panic(err)
    165 	}
    166 }