From 1ee96a502ece3e0427fb9c7ae575d7ce28a5c046 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 22 Mar 2026 20:35:06 -0400 Subject: [PATCH] gen: regenerate database code with new search queries Run sqlc generate to create Go code for new search queries: Added methods to Querier interface: - SearchMediaItemsUnified - Main unified search with fuzzy/exact matching - SearchAuthorValues - Author field autocomplete - SearchGenreValues - Genre field autocomplete - SearchSeriesValues - Series field autocomplete - SearchLanguageValues - Language field autocomplete Generated parameter structs and row types for all new queries. All queries include proper library visibility checks. --- internal/database/querier.go | 5 + internal/database/queries.sql.go | 448 +++++++++++++++++++++++++++++++ 2 files changed, 453 insertions(+) diff --git a/internal/database/querier.go b/internal/database/querier.go index a3e94db..1eda8b7 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -284,9 +284,14 @@ type Querier interface { // Revoke OPDS token RevokeOpdsToken(ctx context.Context, token string) error RevokeRefreshToken(ctx context.Context, token pgtype.UUID) error + SearchAuthorValues(ctx context.Context, arg SearchAuthorValuesParams) ([]SearchAuthorValuesRow, error) + SearchGenreValues(ctx context.Context, arg SearchGenreValuesParams) ([]SearchGenreValuesRow, error) + SearchLanguageValues(ctx context.Context, arg SearchLanguageValuesParams) ([]SearchLanguageValuesRow, error) // Search Media Items queries SearchMediaItems(ctx context.Context, arg SearchMediaItemsParams) ([]SearchMediaItemsRow, error) SearchMediaItemsFuzzy(ctx context.Context, arg SearchMediaItemsFuzzyParams) ([]SearchMediaItemsFuzzyRow, error) + SearchMediaItemsUnified(ctx context.Context, arg SearchMediaItemsUnifiedParams) ([]SearchMediaItemsUnifiedRow, error) + SearchSeriesValues(ctx context.Context, arg SearchSeriesValuesParams) ([]SearchSeriesValuesRow, error) // Library Visibility queries SetLibraryVisibility(ctx context.Context, arg SetLibraryVisibilityParams) (LibraryVisibility, error) // Set system config diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index 55534e1..4de8d0b 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -7047,6 +7047,180 @@ func (q *Queries) RevokeRefreshToken(ctx context.Context, token pgtype.UUID) err return err } +const SearchAuthorValues = `-- name: SearchAuthorValues :many +SELECT + mi.author as value, + COUNT(*) as count, + word_similarity($1, COALESCE(mi.author, ''))::float8 as score +FROM media_items mi +JOIN libraries l ON mi.library_id = l.id +LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2 +WHERE COALESCE(lv.is_visible, true) = true + AND mi.library_id = $3 + AND word_similarity($1, COALESCE(mi.author, '')) > 0.3 + AND mi.author IS NOT NULL + AND mi.author != '' +GROUP BY mi.author, word_similarity($1, COALESCE(mi.author, ''))::float8 +ORDER BY score DESC, count DESC +LIMIT $5 OFFSET $4 +` + +type SearchAuthorValuesParams struct { + SearchQuery interface{} `db:"search_query" json:"search_query"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + Offset pgtype.Int4 `db:"offset" json:"offset"` + Limit pgtype.Int4 `db:"limit" json:"limit"` +} + +type SearchAuthorValuesRow struct { + Value pgtype.Text `db:"value" json:"value"` + Count int64 `db:"count" json:"count"` + Score float64 `db:"score" json:"score"` +} + +func (q *Queries) SearchAuthorValues(ctx context.Context, arg SearchAuthorValuesParams) ([]SearchAuthorValuesRow, error) { + rows, err := q.db.Query(ctx, SearchAuthorValues, + arg.SearchQuery, + arg.UserID, + arg.LibraryID, + arg.Offset, + arg.Limit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []SearchAuthorValuesRow{} + for rows.Next() { + var i SearchAuthorValuesRow + if err := rows.Scan(&i.Value, &i.Count, &i.Score); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const SearchGenreValues = `-- name: SearchGenreValues :many +SELECT + mi.genre as value, + COUNT(*) as count, + word_similarity($1, COALESCE(mi.genre, ''))::float8 as score +FROM media_items mi +JOIN libraries l ON mi.library_id = l.id +LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2 +WHERE COALESCE(lv.is_visible, true) = true + AND mi.library_id = $3 + AND word_similarity($1, COALESCE(mi.genre, '')) > 0.3 + AND mi.genre IS NOT NULL + AND mi.genre != '' +GROUP BY mi.genre, word_similarity($1, COALESCE(mi.genre, ''))::float8 +ORDER BY score DESC, count DESC +LIMIT $5 OFFSET $4 +` + +type SearchGenreValuesParams struct { + SearchQuery interface{} `db:"search_query" json:"search_query"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + Offset pgtype.Int4 `db:"offset" json:"offset"` + Limit pgtype.Int4 `db:"limit" json:"limit"` +} + +type SearchGenreValuesRow struct { + Value pgtype.Text `db:"value" json:"value"` + Count int64 `db:"count" json:"count"` + Score float64 `db:"score" json:"score"` +} + +func (q *Queries) SearchGenreValues(ctx context.Context, arg SearchGenreValuesParams) ([]SearchGenreValuesRow, error) { + rows, err := q.db.Query(ctx, SearchGenreValues, + arg.SearchQuery, + arg.UserID, + arg.LibraryID, + arg.Offset, + arg.Limit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []SearchGenreValuesRow{} + for rows.Next() { + var i SearchGenreValuesRow + if err := rows.Scan(&i.Value, &i.Count, &i.Score); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const SearchLanguageValues = `-- name: SearchLanguageValues :many +SELECT + mi.language as value, + COUNT(*) as count, + word_similarity($1, COALESCE(mi.language, ''))::float8 as score +FROM media_items mi +JOIN libraries l ON mi.library_id = l.id +LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2 +WHERE COALESCE(lv.is_visible, true) = true + AND mi.library_id = $3 + AND word_similarity($1, COALESCE(mi.language, '')) > 0.3 + AND mi.language IS NOT NULL + AND mi.language != '' +GROUP BY mi.language, word_similarity($1, COALESCE(mi.language, ''))::float8 +ORDER BY score DESC, count DESC +LIMIT $5 OFFSET $4 +` + +type SearchLanguageValuesParams struct { + SearchQuery interface{} `db:"search_query" json:"search_query"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + Offset pgtype.Int4 `db:"offset" json:"offset"` + Limit pgtype.Int4 `db:"limit" json:"limit"` +} + +type SearchLanguageValuesRow struct { + Value pgtype.Text `db:"value" json:"value"` + Count int64 `db:"count" json:"count"` + Score float64 `db:"score" json:"score"` +} + +func (q *Queries) SearchLanguageValues(ctx context.Context, arg SearchLanguageValuesParams) ([]SearchLanguageValuesRow, error) { + rows, err := q.db.Query(ctx, SearchLanguageValues, + arg.SearchQuery, + arg.UserID, + arg.LibraryID, + arg.Offset, + arg.Limit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []SearchLanguageValuesRow{} + for rows.Next() { + var i SearchLanguageValuesRow + if err := rows.Scan(&i.Value, &i.Count, &i.Score); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const SearchMediaItems = `-- name: SearchMediaItems :many SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name FROM media_items mi @@ -7378,6 +7552,280 @@ func (q *Queries) SearchMediaItemsFuzzy(ctx context.Context, arg SearchMediaItem return items, nil } +const SearchMediaItemsUnified = `-- name: SearchMediaItemsUnified :many +SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name +FROM media_items mi +JOIN libraries l ON mi.library_id = l.id +JOIN library_types lt ON l.library_type_id = lt.id +LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1 +WHERE COALESCE(lv.is_visible, true) = true + AND mi.library_id = $2 + -- Fuzzy author filter + AND ($3 = '' OR word_similarity($3, COALESCE(mi.author, '')) > 0.3) + -- Fuzzy series filter + AND ($4 = '' OR word_similarity($4, COALESCE(mi.series, '')) > 0.3) + -- Fuzzy genre filter + AND ($5 = '' OR word_similarity($5, COALESCE(mi.genre, '')) > 0.3) + -- Fuzzy language filter + AND ($6 = '' OR word_similarity($6, COALESCE(mi.language, '')) > 0.3) + -- Year range (exact) + AND ($7 = 0 OR mi.copyright_year >= $7) + AND ($8 = 0 OR mi.copyright_year <= $8) + -- Boolean (exact) + AND ($9 = false OR mi.cover_image_path IS NOT NULL) + -- Search query (fuzzy or exact based on quotes) + AND ( + $10 = '' OR + -- Fuzzy search (default) + $11 = false AND ( + word_similarity($10, mi.title) > 0.3 OR + word_similarity($10, COALESCE(mi.author, '')) > 0.3 OR + word_similarity($10, COALESCE(mi.series, '')) > 0.3 OR + EXISTS ( + SELECT 1 FROM unnest(mi.tags_search) AS tag + WHERE word_similarity($10, tag) > 0.3 + LIMIT 1 + ) OR + EXISTS ( + SELECT 1 FROM unnest(mi.contributors_search) AS contributor + WHERE word_similarity($10, contributor) > 0.3 + LIMIT 1 + ) + ) OR + -- Exact search (with quotes) + $11 = true AND ( + mi.title ILIKE $12 OR + mi.author ILIKE $12 OR + mi.series ILIKE $12 OR + $12 = ANY(mi.tags_search) OR + $12 = ANY(mi.contributors_search) + ) + ) +ORDER BY + CASE + WHEN $10 != '' THEN + GREATEST( + CASE WHEN $11 = false THEN + word_similarity($10, mi.title) + ELSE 0 END, + CASE WHEN $11 = false THEN + word_similarity($10, COALESCE(mi.author, '')) + ELSE 0 END, + word_similarity($3, COALESCE(mi.author, '')), + word_similarity($5, COALESCE(mi.genre, '')) + ) + ELSE 0 + END DESC, + mi.title ASC +LIMIT $14 OFFSET $13 +` + +type SearchMediaItemsUnifiedParams struct { + UserID pgtype.UUID `db:"user_id" json:"user_id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + AuthorFilter interface{} `db:"author_filter" json:"author_filter"` + SeriesFilter interface{} `db:"series_filter" json:"series_filter"` + GenreFilter interface{} `db:"genre_filter" json:"genre_filter"` + LanguageFilter interface{} `db:"language_filter" json:"language_filter"` + YearMin interface{} `db:"year_min" json:"year_min"` + YearMax interface{} `db:"year_max" json:"year_max"` + HasCover interface{} `db:"has_cover" json:"has_cover"` + SearchQuery interface{} `db:"search_query" json:"search_query"` + IsExactSearch interface{} `db:"is_exact_search" json:"is_exact_search"` + SearchPattern pgtype.Text `db:"search_pattern" json:"search_pattern"` + Offset pgtype.Int4 `db:"offset" json:"offset"` + Limit pgtype.Int4 `db:"limit" json:"limit"` +} + +type SearchMediaItemsUnifiedRow struct { + ID pgtype.UUID `db:"id" json:"id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + Title string `db:"title" json:"title"` + Author pgtype.Text `db:"author" json:"author"` + Isbn pgtype.Text `db:"isbn" json:"isbn"` + Description pgtype.Text `db:"description" json:"description"` + FilePath string `db:"file_path" json:"file_path"` + FileSize pgtype.Int8 `db:"file_size" json:"file_size"` + MimeType pgtype.Text `db:"mime_type" json:"mime_type"` + CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` + Series pgtype.Text `db:"series" json:"series"` + SeriesNumber pgtype.Int4 `db:"series_number" json:"series_number"` + Tags []string `db:"tags" json:"tags"` + Asin pgtype.Text `db:"asin" json:"asin"` + DatePublished pgtype.Date `db:"date_published" json:"date_published"` + Publisher pgtype.Text `db:"publisher" json:"publisher"` + Contributors []string `db:"contributors" json:"contributors"` + Language pgtype.Text `db:"language" json:"language"` + Edition pgtype.Text `db:"edition" json:"edition"` + PageCount pgtype.Int4 `db:"page_count" json:"page_count"` + Genre pgtype.Text `db:"genre" json:"genre"` + CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"` + GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"` + OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"` + GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"` + AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + FormatGroup string `db:"format_group" json:"format_group"` + FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"` + IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"` + HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"` + TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"` + ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` + EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"` + RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"` + KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"` + KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"` + TagsSearch []string `db:"tags_search" json:"tags_search"` + ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` + FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` + OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` + OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` + HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` + LibraryName string `db:"library_name" json:"library_name"` + LibraryTypeName string `db:"library_type_name" json:"library_type_name"` +} + +func (q *Queries) SearchMediaItemsUnified(ctx context.Context, arg SearchMediaItemsUnifiedParams) ([]SearchMediaItemsUnifiedRow, error) { + rows, err := q.db.Query(ctx, SearchMediaItemsUnified, + arg.UserID, + arg.LibraryID, + arg.AuthorFilter, + arg.SeriesFilter, + arg.GenreFilter, + arg.LanguageFilter, + arg.YearMin, + arg.YearMax, + arg.HasCover, + arg.SearchQuery, + arg.IsExactSearch, + arg.SearchPattern, + arg.Offset, + arg.Limit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []SearchMediaItemsUnifiedRow{} + for rows.Next() { + var i SearchMediaItemsUnifiedRow + if err := rows.Scan( + &i.ID, + &i.LibraryID, + &i.Title, + &i.Author, + &i.Isbn, + &i.Description, + &i.FilePath, + &i.FileSize, + &i.MimeType, + &i.CoverImagePath, + &i.Series, + &i.SeriesNumber, + &i.Tags, + &i.Asin, + &i.DatePublished, + &i.Publisher, + &i.Contributors, + &i.Language, + &i.Edition, + &i.PageCount, + &i.Genre, + &i.CopyrightYear, + &i.GoodreadsID, + &i.OpenlibraryID, + &i.GoogleBooksID, + &i.AddedByAdminID, + &i.CreatedAt, + &i.UpdatedAt, + &i.FormatGroup, + &i.FormatMimetype, + &i.IsReflowable, + &i.HasFixedLayout, + &i.TotalCharacters, + &i.ChapterCount, + &i.EntitlementID, + &i.RevisionNumber, + &i.KoboContentID, + &i.KoboMetadata, + &i.TagsSearch, + &i.ContributorsSearch, + &i.FileSha256, + &i.OpfIdentifier, + &i.OpfUuid, + &i.HashConfidence, + &i.LibraryName, + &i.LibraryTypeName, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const SearchSeriesValues = `-- name: SearchSeriesValues :many +SELECT + mi.series as value, + COUNT(*) as count, + word_similarity($1, COALESCE(mi.series, ''))::float8 as score +FROM media_items mi +JOIN libraries l ON mi.library_id = l.id +LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2 +WHERE COALESCE(lv.is_visible, true) = true + AND mi.library_id = $3 + AND word_similarity($1, COALESCE(mi.series, '')) > 0.3 + AND mi.series IS NOT NULL + AND mi.series != '' +GROUP BY mi.series, word_similarity($1, COALESCE(mi.series, ''))::float8 +ORDER BY score DESC, count DESC +LIMIT $5 OFFSET $4 +` + +type SearchSeriesValuesParams struct { + SearchQuery interface{} `db:"search_query" json:"search_query"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + Offset pgtype.Int4 `db:"offset" json:"offset"` + Limit pgtype.Int4 `db:"limit" json:"limit"` +} + +type SearchSeriesValuesRow struct { + Value pgtype.Text `db:"value" json:"value"` + Count int64 `db:"count" json:"count"` + Score float64 `db:"score" json:"score"` +} + +func (q *Queries) SearchSeriesValues(ctx context.Context, arg SearchSeriesValuesParams) ([]SearchSeriesValuesRow, error) { + rows, err := q.db.Query(ctx, SearchSeriesValues, + arg.SearchQuery, + arg.UserID, + arg.LibraryID, + arg.Offset, + arg.Limit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []SearchSeriesValuesRow{} + for rows.Next() { + var i SearchSeriesValuesRow + if err := rows.Scan(&i.Value, &i.Count, &i.Score); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const SetLibraryVisibility = `-- name: SetLibraryVisibility :one INSERT INTO library_visibility (user_id, library_id, is_visible) VALUES ($1, $2, $3)