mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
Performer disambiguation and aliases (#3113)
* Refactor performer relationships * Remove checksum from performer * Add disambiguation, overhaul aliases * Add disambiguation filter criterion * Improve name matching during import * Add disambiguation filtering in UI * Include aliases in performer select
This commit is contained in:
@@ -21,7 +21,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
var appSchemaVersion uint = 41
|
||||
var appSchemaVersion uint = 42
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsBox embed.FS
|
||||
|
||||
18
pkg/sqlite/migrations/42_performer_disambig_aliases.up.sql
Normal file
18
pkg/sqlite/migrations/42_performer_disambig_aliases.up.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
CREATE TABLE `performer_aliases` (
|
||||
`performer_id` integer NOT NULL,
|
||||
`alias` varchar(255) NOT NULL,
|
||||
foreign key(`performer_id`) references `performers`(`id`) on delete CASCADE,
|
||||
PRIMARY KEY(`performer_id`, `alias`)
|
||||
);
|
||||
|
||||
CREATE INDEX `performer_aliases_alias` on `performer_aliases` (`alias`);
|
||||
|
||||
DROP INDEX `performers_checksum_unique`;
|
||||
ALTER TABLE `performers` DROP COLUMN `checksum`;
|
||||
ALTER TABLE `performers` ADD COLUMN `disambiguation` varchar(255);
|
||||
|
||||
-- these will be executed in the post-migration
|
||||
|
||||
-- ALTER TABLE `performers` DROP COLUMN `aliases`
|
||||
-- CREATE UNIQUE INDEX `performers_name_disambiguation_unique` on `performers` (`name`, `disambiguation`) WHERE `disambiguation` IS NOT NULL;
|
||||
-- CREATE UNIQUE INDEX `performers_name_unique` on `performers` (`name`) WHERE `disambiguation` IS NULL;
|
||||
243
pkg/sqlite/migrations/42_postmigrate.go
Normal file
243
pkg/sqlite/migrations/42_postmigrate.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
"github.com/stashapp/stash/pkg/sqlite"
|
||||
)
|
||||
|
||||
type schema42Migrator struct {
|
||||
migrator
|
||||
}
|
||||
|
||||
func post42(ctx context.Context, db *sqlx.DB) error {
|
||||
logger.Info("Running post-migration for schema version 42")
|
||||
|
||||
m := schema42Migrator{
|
||||
migrator: migrator{
|
||||
db: db,
|
||||
},
|
||||
}
|
||||
|
||||
if err := m.migrate(ctx); err != nil {
|
||||
return fmt.Errorf("migrating performer aliases: %w", err)
|
||||
}
|
||||
|
||||
if err := m.migrateDuplicatePerformers(ctx); err != nil {
|
||||
return fmt.Errorf("migrating performer aliases: %w", err)
|
||||
}
|
||||
|
||||
if err := m.executeSchemaChanges(); err != nil {
|
||||
return fmt.Errorf("executing schema changes: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *schema42Migrator) migrate(ctx context.Context) error {
|
||||
logger.Info("Migrating performer aliases")
|
||||
|
||||
const (
|
||||
limit = 1000
|
||||
logEvery = 10000
|
||||
)
|
||||
|
||||
lastID := 0
|
||||
count := 0
|
||||
|
||||
for {
|
||||
gotSome := false
|
||||
|
||||
if err := m.withTxn(ctx, func(tx *sqlx.Tx) error {
|
||||
query := "SELECT `id`, `aliases` FROM `performers` WHERE `aliases` IS NOT NULL AND `aliases` != ''"
|
||||
|
||||
if lastID != 0 {
|
||||
query += fmt.Sprintf(" AND `id` > %d ", lastID)
|
||||
}
|
||||
|
||||
query += fmt.Sprintf(" ORDER BY `id` LIMIT %d", limit)
|
||||
|
||||
rows, err := m.db.Query(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
id int
|
||||
aliases string
|
||||
)
|
||||
|
||||
err := rows.Scan(&id, &aliases)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lastID = id
|
||||
gotSome = true
|
||||
count++
|
||||
|
||||
if err := m.migratePerformerAliases(id, aliases); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return rows.Err()
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !gotSome {
|
||||
break
|
||||
}
|
||||
|
||||
if count%logEvery == 0 {
|
||||
logger.Infof("Migrated %d rows", count)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *schema42Migrator) migratePerformerAliases(id int, aliases string) error {
|
||||
// split aliases by , or /
|
||||
aliasList := strings.FieldsFunc(aliases, func(r rune) bool {
|
||||
return strings.ContainsRune(",/", r)
|
||||
})
|
||||
|
||||
// trim whitespace from each alias
|
||||
for i, alias := range aliasList {
|
||||
aliasList[i] = strings.TrimSpace(alias)
|
||||
}
|
||||
|
||||
// remove duplicates
|
||||
aliasList = stringslice.StrAppendUniques(nil, aliasList)
|
||||
|
||||
// insert aliases into table
|
||||
for _, alias := range aliasList {
|
||||
_, err := m.db.Exec("INSERT INTO `performer_aliases` (`performer_id`, `alias`) VALUES (?, ?)", id, alias)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *schema42Migrator) migrateDuplicatePerformers(ctx context.Context) error {
|
||||
logger.Info("Migrating duplicate performers")
|
||||
|
||||
const (
|
||||
limit = 1000
|
||||
logEvery = 10000
|
||||
)
|
||||
|
||||
count := 0
|
||||
|
||||
for {
|
||||
gotSome := false
|
||||
|
||||
if err := m.withTxn(ctx, func(tx *sqlx.Tx) error {
|
||||
query := `
|
||||
SELECT id, name FROM performers WHERE performers.disambiguation IS NULL AND EXISTS (
|
||||
SELECT 1 FROM performers p2 WHERE
|
||||
performers.name = p2.name AND
|
||||
performers.rowid > p2.rowid
|
||||
)`
|
||||
|
||||
query += fmt.Sprintf(" ORDER BY `id` LIMIT %d", limit)
|
||||
|
||||
rows, err := m.db.Query(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
id int
|
||||
name string
|
||||
)
|
||||
|
||||
err := rows.Scan(&id, &name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gotSome = true
|
||||
count++
|
||||
|
||||
if err := m.migrateDuplicatePerformer(id, name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return rows.Err()
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !gotSome {
|
||||
break
|
||||
}
|
||||
|
||||
if count%logEvery == 0 {
|
||||
logger.Infof("Migrated %d performers", count)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *schema42Migrator) migrateDuplicatePerformer(performerID int, name string) error {
|
||||
// get the highest value of disambiguation for this performer name
|
||||
query := `
|
||||
SELECT disambiguation FROM performers WHERE name = ? ORDER BY disambiguation DESC LIMIT 1`
|
||||
|
||||
var disambiguation sql.NullString
|
||||
if err := m.db.Get(&disambiguation, query, name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newDisambiguation := 1
|
||||
|
||||
// if there is no disambiguation, set it to 1
|
||||
if disambiguation.Valid {
|
||||
numericDis, err := strconv.Atoi(disambiguation.String)
|
||||
if err != nil {
|
||||
// shouldn't happen
|
||||
return err
|
||||
}
|
||||
|
||||
newDisambiguation = numericDis + 1
|
||||
}
|
||||
|
||||
logger.Infof("Adding disambiguation '%d' for performer %q", newDisambiguation, name)
|
||||
|
||||
_, err := m.db.Exec("UPDATE performers SET disambiguation = ? WHERE id = ?", strconv.Itoa(newDisambiguation), performerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *schema42Migrator) executeSchemaChanges() error {
|
||||
return m.execAll([]string{
|
||||
"ALTER TABLE `performers` DROP COLUMN `aliases`",
|
||||
"CREATE UNIQUE INDEX `performers_name_disambiguation_unique` on `performers` (`name`, `disambiguation`) WHERE `disambiguation` IS NOT NULL",
|
||||
"CREATE UNIQUE INDEX `performers_name_unique` on `performers` (`name`) WHERE `disambiguation` IS NULL",
|
||||
})
|
||||
}
|
||||
|
||||
func init() {
|
||||
sqlite.RegisterPostMigration(42, post42)
|
||||
}
|
||||
@@ -36,3 +36,13 @@ func (m *migrator) withTxn(ctx context.Context, fn func(tx *sqlx.Tx) error) erro
|
||||
err = fn(tx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *migrator) execAll(stmts []string) error {
|
||||
for _, stmt := range stmts {
|
||||
if _, err := m.db.Exec(stmt); err != nil {
|
||||
return fmt.Errorf("executing statement %s: %w", stmt, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -17,33 +17,36 @@ import (
|
||||
"gopkg.in/guregu/null.v4/zero"
|
||||
)
|
||||
|
||||
const performerTable = "performers"
|
||||
const performerIDColumn = "performer_id"
|
||||
const performersTagsTable = "performers_tags"
|
||||
const performersImageTable = "performers_image" // performer cover image
|
||||
const (
|
||||
performerTable = "performers"
|
||||
performerIDColumn = "performer_id"
|
||||
performersAliasesTable = "performer_aliases"
|
||||
performerAliasColumn = "alias"
|
||||
performersTagsTable = "performers_tags"
|
||||
performersImageTable = "performers_image" // performer cover image
|
||||
)
|
||||
|
||||
type performerRow struct {
|
||||
ID int `db:"id" goqu:"skipinsert"`
|
||||
Checksum string `db:"checksum"`
|
||||
Name zero.String `db:"name"`
|
||||
Gender zero.String `db:"gender"`
|
||||
URL zero.String `db:"url"`
|
||||
Twitter zero.String `db:"twitter"`
|
||||
Instagram zero.String `db:"instagram"`
|
||||
Birthdate models.SQLiteDate `db:"birthdate"`
|
||||
Ethnicity zero.String `db:"ethnicity"`
|
||||
Country zero.String `db:"country"`
|
||||
EyeColor zero.String `db:"eye_color"`
|
||||
Height null.Int `db:"height"`
|
||||
Measurements zero.String `db:"measurements"`
|
||||
FakeTits zero.String `db:"fake_tits"`
|
||||
CareerLength zero.String `db:"career_length"`
|
||||
Tattoos zero.String `db:"tattoos"`
|
||||
Piercings zero.String `db:"piercings"`
|
||||
Aliases zero.String `db:"aliases"`
|
||||
Favorite sql.NullBool `db:"favorite"`
|
||||
CreatedAt models.SQLiteTimestamp `db:"created_at"`
|
||||
UpdatedAt models.SQLiteTimestamp `db:"updated_at"`
|
||||
ID int `db:"id" goqu:"skipinsert"`
|
||||
Name string `db:"name"`
|
||||
Disambigation zero.String `db:"disambiguation"`
|
||||
Gender zero.String `db:"gender"`
|
||||
URL zero.String `db:"url"`
|
||||
Twitter zero.String `db:"twitter"`
|
||||
Instagram zero.String `db:"instagram"`
|
||||
Birthdate models.SQLiteDate `db:"birthdate"`
|
||||
Ethnicity zero.String `db:"ethnicity"`
|
||||
Country zero.String `db:"country"`
|
||||
EyeColor zero.String `db:"eye_color"`
|
||||
Height null.Int `db:"height"`
|
||||
Measurements zero.String `db:"measurements"`
|
||||
FakeTits zero.String `db:"fake_tits"`
|
||||
CareerLength zero.String `db:"career_length"`
|
||||
Tattoos zero.String `db:"tattoos"`
|
||||
Piercings zero.String `db:"piercings"`
|
||||
Favorite sql.NullBool `db:"favorite"`
|
||||
CreatedAt models.SQLiteTimestamp `db:"created_at"`
|
||||
UpdatedAt models.SQLiteTimestamp `db:"updated_at"`
|
||||
// expressed as 1-100
|
||||
Rating null.Int `db:"rating"`
|
||||
Details zero.String `db:"details"`
|
||||
@@ -55,8 +58,8 @@ type performerRow struct {
|
||||
|
||||
func (r *performerRow) fromPerformer(o models.Performer) {
|
||||
r.ID = o.ID
|
||||
r.Checksum = o.Checksum
|
||||
r.Name = zero.StringFrom(o.Name)
|
||||
r.Name = o.Name
|
||||
r.Disambigation = zero.StringFrom(o.Disambiguation)
|
||||
if o.Gender.IsValid() {
|
||||
r.Gender = zero.StringFrom(o.Gender.String())
|
||||
}
|
||||
@@ -75,7 +78,6 @@ func (r *performerRow) fromPerformer(o models.Performer) {
|
||||
r.CareerLength = zero.StringFrom(o.CareerLength)
|
||||
r.Tattoos = zero.StringFrom(o.Tattoos)
|
||||
r.Piercings = zero.StringFrom(o.Piercings)
|
||||
r.Aliases = zero.StringFrom(o.Aliases)
|
||||
r.Favorite = sql.NullBool{Bool: o.Favorite, Valid: true}
|
||||
r.CreatedAt = models.SQLiteTimestamp{Timestamp: o.CreatedAt}
|
||||
r.UpdatedAt = models.SQLiteTimestamp{Timestamp: o.UpdatedAt}
|
||||
@@ -91,27 +93,26 @@ func (r *performerRow) fromPerformer(o models.Performer) {
|
||||
|
||||
func (r *performerRow) resolve() *models.Performer {
|
||||
ret := &models.Performer{
|
||||
ID: r.ID,
|
||||
Checksum: r.Checksum,
|
||||
Name: r.Name.String,
|
||||
Gender: models.GenderEnum(r.Gender.String),
|
||||
URL: r.URL.String,
|
||||
Twitter: r.Twitter.String,
|
||||
Instagram: r.Instagram.String,
|
||||
Birthdate: r.Birthdate.DatePtr(),
|
||||
Ethnicity: r.Ethnicity.String,
|
||||
Country: r.Country.String,
|
||||
EyeColor: r.EyeColor.String,
|
||||
Height: nullIntPtr(r.Height),
|
||||
Measurements: r.Measurements.String,
|
||||
FakeTits: r.FakeTits.String,
|
||||
CareerLength: r.CareerLength.String,
|
||||
Tattoos: r.Tattoos.String,
|
||||
Piercings: r.Piercings.String,
|
||||
Aliases: r.Aliases.String,
|
||||
Favorite: r.Favorite.Bool,
|
||||
CreatedAt: r.CreatedAt.Timestamp,
|
||||
UpdatedAt: r.UpdatedAt.Timestamp,
|
||||
ID: r.ID,
|
||||
Name: r.Name,
|
||||
Disambiguation: r.Disambigation.String,
|
||||
Gender: models.GenderEnum(r.Gender.String),
|
||||
URL: r.URL.String,
|
||||
Twitter: r.Twitter.String,
|
||||
Instagram: r.Instagram.String,
|
||||
Birthdate: r.Birthdate.DatePtr(),
|
||||
Ethnicity: r.Ethnicity.String,
|
||||
Country: r.Country.String,
|
||||
EyeColor: r.EyeColor.String,
|
||||
Height: nullIntPtr(r.Height),
|
||||
Measurements: r.Measurements.String,
|
||||
FakeTits: r.FakeTits.String,
|
||||
CareerLength: r.CareerLength.String,
|
||||
Tattoos: r.Tattoos.String,
|
||||
Piercings: r.Piercings.String,
|
||||
Favorite: r.Favorite.Bool,
|
||||
CreatedAt: r.CreatedAt.Timestamp,
|
||||
UpdatedAt: r.UpdatedAt.Timestamp,
|
||||
// expressed as 1-100
|
||||
Rating: nullIntPtr(r.Rating),
|
||||
Details: r.Details.String,
|
||||
@@ -129,8 +130,8 @@ type performerRowRecord struct {
|
||||
}
|
||||
|
||||
func (r *performerRowRecord) fromPartial(o models.PerformerPartial) {
|
||||
r.setNullString("checksum", o.Checksum)
|
||||
r.setNullString("name", o.Name)
|
||||
r.setString("name", o.Name)
|
||||
r.setNullString("disambiguation", o.Disambiguation)
|
||||
r.setNullString("gender", o.Gender)
|
||||
r.setNullString("url", o.URL)
|
||||
r.setNullString("twitter", o.Twitter)
|
||||
@@ -145,7 +146,6 @@ func (r *performerRowRecord) fromPartial(o models.PerformerPartial) {
|
||||
r.setNullString("career_length", o.CareerLength)
|
||||
r.setNullString("tattoos", o.Tattoos)
|
||||
r.setNullString("piercings", o.Piercings)
|
||||
r.setNullString("aliases", o.Aliases)
|
||||
r.setBool("favorite", o.Favorite)
|
||||
r.setSQLiteTimestamp("created_at", o.CreatedAt)
|
||||
r.setSQLiteTimestamp("updated_at", o.UpdatedAt)
|
||||
@@ -182,6 +182,24 @@ func (qb *PerformerStore) Create(ctx context.Context, newObject *models.Performe
|
||||
return err
|
||||
}
|
||||
|
||||
if newObject.Aliases.Loaded() {
|
||||
if err := performersAliasesTableMgr.insertJoins(ctx, id, newObject.Aliases.List()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if newObject.TagIDs.Loaded() {
|
||||
if err := performersTagsTableMgr.insertJoins(ctx, id, newObject.TagIDs.List()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if newObject.StashIDs.Loaded() {
|
||||
if err := performersStashIDsTableMgr.insertJoins(ctx, id, newObject.StashIDs.List()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
updated, err := qb.Find(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding after create: %w", err)
|
||||
@@ -192,14 +210,14 @@ func (qb *PerformerStore) Create(ctx context.Context, newObject *models.Performe
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *PerformerStore) UpdatePartial(ctx context.Context, id int, updatedObject models.PerformerPartial) (*models.Performer, error) {
|
||||
func (qb *PerformerStore) UpdatePartial(ctx context.Context, id int, partial models.PerformerPartial) (*models.Performer, error) {
|
||||
r := performerRowRecord{
|
||||
updateRecord{
|
||||
Record: make(exp.Record),
|
||||
},
|
||||
}
|
||||
|
||||
r.fromPartial(updatedObject)
|
||||
r.fromPartial(partial)
|
||||
|
||||
if len(r.Record) > 0 {
|
||||
if err := qb.tableMgr.updateByID(ctx, id, r.Record); err != nil {
|
||||
@@ -207,6 +225,23 @@ func (qb *PerformerStore) UpdatePartial(ctx context.Context, id int, updatedObje
|
||||
}
|
||||
}
|
||||
|
||||
if partial.Aliases != nil {
|
||||
if err := performersAliasesTableMgr.modifyJoins(ctx, id, partial.Aliases.Values, partial.Aliases.Mode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if partial.TagIDs != nil {
|
||||
if err := performersTagsTableMgr.modifyJoins(ctx, id, partial.TagIDs.IDs, partial.TagIDs.Mode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if partial.StashIDs != nil {
|
||||
if err := performersStashIDsTableMgr.modifyJoins(ctx, id, partial.StashIDs.StashIDs, partial.StashIDs.Mode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return qb.Find(ctx, id)
|
||||
}
|
||||
|
||||
@@ -218,6 +253,24 @@ func (qb *PerformerStore) Update(ctx context.Context, updatedObject *models.Perf
|
||||
return err
|
||||
}
|
||||
|
||||
if updatedObject.Aliases.Loaded() {
|
||||
if err := performersAliasesTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.Aliases.List()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if updatedObject.TagIDs.Loaded() {
|
||||
if err := performersTagsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.TagIDs.List()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if updatedObject.StashIDs.Loaded() {
|
||||
if err := performersStashIDsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.StashIDs.List()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -397,14 +450,19 @@ func (qb *PerformerStore) QueryForAutoTag(ctx context.Context, words []string) (
|
||||
// TODO - Query needs to be changed to support queries of this type, and
|
||||
// this method should be removed
|
||||
table := qb.table()
|
||||
sq := dialect.From(table).Select(table.Col(idColumn)).Where()
|
||||
sq := dialect.From(table).Select(table.Col(idColumn))
|
||||
// TODO - disabled alias matching until we get finer control over it
|
||||
// .LeftJoin(
|
||||
// performersAliasesJoinTable,
|
||||
// goqu.On(performersAliasesJoinTable.Col(performerIDColumn).Eq(table.Col(idColumn))),
|
||||
// )
|
||||
|
||||
var whereClauses []exp.Expression
|
||||
|
||||
for _, w := range words {
|
||||
whereClauses = append(whereClauses, table.Col("name").Like(w+"%"))
|
||||
// TODO - commented out until alias matching works both ways
|
||||
// whereClauses = append(whereClauses, table.Col("aliases").Like(w+"%")
|
||||
// TODO - see above
|
||||
// whereClauses = append(whereClauses, performersAliasesJoinTable.Col("alias").Like(w+"%"))
|
||||
}
|
||||
|
||||
sq = sq.Where(
|
||||
@@ -483,6 +541,7 @@ func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.Perform
|
||||
|
||||
const tableName = performerTable
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.Name, tableName+".name"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.Disambiguation, tableName+".disambiguation"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.Details, tableName+".details"))
|
||||
|
||||
query.handleCriterion(ctx, boolCriterionHandler(filter.FilterFavorites, tableName+".favorite", nil))
|
||||
@@ -527,12 +586,6 @@ func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.Perform
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.HairColor, tableName+".hair_color"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.URL, tableName+".url"))
|
||||
query.handleCriterion(ctx, intCriterionHandler(filter.Weight, tableName+".weight", nil))
|
||||
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if filter.StashID != nil {
|
||||
qb.stashIDRepository().join(f, "performer_stash_ids", "performers.id")
|
||||
stringCriterionHandler(filter.StashID, "performer_stash_ids.stash_id")(ctx, f)
|
||||
}
|
||||
}))
|
||||
query.handleCriterion(ctx, &stashIDCriterionHandler{
|
||||
c: filter.StashIDEndpoint,
|
||||
stashIDRepository: qb.stashIDRepository(),
|
||||
@@ -540,8 +593,7 @@ func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.Perform
|
||||
parentIDCol: "performers.id",
|
||||
})
|
||||
|
||||
// TODO - need better handling of aliases
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.Aliases, tableName+".aliases"))
|
||||
query.handleCriterion(ctx, performerAliasCriterionHandler(qb, filter.Aliases))
|
||||
|
||||
query.handleCriterion(ctx, performerTagsCriterionHandler(qb, filter.Tags))
|
||||
|
||||
@@ -571,7 +623,8 @@ func (qb *PerformerStore) Query(ctx context.Context, performerFilter *models.Per
|
||||
distinctIDs(&query, performerTable)
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
searchColumns := []string{"performers.name", "performers.aliases"}
|
||||
query.join(performersAliasesTable, "", "performer_aliases.performer_id = performers.id")
|
||||
searchColumns := []string{"performers.name", "performer_aliases.alias"}
|
||||
query.parseQueryString(searchColumns, *q)
|
||||
}
|
||||
|
||||
@@ -607,7 +660,7 @@ func performerIsMissingCriterionHandler(qb *PerformerStore, isMissing *string) c
|
||||
f.addLeftJoin(performersImageTable, "image_join", "image_join.performer_id = performers.id")
|
||||
f.addWhere("image_join.performer_id IS NULL")
|
||||
case "stash_id":
|
||||
qb.stashIDRepository().join(f, "performer_stash_ids", "performers.id")
|
||||
performersStashIDsTableMgr.join(f, "performer_stash_ids", "performers.id")
|
||||
f.addWhere("performer_stash_ids.performer_id IS NULL")
|
||||
default:
|
||||
f.addWhere("(performers." + *isMissing + " IS NULL OR TRIM(performers." + *isMissing + ") = '')")
|
||||
@@ -637,6 +690,18 @@ func performerAgeFilterCriterionHandler(age *models.IntCriterionInput) criterion
|
||||
}
|
||||
}
|
||||
|
||||
func performerAliasCriterionHandler(qb *PerformerStore, alias *models.StringCriterionInput) criterionHandlerFunc {
|
||||
h := stringListCriterionHandlerBuilder{
|
||||
joinTable: performersAliasesTable,
|
||||
stringColumn: performerAliasColumn,
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
performersAliasesTableMgr.join(f, "", "performers.id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(alias)
|
||||
}
|
||||
|
||||
func performerTagsCriterionHandler(qb *PerformerStore, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
h := joinedHierarchicalMultiCriterionHandlerBuilder{
|
||||
tx: qb.tx,
|
||||
@@ -813,11 +878,6 @@ func (qb *PerformerStore) GetTagIDs(ctx context.Context, id int) ([]int, error)
|
||||
return qb.tagsRepository().getIDs(ctx, id)
|
||||
}
|
||||
|
||||
func (qb *PerformerStore) UpdateTags(ctx context.Context, id int, tagIDs []int) error {
|
||||
// Delete the existing joins and then create new ones
|
||||
return qb.tagsRepository().replace(ctx, id, tagIDs)
|
||||
}
|
||||
|
||||
func (qb *PerformerStore) imageRepository() *imageRepository {
|
||||
return &imageRepository{
|
||||
repository: repository{
|
||||
@@ -851,12 +911,12 @@ func (qb *PerformerStore) stashIDRepository() *stashIDRepository {
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *PerformerStore) GetStashIDs(ctx context.Context, performerID int) ([]models.StashID, error) {
|
||||
return qb.stashIDRepository().get(ctx, performerID)
|
||||
func (qb *PerformerStore) GetAliases(ctx context.Context, performerID int) ([]string, error) {
|
||||
return performersAliasesTableMgr.get(ctx, performerID)
|
||||
}
|
||||
|
||||
func (qb *PerformerStore) UpdateStashIDs(ctx context.Context, performerID int, stashIDs []models.StashID) error {
|
||||
return qb.stashIDRepository().replace(ctx, performerID, stashIDs)
|
||||
func (qb *PerformerStore) GetStashIDs(ctx context.Context, performerID int) ([]models.StashID, error) {
|
||||
return performersStashIDsTableMgr.get(ctx, performerID)
|
||||
}
|
||||
|
||||
func (qb *PerformerStore) FindByStashID(ctx context.Context, stashID models.StashID) ([]*models.Performer, error) {
|
||||
|
||||
@@ -12,37 +12,204 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/hash/md5"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func loadPerformerRelationships(ctx context.Context, expected models.Performer, actual *models.Performer) error {
|
||||
if expected.Aliases.Loaded() {
|
||||
if err := actual.LoadAliases(ctx, db.Performer); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if expected.TagIDs.Loaded() {
|
||||
if err := actual.LoadTagIDs(ctx, db.Performer); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if expected.StashIDs.Loaded() {
|
||||
if err := actual.LoadStashIDs(ctx, db.Performer); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Test_PerformerStore_Create(t *testing.T) {
|
||||
var (
|
||||
name = "name"
|
||||
disambiguation = "disambiguation"
|
||||
gender = models.GenderEnumFemale
|
||||
details = "details"
|
||||
url = "url"
|
||||
twitter = "twitter"
|
||||
instagram = "instagram"
|
||||
rating = 3
|
||||
ethnicity = "ethnicity"
|
||||
country = "country"
|
||||
eyeColor = "eyeColor"
|
||||
height = 134
|
||||
measurements = "measurements"
|
||||
fakeTits = "fakeTits"
|
||||
careerLength = "careerLength"
|
||||
tattoos = "tattoos"
|
||||
piercings = "piercings"
|
||||
aliases = []string{"alias1", "alias2"}
|
||||
hairColor = "hairColor"
|
||||
weight = 123
|
||||
ignoreAutoTag = true
|
||||
favorite = true
|
||||
endpoint1 = "endpoint1"
|
||||
endpoint2 = "endpoint2"
|
||||
stashID1 = "stashid1"
|
||||
stashID2 = "stashid2"
|
||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
birthdate = models.NewDate("2003-02-01")
|
||||
deathdate = models.NewDate("2023-02-01")
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
newObject models.Performer
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"full",
|
||||
models.Performer{
|
||||
Name: name,
|
||||
Disambiguation: disambiguation,
|
||||
Gender: gender,
|
||||
URL: url,
|
||||
Twitter: twitter,
|
||||
Instagram: instagram,
|
||||
Birthdate: &birthdate,
|
||||
Ethnicity: ethnicity,
|
||||
Country: country,
|
||||
EyeColor: eyeColor,
|
||||
Height: &height,
|
||||
Measurements: measurements,
|
||||
FakeTits: fakeTits,
|
||||
CareerLength: careerLength,
|
||||
Tattoos: tattoos,
|
||||
Piercings: piercings,
|
||||
Favorite: favorite,
|
||||
Rating: &rating,
|
||||
Details: details,
|
||||
DeathDate: &deathdate,
|
||||
HairColor: hairColor,
|
||||
Weight: &weight,
|
||||
IgnoreAutoTag: ignoreAutoTag,
|
||||
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]}),
|
||||
Aliases: models.NewRelatedStrings(aliases),
|
||||
StashIDs: models.NewRelatedStashIDs([]models.StashID{
|
||||
{
|
||||
StashID: stashID1,
|
||||
Endpoint: endpoint1,
|
||||
},
|
||||
{
|
||||
StashID: stashID2,
|
||||
Endpoint: endpoint2,
|
||||
},
|
||||
}),
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid tag id",
|
||||
models.Performer{
|
||||
Name: name,
|
||||
TagIDs: models.NewRelatedIDs([]int{invalidID}),
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
qb := db.Performer
|
||||
|
||||
for _, tt := range tests {
|
||||
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
|
||||
assert := assert.New(t)
|
||||
|
||||
p := tt.newObject
|
||||
if err := qb.Create(ctx, &p); (err != nil) != tt.wantErr {
|
||||
t.Errorf("PerformerStore.Create() error = %v, wantErr = %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Zero(p.ID)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NotZero(p.ID)
|
||||
|
||||
copy := tt.newObject
|
||||
copy.ID = p.ID
|
||||
|
||||
// load relationships
|
||||
if err := loadPerformerRelationships(ctx, copy, &p); err != nil {
|
||||
t.Errorf("loadPerformerRelationships() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(copy, p)
|
||||
|
||||
// ensure can find the performer
|
||||
found, err := qb.Find(ctx, p.ID)
|
||||
if err != nil {
|
||||
t.Errorf("PerformerStore.Find() error = %v", err)
|
||||
}
|
||||
|
||||
if !assert.NotNil(found) {
|
||||
return
|
||||
}
|
||||
|
||||
// load relationships
|
||||
if err := loadPerformerRelationships(ctx, copy, found); err != nil {
|
||||
t.Errorf("loadPerformerRelationships() error = %v", err)
|
||||
return
|
||||
}
|
||||
assert.Equal(copy, *found)
|
||||
|
||||
return
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_PerformerStore_Update(t *testing.T) {
|
||||
var (
|
||||
name = "name"
|
||||
gender = models.GenderEnumFemale
|
||||
checksum = "checksum"
|
||||
details = "details"
|
||||
url = "url"
|
||||
twitter = "twitter"
|
||||
instagram = "instagram"
|
||||
rating = 3
|
||||
ethnicity = "ethnicity"
|
||||
country = "country"
|
||||
eyeColor = "eyeColor"
|
||||
height = 134
|
||||
measurements = "measurements"
|
||||
fakeTits = "fakeTits"
|
||||
careerLength = "careerLength"
|
||||
tattoos = "tattoos"
|
||||
piercings = "piercings"
|
||||
aliases = "aliases"
|
||||
hairColor = "hairColor"
|
||||
weight = 123
|
||||
ignoreAutoTag = true
|
||||
favorite = true
|
||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
name = "name"
|
||||
disambiguation = "disambiguation"
|
||||
gender = models.GenderEnumFemale
|
||||
details = "details"
|
||||
url = "url"
|
||||
twitter = "twitter"
|
||||
instagram = "instagram"
|
||||
rating = 3
|
||||
ethnicity = "ethnicity"
|
||||
country = "country"
|
||||
eyeColor = "eyeColor"
|
||||
height = 134
|
||||
measurements = "measurements"
|
||||
fakeTits = "fakeTits"
|
||||
careerLength = "careerLength"
|
||||
tattoos = "tattoos"
|
||||
piercings = "piercings"
|
||||
aliases = []string{"alias1", "alias2"}
|
||||
hairColor = "hairColor"
|
||||
weight = 123
|
||||
ignoreAutoTag = true
|
||||
favorite = true
|
||||
endpoint1 = "endpoint1"
|
||||
endpoint2 = "endpoint2"
|
||||
stashID1 = "stashid1"
|
||||
stashID2 = "stashid2"
|
||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
birthdate = models.NewDate("2003-02-01")
|
||||
deathdate = models.NewDate("2023-02-01")
|
||||
@@ -56,43 +223,73 @@ func Test_PerformerStore_Update(t *testing.T) {
|
||||
{
|
||||
"full",
|
||||
&models.Performer{
|
||||
ID: performerIDs[performerIdxWithGallery],
|
||||
Name: name,
|
||||
Checksum: checksum,
|
||||
Gender: gender,
|
||||
URL: url,
|
||||
Twitter: twitter,
|
||||
Instagram: instagram,
|
||||
Birthdate: &birthdate,
|
||||
Ethnicity: ethnicity,
|
||||
Country: country,
|
||||
EyeColor: eyeColor,
|
||||
Height: &height,
|
||||
Measurements: measurements,
|
||||
FakeTits: fakeTits,
|
||||
CareerLength: careerLength,
|
||||
Tattoos: tattoos,
|
||||
Piercings: piercings,
|
||||
Aliases: aliases,
|
||||
Favorite: favorite,
|
||||
Rating: &rating,
|
||||
Details: details,
|
||||
DeathDate: &deathdate,
|
||||
HairColor: hairColor,
|
||||
Weight: &weight,
|
||||
IgnoreAutoTag: ignoreAutoTag,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
ID: performerIDs[performerIdxWithGallery],
|
||||
Name: name,
|
||||
Disambiguation: disambiguation,
|
||||
Gender: gender,
|
||||
URL: url,
|
||||
Twitter: twitter,
|
||||
Instagram: instagram,
|
||||
Birthdate: &birthdate,
|
||||
Ethnicity: ethnicity,
|
||||
Country: country,
|
||||
EyeColor: eyeColor,
|
||||
Height: &height,
|
||||
Measurements: measurements,
|
||||
FakeTits: fakeTits,
|
||||
CareerLength: careerLength,
|
||||
Tattoos: tattoos,
|
||||
Piercings: piercings,
|
||||
Favorite: favorite,
|
||||
Rating: &rating,
|
||||
Details: details,
|
||||
DeathDate: &deathdate,
|
||||
HairColor: hairColor,
|
||||
Weight: &weight,
|
||||
IgnoreAutoTag: ignoreAutoTag,
|
||||
Aliases: models.NewRelatedStrings(aliases),
|
||||
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]}),
|
||||
StashIDs: models.NewRelatedStashIDs([]models.StashID{
|
||||
{
|
||||
StashID: stashID1,
|
||||
Endpoint: endpoint1,
|
||||
},
|
||||
{
|
||||
StashID: stashID2,
|
||||
Endpoint: endpoint2,
|
||||
},
|
||||
}),
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"clear all",
|
||||
"clear nullables",
|
||||
&models.Performer{
|
||||
ID: performerIDs[performerIdxWithGallery],
|
||||
ID: performerIDs[performerIdxWithGallery],
|
||||
Aliases: models.NewRelatedStrings([]string{}),
|
||||
TagIDs: models.NewRelatedIDs([]int{}),
|
||||
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"clear tag ids",
|
||||
&models.Performer{
|
||||
ID: performerIDs[sceneIdxWithTag],
|
||||
TagIDs: models.NewRelatedIDs([]int{}),
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid tag id",
|
||||
&models.Performer{
|
||||
ID: performerIDs[sceneIdxWithGallery],
|
||||
TagIDs: models.NewRelatedIDs([]int{invalidID}),
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
qb := db.Performer
|
||||
@@ -115,37 +312,80 @@ func Test_PerformerStore_Update(t *testing.T) {
|
||||
t.Errorf("PerformerStore.Find() error = %v", err)
|
||||
}
|
||||
|
||||
// load relationships
|
||||
if err := loadPerformerRelationships(ctx, copy, s); err != nil {
|
||||
t.Errorf("loadPerformerRelationships() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(copy, *s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func clearPerformerPartial() models.PerformerPartial {
|
||||
nullString := models.OptionalString{Set: true, Null: true}
|
||||
nullDate := models.OptionalDate{Set: true, Null: true}
|
||||
nullInt := models.OptionalInt{Set: true, Null: true}
|
||||
|
||||
// leave mandatory fields
|
||||
return models.PerformerPartial{
|
||||
Disambiguation: nullString,
|
||||
Gender: nullString,
|
||||
URL: nullString,
|
||||
Twitter: nullString,
|
||||
Instagram: nullString,
|
||||
Birthdate: nullDate,
|
||||
Ethnicity: nullString,
|
||||
Country: nullString,
|
||||
EyeColor: nullString,
|
||||
Height: nullInt,
|
||||
Measurements: nullString,
|
||||
FakeTits: nullString,
|
||||
CareerLength: nullString,
|
||||
Tattoos: nullString,
|
||||
Piercings: nullString,
|
||||
Aliases: &models.UpdateStrings{Mode: models.RelationshipUpdateModeSet},
|
||||
Rating: nullInt,
|
||||
Details: nullString,
|
||||
DeathDate: nullDate,
|
||||
HairColor: nullString,
|
||||
Weight: nullInt,
|
||||
TagIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet},
|
||||
StashIDs: &models.UpdateStashIDs{Mode: models.RelationshipUpdateModeSet},
|
||||
}
|
||||
}
|
||||
|
||||
func Test_PerformerStore_UpdatePartial(t *testing.T) {
|
||||
var (
|
||||
name = "name"
|
||||
gender = models.GenderEnumFemale
|
||||
checksum = "checksum"
|
||||
details = "details"
|
||||
url = "url"
|
||||
twitter = "twitter"
|
||||
instagram = "instagram"
|
||||
rating = 3
|
||||
ethnicity = "ethnicity"
|
||||
country = "country"
|
||||
eyeColor = "eyeColor"
|
||||
height = 143
|
||||
measurements = "measurements"
|
||||
fakeTits = "fakeTits"
|
||||
careerLength = "careerLength"
|
||||
tattoos = "tattoos"
|
||||
piercings = "piercings"
|
||||
aliases = "aliases"
|
||||
hairColor = "hairColor"
|
||||
weight = 123
|
||||
ignoreAutoTag = true
|
||||
favorite = true
|
||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
name = "name"
|
||||
disambiguation = "disambiguation"
|
||||
gender = models.GenderEnumFemale
|
||||
details = "details"
|
||||
url = "url"
|
||||
twitter = "twitter"
|
||||
instagram = "instagram"
|
||||
rating = 3
|
||||
ethnicity = "ethnicity"
|
||||
country = "country"
|
||||
eyeColor = "eyeColor"
|
||||
height = 143
|
||||
measurements = "measurements"
|
||||
fakeTits = "fakeTits"
|
||||
careerLength = "careerLength"
|
||||
tattoos = "tattoos"
|
||||
piercings = "piercings"
|
||||
aliases = []string{"alias1", "alias2"}
|
||||
hairColor = "hairColor"
|
||||
weight = 123
|
||||
ignoreAutoTag = true
|
||||
favorite = true
|
||||
endpoint1 = "endpoint1"
|
||||
endpoint2 = "endpoint2"
|
||||
stashID1 = "stashid1"
|
||||
stashID2 = "stashid2"
|
||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
birthdate = models.NewDate("2003-02-01")
|
||||
deathdate = models.NewDate("2023-02-01")
|
||||
@@ -162,23 +402,26 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
|
||||
"full",
|
||||
performerIDs[performerIdxWithDupName],
|
||||
models.PerformerPartial{
|
||||
Name: models.NewOptionalString(name),
|
||||
Checksum: models.NewOptionalString(checksum),
|
||||
Gender: models.NewOptionalString(gender.String()),
|
||||
URL: models.NewOptionalString(url),
|
||||
Twitter: models.NewOptionalString(twitter),
|
||||
Instagram: models.NewOptionalString(instagram),
|
||||
Birthdate: models.NewOptionalDate(birthdate),
|
||||
Ethnicity: models.NewOptionalString(ethnicity),
|
||||
Country: models.NewOptionalString(country),
|
||||
EyeColor: models.NewOptionalString(eyeColor),
|
||||
Height: models.NewOptionalInt(height),
|
||||
Measurements: models.NewOptionalString(measurements),
|
||||
FakeTits: models.NewOptionalString(fakeTits),
|
||||
CareerLength: models.NewOptionalString(careerLength),
|
||||
Tattoos: models.NewOptionalString(tattoos),
|
||||
Piercings: models.NewOptionalString(piercings),
|
||||
Aliases: models.NewOptionalString(aliases),
|
||||
Name: models.NewOptionalString(name),
|
||||
Disambiguation: models.NewOptionalString(disambiguation),
|
||||
Gender: models.NewOptionalString(gender.String()),
|
||||
URL: models.NewOptionalString(url),
|
||||
Twitter: models.NewOptionalString(twitter),
|
||||
Instagram: models.NewOptionalString(instagram),
|
||||
Birthdate: models.NewOptionalDate(birthdate),
|
||||
Ethnicity: models.NewOptionalString(ethnicity),
|
||||
Country: models.NewOptionalString(country),
|
||||
EyeColor: models.NewOptionalString(eyeColor),
|
||||
Height: models.NewOptionalInt(height),
|
||||
Measurements: models.NewOptionalString(measurements),
|
||||
FakeTits: models.NewOptionalString(fakeTits),
|
||||
CareerLength: models.NewOptionalString(careerLength),
|
||||
Tattoos: models.NewOptionalString(tattoos),
|
||||
Piercings: models.NewOptionalString(piercings),
|
||||
Aliases: &models.UpdateStrings{
|
||||
Values: aliases,
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
},
|
||||
Favorite: models.NewOptionalBool(favorite),
|
||||
Rating: models.NewOptionalInt(rating),
|
||||
Details: models.NewOptionalString(details),
|
||||
@@ -186,40 +429,89 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
|
||||
HairColor: models.NewOptionalString(hairColor),
|
||||
Weight: models.NewOptionalInt(weight),
|
||||
IgnoreAutoTag: models.NewOptionalBool(ignoreAutoTag),
|
||||
CreatedAt: models.NewOptionalTime(createdAt),
|
||||
UpdatedAt: models.NewOptionalTime(updatedAt),
|
||||
TagIDs: &models.UpdateIDs{
|
||||
IDs: []int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]},
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
},
|
||||
StashIDs: &models.UpdateStashIDs{
|
||||
StashIDs: []models.StashID{
|
||||
{
|
||||
StashID: stashID1,
|
||||
Endpoint: endpoint1,
|
||||
},
|
||||
{
|
||||
StashID: stashID2,
|
||||
Endpoint: endpoint2,
|
||||
},
|
||||
},
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
},
|
||||
CreatedAt: models.NewOptionalTime(createdAt),
|
||||
UpdatedAt: models.NewOptionalTime(updatedAt),
|
||||
},
|
||||
models.Performer{
|
||||
ID: performerIDs[performerIdxWithDupName],
|
||||
Name: name,
|
||||
Checksum: checksum,
|
||||
Gender: gender,
|
||||
URL: url,
|
||||
Twitter: twitter,
|
||||
Instagram: instagram,
|
||||
Birthdate: &birthdate,
|
||||
Ethnicity: ethnicity,
|
||||
Country: country,
|
||||
EyeColor: eyeColor,
|
||||
Height: &height,
|
||||
Measurements: measurements,
|
||||
FakeTits: fakeTits,
|
||||
CareerLength: careerLength,
|
||||
Tattoos: tattoos,
|
||||
Piercings: piercings,
|
||||
Aliases: aliases,
|
||||
Favorite: favorite,
|
||||
Rating: &rating,
|
||||
Details: details,
|
||||
DeathDate: &deathdate,
|
||||
HairColor: hairColor,
|
||||
Weight: &weight,
|
||||
IgnoreAutoTag: ignoreAutoTag,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
ID: performerIDs[performerIdxWithDupName],
|
||||
Name: name,
|
||||
Disambiguation: disambiguation,
|
||||
Gender: gender,
|
||||
URL: url,
|
||||
Twitter: twitter,
|
||||
Instagram: instagram,
|
||||
Birthdate: &birthdate,
|
||||
Ethnicity: ethnicity,
|
||||
Country: country,
|
||||
EyeColor: eyeColor,
|
||||
Height: &height,
|
||||
Measurements: measurements,
|
||||
FakeTits: fakeTits,
|
||||
CareerLength: careerLength,
|
||||
Tattoos: tattoos,
|
||||
Piercings: piercings,
|
||||
Aliases: models.NewRelatedStrings(aliases),
|
||||
Favorite: favorite,
|
||||
Rating: &rating,
|
||||
Details: details,
|
||||
DeathDate: &deathdate,
|
||||
HairColor: hairColor,
|
||||
Weight: &weight,
|
||||
IgnoreAutoTag: ignoreAutoTag,
|
||||
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]}),
|
||||
StashIDs: models.NewRelatedStashIDs([]models.StashID{
|
||||
{
|
||||
StashID: stashID1,
|
||||
Endpoint: endpoint1,
|
||||
},
|
||||
{
|
||||
StashID: stashID2,
|
||||
Endpoint: endpoint2,
|
||||
},
|
||||
}),
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"clear all",
|
||||
performerIDs[performerIdxWithTwoTags],
|
||||
clearPerformerPartial(),
|
||||
models.Performer{
|
||||
ID: performerIDs[performerIdxWithTwoTags],
|
||||
Name: getPerformerStringValue(performerIdxWithTwoTags, "Name"),
|
||||
Favorite: true,
|
||||
Aliases: models.NewRelatedStrings([]string{}),
|
||||
TagIDs: models.NewRelatedIDs([]int{}),
|
||||
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid id",
|
||||
invalidID,
|
||||
models.PerformerPartial{},
|
||||
models.Performer{},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
qb := db.Performer
|
||||
@@ -237,6 +529,11 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := loadPerformerRelationships(ctx, tt.want, got); err != nil {
|
||||
t.Errorf("loadPerformerRelationships() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(tt.want, *got)
|
||||
|
||||
s, err := qb.Find(ctx, tt.id)
|
||||
@@ -244,6 +541,12 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
|
||||
t.Errorf("PerformerStore.Find() error = %v", err)
|
||||
}
|
||||
|
||||
// load relationships
|
||||
if err := loadPerformerRelationships(ctx, tt.want, s); err != nil {
|
||||
t.Errorf("loadPerformerRelationships() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(tt.want, *s)
|
||||
})
|
||||
}
|
||||
@@ -653,6 +956,19 @@ func TestPerformerQuery(t *testing.T) {
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"alias",
|
||||
nil,
|
||||
&models.PerformerFilterType{
|
||||
Aliases: &models.StringCriterionInput{
|
||||
Value: getPerformerStringValue(performerIdxWithGallery, "alias"),
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
},
|
||||
[]int{performerIdxWithGallery},
|
||||
[]int{performerIdxWithScene},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -706,8 +1022,7 @@ func TestPerformerUpdatePerformerImage(t *testing.T) {
|
||||
// create performer to test against
|
||||
const name = "TestPerformerUpdatePerformerImage"
|
||||
performer := models.Performer{
|
||||
Name: name,
|
||||
Checksum: md5.FromString(name),
|
||||
Name: name,
|
||||
}
|
||||
err := qb.Create(ctx, &performer)
|
||||
if err != nil {
|
||||
@@ -746,8 +1061,7 @@ func TestPerformerDestroyPerformerImage(t *testing.T) {
|
||||
// create performer to test against
|
||||
const name = "TestPerformerDestroyPerformerImage"
|
||||
performer := models.Performer{
|
||||
Name: name,
|
||||
Checksum: md5.FromString(name),
|
||||
Name: name,
|
||||
}
|
||||
err := qb.Create(ctx, &performer)
|
||||
if err != nil {
|
||||
@@ -925,6 +1239,7 @@ func verifyPerformerQuery(t *testing.T, filter models.PerformerFilterType, verif
|
||||
}
|
||||
|
||||
func queryPerformers(ctx context.Context, t *testing.T, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) []*models.Performer {
|
||||
t.Helper()
|
||||
performers, _, err := db.Performer.Query(ctx, performerFilter, findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying performers: %s", err.Error())
|
||||
@@ -1253,23 +1568,78 @@ func TestPerformerStashIDs(t *testing.T) {
|
||||
if err := withRollbackTxn(func(ctx context.Context) error {
|
||||
qb := db.Performer
|
||||
|
||||
// create performer to test against
|
||||
const name = "TestStashIDs"
|
||||
performer := models.Performer{
|
||||
Name: name,
|
||||
Checksum: md5.FromString(name),
|
||||
// create scene to test against
|
||||
const name = "TestPerformerStashIDs"
|
||||
performer := &models.Performer{
|
||||
Name: name,
|
||||
}
|
||||
err := qb.Create(ctx, &performer)
|
||||
if err != nil {
|
||||
if err := qb.Create(ctx, performer); err != nil {
|
||||
return fmt.Errorf("Error creating performer: %s", err.Error())
|
||||
}
|
||||
|
||||
testStashIDReaderWriter(ctx, t, qb, performer.ID)
|
||||
if err := performer.LoadStashIDs(ctx, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
testPerformerStashIDs(ctx, t, performer)
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func testPerformerStashIDs(ctx context.Context, t *testing.T, s *models.Performer) {
|
||||
// ensure no stash IDs to begin with
|
||||
assert.Len(t, s.StashIDs.List(), 0)
|
||||
|
||||
// add stash ids
|
||||
const stashIDStr = "stashID"
|
||||
const endpoint = "endpoint"
|
||||
stashID := models.StashID{
|
||||
StashID: stashIDStr,
|
||||
Endpoint: endpoint,
|
||||
}
|
||||
|
||||
qb := db.Performer
|
||||
|
||||
// update stash ids and ensure was updated
|
||||
var err error
|
||||
s, err = qb.UpdatePartial(ctx, s.ID, models.PerformerPartial{
|
||||
StashIDs: &models.UpdateStashIDs{
|
||||
StashIDs: []models.StashID{stashID},
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
if err := s.LoadStashIDs(ctx, qb); err != nil {
|
||||
t.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, []models.StashID{stashID}, s.StashIDs.List())
|
||||
|
||||
// remove stash ids and ensure was updated
|
||||
s, err = qb.UpdatePartial(ctx, s.ID, models.PerformerPartial{
|
||||
StashIDs: &models.UpdateStashIDs{
|
||||
StashIDs: []models.StashID{stashID},
|
||||
Mode: models.RelationshipUpdateModeRemove,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
if err := s.LoadStashIDs(ctx, qb); err != nil {
|
||||
t.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.Len(t, s.StashIDs.List(), 0)
|
||||
}
|
||||
|
||||
func TestPerformerQueryLegacyRating(t *testing.T) {
|
||||
const rating = 3
|
||||
ratingCriterion := models.IntCriterionInput{
|
||||
|
||||
@@ -14,14 +14,14 @@ func (r *updateRecord) set(destField string, v interface{}) {
|
||||
r.Record[destField] = v
|
||||
}
|
||||
|
||||
// func (r *updateRecord) setString(destField string, v models.OptionalString) {
|
||||
// if v.Set {
|
||||
// if v.Null {
|
||||
// panic("null value not allowed in optional string")
|
||||
// }
|
||||
// r.set(destField, v.Value)
|
||||
// }
|
||||
// }
|
||||
func (r *updateRecord) setString(destField string, v models.OptionalString) {
|
||||
if v.Set {
|
||||
if v.Null {
|
||||
panic("null value not allowed in optional string")
|
||||
}
|
||||
r.set(destField, v.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *updateRecord) setNullString(destField string, v models.OptionalString) {
|
||||
if v.Set {
|
||||
@@ -32,7 +32,7 @@ func (r *updateRecord) setNullString(destField string, v models.OptionalString)
|
||||
func (r *updateRecord) setBool(destField string, v models.OptionalBool) {
|
||||
if v.Set {
|
||||
if v.Null {
|
||||
panic("null value not allowed in optional int")
|
||||
panic("null value not allowed in optional bool")
|
||||
}
|
||||
r.set(destField, v.Value)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/hash/md5"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
||||
"github.com/stashapp/stash/pkg/sqlite"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
|
||||
@@ -442,10 +441,9 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
performerTagLinks = [][2]int{
|
||||
{performerIdxWithTag, tagIdxWithPerformer},
|
||||
{performerIdxWithTwoTags, tagIdx1WithPerformer},
|
||||
{performerIdxWithTwoTags, tagIdx2WithPerformer},
|
||||
performerTags = linkMap{
|
||||
performerIdxWithTag: {tagIdxWithPerformer},
|
||||
performerIdxWithTwoTags: {tagIdx1WithPerformer, tagIdx2WithPerformer},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -552,14 +550,14 @@ func populateDB() error {
|
||||
return fmt.Errorf("error creating movies: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := createPerformers(ctx, performersNameCase, performersNameNoCase); err != nil {
|
||||
return fmt.Errorf("error creating performers: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := createTags(ctx, sqlite.TagReaderWriter, tagsNameCase, tagsNameNoCase); err != nil {
|
||||
return fmt.Errorf("error creating tags: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := createPerformers(ctx, performersNameCase, performersNameNoCase); err != nil {
|
||||
return fmt.Errorf("error creating performers: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := createStudios(ctx, sqlite.StudioReaderWriter, studiosNameCase, studiosNameNoCase); err != nil {
|
||||
return fmt.Errorf("error creating studios: %s", err.Error())
|
||||
}
|
||||
@@ -584,10 +582,6 @@ func populateDB() error {
|
||||
return fmt.Errorf("error creating saved filters: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := linkPerformerTags(ctx); err != nil {
|
||||
return fmt.Errorf("error linking performer tags: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := linkMovieStudios(ctx, sqlite.MovieReaderWriter); err != nil {
|
||||
return fmt.Errorf("error linking movie studios: %s", err.Error())
|
||||
}
|
||||
@@ -1335,17 +1329,21 @@ func createPerformers(ctx context.Context, n int, o int) error {
|
||||
} // so count backwards to 0 as needed
|
||||
// performers [ i ] and [ n + o - i - 1 ] should have similar names with only the Name!=NaMe part different
|
||||
|
||||
tids := indexesToIDs(tagIDs, performerTags[i])
|
||||
|
||||
performer := models.Performer{
|
||||
Name: getPerformerStringValue(index, name),
|
||||
Checksum: getPerformerStringValue(i, checksumField),
|
||||
URL: getPerformerNullStringValue(i, urlField),
|
||||
Favorite: getPerformerBoolValue(i),
|
||||
Birthdate: getPerformerBirthdate(i),
|
||||
DeathDate: getPerformerDeathDate(i),
|
||||
Details: getPerformerStringValue(i, "Details"),
|
||||
Ethnicity: getPerformerStringValue(i, "Ethnicity"),
|
||||
Rating: getIntPtr(getRating(i)),
|
||||
IgnoreAutoTag: getIgnoreAutoTag(i),
|
||||
Name: getPerformerStringValue(index, name),
|
||||
Disambiguation: getPerformerStringValue(index, "disambiguation"),
|
||||
Aliases: models.NewRelatedStrings([]string{getPerformerStringValue(index, "alias")}),
|
||||
URL: getPerformerNullStringValue(i, urlField),
|
||||
Favorite: getPerformerBoolValue(i),
|
||||
Birthdate: getPerformerBirthdate(i),
|
||||
DeathDate: getPerformerDeathDate(i),
|
||||
Details: getPerformerStringValue(i, "Details"),
|
||||
Ethnicity: getPerformerStringValue(i, "Ethnicity"),
|
||||
Rating: getIntPtr(getRating(i)),
|
||||
IgnoreAutoTag: getIgnoreAutoTag(i),
|
||||
TagIDs: models.NewRelatedIDs(tids),
|
||||
}
|
||||
|
||||
careerLength := getPerformerCareerLength(i)
|
||||
@@ -1353,20 +1351,18 @@ func createPerformers(ctx context.Context, n int, o int) error {
|
||||
performer.CareerLength = *careerLength
|
||||
}
|
||||
|
||||
if (index+1)%5 != 0 {
|
||||
performer.StashIDs = models.NewRelatedStashIDs([]models.StashID{
|
||||
performerStashID(i),
|
||||
})
|
||||
}
|
||||
|
||||
err := pqb.Create(ctx, &performer)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating performer %v+: %s", performer, err.Error())
|
||||
}
|
||||
|
||||
if (index+1)%5 != 0 {
|
||||
if err := pqb.UpdateStashIDs(ctx, performer.ID, []models.StashID{
|
||||
performerStashID(i),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("setting performer stash ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
performerIDs = append(performerIDs, performer.ID)
|
||||
performerNames = append(performerNames, performer.Name)
|
||||
}
|
||||
@@ -1637,22 +1633,6 @@ func doLinks(links [][2]int, fn func(idx1, idx2 int) error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func linkPerformerTags(ctx context.Context) error {
|
||||
qb := db.Performer
|
||||
return doLinks(performerTagLinks, func(performerIndex, tagIndex int) error {
|
||||
performerID := performerIDs[performerIndex]
|
||||
tagID := tagIDs[tagIndex]
|
||||
tagIDs, err := qb.GetTagIDs(ctx, performerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tagIDs = intslice.IntAppendUnique(tagIDs, tagID)
|
||||
|
||||
return qb.UpdateTags(ctx, performerID, tagIDs)
|
||||
})
|
||||
}
|
||||
|
||||
func linkMovieStudios(ctx context.Context, mqb models.MovieWriter) error {
|
||||
return doLinks(movieStudioLinks, func(movieIndex, studioIndex int) error {
|
||||
movie := models.MoviePartial{
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
type table struct {
|
||||
@@ -129,6 +130,15 @@ func (t *table) destroy(ctx context.Context, ids []int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *table) join(j joiner, as string, parentIDCol string) {
|
||||
tableName := t.table.GetTable()
|
||||
tt := tableName
|
||||
if as != "" {
|
||||
tt = as
|
||||
}
|
||||
j.addLeftJoin(tableName, as, fmt.Sprintf("%s.%s = %s", tt, t.idColumn.GetCol(), parentIDCol))
|
||||
}
|
||||
|
||||
// func (t *table) get(ctx context.Context, q *goqu.SelectDataset, dest interface{}) error {
|
||||
// tx, err := getTx(ctx)
|
||||
// if err != nil {
|
||||
@@ -258,18 +268,18 @@ type stashIDRow struct {
|
||||
Endpoint null.String `db:"endpoint"`
|
||||
}
|
||||
|
||||
func (r *stashIDRow) resolve() *models.StashID {
|
||||
return &models.StashID{
|
||||
func (r *stashIDRow) resolve() models.StashID {
|
||||
return models.StashID{
|
||||
StashID: r.StashID.String,
|
||||
Endpoint: r.Endpoint.String,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *stashIDTable) get(ctx context.Context, id int) ([]*models.StashID, error) {
|
||||
func (t *stashIDTable) get(ctx context.Context, id int) ([]models.StashID, error) {
|
||||
q := dialect.Select("endpoint", "stash_id").From(t.table.table).Where(t.idColumn.Eq(id))
|
||||
|
||||
const single = false
|
||||
var ret []*models.StashID
|
||||
var ret []models.StashID
|
||||
if err := queryFunc(ctx, q, single, func(rows *sqlx.Rows) error {
|
||||
var v stashIDRow
|
||||
if err := rows.StructScan(&v); err != nil {
|
||||
@@ -366,6 +376,102 @@ func (t *stashIDTable) modifyJoins(ctx context.Context, id int, v []models.Stash
|
||||
return nil
|
||||
}
|
||||
|
||||
type stringTable struct {
|
||||
table
|
||||
stringColumn exp.IdentifierExpression
|
||||
}
|
||||
|
||||
func (t *stringTable) get(ctx context.Context, id int) ([]string, error) {
|
||||
q := dialect.Select(t.stringColumn).From(t.table.table).Where(t.idColumn.Eq(id))
|
||||
|
||||
const single = false
|
||||
var ret []string
|
||||
if err := queryFunc(ctx, q, single, func(rows *sqlx.Rows) error {
|
||||
var v string
|
||||
if err := rows.Scan(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = append(ret, v)
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("getting stash ids from %s: %w", t.table.table.GetTable(), err)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (t *stringTable) insertJoin(ctx context.Context, id int, v string) (sql.Result, error) {
|
||||
q := dialect.Insert(t.table.table).Cols(t.idColumn.GetCol(), t.stringColumn.GetCol()).Vals(
|
||||
goqu.Vals{id, v},
|
||||
)
|
||||
ret, err := exec(ctx, q)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inserting into %s: %w", t.table.table.GetTable(), err)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (t *stringTable) insertJoins(ctx context.Context, id int, v []string) error {
|
||||
for _, fk := range v {
|
||||
if _, err := t.insertJoin(ctx, id, fk); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *stringTable) replaceJoins(ctx context.Context, id int, v []string) error {
|
||||
if err := t.destroy(ctx, []int{id}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return t.insertJoins(ctx, id, v)
|
||||
}
|
||||
|
||||
func (t *stringTable) addJoins(ctx context.Context, id int, v []string) error {
|
||||
// get existing foreign keys
|
||||
existing, err := t.get(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// only add values that are not already present
|
||||
filtered := stringslice.StrExclude(v, existing)
|
||||
return t.insertJoins(ctx, id, filtered)
|
||||
}
|
||||
|
||||
func (t *stringTable) destroyJoins(ctx context.Context, id int, v []string) error {
|
||||
for _, vv := range v {
|
||||
q := dialect.Delete(t.table.table).Where(
|
||||
t.idColumn.Eq(id),
|
||||
t.stringColumn.Eq(vv),
|
||||
)
|
||||
|
||||
if _, err := exec(ctx, q); err != nil {
|
||||
return fmt.Errorf("destroying %s: %w", t.table.table.GetTable(), err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *stringTable) modifyJoins(ctx context.Context, id int, v []string, mode models.RelationshipUpdateMode) error {
|
||||
switch mode {
|
||||
case models.RelationshipUpdateModeSet:
|
||||
return t.replaceJoins(ctx, id, v)
|
||||
case models.RelationshipUpdateModeAdd:
|
||||
return t.addJoins(ctx, id, v)
|
||||
case models.RelationshipUpdateModeRemove:
|
||||
return t.destroyJoins(ctx, id, v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type scenesMoviesTable struct {
|
||||
table
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ var (
|
||||
scenesStashIDsJoinTable = goqu.T("scene_stash_ids")
|
||||
scenesMoviesJoinTable = goqu.T(moviesScenesTable)
|
||||
|
||||
performersAliasesJoinTable = goqu.T(performersAliasesTable)
|
||||
performersTagsJoinTable = goqu.T(performersTagsTable)
|
||||
performersStashIDsJoinTable = goqu.T("performer_stash_ids")
|
||||
)
|
||||
@@ -183,6 +184,29 @@ var (
|
||||
table: goqu.T(performerTable),
|
||||
idColumn: goqu.T(performerTable).Col(idColumn),
|
||||
}
|
||||
|
||||
performersAliasesTableMgr = &stringTable{
|
||||
table: table{
|
||||
table: performersAliasesJoinTable,
|
||||
idColumn: performersAliasesJoinTable.Col(performerIDColumn),
|
||||
},
|
||||
stringColumn: performersAliasesJoinTable.Col(performerAliasColumn),
|
||||
}
|
||||
|
||||
performersTagsTableMgr = &joinTable{
|
||||
table: table{
|
||||
table: performersTagsJoinTable,
|
||||
idColumn: performersTagsJoinTable.Col(performerIDColumn),
|
||||
},
|
||||
fkColumn: performersTagsJoinTable.Col(tagIDColumn),
|
||||
}
|
||||
|
||||
performersStashIDsTableMgr = &stashIDTable{
|
||||
table: table{
|
||||
table: performersStashIDsJoinTable,
|
||||
idColumn: performersStashIDsJoinTable.Col(performerIDColumn),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
Reference in New Issue
Block a user