Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 102 additions & 32 deletions backend/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,14 +136,29 @@ type assetFilter struct {
aliased bool
}

func buildAssetFilter(userID, albumID string, withGPS bool, hiddenFilter, startDate, endDate string) assetFilter {
func buildAssetFilter(userID, albumID, tagID string, withGPS bool, hiddenFilter, startDate, endDate string) assetFilter {
var f assetFilter
if albumID != "" {
if albumID != "" || tagID != "" {
f.aliased = true
f.fromClause = `FROM assets a
JOIN albumAssets aa ON aa.userID = a.userID AND aa.assetID = a.immichID
WHERE a.userID = ? AND aa.albumID = ?`
f.args = append(f.args, userID, albumID)
f.fromClause = `FROM assets a`
if albumID != "" {
f.fromClause += `
JOIN albumAssets aa ON aa.userID = a.userID AND aa.assetID = a.immichID`
}
if tagID != "" {
f.fromClause += `
JOIN assetTags at ON at.userID = a.userID AND at.assetID = a.immichID`
}
f.fromClause += ` WHERE a.userID = ?`
f.args = append(f.args, userID)
if albumID != "" {
f.fromClause += ` AND aa.albumID = ?`
f.args = append(f.args, albumID)
}
if tagID != "" {
f.fromClause += ` AND at.tagID = ?`
f.args = append(f.args, tagID)
}
if withGPS {
f.fromClause += ` AND a.latitude IS NOT NULL AND a.longitude IS NOT NULL`
} else {
Expand Down Expand Up @@ -197,8 +212,8 @@ func buildAssetFilter(userID, albumID string, withGPS bool, hiddenFilter, startD
return f
}

func (d *Database) getFilteredAssets(ctx context.Context, userID, albumID string, withGPS bool, hiddenFilter, startDate, endDate string, page, pageSize int) ([]AssetRow, error) {
f := buildAssetFilter(userID, albumID, withGPS, hiddenFilter, startDate, endDate)
func (d *Database) getFilteredAssets(ctx context.Context, userID, albumID, tagID string, withGPS bool, hiddenFilter, startDate, endDate string, page, pageSize int) ([]AssetRow, error) {
f := buildAssetFilter(userID, albumID, tagID, withGPS, hiddenFilter, startDate, endDate)

cols := assetColumns
orderPrefix := ""
Expand All @@ -220,17 +235,17 @@ func (d *Database) getFilteredAssets(ctx context.Context, userID, albumID string
return scanAssetRows(rows)
}

func (d *Database) countFilteredAssets(ctx context.Context, userID, albumID string, withGPS bool, hiddenFilter, startDate, endDate string) (int, error) {
f := buildAssetFilter(userID, albumID, withGPS, hiddenFilter, startDate, endDate)
func (d *Database) countFilteredAssets(ctx context.Context, userID, albumID, tagID string, withGPS bool, hiddenFilter, startDate, endDate string) (int, error) {
f := buildAssetFilter(userID, albumID, tagID, withGPS, hiddenFilter, startDate, endDate)
query := `SELECT COUNT(*) ` + f.fromClause

var count int
err := d.db.QueryRowContext(ctx, query, f.args...).Scan(&count)
return count, err
}

func (d *Database) countAssetsByDay(ctx context.Context, userID, albumID string, withGPS bool, hiddenFilter, startDate, endDate string) (map[string]int, error) {
f := buildAssetFilter(userID, albumID, withGPS, hiddenFilter, startDate, endDate)
func (d *Database) countAssetsByDay(ctx context.Context, userID, albumID, tagID string, withGPS bool, hiddenFilter, startDate, endDate string) (map[string]int, error) {
f := buildAssetFilter(userID, albumID, tagID, withGPS, hiddenFilter, startDate, endDate)

dateCol := "dateTimeOriginal"
if f.aliased {
Expand Down Expand Up @@ -263,22 +278,47 @@ type markerFilter struct {
prefix string
}

func buildMarkerFilter(userID, albumID string, bounds *TViewportBounds) markerFilter {
func buildMarkerFilter(userID, albumID, tagID, startDate, endDate string, bounds *TViewportBounds) markerFilter {
var f markerFilter
if albumID != "" {
if albumID != "" || tagID != "" {
f.prefix = "a."
f.fromClause = `FROM assets a
JOIN albumAssets aa ON aa.userID = a.userID AND aa.assetID = a.immichID
WHERE a.userID = ? AND aa.albumID = ? AND a.latitude IS NOT NULL AND a.longitude IS NOT NULL
f.fromClause = `FROM assets a`
if albumID != "" {
f.fromClause += `
JOIN albumAssets aa ON aa.userID = a.userID AND aa.assetID = a.immichID`
}
if tagID != "" {
f.fromClause += `
JOIN assetTags at ON at.userID = a.userID AND at.assetID = a.immichID`
}
f.fromClause += ` WHERE a.userID = ?`
f.args = append(f.args, userID)
if albumID != "" {
f.fromClause += ` AND aa.albumID = ?`
f.args = append(f.args, albumID)
}
if tagID != "" {
f.fromClause += ` AND at.tagID = ?`
f.args = append(f.args, tagID)
}
f.fromClause += ` AND a.latitude IS NOT NULL AND a.longitude IS NOT NULL
AND a.stackPrimaryAssetID IS NULL` + hiddenLibraryFilterAliased
f.args = append(f.args, userID, albumID)
} else {
f.fromClause = `FROM assets
WHERE userID = ? AND latitude IS NOT NULL AND longitude IS NOT NULL
AND stackPrimaryAssetID IS NULL` + hiddenLibraryFilter
f.args = append(f.args, userID)
}

if startDate != "" {
f.fromClause += fmt.Sprintf(` AND %sdateTimeOriginal >= ?`, f.prefix)
f.args = append(f.args, startDate)
}
if endDate != "" {
f.fromClause += fmt.Sprintf(` AND %sdateTimeOriginal < ?`, f.prefix)
f.args = append(f.args, endDate+"T99")
}

if bounds != nil {
f.fromClause += fmt.Sprintf(` AND %slatitude BETWEEN ? AND ?`, f.prefix)
f.args = append(f.args, bounds.South, bounds.North)
Expand All @@ -292,8 +332,8 @@ func buildMarkerFilter(userID, albumID string, bounds *TViewportBounds) markerFi
return f
}

func (d *Database) getMapMarkers(ctx context.Context, userID, albumID string, bounds *TViewportBounds, limit int) ([]MapMarker, error) {
f := buildMarkerFilter(userID, albumID, bounds)
func (d *Database) getMapMarkers(ctx context.Context, userID, albumID, tagID, startDate, endDate string, bounds *TViewportBounds, limit int) ([]MapMarker, error) {
f := buildMarkerFilter(userID, albumID, tagID, startDate, endDate, bounds)

selectCols := "immichID, latitude, longitude"
if f.prefix != "" {
Expand Down Expand Up @@ -321,8 +361,8 @@ func (d *Database) getMapMarkers(ctx context.Context, userID, albumID string, bo
return markers, rows.Err()
}

func (d *Database) countMapMarkers(ctx context.Context, userID, albumID string, bounds *TViewportBounds) (int, error) {
f := buildMarkerFilter(userID, albumID, bounds)
func (d *Database) countMapMarkers(ctx context.Context, userID, albumID, tagID, startDate, endDate string, bounds *TViewportBounds) (int, error) {
f := buildMarkerFilter(userID, albumID, tagID, startDate, endDate, bounds)
query := "SELECT COUNT(*) " + f.fromClause

var count int
Expand Down Expand Up @@ -538,7 +578,7 @@ func (d *Database) computeFrequentLocationClusters(ctx context.Context, userID s
return clusters, rows.Err()
}

func (d *Database) getAssetPageInfo(ctx context.Context, userID, assetID string, albumID string, pageSize int) (*AssetPageInfo, error) {
func (d *Database) getAssetPageInfo(ctx context.Context, userID, assetID string, albumID, tagID string, pageSize int) (*AssetPageInfo, error) {
var fileCreatedAt string
err := d.db.QueryRowContext(ctx, "SELECT fileCreatedAt FROM assets WHERE immichID = ? AND userID = ?"+hiddenLibraryFilter, assetID, userID).Scan(&fileCreatedAt)
if err != nil {
Expand All @@ -555,17 +595,45 @@ func (d *Database) getAssetPageInfo(ctx context.Context, userID, assetID string,
}
}

if tagID != "" {
var inTag bool
err := d.db.QueryRowContext(ctx,
`SELECT EXISTS(SELECT 1 FROM assetTags WHERE userID = ? AND assetID = ? AND tagID = ?)`,
userID, assetID, tagID,
).Scan(&inTag)
if err != nil {
return nil, fmt.Errorf("failed to check tag membership: %w", err)
}
if !inTag {
tagID = ""
}
}

var position int
if albumID != "" {
err = d.db.QueryRowContext(ctx,
`SELECT COUNT(*) FROM assets a
JOIN albumAssets aa ON aa.userID = a.userID AND aa.assetID = a.immichID
WHERE a.userID = ? AND aa.albumID = ?
AND a.latitude IS NOT NULL AND a.longitude IS NOT NULL
if albumID != "" || tagID != "" {
query := `SELECT COUNT(*) FROM assets a`
var args []interface{}
if albumID != "" {
query += ` JOIN albumAssets aa ON aa.userID = a.userID AND aa.assetID = a.immichID`
}
if tagID != "" {
query += ` JOIN assetTags at ON at.userID = a.userID AND at.assetID = a.immichID`
}
query += ` WHERE a.userID = ?`
args = append(args, userID)
if albumID != "" {
query += ` AND aa.albumID = ?`
args = append(args, albumID)
}
if tagID != "" {
query += ` AND at.tagID = ?`
args = append(args, tagID)
}
query += ` AND a.latitude IS NOT NULL AND a.longitude IS NOT NULL
AND a.stackPrimaryAssetID IS NULL
AND (a.fileCreatedAt > ? OR (a.fileCreatedAt = ? AND a.immichID > ?))`+hiddenLibraryFilterAliased,
userID, albumID, fileCreatedAt, fileCreatedAt, assetID,
).Scan(&position)
AND (a.fileCreatedAt > ? OR (a.fileCreatedAt = ? AND a.immichID > ?))` + hiddenLibraryFilterAliased
args = append(args, fileCreatedAt, fileCreatedAt, assetID)
err = d.db.QueryRowContext(ctx, query, args...).Scan(&position)
} else {
err = d.db.QueryRowContext(ctx,
`SELECT COUNT(*) FROM assets
Expand Down Expand Up @@ -829,6 +897,8 @@ func (d *Database) deleteUserSyncData(ctx context.Context, userID string) error
"dawarichTracks",
"albumAssets",
"albums",
"assetTags",
"tags",
"frequentLocations",
"favoritePlaces",
"assets",
Expand Down
8 changes: 8 additions & 0 deletions backend/databaseAlbums.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ func (d *Database) upsertAlbum(ctx context.Context, userID, albumID, albumName s
return err
}

func (d *Database) setAlbumSynced(ctx context.Context, userID, albumID, updatedAt string) error {
_, err := d.db.ExecContext(ctx,
`UPDATE albums SET updatedAt = ? WHERE userID = ? AND immichID = ?`,
updatedAt, userID, albumID,
)
return err
}

func (d *Database) replaceAlbumAssets(ctx context.Context, userID, albumID string, assetIDs []string) error {
tx, err := d.db.BeginTx(ctx, nil)
if err != nil {
Expand Down
8 changes: 4 additions & 4 deletions backend/databaseLibraries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ func TestHiddenLibraryAssetsExcludedFromMapMarkers(t *testing.T) {
db.upsertLibrary(ctx, "lib1", "External", 1)
db.updateLibraryVisibility(ctx, "lib1", true)

markers, err := db.getMapMarkers(ctx, testUserID, "", nil, maxMapMarkers)
markers, err := db.getMapMarkers(ctx, testUserID, "", "", "", "", nil, maxMapMarkers)
if err != nil {
t.Fatalf("getMapMarkers: %v", err)
}
Expand All @@ -305,15 +305,15 @@ func TestHiddenLibraryAssetsExcludedFromFilteredAssets(t *testing.T) {
db.upsertLibrary(ctx, "lib1", "External", 1)
db.updateLibraryVisibility(ctx, "lib1", true)

assets, err := db.getFilteredAssets(ctx, testUserID, "", true, "all", "", "", 1, 10)
assets, err := db.getFilteredAssets(ctx, testUserID, "", "", true, "all", "", "", 1, 10)
if err != nil {
t.Fatalf("getFilteredAssets: %v", err)
}
if len(assets) != 1 {
t.Errorf("expected 1 filtered asset, got %d", len(assets))
}

count, err := db.countFilteredAssets(ctx, testUserID, "", true, "all", "", "")
count, err := db.countFilteredAssets(ctx, testUserID, "", "", true, "all", "", "")
if err != nil {
t.Fatalf("countFilteredAssets: %v", err)
}
Expand Down Expand Up @@ -355,7 +355,7 @@ func TestUnknownLibraryAssetsVisible(t *testing.T) {
t.Errorf("expected 3 assets, got %d", total)
}

markers, _ := db.getMapMarkers(ctx, testUserID, "", nil, maxMapMarkers)
markers, _ := db.getMapMarkers(ctx, testUserID, "", "", "", "", nil, maxMapMarkers)
if len(markers) != 3 {
t.Errorf("expected 3 markers, got %d", len(markers))
}
Expand Down
128 changes: 128 additions & 0 deletions backend/databaseTags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package main

import (
"context"
"fmt"
)

func (d *Database) getTagUpdatedAtMap(ctx context.Context, userID string) (map[string]string, error) {
result := make(map[string]string)
rows, err := d.db.QueryContext(ctx, "SELECT immichID, updatedAt FROM tags WHERE userID = ?", userID)
if err != nil {
return nil, fmt.Errorf("failed to query tag updatedAt: %w", err)
}
defer rows.Close()
for rows.Next() {
var id, updatedAt string
if err := rows.Scan(&id, &updatedAt); err != nil {
return nil, fmt.Errorf("failed to scan tag updatedAt: %w", err)
}
result[id] = updatedAt
}
return result, rows.Err()
}

func (d *Database) upsertTag(ctx context.Context, userID, tagID, name, value string, parentID, color *string) error {
_, err := d.db.ExecContext(ctx,
`INSERT INTO tags (userID, immichID, name, value, parentID, color, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, '')
ON CONFLICT(userID, immichID) DO UPDATE SET
name = excluded.name,
value = excluded.value,
parentID = excluded.parentID,
color = excluded.color`,
userID, tagID, name, value, parentID, color,
)
return err
}

func (d *Database) setTagSynced(ctx context.Context, userID, tagID, updatedAt string) error {
_, err := d.db.ExecContext(ctx,
`UPDATE tags SET updatedAt = ? WHERE userID = ? AND immichID = ?`,
updatedAt, userID, tagID,
)
return err
}

func (d *Database) replaceTagAssets(ctx context.Context, userID, tagID string, assetIDs []string) error {
tx, err := d.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()

if err := bulkInsertTemp(ctx, tx, "tmpDesiredTagAssets", assetIDs); err != nil {
return fmt.Errorf("populate temp table: %w", err)
}

if _, err := tx.ExecContext(ctx,
`DELETE FROM assetTags WHERE userID = ? AND tagID = ? AND assetID NOT IN (SELECT val FROM tmpDesiredTagAssets)`,
userID, tagID,
); err != nil {
return fmt.Errorf("delete stale tag assets: %w", err)
}

if _, err := tx.ExecContext(ctx,
`INSERT INTO assetTags (userID, tagID, assetID)
SELECT ?, ?, val FROM tmpDesiredTagAssets
WHERE val NOT IN (SELECT assetID FROM assetTags WHERE userID = ? AND tagID = ?)`,
userID, tagID, userID, tagID,
); err != nil {
return fmt.Errorf("insert new tag assets: %w", err)
}

tx.ExecContext(ctx, "DROP TABLE IF EXISTS tmpDesiredTagAssets")
return tx.Commit()
}

func (d *Database) deleteTagsNotIn(ctx context.Context, userID string, tagIDs []string) error {
if len(tagIDs) == 0 {
_, err := d.db.ExecContext(ctx, "DELETE FROM tags WHERE userID = ?", userID)
return err
}

tx, err := d.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()

if err := bulkInsertTemp(ctx, tx, "tmpKeepTags", tagIDs); err != nil {
return fmt.Errorf("populate temp table: %w", err)
}

if _, err := tx.ExecContext(ctx, "DELETE FROM tags WHERE userID = ? AND immichID NOT IN (SELECT val FROM tmpKeepTags)", userID); err != nil {
return fmt.Errorf("delete stale tags: %w", err)
}

tx.ExecContext(ctx, "DROP TABLE IF EXISTS tmpKeepTags")
return tx.Commit()
}

func (d *Database) getTags(ctx context.Context, userID string) ([]TagRow, error) {
rows, err := d.db.QueryContext(ctx,
`SELECT t.immichID, t.name, t.value, t.parentID, t.color, COUNT(a.immichID) AS assetCount
FROM tags t
LEFT JOIN assetTags at ON at.userID = t.userID AND at.tagID = t.immichID
LEFT JOIN assets a ON a.userID = at.userID AND a.immichID = at.assetID
AND a.stackPrimaryAssetID IS NULL`+hiddenLibraryFilterAliased+`
WHERE t.userID = ?
GROUP BY t.immichID
ORDER BY t.value ASC`,
userID,
)
if err != nil {
return nil, fmt.Errorf("failed to query tags: %w", err)
}
defer rows.Close()

var tags []TagRow
for rows.Next() {
var t TagRow
if err := rows.Scan(&t.ImmichID, &t.Name, &t.Value, &t.ParentID, &t.Color, &t.AssetCount); err != nil {
return nil, fmt.Errorf("failed to scan tag: %w", err)
}
tags = append(tags, t)
}
return tags, rows.Err()
}
Loading
Loading