From 8e070717e51ae05ae3fbb248bb5b0f5cac8c3384 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:38:11 +1000 Subject: [PATCH] Optimise table joins (#6648) * Use inner joins where it makes sense to do so * Don't trim stash ids --- pkg/sqlite/criterion_handlers.go | 138 +++++++++++++++++++++++------- pkg/sqlite/file_filter.go | 20 +++-- pkg/sqlite/filter.go | 48 +++++++++-- pkg/sqlite/gallery_filter.go | 24 +++--- pkg/sqlite/group_filter.go | 8 +- pkg/sqlite/image.go | 22 ++--- pkg/sqlite/image_filter.go | 36 ++++---- pkg/sqlite/performer_filter.go | 16 ++-- pkg/sqlite/repository.go | 10 ++- pkg/sqlite/scene_filter.go | 109 +++++++++++++---------- pkg/sqlite/scene_marker_filter.go | 8 +- pkg/sqlite/studio_filter.go | 20 ++--- pkg/sqlite/table.go | 17 +++- pkg/sqlite/tag_filter.go | 8 +- 14 files changed, 319 insertions(+), 165 deletions(-) diff --git a/pkg/sqlite/criterion_handlers.go b/pkg/sqlite/criterion_handlers.go index ae245f1b5..c703a85e3 100644 --- a/pkg/sqlite/criterion_handlers.go +++ b/pkg/sqlite/criterion_handlers.go @@ -70,11 +70,52 @@ func stringCriterionHandler(c *models.StringCriterionInput, column string) crite } } -func joinedStringCriterionHandler(c *models.StringCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc { +func stringNoTrimCriterionHandler(c *models.StringCriterionInput, column string) criterionHandlerFunc { + return func(ctx context.Context, f *filterBuilder) { + if c != nil { + if modifier := c.Modifier; c.Modifier.IsValid() { + switch modifier { + case models.CriterionModifierIncludes: + f.whereClauses = append(f.whereClauses, getStringSearchClause([]string{column}, c.Value, false)) + case models.CriterionModifierExcludes: + f.whereClauses = append(f.whereClauses, getStringSearchClause([]string{column}, c.Value, true)) + case models.CriterionModifierEquals: + f.addWhere(column+" LIKE ?", c.Value) + case models.CriterionModifierNotEquals: + f.addWhere(column+" NOT LIKE ?", c.Value) + case models.CriterionModifierMatchesRegex: + if _, err := regexp.Compile(c.Value); err != nil { + f.setError(err) + return + } + f.addWhere(fmt.Sprintf("(%s IS NOT NULL AND %[1]s regexp ?)", column), c.Value) + case models.CriterionModifierNotMatchesRegex: + if _, err := regexp.Compile(c.Value); err != nil { + f.setError(err) + return + } + f.addWhere(fmt.Sprintf("(%s IS NULL OR %[1]s NOT regexp ?)", column), c.Value) + case models.CriterionModifierIsNull: + f.addWhere("(" + column + " IS NULL)") + case models.CriterionModifierNotNull: + f.addWhere("(" + column + " IS NOT NULL)") + default: + panic("unsupported string filter modifier") + } + } + } + } +} + +func joinedStringCriterionHandler(c *models.StringCriterionInput, column string, addJoinFn func(f *filterBuilder, joinType joinType)) criterionHandlerFunc { return func(ctx context.Context, f *filterBuilder) { if c != nil { if addJoinFn != nil { - addJoinFn(f) + joinType := joinTypeInner + if c.Modifier == models.CriterionModifierIsNull || c.Modifier == models.CriterionModifierNotMatchesRegex { + joinType = joinTypeLeft + } + addJoinFn(f, joinType) } stringCriterionHandler(c, column)(ctx, f) } @@ -104,16 +145,20 @@ func enumCriterionHandler(modifier models.CriterionModifier, values []string, co } } -func pathCriterionHandler(c *models.StringCriterionInput, pathColumn string, basenameColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc { +func pathCriterionHandler(c *models.StringCriterionInput, pathColumn string, basenameColumn string, addJoinFn func(f *filterBuilder, joinType joinType)) criterionHandlerFunc { return func(ctx context.Context, f *filterBuilder) { if c != nil { - if addJoinFn != nil { - addJoinFn(f) - } - addWildcards := true - not := false - if modifier := c.Modifier; c.Modifier.IsValid() { + if addJoinFn != nil { + joinType := joinTypeInner + if modifier == models.CriterionModifierIsNull || modifier == models.CriterionModifierNotMatchesRegex { + joinType = joinTypeLeft + } + addJoinFn(f, joinType) + } + addWildcards := true + not := false + switch modifier { case models.CriterionModifierIncludes: f.whereClauses = append(f.whereClauses, getPathSearchClauseMany(pathColumn, basenameColumn, c.Value, addWildcards, not)) @@ -194,11 +239,15 @@ func getPathSearchClauseMany(pathColumn, basenameColumn, p string, addWildcards, return getPathSearchClause(pathColumn, basenameColumn, trimmedQuery, addWildcards, not) } -func intCriterionHandler(c *models.IntCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc { +func intCriterionHandler(c *models.IntCriterionInput, column string, addJoinFn func(f *filterBuilder, joinType joinType)) criterionHandlerFunc { return func(ctx context.Context, f *filterBuilder) { if c != nil { if addJoinFn != nil { - addJoinFn(f) + joinType := joinTypeInner + if c.Modifier == models.CriterionModifierIsNull { + joinType = joinTypeLeft + } + addJoinFn(f, joinType) } clause, args := getIntCriterionWhereClause(column, *c) f.addWhere(clause, args...) @@ -206,11 +255,15 @@ func intCriterionHandler(c *models.IntCriterionInput, column string, addJoinFn f } } -func floatCriterionHandler(c *models.FloatCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc { +func floatCriterionHandler(c *models.FloatCriterionInput, column string, addJoinFn func(f *filterBuilder, joinType joinType)) criterionHandlerFunc { return func(ctx context.Context, f *filterBuilder) { if c != nil { if addJoinFn != nil { - addJoinFn(f) + joinType := joinTypeInner + if c.Modifier == models.CriterionModifierIsNull { + joinType = joinTypeLeft + } + addJoinFn(f, joinType) } clause, args := getFloatCriterionWhereClause(column, *c) f.addWhere(clause, args...) @@ -218,11 +271,15 @@ func floatCriterionHandler(c *models.FloatCriterionInput, column string, addJoin } } -func floatIntCriterionHandler(durationFilter *models.IntCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc { +func floatIntCriterionHandler(durationFilter *models.IntCriterionInput, column string, addJoinFn func(f *filterBuilder, joinType joinType)) criterionHandlerFunc { return func(ctx context.Context, f *filterBuilder) { if durationFilter != nil { if addJoinFn != nil { - addJoinFn(f) + joinType := joinTypeInner + if durationFilter.Modifier == models.CriterionModifierIsNull { + joinType = joinTypeLeft + } + addJoinFn(f, joinType) } clause, args := getIntCriterionWhereClause("cast("+column+" as int)", *durationFilter) f.addWhere(clause, args...) @@ -230,11 +287,11 @@ func floatIntCriterionHandler(durationFilter *models.IntCriterionInput, column s } } -func boolCriterionHandler(c *bool, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc { +func boolCriterionHandler(c *bool, column string, addJoinFn func(f *filterBuilder, joinType joinType)) criterionHandlerFunc { return func(ctx context.Context, f *filterBuilder) { if c != nil { if addJoinFn != nil { - addJoinFn(f) + addJoinFn(f, joinTypeInner) } var v string if *c { @@ -289,11 +346,11 @@ func yearFilterCriterionHandler(year *models.IntCriterionInput, col string) crit } } -func resolutionCriterionHandler(resolution *models.ResolutionCriterionInput, heightColumn string, widthColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc { +func resolutionCriterionHandler(resolution *models.ResolutionCriterionInput, heightColumn string, widthColumn string, addJoinFn func(f *filterBuilder, joinType joinType)) criterionHandlerFunc { return func(ctx context.Context, f *filterBuilder) { if resolution != nil && resolution.Value.IsValid() { if addJoinFn != nil { - addJoinFn(f) + addJoinFn(f, joinTypeInner) } mn := resolution.Value.GetMinResolution() @@ -315,11 +372,11 @@ func resolutionCriterionHandler(resolution *models.ResolutionCriterionInput, hei } } -func orientationCriterionHandler(orientation *models.OrientationCriterionInput, heightColumn string, widthColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc { +func orientationCriterionHandler(orientation *models.OrientationCriterionInput, heightColumn string, widthColumn string, addJoinFn func(f *filterBuilder, joinType joinType)) criterionHandlerFunc { return func(ctx context.Context, f *filterBuilder) { if orientation != nil { if addJoinFn != nil { - addJoinFn(f) + addJoinFn(f, joinTypeInner) } var clauses []sqlClause @@ -362,7 +419,7 @@ type joinedMultiCriterionHandlerBuilder struct { // foreign key of the foreign object on the join table foreignFK string - addJoinTable func(f *filterBuilder) + addJoinTable func(f *filterBuilder, joinType joinType) } func (m *joinedMultiCriterionHandlerBuilder) handler(c *models.MultiCriterionInput) criterionHandlerFunc { @@ -378,11 +435,13 @@ func (m *joinedMultiCriterionHandlerBuilder) handler(c *models.MultiCriterionInp if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull { var notClause string + joinType := joinTypeLeft if criterion.Modifier == models.CriterionModifierNotNull { notClause = "NOT" + joinType = joinTypeInner } - m.addJoinTable(f) + m.addJoinTable(f, joinType) f.addWhere(utils.StrFormat("{table}.{column} IS {not} NULL", utils.StrFormatMap{ "table": joinAlias, @@ -415,11 +474,11 @@ func (m *joinedMultiCriterionHandlerBuilder) handler(c *models.MultiCriterionInp switch criterion.Modifier { case models.CriterionModifierIncludes: // includes any of the provided ids - m.addJoinTable(f) + m.addJoinTable(f, joinTypeInner) whereClause = fmt.Sprintf("%s.%s IN %s", joinAlias, m.foreignFK, getInBinding(len(criterion.Value))) case models.CriterionModifierEquals: // includes only the provided ids - m.addJoinTable(f) + m.addJoinTable(f, joinTypeInner) whereClause = utils.StrFormat("{joinAlias}.{foreignFK} IN {inBinding} AND (SELECT COUNT(*) FROM {joinTable} s WHERE s.{primaryFK} = {primaryTable}.id) = ?", utils.StrFormatMap{ "joinAlias": joinAlias, "foreignFK": m.foreignFK, @@ -434,7 +493,7 @@ func (m *joinedMultiCriterionHandlerBuilder) handler(c *models.MultiCriterionInp f.setError(fmt.Errorf("not equals modifier is not supported for multi criterion input")) case models.CriterionModifierIncludesAll: // includes all of the provided ids - m.addJoinTable(f) + m.addJoinTable(f, joinTypeInner) whereClause = fmt.Sprintf("%s.%s IN %s", joinAlias, m.foreignFK, getInBinding(len(criterion.Value))) havingClause = fmt.Sprintf("count(distinct %s.%s) IS %d", joinAlias, m.foreignFK, len(criterion.Value)) } @@ -468,7 +527,7 @@ type multiCriterionHandlerBuilder struct { foreignFK string // function that will be called to perform any necessary joins - addJoinsFunc func(f *filterBuilder) + addJoinsFunc func(f *filterBuilder, joinType joinType) } func (m *multiCriterionHandlerBuilder) handler(criterion *models.MultiCriterionInput) criterionHandlerFunc { @@ -500,7 +559,7 @@ func (m *multiCriterionHandlerBuilder) handler(criterion *models.MultiCriterionI } if m.addJoinsFunc != nil { - m.addJoinsFunc(f) + m.addJoinsFunc(f, joinTypeInner) } whereClause, havingClause := getMultiCriterionClause(m.primaryTable, m.foreignTable, m.joinTable, m.primaryFK, m.foreignFK, criterion) @@ -536,7 +595,7 @@ type stringListCriterionHandlerBuilder struct { // string field on the join table stringColumn string - addJoinTable func(f *filterBuilder) + addJoinTable func(f *filterBuilder, joinType joinType) excludeHandler func(f *filterBuilder, criterion *models.StringCriterionInput) } @@ -570,7 +629,11 @@ func (m *stringListCriterionHandlerBuilder) handler(criterion *models.StringCrit // Modifier: models.CriterionModifierNotNull, // }, m.joinTable+"."+m.stringColumn)(ctx, f) } else { - m.addJoinTable(f) + joinType := joinTypeInner + if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotMatchesRegex { + joinType = joinTypeLeft + } + m.addJoinTable(f, joinType) stringCriterionHandler(criterion, m.joinTable+"."+m.stringColumn)(ctx, f) } } @@ -1028,14 +1091,18 @@ func (h *stashIDCriterionHandler) handle(ctx context.Context, f *filterBuilder) joinClause += fmt.Sprintf(" AND %s.endpoint = '%s'", t, *h.c.Endpoint) } - f.addLeftJoin(stashIDRepo.tableName, h.stashIDTableAs, joinClause) + joinType := joinTypeInner + if h.c.Modifier == models.CriterionModifierIsNull || h.c.Modifier == models.CriterionModifierNotMatchesRegex { + joinType = joinTypeLeft + } + f.addJoin(joinType, stashIDRepo.tableName, h.stashIDTableAs, joinClause) v := "" if h.c.StashID != nil { v = *h.c.StashID } - stringCriterionHandler(&models.StringCriterionInput{ + stringNoTrimCriterionHandler(&models.StringCriterionInput{ Value: v, Modifier: h.c.Modifier, }, t+".stash_id")(ctx, f) @@ -1064,7 +1131,12 @@ func (h *stashIDsCriterionHandler) handle(ctx context.Context, f *filterBuilder) joinClause += fmt.Sprintf(" AND %s.endpoint = '%s'", t, *h.c.Endpoint) } - f.addLeftJoin(stashIDRepo.tableName, h.stashIDTableAs, joinClause) + joinType := joinTypeInner + if h.c.Modifier == models.CriterionModifierIsNull { + joinType = joinTypeLeft + } + + f.addJoin(joinType, stashIDRepo.tableName, h.stashIDTableAs, joinClause) switch h.c.Modifier { case models.CriterionModifierIsNull: diff --git a/pkg/sqlite/file_filter.go b/pkg/sqlite/file_filter.go index 29946a8ce..b8e9253a0 100644 --- a/pkg/sqlite/file_filter.go +++ b/pkg/sqlite/file_filter.go @@ -300,15 +300,19 @@ func (qb *videoFileFilterHandler) criterionHandler() criterionHandler { } } -func (qb *videoFileFilterHandler) addVideoFilesTable(f *filterBuilder) { - f.addLeftJoin(videoFileTable, "", "video_files.file_id = files.id") +func (qb *videoFileFilterHandler) addVideoFilesTable(f *filterBuilder, joinType joinType) { + f.addJoin(joinType, videoFileTable, "", "video_files.file_id = files.id") } -func (qb *videoFileFilterHandler) codecCriterionHandler(codec *models.StringCriterionInput, codecColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc { +func (qb *videoFileFilterHandler) codecCriterionHandler(codec *models.StringCriterionInput, codecColumn string, addJoinFn func(f *filterBuilder, joinType joinType)) criterionHandlerFunc { return func(ctx context.Context, f *filterBuilder) { if codec != nil { if addJoinFn != nil { - addJoinFn(f) + joinType := joinTypeInner + if codec.Modifier == models.CriterionModifierIsNull || codec.Modifier == models.CriterionModifierNotMatchesRegex { + joinType = joinTypeLeft + } + addJoinFn(f, joinType) } stringCriterionHandler(codec, codecColumn)(ctx, f) @@ -322,8 +326,8 @@ func (qb *videoFileFilterHandler) captionCriterionHandler(captions *models.Strin primaryFK: sceneIDColumn, joinTable: videoCaptionsTable, stringColumn: captionCodeColumn, - addJoinTable: func(f *filterBuilder) { - f.addLeftJoin(videoCaptionsTable, "", "video_captions.file_id = files.id") + addJoinTable: func(f *filterBuilder, joinType joinType) { + f.addJoin(joinType, videoCaptionsTable, "", "video_captions.file_id = files.id") }, excludeHandler: func(f *filterBuilder, criterion *models.StringCriterionInput) { excludeClause := `files.id NOT IN ( @@ -361,6 +365,6 @@ func (qb *imageFileFilterHandler) criterionHandler() criterionHandler { } } -func (qb *imageFileFilterHandler) addImageFilesTable(f *filterBuilder) { - f.addLeftJoin(imageFileTable, "", "image_files.file_id = files.id") +func (qb *imageFileFilterHandler) addImageFilesTable(f *filterBuilder, joinType joinType) { + f.addJoin(joinType, imageFileTable, "", "image_files.file_id = files.id") } diff --git a/pkg/sqlite/filter.go b/pkg/sqlite/filter.go index fa6759ae6..c5e78c1d3 100644 --- a/pkg/sqlite/filter.go +++ b/pkg/sqlite/filter.go @@ -90,11 +90,18 @@ func andClauses(clauses ...sqlClause) sqlClause { return joinClauses("AND", clauses...) } +type joinType string + +const ( + joinTypeLeft joinType = "LEFT" + joinTypeInner joinType = "INNER" +) + type join struct { table string as string onClause string - joinType string + joinType joinType args []interface{} // if true, indicates this is required for sorting only @@ -115,15 +122,19 @@ func (j join) alias() string { return j.as } +func (j join) getJoinType() joinType { + if j.joinType == "" { + return joinTypeLeft + } + return j.joinType +} + func (j join) toSQL() string { asStr := "" - joinStr := j.joinType + joinStr := j.getJoinType() if j.as != "" && j.as != j.table { asStr = " AS " + j.as } - if j.joinType == "" { - joinStr = "LEFT" - } return fmt.Sprintf("%s JOIN %s%s ON %s", joinStr, j.table, asStr, j.onClause) } @@ -141,6 +152,12 @@ func (j *joins) addUnique(newJoin join) bool { if !newJoin.sort && jj.sort { (*j)[i].sort = false } + + // if the new join is inner, override existing left join + if newJoin.getJoinType() == joinTypeInner && jj.getJoinType() == joinTypeLeft { + (*j)[i].joinType = joinTypeInner + } + break } } @@ -243,6 +260,23 @@ func (f *filterBuilder) not(n *filterBuilder) { f.subFilterOp = notOp } +// addJoin adds a join to the filter. The join is expressed in SQL as: +// JOIN [AS ] ON +// The AS is omitted if as is empty. +// This method does not add a join if it its alias/table name is already +// present in another existing join. +func (f *filterBuilder) addJoin(joinType joinType, table, as, onClause string, args ...interface{}) { + newJoin := join{ + table: table, + as: as, + onClause: onClause, + joinType: joinType, + args: args, + } + + f.joins.add(newJoin) +} + // addLeftJoin adds a left join to the filter. The join is expressed in SQL as: // LEFT JOIN
[AS ] ON // The AS is omitted if as is empty. @@ -253,7 +287,7 @@ func (f *filterBuilder) addLeftJoin(table, as, onClause string, args ...interfac table: table, as: as, onClause: onClause, - joinType: "LEFT", + joinType: joinTypeLeft, args: args, } @@ -270,7 +304,7 @@ func (f *filterBuilder) addInnerJoin(table, as, onClause string, args ...interfa table: table, as: as, onClause: onClause, - joinType: "INNER", + joinType: joinTypeInner, args: args, } diff --git a/pkg/sqlite/gallery_filter.go b/pkg/sqlite/gallery_filter.go index 0435f3f57..c70af1308 100644 --- a/pkg/sqlite/gallery_filter.go +++ b/pkg/sqlite/gallery_filter.go @@ -193,15 +193,15 @@ func (qb *galleryFilterHandler) urlsCriterionHandler(url *models.StringCriterion primaryFK: galleryIDColumn, joinTable: galleriesURLsTable, stringColumn: galleriesURLColumn, - addJoinTable: func(f *filterBuilder) { - galleriesURLsTableMgr.join(f, "", "galleries.id") + addJoinTable: func(f *filterBuilder, joinType joinType) { + galleriesURLsTableMgr.join(f, joinType, "", "galleries.id") }, } return h.handler(url) } -func (qb *galleryFilterHandler) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder { +func (qb *galleryFilterHandler) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder, joinType joinType)) multiCriterionHandlerBuilder { return multiCriterionHandlerBuilder{ primaryTable: galleryTable, foreignTable: foreignTable, @@ -353,7 +353,7 @@ func (qb *galleryFilterHandler) missingCriterionHandler(isMissing *string) crite if isMissing != nil && *isMissing != "" { switch *isMissing { case "url": - galleriesURLsTableMgr.join(f, "", "galleries.id") + galleriesURLsTableMgr.leftJoin(f, "", "galleries.id") f.addWhere("gallery_urls.url IS NULL") case "scenes": f.addLeftJoin("scenes_galleries", "scenes_join", "scenes_join.gallery_id = galleries.id") @@ -361,12 +361,12 @@ func (qb *galleryFilterHandler) missingCriterionHandler(isMissing *string) crite case "studio": f.addWhere("galleries.studio_id IS NULL") case "performers": - galleryRepository.performers.join(f, "performers_join", "galleries.id") + galleryRepository.performers.leftJoin(f, "performers_join", "galleries.id") f.addWhere("performers_join.gallery_id IS NULL") case "date": f.addWhere("galleries.date IS NULL OR galleries.date IS \"\"") case "tags": - galleryRepository.tags.join(f, "tags_join", "galleries.id") + galleryRepository.tags.leftJoin(f, "tags_join", "galleries.id") f.addWhere("tags_join.gallery_id IS NULL") case "cover": f.addLeftJoin("galleries_images", "cover_join", "cover_join.gallery_id = galleries.id AND cover_join.cover = 1") @@ -410,9 +410,9 @@ func (qb *galleryFilterHandler) tagCountCriterionHandler(tagCount *models.IntCri } func (qb *galleryFilterHandler) scenesCriterionHandler(scenes *models.MultiCriterionInput) criterionHandlerFunc { - addJoinsFunc := func(f *filterBuilder) { - galleryRepository.scenes.join(f, "", "galleries.id") - f.addLeftJoin("scenes", "", "scenes_galleries.scene_id = scenes.id") + addJoinsFunc := func(f *filterBuilder, joinType joinType) { + galleryRepository.scenes.join(f, joinType, "", "galleries.id") + f.addJoin(joinType, "scenes", "", "scenes_galleries.scene_id = scenes.id") } h := qb.getMultiCriterionHandlerBuilder(sceneTable, galleriesScenesTable, "scene_id", addJoinsFunc) return h.handler(scenes) @@ -426,8 +426,8 @@ func (qb *galleryFilterHandler) performersCriterionHandler(performers *models.Mu primaryFK: galleryIDColumn, foreignFK: performerIDColumn, - addJoinTable: func(f *filterBuilder) { - galleryRepository.performers.join(f, "performers_join", "galleries.id") + addJoinTable: func(f *filterBuilder, joinType joinType) { + galleryRepository.performers.join(f, joinType, "performers_join", "galleries.id") }, } @@ -515,7 +515,7 @@ func (qb *galleryFilterHandler) performerAgeCriterionHandler(performerAge *model func (qb *galleryFilterHandler) averageResolutionCriterionHandler(resolution *models.ResolutionCriterionInput) criterionHandlerFunc { return func(ctx context.Context, f *filterBuilder) { if resolution != nil && resolution.Value.IsValid() { - galleryRepository.images.join(f, "images_join", "galleries.id") + galleryRepository.images.leftJoin(f, "images_join", "galleries.id") f.addLeftJoin("images", "", "images_join.image_id = images.id") f.addLeftJoin("images_files", "", "images.id = images_files.image_id") f.addLeftJoin("image_files", "", "images_files.file_id = image_files.file_id") diff --git a/pkg/sqlite/group_filter.go b/pkg/sqlite/group_filter.go index 14f3841f4..63d056679 100644 --- a/pkg/sqlite/group_filter.go +++ b/pkg/sqlite/group_filter.go @@ -120,7 +120,7 @@ func (qb *groupFilterHandler) missingCriterionHandler(isMissing *string) criteri f.addLeftJoin("groups_scenes", "", "groups_scenes.group_id = groups.id") f.addWhere("groups_scenes.scene_id IS NULL") case "url": - groupsURLsTableMgr.join(f, "", "groups.id") + groupsURLsTableMgr.leftJoin(f, "", "groups.id") f.addWhere("group_urls.url IS NULL") case "studio": f.addWhere("groups.studio_id IS NULL") @@ -129,7 +129,7 @@ func (qb *groupFilterHandler) missingCriterionHandler(isMissing *string) criteri f.addLeftJoin("performers_scenes", "ps_perf", "gs_perf.scene_id = ps_perf.scene_id") f.addWhere("ps_perf.performer_id IS NULL") case "tags": - groupRepository.tags.join(f, "tags_join", "groups.id") + groupRepository.tags.leftJoin(f, "tags_join", "groups.id") f.addWhere("tags_join.group_id IS NULL") default: if err := validateIsMissing(*isMissing, []string{ @@ -150,8 +150,8 @@ func (qb *groupFilterHandler) urlsCriterionHandler(url *models.StringCriterionIn primaryFK: groupIDColumn, joinTable: groupURLsTable, stringColumn: groupURLColumn, - addJoinTable: func(f *filterBuilder) { - groupsURLsTableMgr.join(f, "", "groups.id") + addJoinTable: func(f *filterBuilder, joinType joinType) { + groupsURLsTableMgr.join(f, joinType, "", "groups.id") }, } diff --git a/pkg/sqlite/image.go b/pkg/sqlite/image.go index e0ac576d8..4d9ebad1b 100644 --- a/pkg/sqlite/image.go +++ b/pkg/sqlite/image.go @@ -123,23 +123,23 @@ type imageRepositoryType struct { files filesRepository } -func (r *imageRepositoryType) addImagesFilesTable(f *filterBuilder) { - f.addLeftJoin(imagesFilesTable, "", "images_files.image_id = images.id") +func (r *imageRepositoryType) addImagesFilesTable(f *filterBuilder, joinType joinType) { + f.addJoin(joinType, imagesFilesTable, "", "images_files.image_id = images.id") } -func (r *imageRepositoryType) addFilesTable(f *filterBuilder) { - r.addImagesFilesTable(f) - f.addLeftJoin(fileTable, "", "images_files.file_id = files.id") +func (r *imageRepositoryType) addFilesTable(f *filterBuilder, joinType joinType) { + r.addImagesFilesTable(f, joinType) + f.addJoin(joinType, fileTable, "", "images_files.file_id = files.id") } -func (r *imageRepositoryType) addFoldersTable(f *filterBuilder) { - r.addFilesTable(f) - f.addLeftJoin(folderTable, "", "files.parent_folder_id = folders.id") +func (r *imageRepositoryType) addFoldersTable(f *filterBuilder, joinType joinType) { + r.addFilesTable(f, joinType) + f.addJoin(joinType, folderTable, "", "files.parent_folder_id = folders.id") } -func (r *imageRepositoryType) addImageFilesTable(f *filterBuilder) { - r.addImagesFilesTable(f) - f.addLeftJoin(imageFileTable, "", "image_files.file_id = images_files.file_id") +func (r *imageRepositoryType) addImageFilesTable(f *filterBuilder, joinType joinType) { + r.addImagesFilesTable(f, joinType) + f.addJoin(joinType, imageFileTable, "", "image_files.file_id = images_files.file_id") } var ( diff --git a/pkg/sqlite/image_filter.go b/pkg/sqlite/image_filter.go index 4d1d2c4b3..a7351e52e 100644 --- a/pkg/sqlite/image_filter.go +++ b/pkg/sqlite/image_filter.go @@ -56,8 +56,12 @@ func (qb *imageFilterHandler) criterionHandler() criterionHandler { intCriterionHandler(imageFilter.ID, "images.id", nil), criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) { if imageFilter.Checksum != nil { - imageRepository.addImagesFilesTable(f) - f.addInnerJoin(fingerprintTable, "fingerprints_md5", "images_files.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'") + joinType := joinTypeInner + if imageFilter.Checksum.Modifier == models.CriterionModifierIsNull || imageFilter.Checksum.Modifier == models.CriterionModifierNotMatchesRegex { + joinType = joinTypeLeft + } + imageRepository.addImagesFilesTable(f, joinType) + f.addJoin(joinType, fingerprintTable, "fingerprints_md5", "images_files.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'") } stringCriterionHandler(imageFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f) @@ -65,8 +69,8 @@ func (qb *imageFilterHandler) criterionHandler() criterionHandler { &phashDistanceCriterionHandler{ joinFn: func(f *filterBuilder) { - imageRepository.addImagesFilesTable(f) - f.addLeftJoin(fingerprintTable, "fingerprints_phash", "images_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'") + imageRepository.addImagesFilesTable(f, joinTypeInner) + f.addInnerJoin(fingerprintTable, "fingerprints_phash", "images_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'") }, criterion: imageFilter.PhashDistance, }, @@ -148,8 +152,8 @@ func (qb *imageFilterHandler) criterionHandler() criterionHandler { isRelated: true, }, joinFn: func(f *filterBuilder) { - imageRepository.addFilesTable(f) - imageRepository.addFoldersTable(f) + imageRepository.addFilesTable(f, joinTypeInner) + imageRepository.addFoldersTable(f, joinTypeInner) }, // don't use a subquery; join directly directJoin: true, @@ -172,18 +176,18 @@ func (qb *imageFilterHandler) missingCriterionHandler(isMissing *string) criteri if isMissing != nil && *isMissing != "" { switch *isMissing { case "url": - imagesURLsTableMgr.join(f, "", "images.id") + imagesURLsTableMgr.leftJoin(f, "", "images.id") f.addWhere("image_urls.url IS NULL") case "studio": f.addWhere("images.studio_id IS NULL") case "performers": - imageRepository.performers.join(f, "performers_join", "images.id") + imageRepository.performers.leftJoin(f, "performers_join", "images.id") f.addWhere("performers_join.image_id IS NULL") case "galleries": - imageRepository.galleries.join(f, "galleries_join", "images.id") + imageRepository.galleries.leftJoin(f, "galleries_join", "images.id") f.addWhere("galleries_join.image_id IS NULL") case "tags": - imageRepository.tags.join(f, "tags_join", "images.id") + imageRepository.tags.leftJoin(f, "tags_join", "images.id") f.addWhere("tags_join.image_id IS NULL") default: if err := validateIsMissing(*isMissing, []string{ @@ -204,15 +208,15 @@ func (qb *imageFilterHandler) urlsCriterionHandler(url *models.StringCriterionIn primaryFK: imageIDColumn, joinTable: imagesURLsTable, stringColumn: imageURLColumn, - addJoinTable: func(f *filterBuilder) { - imagesURLsTableMgr.join(f, "", "images.id") + addJoinTable: func(f *filterBuilder, joinType joinType) { + imagesURLsTableMgr.join(f, joinType, "", "images.id") }, } return h.handler(url) } -func (qb *imageFilterHandler) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder { +func (qb *imageFilterHandler) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder, joinType joinType)) multiCriterionHandlerBuilder { return multiCriterionHandlerBuilder{ primaryTable: imageTable, foreignTable: foreignTable, @@ -249,7 +253,7 @@ func (qb *imageFilterHandler) tagCountCriterionHandler(tagCount *models.IntCrite } func (qb *imageFilterHandler) galleriesCriterionHandler(galleries *models.MultiCriterionInput) criterionHandlerFunc { - addJoinsFunc := func(f *filterBuilder) { + addJoinsFunc := func(f *filterBuilder, joinType joinType) { if galleries.Modifier == models.CriterionModifierIncludes || galleries.Modifier == models.CriterionModifierIncludesAll { f.addInnerJoin(galleriesImagesTable, "", "galleries_images.image_id = images.id") f.addInnerJoin(galleryTable, "", "galleries_images.gallery_id = galleries.id") @@ -268,8 +272,8 @@ func (qb *imageFilterHandler) performersCriterionHandler(performers *models.Mult primaryFK: imageIDColumn, foreignFK: performerIDColumn, - addJoinTable: func(f *filterBuilder) { - imageRepository.performers.join(f, "performers_join", "images.id") + addJoinTable: func(f *filterBuilder, joinType joinType) { + imageRepository.performers.join(f, joinType, "performers_join", "images.id") }, } diff --git a/pkg/sqlite/performer_filter.go b/pkg/sqlite/performer_filter.go index 4336e998c..1e54bbf96 100644 --- a/pkg/sqlite/performer_filter.go +++ b/pkg/sqlite/performer_filter.go @@ -188,7 +188,7 @@ func (qb *performerFilterHandler) criterionHandler() criterionHandler { intCriterionHandler(filter.Weight, tableName+".weight", nil), criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) { if filter.StashID != nil { - performerRepository.stashIDs.join(f, "performer_stash_ids", "performers.id") + performerRepository.stashIDs.leftJoin(f, "performer_stash_ids", "performers.id") stringCriterionHandler(filter.StashID, "performer_stash_ids.stash_id")(ctx, f) } }), @@ -333,7 +333,7 @@ func (qb *performerFilterHandler) performerIsMissingCriterionHandler(isMissing * if isMissing != nil && *isMissing != "" { switch *isMissing { case "url": - performersURLsTableMgr.join(f, "", "performers.id") + performersURLsTableMgr.leftJoin(f, "", "performers.id") f.addWhere("performer_urls.url IS NULL") case "scenes": // Deprecated: use `scene_count == 0` filter instead f.addLeftJoin(performersScenesTable, "scenes_join", "scenes_join.performer_id = performers.id") @@ -341,10 +341,10 @@ func (qb *performerFilterHandler) performerIsMissingCriterionHandler(isMissing * case "image": f.addWhere("performers.image_blob IS NULL") case "stash_id": - performersStashIDsTableMgr.join(f, "performer_stash_ids", "performers.id") + performersStashIDsTableMgr.leftJoin(f, "performer_stash_ids", "performers.id") f.addWhere("performer_stash_ids.performer_id IS NULL") case "aliases": - performersAliasesTableMgr.join(f, "", "performers.id") + performersAliasesTableMgr.leftJoin(f, "", "performers.id") f.addWhere("performer_aliases.alias IS NULL") case "tags": f.addLeftJoin(performersTagsTable, "tags_join", "tags_join.performer_id = performers.id") @@ -383,8 +383,8 @@ func (qb *performerFilterHandler) urlsCriterionHandler(url *models.StringCriteri primaryFK: performerIDColumn, joinTable: performerURLsTable, stringColumn: performerURLColumn, - addJoinTable: func(f *filterBuilder) { - performersURLsTableMgr.join(f, "", "performers.id") + addJoinTable: func(f *filterBuilder, joinType joinType) { + performersURLsTableMgr.join(f, joinType, "", "performers.id") }, } @@ -397,8 +397,8 @@ func (qb *performerFilterHandler) aliasCriterionHandler(alias *models.StringCrit primaryFK: performerIDColumn, joinTable: performersAliasesTable, stringColumn: performerAliasColumn, - addJoinTable: func(f *filterBuilder) { - performersAliasesTableMgr.join(f, "", "performers.id") + addJoinTable: func(f *filterBuilder, joinType joinType) { + performersAliasesTableMgr.join(f, joinType, "", "performers.id") }, } diff --git a/pkg/sqlite/repository.go b/pkg/sqlite/repository.go index 18d501e3a..1b0c03113 100644 --- a/pkg/sqlite/repository.go +++ b/pkg/sqlite/repository.go @@ -204,7 +204,15 @@ func (r *repository) newQuery() queryBuilder { } } -func (r *repository) join(j joiner, as string, parentIDCol string) { +func (r *repository) join(j joiner, t joinType, as string, parentIDCol string) { + fn := r.innerJoin + if t == joinTypeLeft { + fn = r.leftJoin + } + fn(j, as, parentIDCol) +} + +func (r *repository) leftJoin(j joiner, as string, parentIDCol string) { t := r.tableName if as != "" { t = as diff --git a/pkg/sqlite/scene_filter.go b/pkg/sqlite/scene_filter.go index 712c3d83d..255a8e0b3 100644 --- a/pkg/sqlite/scene_filter.go +++ b/pkg/sqlite/scene_filter.go @@ -63,8 +63,12 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler { stringCriterionHandler(sceneFilter.Director, "scenes.director"), criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) { if sceneFilter.Oshash != nil { - qb.addSceneFilesTable(f) - f.addLeftJoin(fingerprintTable, "fingerprints_oshash", "scenes_files.file_id = fingerprints_oshash.file_id AND fingerprints_oshash.type = 'oshash'") + joinType := joinTypeInner + if sceneFilter.Oshash.Modifier == models.CriterionModifierIsNull { + joinType = joinTypeLeft + } + qb.addSceneFilesTable(f, joinType) + f.addJoin(joinType, fingerprintTable, "fingerprints_oshash", "scenes_files.file_id = fingerprints_oshash.file_id AND fingerprints_oshash.type = 'oshash'") } stringCriterionHandler(sceneFilter.Oshash, "fingerprints_oshash.fingerprint")(ctx, f) @@ -72,8 +76,12 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler { criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) { if sceneFilter.Checksum != nil { - qb.addSceneFilesTable(f) - f.addLeftJoin(fingerprintTable, "fingerprints_md5", "scenes_files.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'") + joinType := joinTypeInner + if sceneFilter.Checksum.Modifier == models.CriterionModifierIsNull { + joinType = joinTypeLeft + } + qb.addSceneFilesTable(f, joinType) + f.addJoin(joinType, fingerprintTable, "fingerprints_md5", "scenes_files.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'") } stringCriterionHandler(sceneFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f) @@ -84,8 +92,12 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler { // backwards compatibility h := phashDistanceCriterionHandler{ joinFn: func(f *filterBuilder) { - qb.addSceneFilesTable(f) - f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'") + joinType := joinTypeInner + if sceneFilter.Phash.Modifier == models.CriterionModifierIsNull { + joinType = joinTypeLeft + } + qb.addSceneFilesTable(f, joinType) + f.addJoin(joinType, fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'") }, criterion: &models.PhashDistanceCriterionInput{ Value: sceneFilter.Phash.Value, @@ -98,8 +110,9 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler { &phashDistanceCriterionHandler{ joinFn: func(f *filterBuilder) { - qb.addSceneFilesTable(f) - f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'") + const joinType = joinTypeInner + qb.addSceneFilesTable(f, joinType) + f.addJoin(joinType, fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'") }, criterion: sceneFilter.PhashDistance, }, @@ -122,7 +135,7 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler { criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) { if sceneFilter.StashID != nil { - sceneRepository.stashIDs.join(f, "scene_stash_ids", "scenes.id") + sceneRepository.stashIDs.leftJoin(f, "scene_stash_ids", "scenes.id") stringCriterionHandler(sceneFilter.StashID, "scene_stash_ids.stash_id")(ctx, f) } }), @@ -236,8 +249,8 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler { isRelated: true, }, joinFn: func(f *filterBuilder) { - qb.addFilesTable(f) - qb.addFoldersTable(f) + qb.addFilesTable(f, joinTypeInner) + qb.addFoldersTable(f, joinTypeInner) }, // don't use a subquery; join directly directJoin: true, @@ -254,23 +267,23 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler { } } -func (qb *sceneFilterHandler) addSceneFilesTable(f *filterBuilder) { - f.addLeftJoin(scenesFilesTable, "", "scenes_files.scene_id = scenes.id") +func (qb *sceneFilterHandler) addSceneFilesTable(f *filterBuilder, joinType joinType) { + f.addJoin(joinType, scenesFilesTable, "", "scenes_files.scene_id = scenes.id") } -func (qb *sceneFilterHandler) addFilesTable(f *filterBuilder) { - qb.addSceneFilesTable(f) - f.addLeftJoin(fileTable, "", "scenes_files.file_id = files.id") +func (qb *sceneFilterHandler) addFilesTable(f *filterBuilder, joinType joinType) { + qb.addSceneFilesTable(f, joinType) + f.addJoin(joinType, fileTable, "", "scenes_files.file_id = files.id") } -func (qb *sceneFilterHandler) addFoldersTable(f *filterBuilder) { - qb.addFilesTable(f) - f.addLeftJoin(folderTable, "", "files.parent_folder_id = folders.id") +func (qb *sceneFilterHandler) addFoldersTable(f *filterBuilder, joinType joinType) { + qb.addFilesTable(f, joinType) + f.addJoin(joinType, folderTable, "", "files.parent_folder_id = folders.id") } -func (qb *sceneFilterHandler) addVideoFilesTable(f *filterBuilder) { - qb.addSceneFilesTable(f) - f.addLeftJoin(videoFileTable, "", "video_files.file_id = scenes_files.file_id") +func (qb *sceneFilterHandler) addVideoFilesTable(f *filterBuilder, joinType joinType) { + qb.addSceneFilesTable(f, joinType) + f.addJoin(joinType, videoFileTable, "", "video_files.file_id = scenes_files.file_id") } func (qb *sceneFilterHandler) playCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc { @@ -318,7 +331,7 @@ func (qb *sceneFilterHandler) duplicatedCriterionHandler(duplicatedFilter *model // Handle explicit fields if duplicatedFilter.Phash != nil { - qb.addSceneFilesTable(f) + qb.addSceneFilesTable(f, joinTypeInner) qb.applyPhashDuplication(f, *duplicatedFilter.Phash) } @@ -368,11 +381,15 @@ func (qb *sceneFilterHandler) applyURLDuplication(f *filterBuilder, duplicated b f.addInnerJoin("(SELECT scene_id FROM scene_urls INNER JOIN (SELECT url FROM scene_urls GROUP BY url HAVING COUNT(DISTINCT scene_id) "+v+" 1) dupes ON scene_urls.url = dupes.url)", "scurl", "scenes.id = scurl.scene_id") } -func (qb *sceneFilterHandler) codecCriterionHandler(codec *models.StringCriterionInput, codecColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc { +func (qb *sceneFilterHandler) codecCriterionHandler(codec *models.StringCriterionInput, codecColumn string, addJoinFn func(f *filterBuilder, joinType joinType)) criterionHandlerFunc { return func(ctx context.Context, f *filterBuilder) { if codec != nil { if addJoinFn != nil { - addJoinFn(f) + joinType := joinTypeInner + if codec.Modifier == models.CriterionModifierIsNull { + joinType = joinTypeLeft + } + addJoinFn(f, joinType) } stringCriterionHandler(codec, codecColumn)(ctx, f) @@ -398,29 +415,29 @@ func (qb *sceneFilterHandler) isMissingCriterionHandler(isMissing *string) crite if isMissing != nil && *isMissing != "" { switch *isMissing { case "url": - scenesURLsTableMgr.join(f, "", "scenes.id") + scenesURLsTableMgr.leftJoin(f, "", "scenes.id") f.addWhere("scene_urls.url IS NULL") case "galleries": - sceneRepository.galleries.join(f, "galleries_join", "scenes.id") + sceneRepository.galleries.leftJoin(f, "galleries_join", "scenes.id") f.addWhere("galleries_join.scene_id IS NULL") case "studio": f.addWhere("scenes.studio_id IS NULL") case "movie", "group": - sceneRepository.groups.join(f, "groups_join", "scenes.id") + sceneRepository.groups.leftJoin(f, "groups_join", "scenes.id") f.addWhere("groups_join.scene_id IS NULL") case "performers": - sceneRepository.performers.join(f, "performers_join", "scenes.id") + sceneRepository.performers.leftJoin(f, "performers_join", "scenes.id") f.addWhere("performers_join.scene_id IS NULL") case "date": f.addWhere(`scenes.date IS NULL OR scenes.date IS ""`) case "tags": - sceneRepository.tags.join(f, "tags_join", "scenes.id") + sceneRepository.tags.leftJoin(f, "tags_join", "scenes.id") f.addWhere("tags_join.scene_id IS NULL") case "stash_id": - sceneRepository.stashIDs.join(f, "scene_stash_ids", "scenes.id") + sceneRepository.stashIDs.leftJoin(f, "scene_stash_ids", "scenes.id") f.addWhere("scene_stash_ids.scene_id IS NULL") case "phash": - qb.addSceneFilesTable(f) + qb.addSceneFilesTable(f, joinTypeLeft) f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'") f.addWhere("fingerprints_phash.fingerprint IS NULL") case "cover": @@ -444,15 +461,15 @@ func (qb *sceneFilterHandler) urlsCriterionHandler(url *models.StringCriterionIn primaryFK: sceneIDColumn, joinTable: scenesURLsTable, stringColumn: sceneURLColumn, - addJoinTable: func(f *filterBuilder) { - scenesURLsTableMgr.join(f, "", "scenes.id") + addJoinTable: func(f *filterBuilder, joinType joinType) { + scenesURLsTableMgr.join(f, joinType, "", "scenes.id") }, } return h.handler(url) } -func (qb *sceneFilterHandler) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder { +func (qb *sceneFilterHandler) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder, joinType joinType)) multiCriterionHandlerBuilder { return multiCriterionHandlerBuilder{ primaryTable: sceneTable, foreignTable: foreignTable, @@ -469,9 +486,9 @@ func (qb *sceneFilterHandler) captionCriterionHandler(captions *models.StringCri primaryFK: sceneIDColumn, joinTable: videoCaptionsTable, stringColumn: captionCodeColumn, - addJoinTable: func(f *filterBuilder) { - qb.addSceneFilesTable(f) - f.addLeftJoin(videoCaptionsTable, "", "video_captions.file_id = scenes_files.file_id") + addJoinTable: func(f *filterBuilder, joinType joinType) { + qb.addSceneFilesTable(f, joinTypeLeft) + f.addJoin(joinType, videoCaptionsTable, "", "video_captions.file_id = scenes_files.file_id") }, excludeHandler: func(f *filterBuilder, criterion *models.StringCriterionInput) { excludeClause := `scenes.id NOT IN ( @@ -531,8 +548,8 @@ func (qb *sceneFilterHandler) performersCriterionHandler(performers *models.Mult primaryFK: sceneIDColumn, foreignFK: performerIDColumn, - addJoinTable: func(f *filterBuilder) { - sceneRepository.performers.join(f, "performers_join", "scenes.id") + addJoinTable: func(f *filterBuilder, joinType joinType) { + sceneRepository.performers.join(f, joinType, "performers_join", "scenes.id") }, } @@ -587,9 +604,9 @@ func (qb *sceneFilterHandler) performerAgeCriterionHandler(performerAge *models. // legacy handler func (qb *sceneFilterHandler) moviesCriterionHandler(movies *models.MultiCriterionInput) criterionHandlerFunc { - addJoinsFunc := func(f *filterBuilder) { - sceneRepository.groups.join(f, "", "scenes.id") - f.addLeftJoin("groups", "", "groups_scenes.group_id = groups.id") + addJoinsFunc := func(f *filterBuilder, joinType joinType) { + sceneRepository.groups.leftJoin(f, "", "scenes.id") + f.addJoin(joinType, "groups", "", "groups_scenes.group_id = groups.id") } h := qb.getMultiCriterionHandlerBuilder(groupTable, groupsScenesTable, "group_id", addJoinsFunc) return h.handler(movies) @@ -613,9 +630,9 @@ func (qb *sceneFilterHandler) groupsCriterionHandler(groups *models.Hierarchical } func (qb *sceneFilterHandler) galleriesCriterionHandler(galleries *models.MultiCriterionInput) criterionHandlerFunc { - addJoinsFunc := func(f *filterBuilder) { - sceneRepository.galleries.join(f, "", "scenes.id") - f.addLeftJoin("galleries", "", "scenes_galleries.gallery_id = galleries.id") + addJoinsFunc := func(f *filterBuilder, joinType joinType) { + sceneRepository.galleries.leftJoin(f, "", "scenes.id") + f.addJoin(joinType, "galleries", "", "scenes_galleries.gallery_id = galleries.id") } h := qb.getMultiCriterionHandlerBuilder(galleryTable, scenesGalleriesTable, "gallery_id", addJoinsFunc) return h.handler(galleries) diff --git a/pkg/sqlite/scene_marker_filter.go b/pkg/sqlite/scene_marker_filter.go index 34fa0f39b..26f5e0f8d 100644 --- a/pkg/sqlite/scene_marker_filter.go +++ b/pkg/sqlite/scene_marker_filter.go @@ -173,8 +173,8 @@ func (qb *sceneMarkerFilterHandler) performersCriterionHandler(performers *model primaryFK: sceneIDColumn, foreignFK: performerIDColumn, - addJoinTable: func(f *filterBuilder) { - f.addLeftJoin(performersScenesTable, "performers_join", "performers_join.scene_id = scene_markers.scene_id") + addJoinTable: func(f *filterBuilder, joinType joinType) { + f.addJoin(joinType, performersScenesTable, "performers_join", "performers_join.scene_id = scene_markers.scene_id") }, } @@ -191,8 +191,8 @@ func (qb *sceneMarkerFilterHandler) performersCriterionHandler(performers *model } func (qb *sceneMarkerFilterHandler) scenesCriterionHandler(scenes *models.MultiCriterionInput) criterionHandlerFunc { - addJoinsFunc := func(f *filterBuilder) { - f.addLeftJoin(sceneTable, "markers_scenes", "markers_scenes.id = scene_markers.scene_id") + addJoinsFunc := func(f *filterBuilder, joinType joinType) { + f.addJoin(joinType, sceneTable, "markers_scenes", "markers_scenes.id = scene_markers.scene_id") } h := multiCriterionHandlerBuilder{ primaryTable: sceneMarkerTable, diff --git a/pkg/sqlite/studio_filter.go b/pkg/sqlite/studio_filter.go index 6d5a8fe7c..9ad3494dc 100644 --- a/pkg/sqlite/studio_filter.go +++ b/pkg/sqlite/studio_filter.go @@ -63,7 +63,7 @@ func (qb *studioFilterHandler) criterionHandler() criterionHandler { criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) { if studioFilter.StashID != nil { - studioRepository.stashIDs.join(f, "studio_stash_ids", "studios.id") + studioRepository.stashIDs.leftJoin(f, "studio_stash_ids", "studios.id") stringCriterionHandler(studioFilter.StashID, "studio_stash_ids.stash_id")(ctx, f) } }), @@ -143,15 +143,15 @@ func (qb *studioFilterHandler) isMissingCriterionHandler(isMissing *string) crit if isMissing != nil && *isMissing != "" { switch *isMissing { case "url": - studiosURLsTableMgr.join(f, "", "studios.id") + studiosURLsTableMgr.leftJoin(f, "", "studios.id") f.addWhere("studio_urls.url IS NULL") case "image": f.addWhere("studios.image_blob IS NULL") case "stash_id": - studioRepository.stashIDs.join(f, "studio_stash_ids", "studios.id") + studioRepository.stashIDs.leftJoin(f, "studio_stash_ids", "studios.id") f.addWhere("studio_stash_ids.studio_id IS NULL") case "aliases": - studiosAliasesTableMgr.join(f, "", "studios.id") + studiosAliasesTableMgr.leftJoin(f, "", "studios.id") f.addWhere("studio_aliases.alias IS NULL") case "tags": f.addLeftJoin(studiosTagsTable, "tags_join", "tags_join.studio_id = studios.id") @@ -224,8 +224,8 @@ func (qb *studioFilterHandler) tagCountCriterionHandler(tagCount *models.IntCrit } func (qb *studioFilterHandler) parentCriterionHandler(parents *models.MultiCriterionInput) criterionHandlerFunc { - addJoinsFunc := func(f *filterBuilder) { - f.addLeftJoin("studios", "parent_studio", "parent_studio.id = studios.parent_id") + addJoinsFunc := func(f *filterBuilder, joinType joinType) { + f.addJoin(joinType, "studios", "parent_studio", "parent_studio.id = studios.parent_id") } h := multiCriterionHandlerBuilder{ primaryTable: studioTable, @@ -244,8 +244,8 @@ func (qb *studioFilterHandler) aliasCriterionHandler(alias *models.StringCriteri primaryFK: studioIDColumn, joinTable: studioAliasesTable, stringColumn: studioAliasColumn, - addJoinTable: func(f *filterBuilder) { - studiosAliasesTableMgr.join(f, "", "studios.id") + addJoinTable: func(f *filterBuilder, joinType joinType) { + studiosAliasesTableMgr.join(f, joinType, "", "studios.id") }, } @@ -258,8 +258,8 @@ func (qb *studioFilterHandler) urlsCriterionHandler(url *models.StringCriterionI primaryFK: studioIDColumn, joinTable: studioURLsTable, stringColumn: studioURLColumn, - addJoinTable: func(f *filterBuilder) { - studiosURLsTableMgr.join(f, "", "studios.id") + addJoinTable: func(f *filterBuilder, joinType joinType) { + studiosURLsTableMgr.join(f, joinType, "", "studios.id") }, } diff --git a/pkg/sqlite/table.go b/pkg/sqlite/table.go index 3f8dfb70f..434fe0e49 100644 --- a/pkg/sqlite/table.go +++ b/pkg/sqlite/table.go @@ -129,7 +129,22 @@ func (t *table) destroy(ctx context.Context, ids []int) error { return nil } -func (t *table) join(j joiner, as string, parentIDCol string) { +func (t *table) join(j joiner, jt joinType, as string, parentIDCol string) { + tableName := t.table.GetTable() + tt := tableName + if as != "" { + tt = as + } + + fn := j.addInnerJoin + if jt == joinTypeLeft { + fn = j.addLeftJoin + } + + fn(tableName, as, fmt.Sprintf("%s.%s = %s", tt, t.idColumn.GetCol(), parentIDCol)) +} + +func (t *table) leftJoin(j joiner, as string, parentIDCol string) { tableName := t.table.GetTable() tt := tableName if as != "" { diff --git a/pkg/sqlite/tag_filter.go b/pkg/sqlite/tag_filter.go index 5fd41e80a..6cfe52006 100644 --- a/pkg/sqlite/tag_filter.go +++ b/pkg/sqlite/tag_filter.go @@ -184,8 +184,8 @@ func (qb *tagFilterHandler) aliasCriterionHandler(alias *models.StringCriterionI primaryFK: tagIDColumn, joinTable: tagAliasesTable, stringColumn: tagAliasColumn, - addJoinTable: func(f *filterBuilder) { - tagRepository.aliases.join(f, "", "tags.id") + addJoinTable: func(f *filterBuilder, joinType joinType) { + tagRepository.aliases.join(f, joinType, "", "tags.id") }, } @@ -199,10 +199,10 @@ func (qb *tagFilterHandler) isMissingCriterionHandler(isMissing *string) criteri case "image": f.addWhere("tags.image_blob IS NULL") case "aliases": - tagRepository.aliases.join(f, "", "tags.id") + tagRepository.aliases.leftJoin(f, "", "tags.id") f.addWhere("tag_aliases.alias IS NULL") case "stash_id": - tagRepository.stashIDs.join(f, "tag_stash_ids", "tags.id") + tagRepository.stashIDs.leftJoin(f, "tag_stash_ids", "tags.id") f.addWhere("tag_stash_ids.tag_id IS NULL") default: if err := validateIsMissing(*isMissing, []string{