- Результатов больше нет, было найдено {titles.length} тайтлов.
+
+
+
+ {user.avatar_id ? (
+

+ ) : (
+
+ {user.disp_name?.[0] || "U"}
)}
- >
- )}
+
+
+
+
{user.disp_name || user.nickname}
+
@{user.nickname}
+ {/*
+ Joined: {new Date(user.creation_date).toLocaleDateString()}
+
*/}
+
+
+
+ {user.user_desc &&
{user.user_desc}
}
+
+
);
-}
+};
+
+export default UserPage;
diff --git a/sql/migrations/000001_init.up.sql b/sql/migrations/000001_init.up.sql
index 3499fe2..e6ed628 100644
--- a/sql/migrations/000001_init.up.sql
+++ b/sql/migrations/000001_init.up.sql
@@ -1,3 +1,6 @@
+-- TODO:
+-- maybe jsonb constraints
+-- clean unused images
CREATE TYPE usertitle_status_t AS ENUM ('finished', 'planned', 'dropped', 'in-progress');
CREATE TYPE storage_type_t AS ENUM ('local', 's3');
CREATE TYPE title_status_t AS ENUM ('finished', 'ongoing', 'planned');
@@ -21,24 +24,37 @@ CREATE TABLE images (
image_path text UNIQUE NOT NULL
);
+CREATE TABLE reviews (
+ id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ data text NOT NULL,
+ rating int CHECK (rating >= 0 AND rating <= 10),
+ user_id bigint REFERENCES users (id),
+ title_id bigint REFERENCES titles (id),
+ created_at timestamptz DEFAULT NOW()
+);
+
+CREATE TABLE review_images (
+ PRIMARY KEY (review_id, image_id),
+ review_id bigint NOT NULL REFERENCES reviews(id) ON DELETE CASCADE,
+ image_id bigint NOT NULL REFERENCES images(id) ON DELETE CASCADE
+);
+
CREATE TABLE users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
- avatar_id bigint REFERENCES images (id) ON DELETE SET NULL,
+ avatar_id bigint REFERENCES images (id),
passhash text NOT NULL,
mail text CHECK (mail ~ '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+$'),
nickname text UNIQUE NOT NULL CHECK (nickname ~ '^[a-zA-Z0-9_-]{3,}$'),
disp_name text,
user_desc text,
- creation_date timestamptz NOT NULL DEFAULT NOW(),
+ creation_date timestamptz NOT NULL,
last_login timestamptz
);
-
-
CREATE TABLE studios (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
studio_name text NOT NULL UNIQUE,
- illust_id bigint REFERENCES images (id) ON DELETE SET NULL,
+ illust_id bigint REFERENCES images (id),
studio_desc text
);
@@ -48,7 +64,7 @@ CREATE TABLE titles (
-- example {"ru": ["Атака титанов", "Атака Титанов"],"en": ["Attack on Titan", "AoT"],"ja": ["進撃の巨人", "しんげきのきょじん"]}
title_names jsonb NOT NULL,
studio_id bigint NOT NULL REFERENCES studios (id),
- poster_id bigint REFERENCES images (id) ON DELETE SET NULL,
+ poster_id bigint REFERENCES images (id),
title_status title_status_t NOT NULL,
rating float CHECK (rating >= 0 AND rating <= 10),
rating_count int CHECK (rating_count >= 0),
@@ -64,36 +80,21 @@ CREATE TABLE titles (
AND episodes_aired <= episodes_all))
);
-CREATE TABLE reviews (
- id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
- data text NOT NULL,
- rating int CHECK (rating >= 0 AND rating <= 10),
- user_id bigint REFERENCES users (id) ON DELETE SET NULL,
- title_id bigint REFERENCES titles (id) ON DELETE CASCADE,
- created_at timestamptz DEFAULT NOW()
-);
-
-CREATE TABLE review_images (
- PRIMARY KEY (review_id, image_id),
- review_id bigint NOT NULL REFERENCES reviews(id) ON DELETE CASCADE,
- image_id bigint NOT NULL REFERENCES images(id) ON DELETE CASCADE
-);
-
CREATE TABLE usertitles (
PRIMARY KEY (user_id, title_id),
- user_id bigint NOT NULL REFERENCES users (id) ON DELETE CASCADE,
- title_id bigint NOT NULL REFERENCES titles (id) ON DELETE CASCADE,
+ user_id bigint NOT NULL REFERENCES users (id),
+ title_id bigint NOT NULL REFERENCES titles (id),
status usertitle_status_t NOT NULL,
rate int CHECK (rate > 0 AND rate <= 10),
- review_id bigint REFERENCES reviews (id) ON DELETE SET NULL,
- ctime timestamptz NOT NULL DEFAULT now()
+ review_id bigint REFERENCES reviews (id),
+ ctime timestamptz
-- // TODO: series status
);
CREATE TABLE title_tags (
PRIMARY KEY (title_id, tag_id),
- title_id bigint NOT NULL REFERENCES titles (id) ON DELETE CASCADE,
- tag_id bigint NOT NULL REFERENCES tags (id) ON DELETE CASCADE
+ title_id bigint NOT NULL REFERENCES titles (id),
+ tag_id bigint NOT NULL REFERENCES tags (id)
);
CREATE TABLE signals (
@@ -104,17 +105,17 @@ CREATE TABLE signals (
pending boolean NOT NULL
);
-CREATE TABLE external_services (
- id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
- name text UNIQUE NOT NULL
-);
-
CREATE TABLE external_ids (
user_id bigint NOT NULL REFERENCES users (id),
service_id bigint REFERENCES external_services (id),
external_id text NOT NULL
);
+CREATE TABLE external_services (
+ id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ name text UNIQUE NOT NULL
+);
+
-- Functions
CREATE OR REPLACE FUNCTION update_title_rating()
RETURNS TRIGGER AS $$
@@ -168,17 +169,4 @@ EXECUTE FUNCTION update_title_rating();
CREATE TRIGGER trg_notify_new_signal
AFTER INSERT ON signals
FOR EACH ROW
-EXECUTE FUNCTION notify_new_signal();
-
-CREATE OR REPLACE FUNCTION set_ctime()
-RETURNS TRIGGER AS $$
-BEGIN
- NEW.ctime = now();
- RETURN NEW;
-END;
-$$ LANGUAGE plpgsql;
-
-CREATE TRIGGER set_ctime_on_update
-BEFORE UPDATE ON usertitles
-FOR EACH ROW
-EXECUTE FUNCTION set_ctime();
\ No newline at end of file
+EXECUTE FUNCTION notify_new_signal();
\ No newline at end of file
diff --git a/sql/models.go b/sql/models.go
index 842d58c..93cecca 100644
--- a/sql/models.go
+++ b/sql/models.go
@@ -6,7 +6,6 @@ package sqlc
import (
"database/sql/driver"
- "encoding/json"
"fmt"
"time"
@@ -224,11 +223,11 @@ type ReviewImage struct {
}
type Signal struct {
- ID int64 `json:"id"`
- TitleID *int64 `json:"title_id"`
- RawData json.RawMessage `json:"raw_data"`
- ProviderID int64 `json:"provider_id"`
- Pending bool `json:"pending"`
+ ID int64 `json:"id"`
+ TitleID *int64 `json:"title_id"`
+ RawData []byte `json:"raw_data"`
+ ProviderID int64 `json:"provider_id"`
+ Pending bool `json:"pending"`
}
type Studio struct {
@@ -239,13 +238,13 @@ type Studio struct {
}
type Tag struct {
- ID int64 `json:"id"`
- TagNames json.RawMessage `json:"tag_names"`
+ ID int64 `json:"id"`
+ TagNames []byte `json:"tag_names"`
}
type Title struct {
ID int64 `json:"id"`
- TitleNames json.RawMessage `json:"title_names"`
+ TitleNames []byte `json:"title_names"`
StudioID int64 `json:"studio_id"`
PosterID *int64 `json:"poster_id"`
TitleStatus TitleStatusT `json:"title_status"`
@@ -277,10 +276,10 @@ type User struct {
}
type Usertitle 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 time.Time `json:"ctime"`
+ 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"`
}
diff --git a/sql/queries.sql.go b/sql/queries.sql.go
index 1cca986..5a1d13c 100644
--- a/sql/queries.sql.go
+++ b/sql/queries.sql.go
@@ -7,8 +7,9 @@ package sqlc
import (
"context"
- "encoding/json"
"time"
+
+ "github.com/jackc/pgx/v5/pgtype"
)
const createImage = `-- name: CreateImage :one
@@ -29,51 +30,6 @@ func (q *Queries) CreateImage(ctx context.Context, arg CreateImageParams) (Image
return i, err
}
-const createNewUser = `-- name: CreateNewUser :one
-INSERT
-INTO users (passhash, nickname)
-VALUES ($1, $2)
-RETURNING id
-`
-
-type CreateNewUserParams struct {
- Passhash string `json:"passhash"`
- Nickname string `json:"nickname"`
-}
-
-func (q *Queries) CreateNewUser(ctx context.Context, arg CreateNewUserParams) (int64, error) {
- row := q.db.QueryRow(ctx, createNewUser, arg.Passhash, arg.Nickname)
- var id int64
- err := row.Scan(&id)
- return id, err
-}
-
-const deleteUserTitle = `-- name: DeleteUserTitle :one
-DELETE FROM usertitles
-WHERE user_id = $1
- AND title_id = $2
-RETURNING user_id, title_id, status, rate, review_id, ctime
-`
-
-type DeleteUserTitleParams struct {
- UserID int64 `json:"user_id"`
- TitleID int64 `json:"title_id"`
-}
-
-func (q *Queries) DeleteUserTitle(ctx context.Context, arg DeleteUserTitleParams) (Usertitle, error) {
- row := q.db.QueryRow(ctx, deleteUserTitle, arg.UserID, arg.TitleID)
- var i Usertitle
- err := row.Scan(
- &i.UserID,
- &i.TitleID,
- &i.Status,
- &i.Rate,
- &i.ReviewID,
- &i.Ctime,
- )
- return i, err
-}
-
const getImageByID = `-- name: GetImageByID :one
SELECT id, storage_type, image_path
FROM images
@@ -89,12 +45,40 @@ func (q *Queries) GetImageByID(ctx context.Context, illustID int64) (Image, erro
const getReviewByID = `-- name: GetReviewByID :one
+
+
SELECT id, data, rating, user_id, title_id, created_at
FROM reviews
WHERE review_id = $1::bigint
`
// 100 is default limit
+// -- name: ListTitles :many
+// SELECT title_id, title_names, studio_id, poster_id, signal_ids,
+//
+// title_status, rating, rating_count, release_year, release_season,
+// season, episodes_aired, episodes_all, episodes_len
+//
+// FROM titles
+// ORDER BY title_id
+// LIMIT $1 OFFSET $2;
+// -- name: UpdateTitle :one
+// UPDATE titles
+// SET
+//
+// title_names = COALESCE(sqlc.narg('title_names'), title_names),
+// studio_id = COALESCE(sqlc.narg('studio_id'), studio_id),
+// poster_id = COALESCE(sqlc.narg('poster_id'), poster_id),
+// signal_ids = COALESCE(sqlc.narg('signal_ids'), signal_ids),
+// title_status = COALESCE(sqlc.narg('title_status'), title_status),
+// release_year = COALESCE(sqlc.narg('release_year'), release_year),
+// release_season = COALESCE(sqlc.narg('release_season'), release_season),
+// episodes_aired = COALESCE(sqlc.narg('episodes_aired'), episodes_aired),
+// episodes_all = COALESCE(sqlc.narg('episodes_all'), episodes_all),
+// episodes_len = COALESCE(sqlc.narg('episodes_len'), episodes_len)
+//
+// WHERE title_id = sqlc.arg('title_id')
+// RETURNING *;
func (q *Queries) GetReviewByID(ctx context.Context, reviewID int64) (Review, error) {
row := q.db.QueryRow(ctx, getReviewByID, reviewID)
var i Review
@@ -128,60 +112,41 @@ func (q *Queries) GetStudioByID(ctx context.Context, studioID int64) (Studio, er
}
const getTitleByID = `-- name: GetTitleByID :one
-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,
- i.storage_type as title_storage_type,
- i.image_path as title_image_path,
- COALESCE(
- jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL),
- '[]'::jsonb
- )::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 as studio_storage_type,
- si.image_path as studio_image_path
-FROM titles as t
-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 t.id = $1::bigint
-GROUP BY
- t.id, i.id, s.id, si.id
+
+
+SELECT 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
+WHERE id = $1::bigint
`
-type GetTitleByIDRow struct {
- ID int64 `json:"id"`
- TitleNames json.RawMessage `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"`
- TitleStorageType *StorageTypeT `json:"title_storage_type"`
- TitleImagePath *string `json:"title_image_path"`
- TagNames json.RawMessage `json:"tag_names"`
- StudioName *string `json:"studio_name"`
- StudioIllustID *int64 `json:"studio_illust_id"`
- StudioDesc *string `json:"studio_desc"`
- StudioStorageType *StorageTypeT `json:"studio_storage_type"`
- StudioImagePath *string `json:"studio_image_path"`
-}
-
-// sqlc.struct: TitlesFull
-func (q *Queries) GetTitleByID(ctx context.Context, titleID int64) (GetTitleByIDRow, error) {
+// -- name: ListUsers :many
+// SELECT user_id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date
+// FROM users
+// ORDER BY user_id
+// LIMIT $1 OFFSET $2;
+// -- name: CreateUser :one
+// INSERT INTO users (avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date)
+// VALUES ($1, $2, $3, $4, $5, $6, $7)
+// RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date;
+// -- name: UpdateUser :one
+// UPDATE users
+// SET
+//
+// avatar_id = COALESCE(sqlc.narg('avatar_id'), avatar_id),
+// disp_name = COALESCE(sqlc.narg('disp_name'), disp_name),
+// user_desc = COALESCE(sqlc.narg('user_desc'), user_desc),
+// passhash = COALESCE(sqlc.narg('passhash'), passhash)
+//
+// WHERE user_id = sqlc.arg('user_id')
+// RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date;
+// -- name: DeleteUser :exec
+// DELETE FROM users
+// WHERE user_id = $1;
+func (q *Queries) GetTitleByID(ctx context.Context, titleID int64) (Title, error) {
row := q.db.QueryRow(ctx, getTitleByID, titleID)
- var i GetTitleByIDRow
+ var i Title
err := row.Scan(
&i.ID,
&i.TitleNames,
@@ -196,14 +161,6 @@ func (q *Queries) GetTitleByID(ctx context.Context, titleID int64) (GetTitleByID
&i.EpisodesAired,
&i.EpisodesAll,
&i.EpisodesLen,
- &i.TitleStorageType,
- &i.TitleImagePath,
- &i.TagNames,
- &i.StudioName,
- &i.StudioIllustID,
- &i.StudioDesc,
- &i.StudioStorageType,
- &i.StudioImagePath,
)
return i, err
}
@@ -216,15 +173,15 @@ JOIN title_tags as t ON(t.tag_id = g.id)
WHERE t.title_id = $1::bigint
`
-func (q *Queries) GetTitleTags(ctx context.Context, titleID int64) ([]json.RawMessage, error) {
+func (q *Queries) GetTitleTags(ctx context.Context, titleID int64) ([][]byte, error) {
rows, err := q.db.Query(ctx, getTitleTags, titleID)
if err != nil {
return nil, err
}
defer rows.Close()
- items := []json.RawMessage{}
+ var items [][]byte
for rows.Next() {
- var tag_names json.RawMessage
+ var tag_names []byte
if err := rows.Scan(&tag_names); err != nil {
return nil, err
}
@@ -237,31 +194,19 @@ func (q *Queries) GetTitleTags(ctx context.Context, titleID int64) ([]json.RawMe
}
const getUserByID = `-- name: GetUserByID :one
-SELECT
- t.id as id,
- t.avatar_id as avatar_id,
- t.mail as mail,
- t.nickname as nickname,
- t.disp_name as disp_name,
- t.user_desc as user_desc,
- t.creation_date as creation_date,
- i.storage_type as storage_type,
- i.image_path as image_path
-FROM users as t
-LEFT JOIN images as i ON (t.avatar_id = i.id)
-WHERE t.id = $1::bigint
+SELECT id, avatar_id, mail, nickname, disp_name, user_desc, creation_date
+FROM users
+WHERE id = $1
`
type GetUserByIDRow struct {
- ID int64 `json:"id"`
- AvatarID *int64 `json:"avatar_id"`
- Mail *string `json:"mail"`
- Nickname string `json:"nickname"`
- DispName *string `json:"disp_name"`
- UserDesc *string `json:"user_desc"`
- CreationDate time.Time `json:"creation_date"`
- StorageType *StorageTypeT `json:"storage_type"`
- ImagePath *string `json:"image_path"`
+ ID int64 `json:"id"`
+ AvatarID *int64 `json:"avatar_id"`
+ Mail *string `json:"mail"`
+ Nickname string `json:"nickname"`
+ DispName *string `json:"disp_name"`
+ UserDesc *string `json:"user_desc"`
+ CreationDate time.Time `json:"creation_date"`
}
func (q *Queries) GetUserByID(ctx context.Context, id int64) (GetUserByIDRow, error) {
@@ -275,57 +220,6 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (GetUserByIDRow, er
&i.DispName,
&i.UserDesc,
&i.CreationDate,
- &i.StorageType,
- &i.ImagePath,
- )
- return i, err
-}
-
-const getUserByNickname = `-- name: GetUserByNickname :one
-SELECT id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date, last_login
-FROM users
-WHERE nickname = $1
-`
-
-func (q *Queries) GetUserByNickname(ctx context.Context, nickname string) (User, error) {
- row := q.db.QueryRow(ctx, getUserByNickname, nickname)
- var i User
- err := row.Scan(
- &i.ID,
- &i.AvatarID,
- &i.Passhash,
- &i.Mail,
- &i.Nickname,
- &i.DispName,
- &i.UserDesc,
- &i.CreationDate,
- &i.LastLogin,
- )
- return i, err
-}
-
-const getUserTitleByID = `-- name: GetUserTitleByID :one
-SELECT
- ut.user_id, ut.title_id, ut.status, ut.rate, ut.review_id, ut.ctime
-FROM usertitles as ut
-WHERE ut.title_id = $1::bigint AND ut.user_id = $2::bigint
-`
-
-type GetUserTitleByIDParams struct {
- TitleID int64 `json:"title_id"`
- UserID int64 `json:"user_id"`
-}
-
-func (q *Queries) GetUserTitleByID(ctx context.Context, arg GetUserTitleByIDParams) (Usertitle, error) {
- row := q.db.QueryRow(ctx, getUserTitleByID, arg.TitleID, arg.UserID)
- var i Usertitle
- err := row.Scan(
- &i.UserID,
- &i.TitleID,
- &i.Status,
- &i.Rate,
- &i.ReviewID,
- &i.Ctime,
)
return i, err
}
@@ -364,7 +258,7 @@ VALUES (
RETURNING id, tag_names
`
-func (q *Queries) InsertTag(ctx context.Context, tagNames json.RawMessage) (Tag, error) {
+func (q *Queries) InsertTag(ctx context.Context, tagNames []byte) (Tag, error) {
row := q.db.QueryRow(ctx, insertTag, tagNames)
var i Tag
err := row.Scan(&i.ID, &i.TagNames)
@@ -391,229 +285,94 @@ func (q *Queries) InsertTitleTags(ctx context.Context, arg InsertTitleTagsParams
return i, err
}
-const insertUserTitle = `-- name: InsertUserTitle :one
-INSERT INTO usertitles (user_id, title_id, status, rate, review_id)
-VALUES (
- $1::bigint,
- $2::bigint,
- $3::usertitle_status_t,
- $4::int,
- $5::bigint
-)
-RETURNING user_id, title_id, status, rate, review_id, ctime
-`
-
-type InsertUserTitleParams struct {
- UserID int64 `json:"user_id"`
- TitleID int64 `json:"title_id"`
- Status UsertitleStatusT `json:"status"`
- Rate *int32 `json:"rate"`
- ReviewID *int64 `json:"review_id"`
-}
-
-func (q *Queries) InsertUserTitle(ctx context.Context, arg InsertUserTitleParams) (Usertitle, error) {
- row := q.db.QueryRow(ctx, insertUserTitle,
- arg.UserID,
- arg.TitleID,
- arg.Status,
- arg.Rate,
- arg.ReviewID,
- )
- var i Usertitle
- err := row.Scan(
- &i.UserID,
- &i.TitleID,
- &i.Status,
- &i.Rate,
- &i.ReviewID,
- &i.Ctime,
- )
- return i, err
-}
-
const searchTitles = `-- name: SearchTitles :many
SELECT
- t.id as id,
- t.title_names as title_names,
- t.poster_id as poster_id,
- t.title_status as title_status,
- t.rating as rating,
- t.rating_count as rating_count,
- t.release_year as release_year,
- t.release_season as release_season,
- t.season as season,
- t.episodes_aired as episodes_aired,
- t.episodes_all as episodes_all,
- i.storage_type as title_storage_type,
- i.image_path as title_image_path,
- COALESCE(
- jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL),
- '[]'::jsonb
- )::jsonb as tag_names,
- s.studio_name as studio_name
-
-FROM titles as t
-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)
-
-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
+ 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
+WHERE
+ CASE
+ WHEN $1::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($1::text, ' ')) AS w
+ WHERE trim(w) <> ''
+ )
+ ) AS pattern
+ )
+ ELSE true
END
- 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
- )
-
- AND (
- $7::title_status_t[] IS NULL
- OR array_length($7::title_status_t[], 1) IS NULL
- OR array_length($7::title_status_t[], 1) = 0
- OR t.title_status = ANY($7::title_status_t[])
- )
- AND ($8::float IS NULL OR t.rating >= $8::float)
- AND ($9::int IS NULL OR t.release_year = $9::int)
- AND ($10::release_season_t IS NULL OR t.release_season = $10::release_season_t)
-
-GROUP BY
- t.id, i.id, s.id
-
+ AND ($2::title_status_t IS NULL OR title_status = $2::title_status_t)
+ AND ($3::float IS NULL OR rating >= $3::float)
+ AND ($4::int IS NULL OR release_year = $4::int)
+ AND ($5::release_season_t IS NULL OR release_season = $5::release_season_t)
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
- 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
- END
- END DESC,
+ -- Основной ключ: выбранное поле
+ CASE
+ WHEN $6::boolean AND $7::text = 'id' THEN id
+ WHEN $6::boolean AND $7::text = 'year' THEN release_year
+ WHEN $6::boolean AND $7::text = 'rating' THEN rating
+ -- WHEN sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'views' THEN views
+ END ASC,
+ CASE
+ WHEN NOT $6::boolean AND $7::text = 'id' THEN id
+ WHEN NOT $6::boolean AND $7::text = 'year' THEN release_year
+ WHEN NOT $6::boolean AND $7::text = 'rating' THEN rating
+ -- WHEN NOT sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'views' THEN views
+ END DESC,
- CASE WHEN $2::text <> 'id' THEN t.id END ASC
-
-LIMIT COALESCE($11::int, 100)
+ -- Вторичный ключ: id — только если НЕ сортируем по id
+ CASE
+ 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 {
- 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"`
- TitleStatuses []TitleStatusT `json:"title_statuses"`
+ Status *TitleStatusT `json:"status"`
Rating *float64 `json:"rating"`
ReleaseYear *int32 `json:"release_year"`
ReleaseSeason *ReleaseSeasonT `json:"release_season"`
+ Forward bool `json:"forward"`
+ SortBy string `json:"sort_by"`
Limit *int32 `json:"limit"`
}
-type SearchTitlesRow struct {
- ID int64 `json:"id"`
- TitleNames json.RawMessage `json:"title_names"`
- 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"`
- TitleStorageType *StorageTypeT `json:"title_storage_type"`
- TitleImagePath *string `json:"title_image_path"`
- TagNames json.RawMessage `json:"tag_names"`
- StudioName *string `json:"studio_name"`
-}
-
-func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]SearchTitlesRow, error) {
+func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]Title, error) {
rows, err := q.db.Query(ctx, searchTitles,
- arg.Forward,
- arg.SortBy,
- arg.CursorYear,
- arg.CursorID,
- arg.CursorRating,
arg.Word,
- arg.TitleStatuses,
+ arg.Status,
arg.Rating,
arg.ReleaseYear,
arg.ReleaseSeason,
+ arg.Forward,
+ arg.SortBy,
arg.Limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
- items := []SearchTitlesRow{}
+ var items []Title
for rows.Next() {
- var i SearchTitlesRow
+ var i Title
if err := rows.Scan(
&i.ID,
&i.TitleNames,
+ &i.StudioID,
&i.PosterID,
&i.TitleStatus,
&i.Rating,
@@ -623,10 +382,7 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]S
&i.Season,
&i.EpisodesAired,
&i.EpisodesAll,
- &i.TitleStorageType,
- &i.TitleImagePath,
- &i.TagNames,
- &i.StudioName,
+ &i.EpisodesLen,
); err != nil {
return nil, err
}
@@ -640,217 +396,102 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]S
const searchUserTitles = `-- name: SearchUserTitles :many
-SELECT
- t.id as id,
- t.title_names as title_names,
- t.poster_id as poster_id,
- t.title_status as title_status,
- t.rating as rating,
- t.rating_count as rating_count,
- t.release_year as release_year,
- t.release_season as release_season,
- t.season as season,
- t.episodes_aired as episodes_aired,
- t.episodes_all as episodes_all,
- 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 as title_storage_type,
- i.image_path as title_image_path,
- COALESCE(
- jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL),
- '[]'::jsonb
- )::jsonb as tag_names,
- s.studio_name as studio_name
-
-FROM usertitles as u
-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)
-
-WHERE
- u.user_id = $1::bigint
- AND
- CASE
- WHEN $2::boolean THEN
- -- forward: greater than cursor (next page)
- CASE $3::text
- WHEN 'year' THEN
- ($4::int IS NULL) OR
- (t.release_year > $4::int) OR
- (t.release_year = $4::int AND t.id > $5::bigint)
-
- WHEN 'rating' THEN
- ($6::float IS NULL) OR
- (t.rating > $6::float) OR
- (t.rating = $6::float AND t.id > $5::bigint)
-
- WHEN 'id' THEN
- ($5::bigint IS NULL) OR
- (t.id > $5::bigint)
-
- ELSE true -- fallback
- END
-
- ELSE
- -- backward: less than cursor (prev page)
- CASE $3::text
- WHEN 'year' THEN
- ($4::int IS NULL) OR
- (t.release_year < $4::int) OR
- (t.release_year = $4::int AND t.id < $5::bigint)
-
- WHEN 'rating' THEN
- ($6::float IS NULL) OR
- (t.rating < $6::float) OR
- (t.rating = $6::float AND t.id < $5::bigint)
-
- WHEN 'id' THEN
- ($5::bigint IS NULL) OR
- (t.id < $5::bigint)
-
- ELSE true
- END
+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
END
- AND (
- CASE
- WHEN $7::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($7::text, ' ')) AS w
- WHERE trim(w) <> ''
- )
- ) AS pattern
- )
- ELSE true
- 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 (
- $8::title_status_t[] IS NULL
- OR array_length($8::title_status_t[], 1) IS NULL
- OR array_length($8::title_status_t[], 1) = 0
- OR t.title_status = ANY($8::title_status_t[])
- )
- AND (
- $9::usertitle_status_t[] IS NULL
- OR array_length($9::usertitle_status_t[], 1) IS NULL
- OR array_length($9::usertitle_status_t[], 1) = 0
- OR u.status = ANY($9::usertitle_status_t[])
- )
- AND ($10::int IS NULL OR u.rate >= $10::int)
- AND ($11::float IS NULL OR t.rating >= $11::float)
- AND ($12::int IS NULL OR t.release_year = $12::int)
- AND ($13::release_season_t IS NULL OR t.release_season = $13::release_season_t)
-
-GROUP BY
- t.id, u.user_id, u.status, u.rate, u.review_id, u.ctime, i.id, s.id
-
-ORDER BY
- CASE WHEN $2::boolean THEN
- CASE
- WHEN $3::text = 'id' THEN t.id
- WHEN $3::text = 'year' THEN t.release_year
- WHEN $3::text = 'rating' THEN t.rating
- WHEN $3::text = 'rate' THEN u.rate
- END
- END ASC,
- CASE WHEN NOT $2::boolean THEN
- CASE
- WHEN $3::text = 'id' THEN t.id
- WHEN $3::text = 'year' THEN t.release_year
- WHEN $3::text = 'rating' THEN t.rating
- WHEN $3::text = 'rate' THEN u.rate
- END
- END DESC,
-
- CASE WHEN $3::text <> 'id' THEN t.id END ASC
-
-LIMIT COALESCE($14::int, 100)
+LIMIT COALESCE($7::int, 100)
`
type SearchUserTitlesParams struct {
- UserID int64 `json:"user_id"`
- 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"`
- TitleStatuses []TitleStatusT `json:"title_statuses"`
- UsertitleStatuses []UsertitleStatusT `json:"usertitle_statuses"`
- Rate *int32 `json:"rate"`
- Rating *float64 `json:"rating"`
- ReleaseYear *int32 `json:"release_year"`
- ReleaseSeason *ReleaseSeasonT `json:"release_season"`
- Limit *int32 `json:"limit"`
+ 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"`
}
type SearchUserTitlesRow struct {
- ID int64 `json:"id"`
- TitleNames json.RawMessage `json:"title_names"`
- 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"`
- UserID int64 `json:"user_id"`
- UsertitleStatus UsertitleStatusT `json:"usertitle_status"`
- UserRate *int32 `json:"user_rate"`
- ReviewID *int64 `json:"review_id"`
- UserCtime time.Time `json:"user_ctime"`
- TitleStorageType *StorageTypeT `json:"title_storage_type"`
- TitleImagePath *string `json:"title_image_path"`
- TagNames json.RawMessage `json:"tag_names"`
- StudioName *string `json:"studio_name"`
+ 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"`
}
// 100 is default limit
+// OFFSET sqlc.narg('offset')::int;
func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesParams) ([]SearchUserTitlesRow, error) {
rows, err := q.db.Query(ctx, searchUserTitles,
- arg.UserID,
- arg.Forward,
- arg.SortBy,
- arg.CursorYear,
- arg.CursorID,
- arg.CursorRating,
arg.Word,
- arg.TitleStatuses,
- arg.UsertitleStatuses,
- arg.Rate,
+ arg.Status,
arg.Rating,
arg.ReleaseYear,
arg.ReleaseSeason,
+ arg.UsertitleStatus,
arg.Limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
- items := []SearchUserTitlesRow{}
+ var items []SearchUserTitlesRow
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,
&i.PosterID,
&i.TitleStatus,
&i.Rating,
@@ -860,15 +501,7 @@ func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesPara
&i.Season,
&i.EpisodesAired,
&i.EpisodesAll,
- &i.UserID,
- &i.UsertitleStatus,
- &i.UserRate,
- &i.ReviewID,
- &i.UserCtime,
- &i.TitleStorageType,
- &i.TitleImagePath,
- &i.TagNames,
- &i.StudioName,
+ &i.EpisodesLen,
); err != nil {
return nil, err
}
@@ -879,91 +512,3 @@ func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesPara
}
return items, nil
}
-
-const updateUser = `-- name: UpdateUser :one
-UPDATE users
-SET
- avatar_id = COALESCE($1, avatar_id),
- disp_name = COALESCE($2, disp_name),
- user_desc = COALESCE($3, user_desc),
- mail = COALESCE($4, mail)
-WHERE id = $5
-RETURNING id, avatar_id, nickname, disp_name, user_desc, creation_date, mail
-`
-
-type UpdateUserParams struct {
- AvatarID *int64 `json:"avatar_id"`
- DispName *string `json:"disp_name"`
- UserDesc *string `json:"user_desc"`
- Mail *string `json:"mail"`
- UserID int64 `json:"user_id"`
-}
-
-type UpdateUserRow struct {
- ID int64 `json:"id"`
- AvatarID *int64 `json:"avatar_id"`
- Nickname string `json:"nickname"`
- DispName *string `json:"disp_name"`
- UserDesc *string `json:"user_desc"`
- CreationDate time.Time `json:"creation_date"`
- Mail *string `json:"mail"`
-}
-
-func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (UpdateUserRow, error) {
- row := q.db.QueryRow(ctx, updateUser,
- arg.AvatarID,
- arg.DispName,
- arg.UserDesc,
- arg.Mail,
- arg.UserID,
- )
- var i UpdateUserRow
- err := row.Scan(
- &i.ID,
- &i.AvatarID,
- &i.Nickname,
- &i.DispName,
- &i.UserDesc,
- &i.CreationDate,
- &i.Mail,
- )
- return i, err
-}
-
-const updateUserTitle = `-- name: UpdateUserTitle :one
-UPDATE usertitles
-SET
- status = COALESCE($1::usertitle_status_t, status),
- rate = COALESCE($2::int, rate)
-WHERE
- user_id = $3
- AND title_id = $4
-RETURNING user_id, title_id, status, rate, review_id, ctime
-`
-
-type UpdateUserTitleParams struct {
- Status *UsertitleStatusT `json:"status"`
- Rate *int32 `json:"rate"`
- UserID int64 `json:"user_id"`
- TitleID int64 `json:"title_id"`
-}
-
-// Fails with sql.ErrNoRows if (user_id, title_id) not found
-func (q *Queries) UpdateUserTitle(ctx context.Context, arg UpdateUserTitleParams) (Usertitle, error) {
- row := q.db.QueryRow(ctx, updateUserTitle,
- arg.Status,
- arg.Rate,
- arg.UserID,
- arg.TitleID,
- )
- var i Usertitle
- err := row.Scan(
- &i.UserID,
- &i.TitleID,
- &i.Status,
- &i.Rate,
- &i.ReviewID,
- &i.Ctime,
- )
- return i, err
-}
diff --git a/sql/sqlc.yaml b/sql/sqlc.yaml
index 904abaf..f74d2ad 100644
--- a/sql/sqlc.yaml
+++ b/sql/sqlc.yaml
@@ -3,7 +3,6 @@ sql:
- engine: "postgresql"
queries:
- "../modules/backend/queries.sql"
- - "../modules/auth/queries.sql"
schema: "migrations"
gen:
go:
@@ -13,20 +12,7 @@ sql:
sql_driver: "github.com/jackc/pgx/v5"
emit_json_tags: true
emit_pointers_for_null_types: true
- emit_empty_slices: true #slices returned by :many queries will be empty instead of nil
overrides:
- - db_type: "usertitle_status_t"
- nullable: true
- go_type:
- type: "UsertitleStatusT"
- pointer: true
- - db_type: "storage_type_t"
- nullable: true
- go_type:
- type: "StorageTypeT"
- pointer: true
- - db_type: "jsonb"
- go_type: "encoding/json.RawMessage"
- db_type: "uuid"
nullable: false
go_type: