diff --git a/pkg/sqlite/filter.go b/pkg/sqlite/filter.go index 9d5edeb8f..8935140c2 100644 --- a/pkg/sqlite/filter.go +++ b/pkg/sqlite/filter.go @@ -39,6 +39,7 @@ type join struct { table string as string onClause string + joinType string } // equals returns true if the other join alias/table is equal to this one @@ -57,11 +58,15 @@ func (j join) alias() string { func (j join) toSQL() string { asStr := "" + joinStr := j.joinType if j.as != "" && j.as != j.table { asStr = " AS " + j.as } + if j.joinType == "" { + joinStr = "LEFT" + } - return fmt.Sprintf("LEFT JOIN %s%s ON %s", j.table, asStr, j.onClause) + return fmt.Sprintf("%s JOIN %s%s ON %s", joinStr, j.table, asStr, j.onClause) } type joins []join @@ -154,16 +159,33 @@ func (f *filterBuilder) not(n *filterBuilder) { f.subFilterOp = notOp } -// addJoin adds a join to the filter. The join is expressed in SQL as: +// 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. // This method does not add a join if it its alias/table name is already // present in another existing join. -func (f *filterBuilder) addJoin(table, as, onClause string) { +func (f *filterBuilder) addLeftJoin(table, as, onClause string) { newJoin := join{ table: table, as: as, onClause: onClause, + joinType: "LEFT", + } + + f.joins.add(newJoin) +} + +// addInnerJoin adds an inner join to the filter. The join is expressed in SQL as: +// INNER 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) addInnerJoin(table, as, onClause string) { + newJoin := join{ + table: table, + as: as, + onClause: onClause, + joinType: "INNER", } f.joins.add(newJoin) @@ -505,7 +527,7 @@ func (m *multiCriterionHandlerBuilder) handler(criterion *models.MultiCriterionI table := m.primaryTable if m.joinTable != "" { table = m.joinTable - f.addJoin(table, "", fmt.Sprintf("%s.%s = %s.id", table, m.primaryFK, m.primaryTable)) + f.addLeftJoin(table, "", fmt.Sprintf("%s.%s = %s.id", table, m.primaryFK, m.primaryTable)) } f.addWhere(fmt.Sprintf("%s.%s IS %s NULL", table, m.foreignFK, notClause)) @@ -698,7 +720,7 @@ func (m *hierarchicalMultiCriterionHandlerBuilder) handler(criterion *models.Hie valuesClause := getHierarchicalValues(m.tx, criterion.Value, m.foreignTable, m.relationsTable, m.parentFK, criterion.Depth) - f.addJoin("(SELECT column1 AS root_id, column2 AS item_id FROM ("+valuesClause+"))", m.derivedTable, fmt.Sprintf("%s.item_id = %s.%s", m.derivedTable, m.primaryTable, m.foreignFK)) + f.addLeftJoin("(SELECT column1 AS root_id, column2 AS item_id FROM ("+valuesClause+"))", m.derivedTable, fmt.Sprintf("%s.item_id = %s.%s", m.derivedTable, m.primaryTable, m.foreignFK)) addHierarchicalConditionClauses(f, criterion, m.derivedTable, "root_id") } @@ -731,7 +753,7 @@ func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(criterion *mode notClause = "NOT" } - f.addJoin(m.joinTable, joinAlias, fmt.Sprintf("%s.%s = %s.id", joinAlias, m.primaryFK, m.primaryTable)) + f.addLeftJoin(m.joinTable, joinAlias, fmt.Sprintf("%s.%s = %s.id", joinAlias, m.primaryFK, m.primaryTable)) f.addWhere(utils.StrFormat("{table}.{column} IS {not} NULL", utils.StrFormatMap{ "table": joinAlias, @@ -757,7 +779,7 @@ func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(criterion *mode "valuesClause": valuesClause, }) - f.addJoin(joinTable, joinAlias, fmt.Sprintf("%s.%s = %s.id", joinAlias, m.primaryFK, m.primaryTable)) + f.addLeftJoin(joinTable, joinAlias, fmt.Sprintf("%s.%s = %s.id", joinAlias, m.primaryFK, m.primaryTable)) addHierarchicalConditionClauses(f, criterion, joinAlias, "root_id") } diff --git a/pkg/sqlite/filter_internal_test.go b/pkg/sqlite/filter_internal_test.go index 9a5042ba1..e9f173de0 100644 --- a/pkg/sqlite/filter_internal_test.go +++ b/pkg/sqlite/filter_internal_test.go @@ -152,36 +152,36 @@ func TestAddJoin(t *testing.T) { onClause = "onClause1" ) - f.addJoin(table1Name, as1Name, onClause) + f.addLeftJoin(table1Name, as1Name, onClause) // ensure join is added assert.Len(f.joins, 1) assert.Equal(fmt.Sprintf("LEFT JOIN %s AS %s ON %s", table1Name, as1Name, onClause), f.joins[0].toSQL()) // ensure join with same as is not added - f.addJoin(table2Name, as1Name, onClause) + f.addLeftJoin(table2Name, as1Name, onClause) assert.Len(f.joins, 1) // ensure same table with different alias can be added - f.addJoin(table1Name, as2Name, onClause) + f.addLeftJoin(table1Name, as2Name, onClause) assert.Len(f.joins, 2) assert.Equal(fmt.Sprintf("LEFT JOIN %s AS %s ON %s", table1Name, as2Name, onClause), f.joins[1].toSQL()) // ensure table without alias can be added if tableName != existing alias/tableName - f.addJoin(table1Name, "", onClause) + f.addLeftJoin(table1Name, "", onClause) assert.Len(f.joins, 3) assert.Equal(fmt.Sprintf("LEFT JOIN %s ON %s", table1Name, onClause), f.joins[2].toSQL()) // ensure table with alias == table name of a join without alias is not added - f.addJoin(table2Name, table1Name, onClause) + f.addLeftJoin(table2Name, table1Name, onClause) assert.Len(f.joins, 3) // ensure table without alias cannot be added if tableName == existing alias - f.addJoin(as2Name, "", onClause) + f.addLeftJoin(as2Name, "", onClause) assert.Len(f.joins, 3) // ensure AS is not used if same as table name - f.addJoin(table2Name, table2Name, onClause) + f.addLeftJoin(table2Name, table2Name, onClause) assert.Len(f.joins, 4) assert.Equal(fmt.Sprintf("LEFT JOIN %s ON %s", table2Name, onClause), f.joins[3].toSQL()) } @@ -407,7 +407,7 @@ func TestGetAllJoins(t *testing.T) { onClause = "onClause1" ) - f.addJoin(table1Name, as1Name, onClause) + f.addLeftJoin(table1Name, as1Name, onClause) // ensure join is returned joins := f.getAllJoins() @@ -417,14 +417,14 @@ func TestGetAllJoins(t *testing.T) { // ensure joins in sub-filter are returned subFilter := &filterBuilder{} f.and(subFilter) - subFilter.addJoin(table2Name, as2Name, onClause) + subFilter.addLeftJoin(table2Name, as2Name, onClause) joins = f.getAllJoins() assert.Len(joins, 2) assert.Equal(fmt.Sprintf("LEFT JOIN %s AS %s ON %s", table2Name, as2Name, onClause), joins[1].toSQL()) // ensure redundant joins are not returned - subFilter.addJoin(as1Name, "", onClause) + subFilter.addLeftJoin(as1Name, "", onClause) joins = f.getAllJoins() assert.Len(joins, 2) } diff --git a/pkg/sqlite/gallery.go b/pkg/sqlite/gallery.go index 6f3b9522b..f76f84d4c 100644 --- a/pkg/sqlite/gallery.go +++ b/pkg/sqlite/gallery.go @@ -290,7 +290,7 @@ func galleryIsMissingCriterionHandler(qb *galleryQueryBuilder, isMissing *string if isMissing != nil && *isMissing != "" { switch *isMissing { case "scenes": - f.addJoin("scenes_galleries", "scenes_join", "scenes_join.gallery_id = galleries.id") + f.addLeftJoin("scenes_galleries", "scenes_join", "scenes_join.gallery_id = galleries.id") f.addWhere("scenes_join.gallery_id IS NULL") case "studio": f.addWhere("galleries.studio_id IS NULL") @@ -395,8 +395,8 @@ func galleryPerformerTagsCriterionHandler(qb *galleryQueryBuilder, tags *models. notClause = "NOT" } - f.addJoin("performers_galleries", "", "galleries.id = performers_galleries.gallery_id") - f.addJoin("performers_tags", "", "performers_galleries.performer_id = performers_tags.performer_id") + f.addLeftJoin("performers_galleries", "", "galleries.id = performers_galleries.gallery_id") + f.addLeftJoin("performers_tags", "", "performers_galleries.performer_id = performers_tags.performer_id") f.addWhere(fmt.Sprintf("performers_tags.tag_id IS %s NULL", notClause)) return @@ -414,7 +414,7 @@ INNER JOIN performers_tags pt ON pt.performer_id = pg.performer_id INNER JOIN (` + valuesClause + `) t ON t.column2 = pt.tag_id )`) - f.addJoin("performer_tags", "", "performer_tags.gallery_id = galleries.id") + f.addLeftJoin("performer_tags", "", "performer_tags.gallery_id = galleries.id") addHierarchicalConditionClauses(f, tags, "performer_tags", "root_tag_id") } @@ -425,7 +425,7 @@ func galleryAverageResolutionCriterionHandler(qb *galleryQueryBuilder, resolutio return func(f *filterBuilder) { if resolution != nil && resolution.Value.IsValid() { qb.imagesRepository().join(f, "images_join", "galleries.id") - f.addJoin("images", "", "images_join.image_id = images.id") + f.addLeftJoin("images", "", "images_join.image_id = images.id") min := resolution.Value.GetMinResolution() max := resolution.Value.GetMaxResolution() diff --git a/pkg/sqlite/image.go b/pkg/sqlite/image.go index 96ffad6a7..aa2125cd0 100644 --- a/pkg/sqlite/image.go +++ b/pkg/sqlite/image.go @@ -14,7 +14,7 @@ const performersImagesTable = "performers_images" const imagesTagsTable = "images_tags" var imagesForGalleryQuery = selectAll(imageTable) + ` -LEFT JOIN galleries_images as galleries_join on galleries_join.image_id = images.id +INNER JOIN galleries_images as galleries_join on galleries_join.image_id = images.id WHERE galleries_join.gallery_id = ? GROUP BY images.id ` @@ -360,7 +360,7 @@ func imageIsMissingCriterionHandler(qb *imageQueryBuilder, isMissing *string) cr qb.performersRepository().join(f, "performers_join", "images.id") f.addWhere("performers_join.image_id IS NULL") case "galleries": - qb.galleriesRepository().join(f, "galleries_join", "images.id") + qb.galleriesRepository().innerJoin(f, "galleries_join", "images.id") f.addWhere("galleries_join.image_id IS NULL") case "tags": qb.tagsRepository().join(f, "tags_join", "images.id") @@ -412,8 +412,8 @@ func imageTagCountCriterionHandler(qb *imageQueryBuilder, tagCount *models.IntCr func imageGalleriesCriterionHandler(qb *imageQueryBuilder, galleries *models.MultiCriterionInput) criterionHandlerFunc { addJoinsFunc := func(f *filterBuilder) { - qb.galleriesRepository().join(f, "galleries_join", "images.id") - f.addJoin(galleryTable, "", "galleries_join.gallery_id = galleries.id") + qb.galleriesRepository().innerJoin(f, "galleries_join", "images.id") + f.addInnerJoin(galleryTable, "", "galleries_join.gallery_id = galleries.id") } h := qb.getMultiCriterionHandlerBuilder(galleryTable, galleriesImagesTable, galleryIDColumn, addJoinsFunc) @@ -469,8 +469,8 @@ func imagePerformerTagsCriterionHandler(qb *imageQueryBuilder, tags *models.Hier notClause = "NOT" } - f.addJoin("performers_images", "", "images.id = performers_images.image_id") - f.addJoin("performers_tags", "", "performers_images.performer_id = performers_tags.performer_id") + f.addLeftJoin("performers_images", "", "images.id = performers_images.image_id") + f.addLeftJoin("performers_tags", "", "performers_images.performer_id = performers_tags.performer_id") f.addWhere(fmt.Sprintf("performers_tags.tag_id IS %s NULL", notClause)) return @@ -488,7 +488,7 @@ INNER JOIN performers_tags pt ON pt.performer_id = pi.performer_id INNER JOIN (` + valuesClause + `) t ON t.column2 = pt.tag_id )`) - f.addJoin("performer_tags", "", "performer_tags.image_id = images.id") + f.addLeftJoin("performer_tags", "", "performer_tags.image_id = images.id") addHierarchicalConditionClauses(f, tags, "performer_tags", "root_tag_id") } diff --git a/pkg/sqlite/image_test.go b/pkg/sqlite/image_test.go index 141fbb3d6..c90fb4547 100644 --- a/pkg/sqlite/image_test.go +++ b/pkg/sqlite/image_test.go @@ -69,6 +69,32 @@ func TestImageFindByPath(t *testing.T) { }) } +func TestImageFindByGalleryID(t *testing.T) { + withTxn(func(r models.Repository) error { + sqb := r.Image() + + images, err := sqb.FindByGalleryID(galleryIDs[galleryIdxWithTwoImages]) + + if err != nil { + t.Errorf("Error finding images: %s", err.Error()) + } + + assert.Len(t, images, 2) + assert.Equal(t, imageIDs[imageIdx1WithGallery], images[0].ID) + assert.Equal(t, imageIDs[imageIdx2WithGallery], images[1].ID) + + images, err = sqb.FindByGalleryID(galleryIDs[galleryIdxWithScene]) + + if err != nil { + t.Errorf("Error finding images: %s", err.Error()) + } + + assert.Len(t, images, 0) + + return nil + }) +} + func TestImageQueryQ(t *testing.T) { withTxn(func(r models.Repository) error { const imageIdx = 2 diff --git a/pkg/sqlite/movies.go b/pkg/sqlite/movies.go index 2f3ed96c8..eac02ae54 100644 --- a/pkg/sqlite/movies.go +++ b/pkg/sqlite/movies.go @@ -176,13 +176,13 @@ func movieIsMissingCriterionHandler(qb *movieQueryBuilder, isMissing *string) cr if isMissing != nil && *isMissing != "" { switch *isMissing { case "front_image": - f.addJoin("movies_images", "", "movies_images.movie_id = movies.id") + f.addLeftJoin("movies_images", "", "movies_images.movie_id = movies.id") f.addWhere("movies_images.front_image IS NULL") case "back_image": - f.addJoin("movies_images", "", "movies_images.movie_id = movies.id") + f.addLeftJoin("movies_images", "", "movies_images.movie_id = movies.id") f.addWhere("movies_images.back_image IS NULL") case "scenes": - f.addJoin("movies_scenes", "", "movies_scenes.movie_id = movies.id") + f.addLeftJoin("movies_scenes", "", "movies_scenes.movie_id = movies.id") f.addWhere("movies_scenes.scene_id IS NULL") default: f.addWhere("(movies." + *isMissing + " IS NULL OR TRIM(movies." + *isMissing + ") = '')") @@ -214,8 +214,8 @@ func moviePerformersCriterionHandler(qb *movieQueryBuilder, performers *models.M notClause = "NOT" } - f.addJoin("movies_scenes", "", "movies.id = movies_scenes.movie_id") - f.addJoin("performers_scenes", "", "movies_scenes.scene_id = performers_scenes.scene_id") + f.addLeftJoin("movies_scenes", "", "movies.id = movies_scenes.movie_id") + f.addLeftJoin("performers_scenes", "", "movies_scenes.scene_id = performers_scenes.scene_id") f.addWhere(fmt.Sprintf("performers_scenes.performer_id IS %s NULL", notClause)) return @@ -237,7 +237,7 @@ func moviePerformersCriterionHandler(qb *movieQueryBuilder, performers *models.M INNER JOIN performers_scenes ON movies_scenes.scene_id = performers_scenes.scene_id WHERE performers_scenes.performer_id IN`+getInBinding(len(performers.Value))+` )`, args...) - f.addJoin("movies_performers", "", "movies.id = movies_performers.movie_id") + f.addLeftJoin("movies_performers", "", "movies.id = movies_performers.movie_id") switch performers.Modifier { case models.CriterionModifierIncludes: diff --git a/pkg/sqlite/performer.go b/pkg/sqlite/performer.go index 34b6bf6c6..d824dc37e 100644 --- a/pkg/sqlite/performer.go +++ b/pkg/sqlite/performer.go @@ -341,10 +341,10 @@ func performerIsMissingCriterionHandler(qb *performerQueryBuilder, isMissing *st if isMissing != nil && *isMissing != "" { switch *isMissing { case "scenes": // Deprecated: use `scene_count == 0` filter instead - f.addJoin(performersScenesTable, "scenes_join", "scenes_join.performer_id = performers.id") + f.addLeftJoin(performersScenesTable, "scenes_join", "scenes_join.performer_id = performers.id") f.addWhere("scenes_join.scene_id IS NULL") case "image": - f.addJoin(performersImageTable, "image_join", "image_join.performer_id = performers.id") + 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") @@ -463,8 +463,8 @@ func performerStudiosCriterionHandler(qb *performerQueryBuilder, studios *models var conditions []string for _, c := range formatMaps { - f.addJoin(c["joinTable"].(string), "", fmt.Sprintf("%s.performer_id = performers.id", c["joinTable"])) - f.addJoin(c["primaryTable"].(string), "", fmt.Sprintf("%s.%s = %s.id", c["joinTable"], c["primaryFK"], c["primaryTable"])) + f.addLeftJoin(c["joinTable"].(string), "", fmt.Sprintf("%s.performer_id = performers.id", c["joinTable"])) + f.addLeftJoin(c["primaryTable"].(string), "", fmt.Sprintf("%s.%s = %s.id", c["joinTable"], c["primaryFK"], c["primaryTable"])) conditions = append(conditions, fmt.Sprintf("%s.studio_id IS NULL", c["primaryTable"])) } @@ -505,7 +505,7 @@ func performerStudiosCriterionHandler(qb *performerQueryBuilder, studios *models f.addWith(fmt.Sprintf("%s AS (%s)", derivedPerformerStudioTable, strings.Join(unions, " UNION "))) - f.addJoin(derivedPerformerStudioTable, "", fmt.Sprintf("performers.id = %s.performer_id", derivedPerformerStudioTable)) + f.addLeftJoin(derivedPerformerStudioTable, "", fmt.Sprintf("performers.id = %s.performer_id", derivedPerformerStudioTable)) f.addWhere(fmt.Sprintf("%s.performer_id IS %s NULL", derivedPerformerStudioTable, clauseCondition)) } } diff --git a/pkg/sqlite/query.go b/pkg/sqlite/query.go index 95fdef5f3..27ce213b5 100644 --- a/pkg/sqlite/query.go +++ b/pkg/sqlite/query.go @@ -127,6 +127,7 @@ func (qb *queryBuilder) join(table, as, onClause string) { table: table, as: as, onClause: onClause, + joinType: "LEFT", } qb.joins.add(newJoin) diff --git a/pkg/sqlite/repository.go b/pkg/sqlite/repository.go index 160cbbc88..ed0d4ac0d 100644 --- a/pkg/sqlite/repository.go +++ b/pkg/sqlite/repository.go @@ -294,11 +294,20 @@ func (r *repository) join(j joiner, as string, parentIDCol string) { if as != "" { t = as } - j.addJoin(r.tableName, as, fmt.Sprintf("%s.%s = %s", t, r.idColumn, parentIDCol)) + j.addLeftJoin(r.tableName, as, fmt.Sprintf("%s.%s = %s", t, r.idColumn, parentIDCol)) +} + +func (r *repository) innerJoin(j joiner, as string, parentIDCol string) { + t := r.tableName + if as != "" { + t = as + } + j.addInnerJoin(r.tableName, as, fmt.Sprintf("%s.%s = %s", t, r.idColumn, parentIDCol)) } type joiner interface { - addJoin(table, as, onClause string) + addLeftJoin(table, as, onClause string) + addInnerJoin(table, as, onClause string) } type joinRepository struct { diff --git a/pkg/sqlite/scene.go b/pkg/sqlite/scene.go index c8d3ff4ec..3149c4f53 100644 --- a/pkg/sqlite/scene.go +++ b/pkg/sqlite/scene.go @@ -537,7 +537,7 @@ func resolutionCriterionHandler(resolution *models.ResolutionCriterionInput, hei func hasMarkersCriterionHandler(hasMarkers *string) criterionHandlerFunc { return func(f *filterBuilder) { if hasMarkers != nil { - f.addJoin("scene_markers", "", "scene_markers.scene_id = scenes.id") + f.addLeftJoin("scene_markers", "", "scene_markers.scene_id = scenes.id") if *hasMarkers == "true" { f.addHaving("count(scene_markers.scene_id) > 0") } else { @@ -658,7 +658,7 @@ func sceneStudioCriterionHandler(qb *sceneQueryBuilder, studios *models.Hierarch func sceneMoviesCriterionHandler(qb *sceneQueryBuilder, movies *models.MultiCriterionInput) criterionHandlerFunc { addJoinsFunc := func(f *filterBuilder) { qb.moviesRepository().join(f, "movies_join", "scenes.id") - f.addJoin("movies", "", "movies_join.movie_id = movies.id") + f.addLeftJoin("movies", "", "movies_join.movie_id = movies.id") } h := qb.getMultiCriterionHandlerBuilder(movieTable, moviesScenesTable, "movie_id", addJoinsFunc) return h.handler(movies) @@ -673,8 +673,8 @@ func scenePerformerTagsCriterionHandler(qb *sceneQueryBuilder, tags *models.Hier notClause = "NOT" } - f.addJoin("performers_scenes", "", "scenes.id = performers_scenes.scene_id") - f.addJoin("performers_tags", "", "performers_scenes.performer_id = performers_tags.performer_id") + f.addLeftJoin("performers_scenes", "", "scenes.id = performers_scenes.scene_id") + f.addLeftJoin("performers_tags", "", "performers_scenes.performer_id = performers_tags.performer_id") f.addWhere(fmt.Sprintf("performers_tags.tag_id IS %s NULL", notClause)) return @@ -692,7 +692,7 @@ INNER JOIN performers_tags pt ON pt.performer_id = ps.performer_id INNER JOIN (` + valuesClause + `) t ON t.column2 = pt.tag_id )`) - f.addJoin("performer_tags", "", "performer_tags.scene_id = scenes.id") + f.addLeftJoin("performer_tags", "", "performer_tags.scene_id = scenes.id") addHierarchicalConditionClauses(f, tags, "performer_tags", "root_tag_id") } diff --git a/pkg/sqlite/scene_marker.go b/pkg/sqlite/scene_marker.go index ae6e9cda9..c79c1dc16 100644 --- a/pkg/sqlite/scene_marker.go +++ b/pkg/sqlite/scene_marker.go @@ -180,7 +180,7 @@ func (qb *sceneMarkerQueryBuilder) Query(sceneMarkerFilter *models.SceneMarkerFi func sceneMarkerTagIDCriterionHandler(qb *sceneMarkerQueryBuilder, tagID *string) criterionHandlerFunc { return func(f *filterBuilder) { if tagID != nil { - f.addJoin("scene_markers_tags", "", "scene_markers_tags.scene_marker_id = scene_markers.id") + f.addLeftJoin("scene_markers_tags", "", "scene_markers_tags.scene_marker_id = scene_markers.id") f.addWhere("(scene_markers.primary_tag_id = ? OR scene_markers_tags.tag_id = ?)", *tagID, *tagID) } @@ -196,7 +196,7 @@ func sceneMarkerTagsCriterionHandler(qb *sceneMarkerQueryBuilder, tags *models.H notClause = "NOT" } - f.addJoin("scene_markers_tags", "", "scene_markers.id = scene_markers_tags.scene_marker_id") + f.addLeftJoin("scene_markers_tags", "", "scene_markers.id = scene_markers_tags.scene_marker_id") f.addWhere(fmt.Sprintf("%s scene_markers_tags.tag_id IS NULL", notClause)) return @@ -215,7 +215,7 @@ SELECT m.id, t.column1 FROM scene_markers m INNER JOIN (` + valuesClause + `) t ON t.column2 = m.primary_tag_id )`) - f.addJoin("marker_tags", "", "marker_tags.scene_marker_id = scene_markers.id") + f.addLeftJoin("marker_tags", "", "marker_tags.scene_marker_id = scene_markers.id") addHierarchicalConditionClauses(f, tags, "marker_tags", "root_tag_id") } @@ -231,7 +231,7 @@ func sceneMarkerSceneTagsCriterionHandler(qb *sceneMarkerQueryBuilder, tags *mod notClause = "NOT" } - f.addJoin("scenes_tags", "", "scene_markers.scene_id = scenes_tags.scene_id") + f.addLeftJoin("scenes_tags", "", "scene_markers.scene_id = scenes_tags.scene_id") f.addWhere(fmt.Sprintf("scenes_tags.tag_id IS %s NULL", notClause)) return @@ -248,7 +248,7 @@ SELECT st.scene_id, t.column1 AS root_tag_id FROM scenes_tags st INNER JOIN (` + valuesClause + `) t ON t.column2 = st.tag_id )`) - f.addJoin("scene_tags", "", "scene_tags.scene_id = scene_markers.scene_id") + f.addLeftJoin("scene_tags", "", "scene_tags.scene_id = scene_markers.scene_id") addHierarchicalConditionClauses(f, tags, "scene_tags", "root_tag_id") } @@ -264,14 +264,14 @@ func sceneMarkerPerformersCriterionHandler(qb *sceneMarkerQueryBuilder, performe foreignFK: performerIDColumn, addJoinTable: func(f *filterBuilder) { - f.addJoin(performersScenesTable, "performers_join", "performers_join.scene_id = scene_markers.scene_id") + f.addLeftJoin(performersScenesTable, "performers_join", "performers_join.scene_id = scene_markers.scene_id") }, } handler := h.handler(performers) return func(f *filterBuilder) { // Make sure scenes is included, otherwise excludes filter fails - f.addJoin(sceneTable, "", "scenes.id = scene_markers.scene_id") + f.addLeftJoin(sceneTable, "", "scenes.id = scene_markers.scene_id") handler(f) } } diff --git a/pkg/sqlite/studio.go b/pkg/sqlite/studio.go index f3666e85b..91e6c63ea 100644 --- a/pkg/sqlite/studio.go +++ b/pkg/sqlite/studio.go @@ -278,7 +278,7 @@ func studioIsMissingCriterionHandler(qb *studioQueryBuilder, isMissing *string) if isMissing != nil && *isMissing != "" { switch *isMissing { case "image": - f.addJoin("studios_image", "", "studios_image.studio_id = studios.id") + f.addLeftJoin("studios_image", "", "studios_image.studio_id = studios.id") f.addWhere("studios_image.studio_id IS NULL") case "stash_id": qb.stashIDRepository().join(f, "studio_stash_ids", "studios.id") @@ -293,7 +293,7 @@ func studioIsMissingCriterionHandler(qb *studioQueryBuilder, isMissing *string) func studioSceneCountCriterionHandler(qb *studioQueryBuilder, sceneCount *models.IntCriterionInput) criterionHandlerFunc { return func(f *filterBuilder) { if sceneCount != nil { - f.addJoin("scenes", "", "scenes.studio_id = studios.id") + f.addLeftJoin("scenes", "", "scenes.studio_id = studios.id") clause, args := getIntCriterionWhereClause("count(distinct scenes.id)", *sceneCount) f.addHaving(clause, args...) @@ -304,7 +304,7 @@ func studioSceneCountCriterionHandler(qb *studioQueryBuilder, sceneCount *models func studioImageCountCriterionHandler(qb *studioQueryBuilder, imageCount *models.IntCriterionInput) criterionHandlerFunc { return func(f *filterBuilder) { if imageCount != nil { - f.addJoin("images", "", "images.studio_id = studios.id") + f.addLeftJoin("images", "", "images.studio_id = studios.id") clause, args := getIntCriterionWhereClause("count(distinct images.id)", *imageCount) f.addHaving(clause, args...) @@ -315,7 +315,7 @@ func studioImageCountCriterionHandler(qb *studioQueryBuilder, imageCount *models func studioGalleryCountCriterionHandler(qb *studioQueryBuilder, galleryCount *models.IntCriterionInput) criterionHandlerFunc { return func(f *filterBuilder) { if galleryCount != nil { - f.addJoin("galleries", "", "galleries.studio_id = studios.id") + f.addLeftJoin("galleries", "", "galleries.studio_id = studios.id") clause, args := getIntCriterionWhereClause("count(distinct galleries.id)", *galleryCount) f.addHaving(clause, args...) @@ -325,7 +325,7 @@ func studioGalleryCountCriterionHandler(qb *studioQueryBuilder, galleryCount *mo func studioParentCriterionHandler(qb *studioQueryBuilder, parents *models.MultiCriterionInput) criterionHandlerFunc { addJoinsFunc := func(f *filterBuilder) { - f.addJoin("studios", "parent_studio", "parent_studio.id = studios.parent_id") + f.addLeftJoin("studios", "parent_studio", "parent_studio.id = studios.parent_id") } h := multiCriterionHandlerBuilder{ primaryTable: studioTable, diff --git a/pkg/sqlite/tag.go b/pkg/sqlite/tag.go index a9c46f2f3..c5f3858d7 100644 --- a/pkg/sqlite/tag.go +++ b/pkg/sqlite/tag.go @@ -386,7 +386,7 @@ func tagIsMissingCriterionHandler(qb *tagQueryBuilder, isMissing *string) criter func tagSceneCountCriterionHandler(qb *tagQueryBuilder, sceneCount *models.IntCriterionInput) criterionHandlerFunc { return func(f *filterBuilder) { if sceneCount != nil { - f.addJoin("scenes_tags", "", "scenes_tags.tag_id = tags.id") + f.addLeftJoin("scenes_tags", "", "scenes_tags.tag_id = tags.id") clause, args := getIntCriterionWhereClause("count(distinct scenes_tags.scene_id)", *sceneCount) f.addHaving(clause, args...) @@ -397,7 +397,7 @@ func tagSceneCountCriterionHandler(qb *tagQueryBuilder, sceneCount *models.IntCr func tagImageCountCriterionHandler(qb *tagQueryBuilder, imageCount *models.IntCriterionInput) criterionHandlerFunc { return func(f *filterBuilder) { if imageCount != nil { - f.addJoin("images_tags", "", "images_tags.tag_id = tags.id") + f.addLeftJoin("images_tags", "", "images_tags.tag_id = tags.id") clause, args := getIntCriterionWhereClause("count(distinct images_tags.image_id)", *imageCount) f.addHaving(clause, args...) @@ -408,7 +408,7 @@ func tagImageCountCriterionHandler(qb *tagQueryBuilder, imageCount *models.IntCr func tagGalleryCountCriterionHandler(qb *tagQueryBuilder, galleryCount *models.IntCriterionInput) criterionHandlerFunc { return func(f *filterBuilder) { if galleryCount != nil { - f.addJoin("galleries_tags", "", "galleries_tags.tag_id = tags.id") + f.addLeftJoin("galleries_tags", "", "galleries_tags.tag_id = tags.id") clause, args := getIntCriterionWhereClause("count(distinct galleries_tags.gallery_id)", *galleryCount) f.addHaving(clause, args...) @@ -419,7 +419,7 @@ func tagGalleryCountCriterionHandler(qb *tagQueryBuilder, galleryCount *models.I func tagPerformerCountCriterionHandler(qb *tagQueryBuilder, performerCount *models.IntCriterionInput) criterionHandlerFunc { return func(f *filterBuilder) { if performerCount != nil { - f.addJoin("performers_tags", "", "performers_tags.tag_id = tags.id") + f.addLeftJoin("performers_tags", "", "performers_tags.tag_id = tags.id") clause, args := getIntCriterionWhereClause("count(distinct performers_tags.performer_id)", *performerCount) f.addHaving(clause, args...) @@ -430,8 +430,8 @@ func tagPerformerCountCriterionHandler(qb *tagQueryBuilder, performerCount *mode func tagMarkerCountCriterionHandler(qb *tagQueryBuilder, markerCount *models.IntCriterionInput) criterionHandlerFunc { return func(f *filterBuilder) { if markerCount != nil { - f.addJoin("scene_markers_tags", "", "scene_markers_tags.tag_id = tags.id") - f.addJoin("scene_markers", "", "scene_markers_tags.scene_marker_id = scene_markers.id OR scene_markers.primary_tag_id = tags.id") + f.addLeftJoin("scene_markers_tags", "", "scene_markers_tags.tag_id = tags.id") + f.addLeftJoin("scene_markers", "", "scene_markers_tags.scene_marker_id = scene_markers.id OR scene_markers.primary_tag_id = tags.id") clause, args := getIntCriterionWhereClause("count(distinct scene_markers.id)", *markerCount) f.addHaving(clause, args...) @@ -448,7 +448,7 @@ func tagParentsCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalMu notClause = "NOT" } - f.addJoin("tags_relations", "parent_relations", "tags.id = parent_relations.child_id") + f.addLeftJoin("tags_relations", "parent_relations", "tags.id = parent_relations.child_id") f.addWhere(fmt.Sprintf("parent_relations.parent_id IS %s NULL", notClause)) return @@ -481,7 +481,7 @@ func tagParentsCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalMu f.addRecursiveWith(query, args...) - f.addJoin("parents", "", "parents.item_id = tags.id") + f.addLeftJoin("parents", "", "parents.item_id = tags.id") addHierarchicalConditionClauses(f, tags, "parents", "root_id") } @@ -497,7 +497,7 @@ func tagChildrenCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalM notClause = "NOT" } - f.addJoin("tags_relations", "child_relations", "tags.id = child_relations.parent_id") + f.addLeftJoin("tags_relations", "child_relations", "tags.id = child_relations.parent_id") f.addWhere(fmt.Sprintf("child_relations.child_id IS %s NULL", notClause)) return @@ -530,7 +530,7 @@ func tagChildrenCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalM f.addRecursiveWith(query, args...) - f.addJoin("children", "", "children.item_id = tags.id") + f.addLeftJoin("children", "", "children.item_id = tags.id") addHierarchicalConditionClauses(f, tags, "children", "root_id") } @@ -540,7 +540,7 @@ func tagChildrenCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalM func tagParentCountCriterionHandler(qb *tagQueryBuilder, parentCount *models.IntCriterionInput) criterionHandlerFunc { return func(f *filterBuilder) { if parentCount != nil { - f.addJoin("tags_relations", "parents_count", "parents_count.child_id = tags.id") + f.addLeftJoin("tags_relations", "parents_count", "parents_count.child_id = tags.id") clause, args := getIntCriterionWhereClause("count(distinct parents_count.parent_id)", *parentCount) f.addHaving(clause, args...) @@ -551,7 +551,7 @@ func tagParentCountCriterionHandler(qb *tagQueryBuilder, parentCount *models.Int func tagChildCountCriterionHandler(qb *tagQueryBuilder, childCount *models.IntCriterionInput) criterionHandlerFunc { return func(f *filterBuilder) { if childCount != nil { - f.addJoin("tags_relations", "children_count", "children_count.parent_id = tags.id") + f.addLeftJoin("tags_relations", "children_count", "children_count.parent_id = tags.id") clause, args := getIntCriterionWhereClause("count(distinct children_count.child_id)", *childCount) f.addHaving(clause, args...)