Compare commits
No commits in common. "8d98fb0cf8a8ccb22c7f12520d5462d76100f7e2" and "31a95fabeaa860b02d3be9b565238232235156c1" have entirely different histories.
8d98fb0cf8
...
31a95fabea
8 changed files with 103 additions and 432 deletions
|
|
@ -20,4 +20,3 @@ components:
|
||||||
$ref: "./parameters/_index.yaml"
|
$ref: "./parameters/_index.yaml"
|
||||||
schemas:
|
schemas:
|
||||||
$ref: "./schemas/_index.yaml"
|
$ref: "./schemas/_index.yaml"
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
CursorObj:
|
CursorObj:
|
||||||
$ref: "./CursorObj.yaml"
|
$ref: ./CursorObj.yaml
|
||||||
TitleSort:
|
TitleSort:
|
||||||
$ref: "./TitleSort.yaml"
|
$ref: "./TitleSort.yaml"
|
||||||
Image:
|
Image:
|
||||||
|
|
|
||||||
|
|
@ -13,71 +13,6 @@ func NewServer(db *sqlc.Queries) Server {
|
||||||
return Server{db: db}
|
return Server{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
// type Cursor interface {
|
|
||||||
// ParseCursor(sortBy oapi.TitleSort, data oapi.Cursor) (Cursor, error)
|
|
||||||
|
|
||||||
// Values() map[string]interface{}
|
|
||||||
// // for logs only
|
|
||||||
// Type() string
|
|
||||||
// }
|
|
||||||
|
|
||||||
// type CursorByID struct {
|
|
||||||
// ID int64
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (c CursorByID) Values() map[string]interface{} {
|
|
||||||
// return map[string]interface{}{
|
|
||||||
// "cursor_id": c.ID,
|
|
||||||
// "cursor_year": nil,
|
|
||||||
// "cursor_rating": nil,
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (c CursorByID) Type() string { return "id" }
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// decodes a base64-encoded JSON string into a CursorObj
|
|
||||||
// Returns the parsed CursorObj and an error
|
|
||||||
// func parseCursor(encoded oapi.Cursor) (*oapi.CursorObj, error) {
|
|
||||||
|
|
||||||
// // Decode base64
|
|
||||||
// decoded, err := base64.StdEncoding.DecodeString(encoded)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, fmt.Errorf("parseCursor: %v", err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Parse JSON
|
|
||||||
// var cursor oapi.CursorObj
|
|
||||||
// if err := json.Unmarshal(decoded, &cursor); err != nil {
|
|
||||||
// return nil, fmt.Errorf("parseCursor: %v", err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return &cursor, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
func parseInt64(s string) (int32, error) {
|
func parseInt64(s string) (int32, error) {
|
||||||
i, err := strconv.ParseInt(s, 10, 64)
|
i, err := strconv.ParseInt(s, 10, 64)
|
||||||
return int32(i), err
|
return int32(i), err
|
||||||
|
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ParseCursorInto parses an opaque base64 cursor and injects values into target struct.
|
|
||||||
//
|
|
||||||
// Supported sort types:
|
|
||||||
// - "id" → sets CursorID (must be *int64)
|
|
||||||
// - "year" → sets CursorID (*int64) + CursorYear (*int32)
|
|
||||||
// - "rating" → sets CursorID (*int64) + CursorRating (*float64)
|
|
||||||
//
|
|
||||||
// Target struct may have any subset of these fields (e.g. only CursorID).
|
|
||||||
// Unknown fields are ignored. Missing fields → values are dropped (safe).
|
|
||||||
//
|
|
||||||
// Returns error if cursor is invalid or inconsistent with sort_by.
|
|
||||||
func ParseCursorInto(sortBy, cursorStr string, target any) error {
|
|
||||||
if cursorStr == "" {
|
|
||||||
return nil // no cursor → nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Decode cursor
|
|
||||||
payload, err := decodeCursor(cursorStr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Extract ID (required for all types)
|
|
||||||
id, err := extractInt64(payload, "id")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cursor: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Get reflect value of target (must be ptr to struct)
|
|
||||||
v := reflect.ValueOf(target)
|
|
||||||
if v.Kind() != reflect.Ptr || v.IsNil() {
|
|
||||||
return fmt.Errorf("target must be non-nil pointer to struct")
|
|
||||||
}
|
|
||||||
v = v.Elem()
|
|
||||||
if v.Kind() != reflect.Struct {
|
|
||||||
return fmt.Errorf("target must be pointer to struct")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Helper: set field if exists and compatible
|
|
||||||
setField := func(fieldName string, value any) {
|
|
||||||
f := v.FieldByName(fieldName)
|
|
||||||
if !f.IsValid() || !f.CanSet() {
|
|
||||||
return // field not found or unexported
|
|
||||||
}
|
|
||||||
ft := f.Type()
|
|
||||||
vv := reflect.ValueOf(value)
|
|
||||||
|
|
||||||
// Case: field is *T, value is T → wrap in pointer
|
|
||||||
if ft.Kind() == reflect.Ptr {
|
|
||||||
elemType := ft.Elem()
|
|
||||||
if vv.Type().AssignableTo(elemType) {
|
|
||||||
ptr := reflect.New(elemType)
|
|
||||||
ptr.Elem().Set(vv)
|
|
||||||
f.Set(ptr)
|
|
||||||
}
|
|
||||||
// nil → leave as zero (nil pointer)
|
|
||||||
} else if vv.Type().AssignableTo(ft) {
|
|
||||||
f.Set(vv)
|
|
||||||
}
|
|
||||||
// else: type mismatch → silently skip (safe)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Dispatch by sort type
|
|
||||||
switch sortBy {
|
|
||||||
case "id":
|
|
||||||
setField("CursorID", id)
|
|
||||||
|
|
||||||
case "year":
|
|
||||||
setField("CursorID", id)
|
|
||||||
param, err := extractString(payload, "param")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cursor year: %w", err)
|
|
||||||
}
|
|
||||||
year, err := strconv.Atoi(param)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cursor year: param must be integer, got %q", param)
|
|
||||||
}
|
|
||||||
setField("CursorYear", int32(year)) // or int, depending on your schema
|
|
||||||
|
|
||||||
case "rating":
|
|
||||||
setField("CursorID", id)
|
|
||||||
param, err := extractString(payload, "param")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cursor rating: %w", err)
|
|
||||||
}
|
|
||||||
rating, err := strconv.ParseFloat(param, 64)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cursor rating: param must be float, got %q", param)
|
|
||||||
}
|
|
||||||
setField("CursorRating", rating)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported sort_by: %q", sortBy)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- helpers ---
|
|
||||||
func decodeCursor(cursorStr string) (map[string]any, error) {
|
|
||||||
data, err := base64.RawURLEncoding.DecodeString(cursorStr)
|
|
||||||
if err != nil {
|
|
||||||
data, err = base64.StdEncoding.DecodeString(cursorStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid base64 cursor")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var m map[string]any
|
|
||||||
if err := json.Unmarshal(data, &m); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid cursor JSON: %w", err)
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractInt64(m map[string]any, key string) (int64, error) {
|
|
||||||
v, ok := m[key]
|
|
||||||
if !ok {
|
|
||||||
return 0, fmt.Errorf("missing %q", key)
|
|
||||||
}
|
|
||||||
switch x := v.(type) {
|
|
||||||
case float64:
|
|
||||||
if x == float64(int64(x)) {
|
|
||||||
return int64(x), nil
|
|
||||||
}
|
|
||||||
case string:
|
|
||||||
i, err := strconv.ParseInt(x, 10, 64)
|
|
||||||
if err == nil {
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
case int64, int, int32:
|
|
||||||
return reflect.ValueOf(x).Int(), nil
|
|
||||||
}
|
|
||||||
return 0, fmt.Errorf("%q must be integer", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractString(m map[string]interface{}, key string) (string, error) {
|
|
||||||
v, ok := m[key]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("missing %q", key)
|
|
||||||
}
|
|
||||||
s, ok := v.(string)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("%q must be string", key)
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
@ -218,17 +218,9 @@ func (s Server) GetTitlesTitleId(ctx context.Context, request oapi.GetTitlesTitl
|
||||||
|
|
||||||
func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObject) (oapi.GetTitlesResponseObject, error) {
|
func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObject) (oapi.GetTitlesResponseObject, error) {
|
||||||
opai_titles := make([]oapi.Title, 0)
|
opai_titles := make([]oapi.Title, 0)
|
||||||
|
cursor := oapi.CursorObj{
|
||||||
// old_cursor := oapi.CursorObj{
|
Id: 1,
|
||||||
// Id: 1,
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// if request.Params.Cursor != nil {
|
|
||||||
// if old_cursor, err := parseCursor(*request.Params.Cursor); err != nil {
|
|
||||||
// log.Errorf("%v", err)
|
|
||||||
// return oapi.GetTitles400Response{}, err
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
word := Word2Sqlc(request.Params.Word)
|
word := Word2Sqlc(request.Params.Word)
|
||||||
status, err := TitleStatus2Sqlc(request.Params.Status)
|
status, err := TitleStatus2Sqlc(request.Params.Status)
|
||||||
|
|
@ -241,29 +233,17 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje
|
||||||
log.Errorf("%v", err)
|
log.Errorf("%v", err)
|
||||||
return oapi.GetTitles400Response{}, err
|
return oapi.GetTitles400Response{}, err
|
||||||
}
|
}
|
||||||
|
// param = nil means it will not be used
|
||||||
params := sqlc.SearchTitlesParams{
|
titles, err := s.db.SearchTitles(ctx, sqlc.SearchTitlesParams{
|
||||||
Word: word,
|
Word: word,
|
||||||
Status: status,
|
Status: status,
|
||||||
Rating: request.Params.Rating,
|
Rating: request.Params.Rating,
|
||||||
ReleaseYear: request.Params.ReleaseYear,
|
ReleaseYear: request.Params.ReleaseYear,
|
||||||
ReleaseSeason: season,
|
ReleaseSeason: season,
|
||||||
Forward: true,
|
Forward: true,
|
||||||
// SortBy: "id",
|
SortBy: "id",
|
||||||
Limit: request.Params.Limit,
|
Limit: request.Params.Limit,
|
||||||
}
|
})
|
||||||
|
|
||||||
if request.Params.Sort != nil {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// param = nil means it will not be used
|
|
||||||
titles, err := s.db.SearchTitles(ctx, params)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("%v", err)
|
log.Errorf("%v", err)
|
||||||
return oapi.GetTitles500Response{}, nil
|
return oapi.GetTitles500Response{}, nil
|
||||||
|
|
@ -282,5 +262,5 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje
|
||||||
opai_titles = append(opai_titles, t)
|
opai_titles = append(opai_titles, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
return oapi.GetTitles200JSONResponse{Cursor: oapi.CursorObj{}, Data: opai_titles}, nil
|
return oapi.GetTitles200JSONResponse{Cursor: cursor, Data: opai_titles}, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,96 +81,56 @@ WHERE id = sqlc.arg('title_id')::bigint;
|
||||||
SELECT
|
SELECT
|
||||||
*
|
*
|
||||||
FROM titles
|
FROM titles
|
||||||
WHERE
|
WHERE
|
||||||
CASE
|
CASE
|
||||||
WHEN sqlc.arg('forward')::boolean THEN
|
WHEN sqlc.narg('word')::text IS NOT NULL THEN
|
||||||
-- forward: greater than cursor (next page)
|
(
|
||||||
CASE sqlc.arg('sort_by')::text
|
SELECT bool_and(
|
||||||
WHEN 'year' THEN
|
EXISTS (
|
||||||
(sqlc.narg('cursor_year')::int IS NULL) OR
|
SELECT 1
|
||||||
(release_year > sqlc.narg('cursor_year')::int) OR
|
FROM jsonb_each_text(title_names) AS t(key, val)
|
||||||
(release_year = sqlc.narg('cursor_year')::int AND id > sqlc.narg('cursor_id')::bigint)
|
WHERE val ILIKE pattern
|
||||||
|
|
||||||
WHEN 'rating' THEN
|
|
||||||
(sqlc.narg('cursor_rating')::float IS NULL) OR
|
|
||||||
(rating > sqlc.narg('cursor_rating')::float) OR
|
|
||||||
(rating = sqlc.narg('cursor_rating')::float AND id > sqlc.narg('cursor_id')::bigint)
|
|
||||||
|
|
||||||
WHEN 'id' THEN
|
|
||||||
(sqlc.narg('cursor_id')::bigint IS NULL) OR
|
|
||||||
(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
|
|
||||||
(release_year < sqlc.narg('cursor_year')::int) OR
|
|
||||||
(release_year = sqlc.narg('cursor_year')::int AND id < sqlc.narg('cursor_id')::bigint)
|
|
||||||
|
|
||||||
WHEN 'rating' THEN
|
|
||||||
(sqlc.narg('cursor_rating')::float IS NULL) OR
|
|
||||||
(rating < sqlc.narg('cursor_rating')::float) OR
|
|
||||||
(rating = sqlc.narg('cursor_rating')::float AND id < sqlc.narg('cursor_id')::bigint)
|
|
||||||
|
|
||||||
WHEN 'id' THEN
|
|
||||||
(sqlc.narg('cursor_id')::bigint IS NULL) OR
|
|
||||||
(id < sqlc.narg('cursor_id')::bigint)
|
|
||||||
|
|
||||||
ELSE true
|
|
||||||
END
|
|
||||||
END
|
|
||||||
|
|
||||||
AND (
|
|
||||||
CASE
|
|
||||||
WHEN sqlc.narg('word')::text IS NOT NULL THEN
|
|
||||||
(
|
|
||||||
SELECT bool_and(
|
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM jsonb_each_text(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
|
FROM unnest(
|
||||||
END
|
ARRAY(
|
||||||
)
|
SELECT '%' || trim(w) || '%'
|
||||||
|
FROM unnest(string_to_array(sqlc.narg('word')::text, ' ')) AS w
|
||||||
|
WHERE trim(w) <> ''
|
||||||
|
)
|
||||||
|
) AS pattern
|
||||||
|
)
|
||||||
|
ELSE true
|
||||||
|
END
|
||||||
|
|
||||||
AND (sqlc.narg('status')::title_status_t IS NULL OR title_status = sqlc.narg('status')::title_status_t)
|
AND (sqlc.narg('status')::title_status_t IS NULL OR title_status = sqlc.narg('status')::title_status_t)
|
||||||
AND (sqlc.narg('rating')::float IS NULL OR rating >= sqlc.narg('rating')::float)
|
AND (sqlc.narg('rating')::float IS NULL OR rating >= sqlc.narg('rating')::float)
|
||||||
AND (sqlc.narg('release_year')::int IS NULL OR release_year = sqlc.narg('release_year')::int)
|
AND (sqlc.narg('release_year')::int IS NULL OR release_year = sqlc.narg('release_year')::int)
|
||||||
AND (sqlc.narg('release_season')::release_season_t IS NULL OR release_season = sqlc.narg('release_season')::release_season_t)
|
AND (sqlc.narg('release_season')::release_season_t IS NULL OR release_season = sqlc.narg('release_season')::release_season_t)
|
||||||
|
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE WHEN sqlc.arg('forward')::boolean THEN
|
-- Основной ключ: выбранное поле
|
||||||
CASE
|
CASE
|
||||||
WHEN sqlc.arg('sort_by')::text = 'id' THEN id
|
WHEN sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'id' THEN id
|
||||||
WHEN sqlc.arg('sort_by')::text = 'year' THEN release_year
|
WHEN sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'year' THEN release_year
|
||||||
WHEN sqlc.arg('sort_by')::text = 'rating' THEN rating
|
WHEN sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'rating' THEN rating
|
||||||
END
|
-- WHEN sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'views' THEN views
|
||||||
END ASC,
|
END ASC,
|
||||||
CASE WHEN NOT sqlc.arg('forward')::boolean THEN
|
CASE
|
||||||
CASE
|
WHEN NOT sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'id' THEN id
|
||||||
WHEN sqlc.arg('sort_by')::text = 'id' THEN id
|
WHEN NOT sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'year' THEN release_year
|
||||||
WHEN sqlc.arg('sort_by')::text = 'year' THEN release_year
|
WHEN NOT sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'rating' THEN rating
|
||||||
WHEN sqlc.arg('sort_by')::text = 'rating' THEN rating
|
-- WHEN NOT sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'views' THEN views
|
||||||
END
|
END DESC,
|
||||||
END DESC,
|
|
||||||
|
|
||||||
CASE WHEN sqlc.arg('sort_by')::text <> 'id' THEN id END ASC
|
|
||||||
|
|
||||||
|
-- Вторичный ключ: id — только если НЕ сортируем по id
|
||||||
|
CASE
|
||||||
|
WHEN sqlc.arg(sort_by)::text != 'id' AND sqlc.arg(forward)::boolean THEN id
|
||||||
|
END ASC,
|
||||||
|
CASE
|
||||||
|
WHEN sqlc.arg(sort_by)::text != 'id' AND NOT sqlc.arg(forward)::boolean THEN id
|
||||||
|
END DESC
|
||||||
LIMIT COALESCE(sqlc.narg('limit')::int, 100); -- 100 is default limit
|
LIMIT COALESCE(sqlc.narg('limit')::int, 100); -- 100 is default limit
|
||||||
|
-- OFFSET sqlc.narg('offset')::int;
|
||||||
|
|
||||||
-- name: SearchUserTitles :many
|
-- name: SearchUserTitles :many
|
||||||
SELECT
|
SELECT
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ func (q *Queries) GetTitleTags(ctx context.Context, titleID int64) ([][]byte, er
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
items := [][]byte{}
|
var items [][]byte
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var tag_names []byte
|
var tag_names []byte
|
||||||
if err := rows.Scan(&tag_names); err != nil {
|
if err := rows.Scan(&tag_names); err != nil {
|
||||||
|
|
@ -289,131 +289,84 @@ const searchTitles = `-- name: SearchTitles :many
|
||||||
SELECT
|
SELECT
|
||||||
id, title_names, studio_id, poster_id, title_status, rating, rating_count, release_year, release_season, season, episodes_aired, episodes_all, episodes_len
|
id, title_names, studio_id, poster_id, title_status, rating, rating_count, release_year, release_season, season, episodes_aired, episodes_all, episodes_len
|
||||||
FROM titles
|
FROM titles
|
||||||
WHERE
|
WHERE
|
||||||
CASE
|
CASE
|
||||||
WHEN $1::boolean THEN
|
WHEN $1::text IS NOT NULL THEN
|
||||||
-- forward: greater than cursor (next page)
|
(
|
||||||
CASE $2::text
|
SELECT bool_and(
|
||||||
WHEN 'year' THEN
|
EXISTS (
|
||||||
($3::int IS NULL) OR
|
SELECT 1
|
||||||
(release_year > $3::int) OR
|
FROM jsonb_each_text(title_names) AS t(key, val)
|
||||||
(release_year = $3::int AND id > $4::bigint)
|
WHERE val ILIKE pattern
|
||||||
|
)
|
||||||
WHEN 'rating' THEN
|
)
|
||||||
($5::float IS NULL) OR
|
FROM unnest(
|
||||||
(rating > $5::float) OR
|
ARRAY(
|
||||||
(rating = $5::float AND id > $4::bigint)
|
SELECT '%' || trim(w) || '%'
|
||||||
|
FROM unnest(string_to_array($1::text, ' ')) AS w
|
||||||
WHEN 'id' THEN
|
WHERE trim(w) <> ''
|
||||||
($4::bigint IS NULL) OR
|
)
|
||||||
(id > $4::bigint)
|
) AS pattern
|
||||||
|
)
|
||||||
ELSE true -- fallback
|
ELSE true
|
||||||
END
|
|
||||||
|
|
||||||
ELSE
|
|
||||||
-- backward: less than cursor (prev page)
|
|
||||||
CASE $2::text
|
|
||||||
WHEN 'year' THEN
|
|
||||||
($3::int IS NULL) OR
|
|
||||||
(release_year < $3::int) OR
|
|
||||||
(release_year = $3::int AND id < $4::bigint)
|
|
||||||
|
|
||||||
WHEN 'rating' THEN
|
|
||||||
($5::float IS NULL) OR
|
|
||||||
(rating < $5::float) OR
|
|
||||||
(rating = $5::float AND id < $4::bigint)
|
|
||||||
|
|
||||||
WHEN 'id' THEN
|
|
||||||
($4::bigint IS NULL) OR
|
|
||||||
(id < $4::bigint)
|
|
||||||
|
|
||||||
ELSE true
|
|
||||||
END
|
|
||||||
END
|
END
|
||||||
|
|
||||||
AND (
|
AND ($2::title_status_t IS NULL OR title_status = $2::title_status_t)
|
||||||
CASE
|
AND ($3::float IS NULL OR rating >= $3::float)
|
||||||
WHEN $6::text IS NOT NULL THEN
|
AND ($4::int IS NULL OR release_year = $4::int)
|
||||||
(
|
AND ($5::release_season_t IS NULL OR release_season = $5::release_season_t)
|
||||||
SELECT bool_and(
|
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM jsonb_each_text(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
|
|
||||||
)
|
|
||||||
|
|
||||||
AND ($7::title_status_t IS NULL OR title_status = $7::title_status_t)
|
|
||||||
AND ($8::float IS NULL OR rating >= $8::float)
|
|
||||||
AND ($9::int IS NULL OR release_year = $9::int)
|
|
||||||
AND ($10::release_season_t IS NULL OR release_season = $10::release_season_t)
|
|
||||||
|
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE WHEN $1::boolean THEN
|
-- Основной ключ: выбранное поле
|
||||||
CASE
|
CASE
|
||||||
WHEN $2::text = 'id' THEN id
|
WHEN $6::boolean AND $7::text = 'id' THEN id
|
||||||
WHEN $2::text = 'year' THEN release_year
|
WHEN $6::boolean AND $7::text = 'year' THEN release_year
|
||||||
WHEN $2::text = 'rating' THEN rating
|
WHEN $6::boolean AND $7::text = 'rating' THEN rating
|
||||||
END
|
-- WHEN sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'views' THEN views
|
||||||
END ASC,
|
END ASC,
|
||||||
CASE WHEN NOT $1::boolean THEN
|
CASE
|
||||||
CASE
|
WHEN NOT $6::boolean AND $7::text = 'id' THEN id
|
||||||
WHEN $2::text = 'id' THEN id
|
WHEN NOT $6::boolean AND $7::text = 'year' THEN release_year
|
||||||
WHEN $2::text = 'year' THEN release_year
|
WHEN NOT $6::boolean AND $7::text = 'rating' THEN rating
|
||||||
WHEN $2::text = 'rating' THEN rating
|
-- WHEN NOT sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'views' THEN views
|
||||||
END
|
END DESC,
|
||||||
END DESC,
|
|
||||||
|
|
||||||
CASE WHEN $2::text <> 'id' THEN id END ASC
|
-- Вторичный ключ: id — только если НЕ сортируем по id
|
||||||
|
CASE
|
||||||
LIMIT COALESCE($11::int, 100)
|
WHEN $7::text != 'id' AND $6::boolean THEN id
|
||||||
|
END ASC,
|
||||||
|
CASE
|
||||||
|
WHEN $7::text != 'id' AND NOT $6::boolean THEN id
|
||||||
|
END DESC
|
||||||
|
LIMIT COALESCE($8::int, 100)
|
||||||
`
|
`
|
||||||
|
|
||||||
type SearchTitlesParams struct {
|
type SearchTitlesParams struct {
|
||||||
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"`
|
Word *string `json:"word"`
|
||||||
Status *TitleStatusT `json:"status"`
|
Status *TitleStatusT `json:"status"`
|
||||||
Rating *float64 `json:"rating"`
|
Rating *float64 `json:"rating"`
|
||||||
ReleaseYear *int32 `json:"release_year"`
|
ReleaseYear *int32 `json:"release_year"`
|
||||||
ReleaseSeason *ReleaseSeasonT `json:"release_season"`
|
ReleaseSeason *ReleaseSeasonT `json:"release_season"`
|
||||||
|
Forward bool `json:"forward"`
|
||||||
|
SortBy string `json:"sort_by"`
|
||||||
Limit *int32 `json:"limit"`
|
Limit *int32 `json:"limit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]Title, error) {
|
func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]Title, error) {
|
||||||
rows, err := q.db.Query(ctx, searchTitles,
|
rows, err := q.db.Query(ctx, searchTitles,
|
||||||
arg.Forward,
|
|
||||||
arg.SortBy,
|
|
||||||
arg.CursorYear,
|
|
||||||
arg.CursorID,
|
|
||||||
arg.CursorRating,
|
|
||||||
arg.Word,
|
arg.Word,
|
||||||
arg.Status,
|
arg.Status,
|
||||||
arg.Rating,
|
arg.Rating,
|
||||||
arg.ReleaseYear,
|
arg.ReleaseYear,
|
||||||
arg.ReleaseSeason,
|
arg.ReleaseSeason,
|
||||||
|
arg.Forward,
|
||||||
|
arg.SortBy,
|
||||||
arg.Limit,
|
arg.Limit,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
items := []Title{}
|
var items []Title
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i Title
|
var i Title
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
|
|
@ -511,6 +464,7 @@ type SearchUserTitlesRow struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 100 is default limit
|
// 100 is default limit
|
||||||
|
// OFFSET sqlc.narg('offset')::int;
|
||||||
func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesParams) ([]SearchUserTitlesRow, error) {
|
func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesParams) ([]SearchUserTitlesRow, error) {
|
||||||
rows, err := q.db.Query(ctx, searchUserTitles,
|
rows, err := q.db.Query(ctx, searchUserTitles,
|
||||||
arg.Word,
|
arg.Word,
|
||||||
|
|
@ -525,7 +479,7 @@ func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesPara
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
items := []SearchUserTitlesRow{}
|
var items []SearchUserTitlesRow
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i SearchUserTitlesRow
|
var i SearchUserTitlesRow
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ sql:
|
||||||
sql_driver: "github.com/jackc/pgx/v5"
|
sql_driver: "github.com/jackc/pgx/v5"
|
||||||
emit_json_tags: true
|
emit_json_tags: true
|
||||||
emit_pointers_for_null_types: true
|
emit_pointers_for_null_types: true
|
||||||
emit_empty_slices: true #slices returned by :many queries will be empty instead of nil
|
|
||||||
overrides:
|
overrides:
|
||||||
- db_type: "uuid"
|
- db_type: "uuid"
|
||||||
nullable: false
|
nullable: false
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue