From 32566fe7a2cc5d2469f9f47c1dbd55f35859d18f Mon Sep 17 00:00:00 2001 From: Iron_Felix Date: Sat, 22 Nov 2025 07:53:50 +0300 Subject: [PATCH] feat: query for usertitles written --- api/_build/openapi.yaml | 7 +- api/api.gen.go | 10 +- api/paths/users-id-titles.yaml | 7 +- modules/backend/handlers/common.go | 117 +++++++------- modules/backend/handlers/titles.go | 72 --------- modules/backend/handlers/users.go | 138 +++++++++++++--- modules/backend/queries.sql | 144 +++++++++++++---- sql/queries.sql.go | 252 +++++++++++++++++++++-------- 8 files changed, 497 insertions(+), 250 deletions(-) diff --git a/api/_build/openapi.yaml b/api/_build/openapi.yaml index c07dbbc..722b7af 100644 --- a/api/_build/openapi.yaml +++ b/api/_build/openapi.yaml @@ -158,7 +158,12 @@ paths: - in: query name: status schema: - $ref: '#/components/schemas/TitleStatus' + type: array + items: + $ref: '#/components/schemas/TitleStatus' + description: List of title statuses to filter + style: form + explode: false - in: query name: watch_status schema: diff --git a/api/api.gen.go b/api/api.gen.go index 320e7a2..54c8fc1 100644 --- a/api/api.gen.go +++ b/api/api.gen.go @@ -183,9 +183,11 @@ type GetUsersUserIdParams struct { // GetUsersUserIdTitlesParams defines parameters for GetUsersUserIdTitles. type GetUsersUserIdTitlesParams struct { - Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` - Word *string `form:"word,omitempty" json:"word,omitempty"` - Status *TitleStatus `form:"status,omitempty" json:"status,omitempty"` + Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` + Word *string `form:"word,omitempty" json:"word,omitempty"` + + // Status List of title statuses to filter + Status *[]TitleStatus `form:"status,omitempty" json:"status,omitempty"` WatchStatus *UserTitleStatus `form:"watch_status,omitempty" json:"watch_status,omitempty"` Rating *float64 `form:"rating,omitempty" json:"rating,omitempty"` ReleaseYear *int32 `form:"release_year,omitempty" json:"release_year,omitempty"` @@ -811,7 +813,7 @@ func (siw *ServerInterfaceWrapper) GetUsersUserIdTitles(c *gin.Context) { // ------------- Optional query parameter "status" ------------- - err = runtime.BindQueryParameter("form", true, false, "status", c.Request.URL.Query(), ¶ms.Status) + err = runtime.BindQueryParameter("form", false, false, "status", c.Request.URL.Query(), ¶ms.Status) if err != nil { siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter status: %w", err), http.StatusBadRequest) return diff --git a/api/paths/users-id-titles.yaml b/api/paths/users-id-titles.yaml index 0cde5af..0788319 100644 --- a/api/paths/users-id-titles.yaml +++ b/api/paths/users-id-titles.yaml @@ -14,7 +14,12 @@ get: - in: query name: status schema: - $ref: '../schemas/enums/TitleStatus.yaml' + type: array + items: + $ref: '../schemas/enums/TitleStatus.yaml' + description: List of title statuses to filter + style: form + explode: false - in: query name: watch_status schema: diff --git a/modules/backend/handlers/common.go b/modules/backend/handlers/common.go index 3d61b91..6618d49 100644 --- a/modules/backend/handlers/common.go +++ b/modules/backend/handlers/common.go @@ -1,6 +1,10 @@ package handlers import ( + "context" + "encoding/json" + "fmt" + oapi "nyanimedb/api" sqlc "nyanimedb/sql" "strconv" ) @@ -13,70 +17,75 @@ func NewServer(db *sqlc.Queries) Server { return Server{db: db} } -// type Cursor interface { -// ParseCursor(sortBy oapi.TitleSort, data oapi.Cursor) (Cursor, error) +func (s Server) mapTitle(ctx context.Context, title sqlc.SearchTitlesRow) (oapi.Title, error) { -// Values() map[string]interface{} -// // for logs only -// Type() string -// } + title_names := make(map[string][]string, 0) + err := json.Unmarshal(title.TitleNames, &title_names) + if err != nil { + return oapi.Title{}, fmt.Errorf("unmarshal TitleNames: %v", err) + } -// type CursorByID struct { -// ID int64 -// } + episodes_lens := make(map[string]float64, 0) + err = json.Unmarshal(title.EpisodesLen, &episodes_lens) + if err != nil { + return oapi.Title{}, fmt.Errorf("unmarshal EpisodesLen: %v", err) + } -// func (c CursorByID) ParseCursor(sortBy oapi.TitleSort, data oapi.Cursor) (Cursor, error) { -// var cur CursorByID -// if err := json.Unmarshal(data, &cur); err != nil { -// return nil, fmt.Errorf("invalid cursor (id): %w", err) -// } -// if cur.ID == 0 { -// return nil, errors.New("cursor id must be non-zero") -// } -// return cur, nil -// } + oapi_tag_names := make(oapi.Tags, 0) + err = json.Unmarshal(title.TagNames, &oapi_tag_names) + if err != nil { + return oapi.Title{}, fmt.Errorf("unmarshalling title_tag: %v", err) + } -// func (c CursorByID) Values() map[string]interface{} { -// return map[string]interface{}{ -// "cursor_id": c.ID, -// "cursor_year": nil, -// "cursor_rating": nil, -// } -// } + var oapi_studio oapi.Studio -// func (c CursorByID) Type() string { return "id" } + oapi_studio.Id = title.StudioID + if title.StudioName != nil { + oapi_studio.Name = *title.StudioName + } + oapi_studio.Description = title.StudioDesc + if title.StudioIllustID != nil { + oapi_studio.Poster.Id = title.StudioIllustID + oapi_studio.Poster.ImagePath = title.StudioImagePath + oapi_studio.Poster.StorageType = &title.StudioStorageType + } -// func NewCursor(sortBy string) (Cursor, error) { -// switch Type(sortBy) { -// case TypeID: -// return CursorByID{}, nil -// case TypeYear: -// return CursorByYear{}, nil -// case TypeRating: -// return CursorByRating{}, nil -// default: -// return nil, fmt.Errorf("unsupported sort_by: %q", sortBy) -// } -// } + var oapi_image oapi.Image -// decodes a base64-encoded JSON string into a CursorObj -// Returns the parsed CursorObj and an error -// func parseCursor(encoded oapi.Cursor) (*oapi.CursorObj, error) { + if title.PosterID != nil { + oapi_image.Id = title.PosterID + oapi_image.ImagePath = title.TitleImagePath + oapi_image.StorageType = &title.TitleStorageType + } -// // Decode base64 -// decoded, err := base64.StdEncoding.DecodeString(encoded) -// if err != nil { -// return nil, fmt.Errorf("parseCursor: %v", err) -// } + var release_season oapi.ReleaseSeason + if title.ReleaseSeason != nil { + release_season = oapi.ReleaseSeason(*title.ReleaseSeason) + } -// // Parse JSON -// var cursor oapi.CursorObj -// if err := json.Unmarshal(decoded, &cursor); err != nil { -// return nil, fmt.Errorf("parseCursor: %v", err) -// } + oapi_status, err := TitleStatus2oapi(&title.TitleStatus) + if err != nil { + return oapi.Title{}, fmt.Errorf("TitleStatus2oapi: %v", err) + } + oapi_title := oapi.Title{ + EpisodesAired: title.EpisodesAired, + EpisodesAll: title.EpisodesAired, + EpisodesLen: &episodes_lens, + Id: title.ID, + Poster: &oapi_image, + Rating: title.Rating, + RatingCount: title.RatingCount, + ReleaseSeason: &release_season, + ReleaseYear: title.ReleaseYear, + Studio: &oapi_studio, + Tags: oapi_tag_names, + TitleNames: title_names, + TitleStatus: oapi_status, + // AdditionalProperties: + } -// return &cursor, nil -// } + return oapi_title, nil +} func parseInt64(s string) (int32, error) { i, err := strconv.ParseInt(s, 10, 64) diff --git a/modules/backend/handlers/titles.go b/modules/backend/handlers/titles.go index 84fc87e..332a4b6 100644 --- a/modules/backend/handlers/titles.go +++ b/modules/backend/handlers/titles.go @@ -158,78 +158,6 @@ func (s Server) GetStudio(ctx context.Context, id int64) (*oapi.Studio, error) { return &oapi_studio, nil } -func (s Server) mapTitle(ctx context.Context, title sqlc.SearchTitlesRow) (oapi.Title, error) { - - // var oapi_title oapi.Title - - title_names := make(map[string][]string, 0) - err := json.Unmarshal(title.TitleNames, &title_names) - if err != nil { - return oapi.Title{}, fmt.Errorf("unmarshal TitleNames: %v", err) - } - - episodes_lens := make(map[string]float64, 0) - err = json.Unmarshal(title.EpisodesLen, &episodes_lens) - if err != nil { - return oapi.Title{}, fmt.Errorf("unmarshal EpisodesLen: %v", err) - } - - oapi_tag_names := make(oapi.Tags, 0) - err = json.Unmarshal(title.TagNames, &oapi_tag_names) - if err != nil { - return oapi.Title{}, fmt.Errorf("unmarshalling title_tag: %v", err) - } - - var oapi_studio oapi.Studio - - oapi_studio.Id = title.StudioID - if title.StudioName != nil { - oapi_studio.Name = *title.StudioName - } - oapi_studio.Description = title.StudioDesc - if title.StudioIllustID != nil { - oapi_studio.Poster.Id = title.StudioIllustID - oapi_studio.Poster.ImagePath = title.StudioImagePath - oapi_studio.Poster.StorageType = &title.StudioStorageType - } - - var oapi_image oapi.Image - - if title.PosterID != nil { - oapi_image.Id = title.PosterID - oapi_image.ImagePath = title.TitleImagePath - oapi_image.StorageType = &title.TitleStorageType - } - - var release_season oapi.ReleaseSeason - if title.ReleaseSeason != nil { - release_season = oapi.ReleaseSeason(*title.ReleaseSeason) - } - - oapi_status, err := TitleStatus2oapi(&title.TitleStatus) - if err != nil { - return oapi.Title{}, fmt.Errorf("TitleStatus2oapi: %v", err) - } - oapi_title := oapi.Title{ - EpisodesAired: title.EpisodesAired, - EpisodesAll: title.EpisodesAired, - EpisodesLen: &episodes_lens, - Id: title.ID, - Poster: &oapi_image, - Rating: title.Rating, - RatingCount: title.RatingCount, - ReleaseSeason: &release_season, - ReleaseYear: title.ReleaseYear, - Studio: &oapi_studio, - Tags: oapi_tag_names, - TitleNames: title_names, - TitleStatus: oapi_status, - // AdditionalProperties: - } - - return oapi_title, nil -} - func (s Server) GetTitlesTitleId(ctx context.Context, request oapi.GetTitlesTitleIdRequestObject) (oapi.GetTitlesTitleIdResponseObject, error) { var oapi_title oapi.Title diff --git a/modules/backend/handlers/users.go b/modules/backend/handlers/users.go index 0fa903f..0420b91 100644 --- a/modules/backend/handlers/users.go +++ b/modules/backend/handlers/users.go @@ -2,12 +2,15 @@ package handlers import ( "context" + "fmt" oapi "nyanimedb/api" sqlc "nyanimedb/sql" "time" "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" "github.com/oapi-codegen/runtime/types" + log "github.com/sirupsen/logrus" ) // type Server struct { @@ -50,33 +53,120 @@ func (s Server) GetUsersUserId(ctx context.Context, req oapi.GetUsersUserIdReque return oapi.GetUsersUserId200JSONResponse(mapUser(user)), nil } +func sqlDate2oapi(p_date pgtype.Timestamptz) (time.Time, error) { + return time.Time{}, nil +} + +type SqlcUserStatus struct { + dropped string + finished string + planned string + in_progress string +} + +func UserTitleStatus2Sqlc(s *[]oapi.UserTitleStatus) (*SqlcUserStatus, error) { + var sqlc_status SqlcUserStatus + if s == nil { + return &sqlc_status, nil + } + for _, t := range *s { + switch t { + case oapi.UserTitleStatusFinished: + sqlc_status.finished = "finished" + case oapi.UserTitleStatusDropped: + sqlc_status.dropped = "dropped" + case oapi.UserTitleStatusPlanned: + sqlc_status.planned = "planned" + case oapi.UserTitleStatusInProgress: + sqlc_status.in_progress = "in-progress" + default: + return nil, fmt.Errorf("unexpected tittle status: %s", t) + } + } + return &sqlc_status, nil +} + func (s Server) GetUsersUserIdTitles(ctx context.Context, request oapi.GetUsersUserIdTitlesRequestObject) (oapi.GetUsersUserIdTitlesResponseObject, error) { - var rate int32 = 9 - var review_id int64 = 3 - time := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC) + oapi_usertitles := make([]oapi.UserTitle, 0) - var userTitles = []oapi.UserTitle{ - { - UserId: 101, - TitleId: 2001, - Status: oapi.UserTitleStatusFinished, - Rate: &rate, - Ctime: &time, - }, - { - UserId: 102, - TitleId: 2002, - Status: oapi.UserTitleStatusInProgress, - ReviewId: &review_id, - Ctime: &time, - }, - { - UserId: 103, - TitleId: 2003, - Status: oapi.UserTitleStatusDropped, - Ctime: &time, - }, + word := Word2Sqlc(request.Params.Word) + status, err := TitleStatus2Sqlc(request.Params.Status) + if err != nil { + log.Errorf("%v", err) + return oapi.GetUsersUserIdTitles400Response{}, err + } + + season, err := ReleaseSeason2sqlc(request.Params.ReleaseSeason) + if err != nil { + log.Errorf("%v", err) + return oapi.GetUsersUserIdTitles400Response{}, err + } + + // Forward bool `json:"forward"` + // SortBy string `json:"sort_by"` + // CursorYear *int32 `json:"cursor_year"` + // CursorID *int64 `json:"cursor_id"` + // CursorRating *float64 `json:"cursor_rating"` + // Word *string `json:"word"` + // Ongoing string `json:"ongoing"` + // Planned string `json:"planned"` + // Dropped string `json:"dropped"` + // InProgress string `json:"in-progress"` + // Finished string `json:"finished"` + // Rate *int32 `json:"rate"` + // Rating *float64 `json:"rating"` + // ReleaseYear *int32 `json:"release_year"` + // ReleaseSeason *ReleaseSeasonT `json:"release_season"` + // Limit *int32 `json:"limit"` + params := sqlc.SearchTitlesParams{ + Word: word, + Ongoing: status.ongoing, + Finished: status.finished, + Planned: status.planned, + Rating: request.Params.Rating, + ReleaseYear: request.Params.ReleaseYear, + ReleaseSeason: season, + Forward: true, + SortBy: "id", + Limit: request.Params.Limit, + } + + if request.Params.SortForward != nil { + params.Forward = *request.Params.SortForward + } + if request.Params.Sort != nil { + params.SortBy = string(*request.Params.Sort) + if request.Params.Cursor != nil { + err := ParseCursorInto(string(*request.Params.Sort), string(*request.Params.Cursor), ¶ms) + if err != nil { + log.Errorf("%v", err) + return oapi.GetTitles400Response{}, nil + } + } + } + + _sqlc_title := sqlc.SearchTitlesRow{ + ID: sqlc_title.ID, + StudioID: sqlc_title.StudioID, + PosterID: sqlc_title.PosterID, + TitleStatus: sqlc_title.TitleStatus, + Rating: sqlc_title.Rating, + RatingCount: sqlc_title.RatingCount, + ReleaseYear: sqlc_title.ReleaseYear, + ReleaseSeason: sqlc_title.ReleaseSeason, + Season: sqlc_title.Season, + EpisodesAired: sqlc_title.EpisodesAired, + EpisodesAll: sqlc_title.EpisodesAll, + EpisodesLen: sqlc_title.EpisodesLen, + TitleStorageType: sqlc_title.TitleStorageType, + TitleImagePath: sqlc_title.TitleImagePath, + TagNames: sqlc_title.TitleNames, + StudioName: sqlc_title.StudioName, + StudioIllustID: sqlc_title.StudioIllustID, + StudioDesc: sqlc_title.StudioDesc, + StudioStorageType: sqlc_title.StudioStorageType, + StudioImagePath: sqlc_title.StudioImagePath, } return oapi.GetUsersUserIdTitles200JSONResponse(userTitles), nil diff --git a/modules/backend/queries.sql b/modules/backend/queries.sql index 16f8120..b8cb8aa 100644 --- a/modules/backend/queries.sql +++ b/modules/backend/queries.sql @@ -86,7 +86,7 @@ SELECT si.image_path as studio_image_path FROM titles as t -LEFT JOIN images as i ON (t.image_id = i.id) +LEFT JOIN images as i ON (t.poster_id = i.id) LEFT JOIN title_tags as tt ON (t.id = tt.title_id) LEFT JOIN tags as g ON (tt.tag_id = g.id) LEFT JOIN studios as s ON (t.studio_id = s.id) @@ -109,7 +109,7 @@ SELECT si.image_path as studio_image_path FROM titles as t -LEFT JOIN images as i ON (t.image_id = i.id) +LEFT JOIN images as i ON (t.poster_id = i.id) LEFT JOIN title_tags as tt ON (t.id = tt.title_id) LEFT JOIN tags as g ON (tt.tag_id = g.id) LEFT JOIN studios as s ON (t.studio_id = s.id) @@ -210,37 +210,125 @@ ORDER BY LIMIT COALESCE(sqlc.narg('limit')::int, 100); -- 100 is default limit -- name: SearchUserTitles :many -SELECT - * -FROM usertitles as u -JOIN titles as t ON (u.title_id = t.id) -WHERE - CASE - WHEN sqlc.narg('word')::text IS NOT NULL THEN - ( - SELECT bool_and( - EXISTS ( - SELECT 1 - FROM jsonb_each_text(t.title_names) AS t(key, val) - WHERE val ILIKE pattern - ) - ) - FROM unnest( - ARRAY( - SELECT '%' || trim(w) || '%' - FROM unnest(string_to_array(sqlc.narg('word')::text, ' ')) AS w - WHERE trim(w) <> '' - ) - ) AS pattern - ) - ELSE true +SELECT + t.*, + u.user_id as user_id, + u.status as usertitle_status, + u.rate as user_rate, + u.review_id as review_id, + u.ctime as user_ctime, + i.storage_type::text as title_storage_type, + i.image_path as title_image_path, + jsonb_agg_strict(g.tag_name)'[]'::jsonb as tag_names, + s.studio_name as studio_name, + s.illust_id as studio_illust_id, + s.studio_desc as studio_desc, + si.storage_type::text as studio_storage_type, + si.image_path as studio_image_path + +FROM usertitles as u +LEFT JOIN titles as t ON (u.title_id = t.id) +LEFT JOIN images as i ON (t.poster_id = i.id) +LEFT JOIN title_tags as tt ON (t.id = tt.title_id) +LEFT JOIN tags as g ON (tt.tag_id = g.id) +LEFT JOIN studios as s ON (t.studio_id = s.id) +LEFT JOIN images as si ON (s.illust_id = si.id) + +WHERE + CASE + WHEN sqlc.arg('forward')::boolean THEN + -- forward: greater than cursor (next page) + CASE sqlc.arg('sort_by')::text + WHEN 'year' THEN + (sqlc.narg('cursor_year')::int IS NULL) OR + (t.release_year > sqlc.narg('cursor_year')::int) OR + (t.release_year = sqlc.narg('cursor_year')::int AND t.id > sqlc.narg('cursor_id')::bigint) + + WHEN 'rating' THEN + (sqlc.narg('cursor_rating')::float IS NULL) OR + (t.rating > sqlc.narg('cursor_rating')::float) OR + (t.rating = sqlc.narg('cursor_rating')::float AND t.id > sqlc.narg('cursor_id')::bigint) + + WHEN 'id' THEN + (sqlc.narg('cursor_id')::bigint IS NULL) OR + (t.id > sqlc.narg('cursor_id')::bigint) + + ELSE true -- fallback + END + + ELSE + -- backward: less than cursor (prev page) + CASE sqlc.arg('sort_by')::text + WHEN 'year' THEN + (sqlc.narg('cursor_year')::int IS NULL) OR + (t.release_year < sqlc.narg('cursor_year')::int) OR + (t.release_year = sqlc.narg('cursor_year')::int AND t.id < sqlc.narg('cursor_id')::bigint) + + WHEN 'rating' THEN + (sqlc.narg('cursor_rating')::float IS NULL) OR + (t.rating < sqlc.narg('cursor_rating')::float) OR + (t.rating = sqlc.narg('cursor_rating')::float AND t.id < sqlc.narg('cursor_id')::bigint) + + WHEN 'id' THEN + (sqlc.narg('cursor_id')::bigint IS NULL) OR + (t.id < sqlc.narg('cursor_id')::bigint) + + ELSE true + END END - AND (sqlc.narg('status')::title_status_t IS NULL OR t.title_status = sqlc.narg('status')::title_status_t) + AND ( + CASE + WHEN sqlc.narg('word')::text IS NOT NULL THEN + ( + SELECT bool_and( + EXISTS ( + SELECT 1 + FROM jsonb_each_text(t.title_names) AS t(key, val) + WHERE val ILIKE pattern + ) + ) + FROM unnest( + ARRAY( + SELECT '%' || trim(w) || '%' + FROM unnest(string_to_array(sqlc.narg('word')::text, ' ')) AS w + WHERE trim(w) <> '' + ) + ) AS pattern + ) + ELSE true + END + ) + + AND (u.status::text IN (sqlc.arg('ongoing')::text, sqlc.arg('planned')::text, sqlc.arg('dropped')::text, sqlc.arg('in-progress')::text)) + AND (t.title_status::text IN (sqlc.arg('ongoing')::text, sqlc.arg('finished')::text, sqlc.arg('planned')::text)) + AND (sqlc.narg('rate')::int IS NULL OR u.rate >= sqlc.narg('rate')::int) AND (sqlc.narg('rating')::float IS NULL OR t.rating >= sqlc.narg('rating')::float) AND (sqlc.narg('release_year')::int IS NULL OR t.release_year = sqlc.narg('release_year')::int) AND (sqlc.narg('release_season')::release_season_t IS NULL OR t.release_season = sqlc.narg('release_season')::release_season_t) - AND (sqlc.narg('usertitle_status')::usertitle_status_t IS NULL OR u.usertitle_status = sqlc.narg('usertitle_status')::usertitle_status_t) + +GROUP BY + t.id, i.id, s.id, si.id + +ORDER BY + CASE WHEN sqlc.arg('forward')::boolean THEN + CASE + WHEN sqlc.arg('sort_by')::text = 'id' THEN t.id + WHEN sqlc.arg('sort_by')::text = 'year' THEN t.release_year + WHEN sqlc.arg('sort_by')::text = 'rating' THEN t.rating + WHEN sqlc.arg('sort_by')::text = 'rate' THEN u.rate + END + END ASC, + CASE WHEN NOT sqlc.arg('forward')::boolean THEN + CASE + WHEN sqlc.arg('sort_by')::text = 'id' THEN t.id + WHEN sqlc.arg('sort_by')::text = 'year' THEN t.release_year + WHEN sqlc.arg('sort_by')::text = 'rating' THEN t.rating + WHEN sqlc.arg('sort_by')::text = 'rate' THEN u.rate + END + END DESC, + + CASE WHEN sqlc.arg('sort_by')::text <> 'id' THEN t.id END ASC LIMIT COALESCE(sqlc.narg('limit')::int, 100); -- 100 is default limit diff --git a/sql/queries.sql.go b/sql/queries.sql.go index 4342a12..2a90265 100644 --- a/sql/queries.sql.go +++ b/sql/queries.sql.go @@ -128,7 +128,7 @@ SELECT si.image_path as studio_image_path FROM titles as t -LEFT JOIN images as i ON (t.image_id = i.id) +LEFT JOIN images as i ON (t.poster_id = i.id) LEFT JOIN title_tags as tt ON (t.id = tt.title_id) LEFT JOIN tags as g ON (tt.tag_id = g.id) LEFT JOIN studios as s ON (t.studio_id = s.id) @@ -349,7 +349,7 @@ SELECT si.image_path as studio_image_path FROM titles as t -LEFT JOIN images as i ON (t.image_id = i.id) +LEFT JOIN images as i ON (t.poster_id = i.id) LEFT JOIN title_tags as tt ON (t.id = tt.title_id) LEFT JOIN tags as g ON (tt.tag_id = g.id) LEFT JOIN studios as s ON (t.studio_id = s.id) @@ -548,82 +548,195 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]S const searchUserTitles = `-- name: SearchUserTitles :many -SELECT - user_id, title_id, status, rate, review_id, ctime, id, title_names, studio_id, poster_id, title_status, rating, rating_count, release_year, release_season, season, episodes_aired, episodes_all, episodes_len -FROM usertitles as u -JOIN titles as t ON (u.title_id = t.id) -WHERE - CASE - WHEN $1::text IS NOT NULL THEN - ( - SELECT bool_and( - EXISTS ( - SELECT 1 - FROM jsonb_each_text(t.title_names) AS t(key, val) - WHERE val ILIKE pattern - ) - ) - FROM unnest( - ARRAY( - SELECT '%' || trim(w) || '%' - FROM unnest(string_to_array($1::text, ' ')) AS w - WHERE trim(w) <> '' - ) - ) AS pattern - ) - ELSE true +SELECT + t.id, t.title_names, t.studio_id, t.poster_id, t.title_status, t.rating, t.rating_count, t.release_year, t.release_season, t.season, t.episodes_aired, t.episodes_all, t.episodes_len, + u.user_id as user_id, + u.status as usertitle_status, + u.rate as user_rate, + u.review_id as review_id, + u.ctime as user_ctime, + i.storage_type::text as title_storage_type, + i.image_path as title_image_path, + jsonb_agg_strict(g.tag_name)'[]'::jsonb as tag_names, + s.studio_name as studio_name, + s.illust_id as studio_illust_id, + s.studio_desc as studio_desc, + si.storage_type::text as studio_storage_type, + si.image_path as studio_image_path + +FROM usertitles as u +LEFT JOIN titles as t ON (u.title_id = t.id) +LEFT JOIN images as i ON (t.poster_id = i.id) +LEFT JOIN title_tags as tt ON (t.id = tt.title_id) +LEFT JOIN tags as g ON (tt.tag_id = g.id) +LEFT JOIN studios as s ON (t.studio_id = s.id) +LEFT JOIN images as si ON (s.illust_id = si.id) + +WHERE + CASE + WHEN $1::boolean THEN + -- forward: greater than cursor (next page) + CASE $2::text + WHEN 'year' THEN + ($3::int IS NULL) OR + (t.release_year > $3::int) OR + (t.release_year = $3::int AND t.id > $4::bigint) + + WHEN 'rating' THEN + ($5::float IS NULL) OR + (t.rating > $5::float) OR + (t.rating = $5::float AND t.id > $4::bigint) + + WHEN 'id' THEN + ($4::bigint IS NULL) OR + (t.id > $4::bigint) + + ELSE true -- fallback + END + + ELSE + -- backward: less than cursor (prev page) + CASE $2::text + WHEN 'year' THEN + ($3::int IS NULL) OR + (t.release_year < $3::int) OR + (t.release_year = $3::int AND t.id < $4::bigint) + + WHEN 'rating' THEN + ($5::float IS NULL) OR + (t.rating < $5::float) OR + (t.rating = $5::float AND t.id < $4::bigint) + + WHEN 'id' THEN + ($4::bigint IS NULL) OR + (t.id < $4::bigint) + + ELSE true + END END - AND ($2::title_status_t IS NULL OR t.title_status = $2::title_status_t) - AND ($3::float IS NULL OR t.rating >= $3::float) - AND ($4::int IS NULL OR t.release_year = $4::int) - AND ($5::release_season_t IS NULL OR t.release_season = $5::release_season_t) - AND ($6::usertitle_status_t IS NULL OR u.usertitle_status = $6::usertitle_status_t) + AND ( + CASE + WHEN $6::text IS NOT NULL THEN + ( + SELECT bool_and( + EXISTS ( + SELECT 1 + FROM jsonb_each_text(t.title_names) AS t(key, val) + WHERE val ILIKE pattern + ) + ) + FROM unnest( + ARRAY( + SELECT '%' || trim(w) || '%' + FROM unnest(string_to_array($6::text, ' ')) AS w + WHERE trim(w) <> '' + ) + ) AS pattern + ) + ELSE true + END + ) -LIMIT COALESCE($7::int, 100) + AND (u.status::text IN ($7::text, $8::text, $9::text, $10::text)) + AND (t.title_status::text IN ($7::text, $11::text, $8::text)) + AND ($12::int IS NULL OR u.rate >= $12::int) + AND ($13::float IS NULL OR t.rating >= $13::float) + AND ($14::int IS NULL OR t.release_year = $14::int) + AND ($15::release_season_t IS NULL OR t.release_season = $15::release_season_t) + +GROUP BY + t.id, i.id, s.id, si.id + +ORDER BY + CASE WHEN $1::boolean THEN + CASE + WHEN $2::text = 'id' THEN t.id + WHEN $2::text = 'year' THEN t.release_year + WHEN $2::text = 'rating' THEN t.rating + WHEN $2::text = 'rate' THEN u.rate + END + END ASC, + CASE WHEN NOT $1::boolean THEN + CASE + WHEN $2::text = 'id' THEN t.id + WHEN $2::text = 'year' THEN t.release_year + WHEN $2::text = 'rating' THEN t.rating + WHEN $2::text = 'rate' THEN u.rate + END + END DESC, + + CASE WHEN $2::text <> 'id' THEN t.id END ASC + +LIMIT COALESCE($16::int, 100) ` type SearchUserTitlesParams struct { - Word *string `json:"word"` - Status *TitleStatusT `json:"status"` - Rating *float64 `json:"rating"` - ReleaseYear *int32 `json:"release_year"` - ReleaseSeason *ReleaseSeasonT `json:"release_season"` - UsertitleStatus NullUsertitleStatusT `json:"usertitle_status"` - Limit *int32 `json:"limit"` + Forward bool `json:"forward"` + SortBy string `json:"sort_by"` + CursorYear *int32 `json:"cursor_year"` + CursorID *int64 `json:"cursor_id"` + CursorRating *float64 `json:"cursor_rating"` + Word *string `json:"word"` + Ongoing string `json:"ongoing"` + Planned string `json:"planned"` + Dropped string `json:"dropped"` + InProgress string `json:"in-progress"` + Finished string `json:"finished"` + Rate *int32 `json:"rate"` + Rating *float64 `json:"rating"` + ReleaseYear *int32 `json:"release_year"` + ReleaseSeason *ReleaseSeasonT `json:"release_season"` + Limit *int32 `json:"limit"` } type SearchUserTitlesRow struct { - UserID int64 `json:"user_id"` - TitleID int64 `json:"title_id"` - Status UsertitleStatusT `json:"status"` - Rate *int32 `json:"rate"` - ReviewID *int64 `json:"review_id"` - Ctime pgtype.Timestamptz `json:"ctime"` - ID int64 `json:"id"` - TitleNames []byte `json:"title_names"` - StudioID int64 `json:"studio_id"` - PosterID *int64 `json:"poster_id"` - TitleStatus TitleStatusT `json:"title_status"` - Rating *float64 `json:"rating"` - RatingCount *int32 `json:"rating_count"` - ReleaseYear *int32 `json:"release_year"` - ReleaseSeason *ReleaseSeasonT `json:"release_season"` - Season *int32 `json:"season"` - EpisodesAired *int32 `json:"episodes_aired"` - EpisodesAll *int32 `json:"episodes_all"` - EpisodesLen []byte `json:"episodes_len"` + ID *int64 `json:"id"` + TitleNames []byte `json:"title_names"` + StudioID *int64 `json:"studio_id"` + PosterID *int64 `json:"poster_id"` + TitleStatus *TitleStatusT `json:"title_status"` + Rating *float64 `json:"rating"` + RatingCount *int32 `json:"rating_count"` + ReleaseYear *int32 `json:"release_year"` + ReleaseSeason *ReleaseSeasonT `json:"release_season"` + Season *int32 `json:"season"` + EpisodesAired *int32 `json:"episodes_aired"` + EpisodesAll *int32 `json:"episodes_all"` + EpisodesLen []byte `json:"episodes_len"` + UserID int64 `json:"user_id"` + UsertitleStatus UsertitleStatusT `json:"usertitle_status"` + UserRate *int32 `json:"user_rate"` + ReviewID *int64 `json:"review_id"` + UserCtime pgtype.Timestamptz `json:"user_ctime"` + TitleStorageType string `json:"title_storage_type"` + TitleImagePath *string `json:"title_image_path"` + TagNames []byte `json:"tag_names"` + StudioName *string `json:"studio_name"` + StudioIllustID *int64 `json:"studio_illust_id"` + StudioDesc *string `json:"studio_desc"` + StudioStorageType string `json:"studio_storage_type"` + StudioImagePath *string `json:"studio_image_path"` } // 100 is default limit func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesParams) ([]SearchUserTitlesRow, error) { rows, err := q.db.Query(ctx, searchUserTitles, + arg.Forward, + arg.SortBy, + arg.CursorYear, + arg.CursorID, + arg.CursorRating, arg.Word, - arg.Status, + arg.Ongoing, + arg.Planned, + arg.Dropped, + arg.InProgress, + arg.Finished, + arg.Rate, arg.Rating, arg.ReleaseYear, arg.ReleaseSeason, - arg.UsertitleStatus, arg.Limit, ) if err != nil { @@ -634,12 +747,6 @@ func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesPara for rows.Next() { var i SearchUserTitlesRow if err := rows.Scan( - &i.UserID, - &i.TitleID, - &i.Status, - &i.Rate, - &i.ReviewID, - &i.Ctime, &i.ID, &i.TitleNames, &i.StudioID, @@ -653,6 +760,19 @@ func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesPara &i.EpisodesAired, &i.EpisodesAll, &i.EpisodesLen, + &i.UserID, + &i.UsertitleStatus, + &i.UserRate, + &i.ReviewID, + &i.UserCtime, + &i.TitleStorageType, + &i.TitleImagePath, + &i.TagNames, + &i.StudioName, + &i.StudioIllustID, + &i.StudioDesc, + &i.StudioStorageType, + &i.StudioImagePath, ); err != nil { return nil, err }