Compare commits

...

5 commits

6 changed files with 292 additions and 141 deletions

View file

@ -34,6 +34,21 @@ const (
// ReleaseSeason Title release season // ReleaseSeason Title release season
type ReleaseSeason string type ReleaseSeason string
// Studio defines model for Studio.
type Studio struct {
Description *string `json:"description,omitempty"`
Id *int64 `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
PosterId *int64 `json:"poster_id,omitempty"`
PosterPath *string `json:"poster_path,omitempty"`
}
// Tag A localized tag: keys are language codes (ISO 639-1), values are tag names
type Tag map[string]string
// Tags Array of localized tags
type Tags = []Tag
// Title defines model for Title. // Title defines model for Title.
type Title struct { type Title struct {
EpisodesAired *int32 `json:"episodes_aired,omitempty"` EpisodesAired *int32 `json:"episodes_aired,omitempty"`
@ -41,7 +56,7 @@ type Title struct {
EpisodesLen *map[string]float64 `json:"episodes_len,omitempty"` EpisodesLen *map[string]float64 `json:"episodes_len,omitempty"`
// Id Unique title ID (primary key) // Id Unique title ID (primary key)
Id *int64 `json:"id,omitempty"` Id int64 `json:"id"`
PosterId *int64 `json:"poster_id,omitempty"` PosterId *int64 `json:"poster_id,omitempty"`
Rating *float64 `json:"rating,omitempty"` Rating *float64 `json:"rating,omitempty"`
RatingCount *int32 `json:"rating_count,omitempty"` RatingCount *int32 `json:"rating_count,omitempty"`
@ -49,10 +64,13 @@ type Title struct {
// ReleaseSeason Title release season // ReleaseSeason Title release season
ReleaseSeason *ReleaseSeason `json:"release_season,omitempty"` ReleaseSeason *ReleaseSeason `json:"release_season,omitempty"`
ReleaseYear *int32 `json:"release_year,omitempty"` ReleaseYear *int32 `json:"release_year,omitempty"`
StudioId *int64 `json:"studio_id,omitempty"` Studio *Studio `json:"studio,omitempty"`
// Tags Array of localized tags
Tags Tags `json:"tags"`
// TitleNames Localized titles. Key = language (ISO 639-1), value = list of names // TitleNames Localized titles. Key = language (ISO 639-1), value = list of names
TitleNames *map[string][]string `json:"title_names,omitempty"` TitleNames map[string][]string `json:"title_names"`
// TitleStatus Title status // TitleStatus Title status
TitleStatus *TitleStatus `json:"title_status,omitempty"` TitleStatus *TitleStatus `json:"title_status,omitempty"`
@ -93,8 +111,8 @@ type GetTitleParams struct {
Rating *float64 `form:"rating,omitempty" json:"rating,omitempty"` Rating *float64 `form:"rating,omitempty" json:"rating,omitempty"`
ReleaseYear *int32 `form:"release_year,omitempty" json:"release_year,omitempty"` ReleaseYear *int32 `form:"release_year,omitempty" json:"release_year,omitempty"`
ReleaseSeason *ReleaseSeason `form:"release_season,omitempty" json:"release_season,omitempty"` ReleaseSeason *ReleaseSeason `form:"release_season,omitempty" json:"release_season,omitempty"`
Limit *int `form:"limit,omitempty" json:"limit,omitempty"` Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"`
Offset *int `form:"offset,omitempty" json:"offset,omitempty"` Offset *int32 `form:"offset,omitempty" json:"offset,omitempty"`
Fields *string `form:"fields,omitempty" json:"fields,omitempty"` Fields *string `form:"fields,omitempty" json:"fields,omitempty"`
} }
@ -103,9 +121,6 @@ type GetUsersUserIdParams struct {
Fields *string `form:"fields,omitempty" json:"fields,omitempty"` Fields *string `form:"fields,omitempty" json:"fields,omitempty"`
} }
// PostUsersJSONRequestBody defines body for PostUsers for application/json ContentType.
type PostUsersJSONRequestBody = User
// Getter for additional properties for Title. Returns the specified // Getter for additional properties for Title. Returns the specified
// element and whether it was found // element and whether it was found
func (a Title) Get(fieldName string) (value interface{}, found bool) { func (a Title) Get(fieldName string) (value interface{}, found bool) {
@ -203,12 +218,20 @@ func (a *Title) UnmarshalJSON(b []byte) error {
delete(object, "release_year") delete(object, "release_year")
} }
if raw, found := object["studio_id"]; found { if raw, found := object["studio"]; found {
err = json.Unmarshal(raw, &a.StudioId) err = json.Unmarshal(raw, &a.Studio)
if err != nil { if err != nil {
return fmt.Errorf("error reading 'studio_id': %w", err) return fmt.Errorf("error reading 'studio': %w", err)
} }
delete(object, "studio_id") delete(object, "studio")
}
if raw, found := object["tags"]; found {
err = json.Unmarshal(raw, &a.Tags)
if err != nil {
return fmt.Errorf("error reading 'tags': %w", err)
}
delete(object, "tags")
} }
if raw, found := object["title_names"]; found { if raw, found := object["title_names"]; found {
@ -267,11 +290,9 @@ func (a Title) MarshalJSON() ([]byte, error) {
} }
} }
if a.Id != nil { object["id"], err = json.Marshal(a.Id)
object["id"], err = json.Marshal(a.Id) if err != nil {
if err != nil { return nil, fmt.Errorf("error marshaling 'id': %w", err)
return nil, fmt.Errorf("error marshaling 'id': %w", err)
}
} }
if a.PosterId != nil { if a.PosterId != nil {
@ -309,18 +330,21 @@ func (a Title) MarshalJSON() ([]byte, error) {
} }
} }
if a.StudioId != nil { if a.Studio != nil {
object["studio_id"], err = json.Marshal(a.StudioId) object["studio"], err = json.Marshal(a.Studio)
if err != nil { if err != nil {
return nil, fmt.Errorf("error marshaling 'studio_id': %w", err) return nil, fmt.Errorf("error marshaling 'studio': %w", err)
} }
} }
if a.TitleNames != nil { object["tags"], err = json.Marshal(a.Tags)
object["title_names"], err = json.Marshal(a.TitleNames) if err != nil {
if err != nil { return nil, fmt.Errorf("error marshaling 'tags': %w", err)
return nil, fmt.Errorf("error marshaling 'title_names': %w", err) }
}
object["title_names"], err = json.Marshal(a.TitleNames)
if err != nil {
return nil, fmt.Errorf("error marshaling 'title_names': %w", err)
} }
if a.TitleStatus != nil { if a.TitleStatus != nil {
@ -344,9 +368,6 @@ type ServerInterface interface {
// Get titles // Get titles
// (GET /title) // (GET /title)
GetTitle(c *gin.Context, params GetTitleParams) GetTitle(c *gin.Context, params GetTitleParams)
// Add new user
// (POST /users)
PostUsers(c *gin.Context)
// Get user info // Get user info
// (GET /users/{user_id}) // (GET /users/{user_id})
GetUsersUserId(c *gin.Context, userId string, params GetUsersUserIdParams) GetUsersUserId(c *gin.Context, userId string, params GetUsersUserIdParams)
@ -443,19 +464,6 @@ func (siw *ServerInterfaceWrapper) GetTitle(c *gin.Context) {
siw.Handler.GetTitle(c, params) siw.Handler.GetTitle(c, params)
} }
// PostUsers operation middleware
func (siw *ServerInterfaceWrapper) PostUsers(c *gin.Context) {
for _, middleware := range siw.HandlerMiddlewares {
middleware(c)
if c.IsAborted() {
return
}
}
siw.Handler.PostUsers(c)
}
// GetUsersUserId operation middleware // GetUsersUserId operation middleware
func (siw *ServerInterfaceWrapper) GetUsersUserId(c *gin.Context) { func (siw *ServerInterfaceWrapper) GetUsersUserId(c *gin.Context) {
@ -519,7 +527,6 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options
} }
router.GET(options.BaseURL+"/title", wrapper.GetTitle) router.GET(options.BaseURL+"/title", wrapper.GetTitle)
router.POST(options.BaseURL+"/users", wrapper.PostUsers)
router.GET(options.BaseURL+"/users/:user_id", wrapper.GetUsersUserId) router.GET(options.BaseURL+"/users/:user_id", wrapper.GetUsersUserId)
} }
@ -564,27 +571,6 @@ func (response GetTitle500Response) VisitGetTitleResponse(w http.ResponseWriter)
return nil return nil
} }
type PostUsersRequestObject struct {
Body *PostUsersJSONRequestBody
}
type PostUsersResponseObject interface {
VisitPostUsersResponse(w http.ResponseWriter) error
}
type PostUsers200JSONResponse struct {
Error *string `json:"error,omitempty"`
Success *bool `json:"success,omitempty"`
UserJson *User `json:"user_json,omitempty"`
}
func (response PostUsers200JSONResponse) VisitPostUsersResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type GetUsersUserIdRequestObject struct { type GetUsersUserIdRequestObject struct {
UserId string `json:"user_id"` UserId string `json:"user_id"`
Params GetUsersUserIdParams Params GetUsersUserIdParams
@ -632,9 +618,6 @@ type StrictServerInterface interface {
// Get titles // Get titles
// (GET /title) // (GET /title)
GetTitle(ctx context.Context, request GetTitleRequestObject) (GetTitleResponseObject, error) GetTitle(ctx context.Context, request GetTitleRequestObject) (GetTitleResponseObject, error)
// Add new user
// (POST /users)
PostUsers(ctx context.Context, request PostUsersRequestObject) (PostUsersResponseObject, error)
// Get user info // Get user info
// (GET /users/{user_id}) // (GET /users/{user_id})
GetUsersUserId(ctx context.Context, request GetUsersUserIdRequestObject) (GetUsersUserIdResponseObject, error) GetUsersUserId(ctx context.Context, request GetUsersUserIdRequestObject) (GetUsersUserIdResponseObject, error)
@ -679,39 +662,6 @@ func (sh *strictHandler) GetTitle(ctx *gin.Context, params GetTitleParams) {
} }
} }
// PostUsers operation middleware
func (sh *strictHandler) PostUsers(ctx *gin.Context) {
var request PostUsersRequestObject
var body PostUsersJSONRequestBody
if err := ctx.ShouldBindJSON(&body); err != nil {
ctx.Status(http.StatusBadRequest)
ctx.Error(err)
return
}
request.Body = &body
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
return sh.ssi.PostUsers(ctx, request.(PostUsersRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "PostUsers")
}
response, err := handler(ctx, request)
if err != nil {
ctx.Error(err)
ctx.Status(http.StatusInternalServerError)
} else if validResponse, ok := response.(PostUsersResponseObject); ok {
if err := validResponse.VisitPostUsersResponse(ctx.Writer); err != nil {
ctx.Error(err)
}
} else if response != nil {
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
}
}
// GetUsersUserId operation middleware // GetUsersUserId operation middleware
func (sh *strictHandler) GetUsersUserId(ctx *gin.Context, userId string, params GetUsersUserIdParams) { func (sh *strictHandler) GetUsersUserId(ctx *gin.Context, userId string, params GetUsersUserIdParams) {
var request GetUsersUserIdRequestObject var request GetUsersUserIdRequestObject

View file

@ -2,8 +2,10 @@ openapi: 3.1.1
info: info:
title: Titles, Users, Reviews, Tags, and Media API title: Titles, Users, Reviews, Tags, and Media API
version: 1.0.0 version: 1.0.0
servers: servers:
- url: /api/v1 - url: /api/v1
paths: paths:
/title: /title:
get: get:
@ -35,11 +37,13 @@ paths:
name: limit name: limit
schema: schema:
type: integer type: integer
format: int32
default: 10 default: 10
- in: query - in: query
name: offset name: offset
schema: schema:
type: integer type: integer
format: int32
default: 0 default: 0
- in: query - in: query
name: fields name: fields
@ -61,6 +65,7 @@ paths:
description: Request params are not correct description: Request params are not correct
'500': '500':
description: Unknown server error description: Unknown server error
# /title/{title_id}: # /title/{title_id}:
# get: # get:
# summary: Get title description # summary: Get title description
@ -243,28 +248,28 @@ paths:
# items: # items:
# $ref: '#/components/schemas/User' # $ref: '#/components/schemas/User'
post: # post:
summary: Add new user # summary: Add new user
requestBody: # requestBody:
required: true # required: true
content: # content:
application/json: # application/json:
schema: # schema:
$ref: '#/components/schemas/User' # $ref: '#/components/schemas/User'
responses: # responses:
'200': # '200':
description: Add result # description: Add result
content: # content:
application/json: # application/json:
schema: # schema:
type: object # type: object
properties: # properties:
success: # success:
type: boolean # type: boolean
error: # error:
type: string # type: string
user_json: # user_json:
$ref: '#/components/schemas/User' # $ref: '#/components/schemas/User'
# /users/{user_id}/titles: # /users/{user_id}/titles:
# get: # get:
@ -567,6 +572,7 @@ components:
- finished - finished
- ongoing - ongoing
- planned - planned
ReleaseSeason: ReleaseSeason:
type: string type: string
description: Title release season description: Title release season
@ -575,6 +581,7 @@ components:
- spring - spring
- summer - summer
- fall - fall
UserTitleStatus: UserTitleStatus:
type: string type: string
description: User's title status description: User's title status
@ -583,8 +590,57 @@ components:
- planned - planned
- dropped - dropped
- in-progress - in-progress
Review:
type: object
additionalProperties: true
Tag:
type: object
description: "A localized tag: keys are language codes (ISO 639-1), values are tag names"
additionalProperties:
type: string
example:
en: "Shojo"
ru: "Сёдзё"
ja: "少女"
Tags:
type: array
description: "Array of localized tags"
items:
$ref: '#/components/schemas/Tag'
example:
- en: "Shojo"
ru: "Сёдзё"
ja: "少女"
- en: "Shounen"
ru: "Сёнен"
ja: "少年"
Studio:
type: object
properties:
id:
type: integer
format: int64
name:
type: string
poster_id:
type: integer
format: int64
poster_path:
type: string
description:
type: string
Title: Title:
type: object type: object
required:
- id
- title_names
- tags
properties: properties:
id: id:
type: integer type: integer
@ -605,9 +661,10 @@ components:
en: ["Attack on Titan", "AoT"] en: ["Attack on Titan", "AoT"]
ru: ["Атака титанов", "Титаны"] ru: ["Атака титанов", "Титаны"]
ja: ["進撃の巨人"] ja: ["進撃の巨人"]
studio_id: studio:
type: integer $ref: '#/components/schemas/Studio'
format: int64 tags:
$ref: '#/components/schemas/Tags'
poster_id: poster_id:
type: integer type: integer
format: int64 format: int64
@ -636,6 +693,7 @@ components:
type: number type: number
format: double format: double
additionalProperties: true additionalProperties: true
User: User:
type: object type: object
properties: properties:
@ -679,12 +737,7 @@ components:
- user_id - user_id
- nickname - nickname
# - creation_date # - creation_date
UserTitle: UserTitle:
type: object type: object
additionalProperties: true additionalProperties: true
Review:
type: object
additionalProperties: true
Tag:
type: object
additionalProperties: true

View file

@ -57,21 +57,25 @@ func ReleaseSeason2sqlc(s *oapi.ReleaseSeason) (*sqlc.ReleaseSeasonT, error) {
return &t, nil return &t, nil
} }
type TileNames *map[string][]string
// unmarshall jsonb to map[string][]string // unmarshall jsonb to map[string][]string
func jsonb2map4names(b []byte) (*map[string][]string, error) { func jsonb2TitleNames(b []byte) (TileNames, error) {
var t map[string][]string var t TileNames
if err := json.Unmarshal(b, &t); err != nil { if err := json.Unmarshal(b, t); err != nil {
return nil, fmt.Errorf("invalid title_names JSON for title: %w", err) return nil, fmt.Errorf("invalid title_names JSON for title: %w", err)
} }
return &t, nil return t, nil
} }
func jsonb2map4len(b []byte) (*map[string]float64, error) { type EpisodeLens *map[string]float64
var t map[string]float64
if err := json.Unmarshal(b, &t); err != nil { func jsonb2EpisodeLens(b []byte) (EpisodeLens, error) {
var t EpisodeLens
if err := json.Unmarshal(b, t); err != nil {
return nil, fmt.Errorf("invalid episodes_len JSON for title: %w", err) return nil, fmt.Errorf("invalid episodes_len JSON for title: %w", err)
} }
return &t, nil return t, nil
} }
func (s Server) GetTitle(ctx context.Context, request oapi.GetTitleRequestObject) (oapi.GetTitleResponseObject, error) { func (s Server) GetTitle(ctx context.Context, request oapi.GetTitleRequestObject) (oapi.GetTitleResponseObject, error) {
@ -95,6 +99,8 @@ func (s Server) GetTitle(ctx context.Context, request oapi.GetTitleRequestObject
Rating: request.Params.Rating, Rating: request.Params.Rating,
ReleaseYear: request.Params.ReleaseYear, ReleaseYear: request.Params.ReleaseYear,
ReleaseSeason: season, ReleaseSeason: season,
Offset: request.Params.Offset,
Limit: request.Params.Limit,
}) })
if err != nil { if err != nil {
return oapi.GetTitle500Response{}, nil return oapi.GetTitle500Response{}, nil
@ -104,12 +110,12 @@ func (s Server) GetTitle(ctx context.Context, request oapi.GetTitleRequestObject
} }
for _, title := range titles { for _, title := range titles {
title_names, err := jsonb2map4names(title.TitleNames) title_names, err := jsonb2TitleNames(title.TitleNames)
if err != nil { if err != nil {
log.Errorf("%v", err) log.Errorf("%v", err)
return oapi.GetTitle500Response{}, err return oapi.GetTitle500Response{}, err
} }
episodes_lens, err := jsonb2map4len(title.EpisodesLen) episodes_lens, err := jsonb2EpisodeLens(title.EpisodesLen)
if err != nil { if err != nil {
log.Errorf("%v", err) log.Errorf("%v", err)
return oapi.GetTitle500Response{}, err return oapi.GetTitle500Response{}, err
@ -122,6 +128,7 @@ func (s Server) GetTitle(ctx context.Context, request oapi.GetTitleRequestObject
ReleaseSeason: (*oapi.ReleaseSeason)(title.ReleaseSeason), ReleaseSeason: (*oapi.ReleaseSeason)(title.ReleaseSeason),
ReleaseYear: title.ReleaseYear, ReleaseYear: title.ReleaseYear,
StudioId: &title.StudioID, StudioId: &title.StudioID,
// StudioName: ,
TitleNames: title_names, TitleNames: title_names,
TitleStatus: (*oapi.TitleStatus)(&title.TitleStatus), TitleStatus: (*oapi.TitleStatus)(&title.TitleStatus),
EpisodesAired: title.EpisodesAired, EpisodesAired: title.EpisodesAired,

View file

@ -25,7 +25,7 @@ import (
func mapUser(u sqlc.GetUserByIDRow) oapi.User { func mapUser(u sqlc.GetUserByIDRow) oapi.User {
return oapi.User{ return oapi.User{
AvatarId: u.AvatarID, AvatarId: u.AvatarID,
CreationDate: u.CreationDate, CreationDate: &u.CreationDate,
DispName: u.DispName, DispName: u.DispName,
Id: &u.ID, Id: &u.ID,
Mail: (*types.Email)(u.Mail), Mail: (*types.Email)(u.Mail),

View file

@ -13,6 +13,40 @@ SELECT id, avatar_id, mail, nickname, disp_name, user_desc, creation_date
FROM users FROM users
WHERE id = $1; WHERE id = $1;
-- name: GetStudioByID :one
SELECT *
FROM studios
WHERE id = sqlc.arg('studio_id')::int;
-- name: InsertStudio :one
INSERT INTO studios (studio_name, illust_id, studio_desc)
VALUES (
sqlc.arg('studio_name')::text,
sqlc.narg('illust_id')::bigint,
sqlc.narg('studio_desc')::text)
RETURNING id, studio_name, illust_id, studio_desc;
-- name: GetTitleTags :many
SELECT
tag_names
FROM tags as g
JOIN title_tags as t ON(t.tag_id = g.id)
WHERE t.title_id = sqlc.arg('title_id')::bigint;
-- name: InsertTitleTags :one
INSERT INTO title_tags (title_id, tag_id)
VALUES (
sqlc.arg('title_id')::bigint,
sqlc.arg('tag_id')::bigint)
RETURNING title_id, tag_id;
-- name: InsertTag :one
INSERT INTO tags (tag_names)
VALUES (
sqlc.arg('tag_names')::jsonb)
RETURNING id, tag_names;
-- -- name: ListUsers :many -- -- name: ListUsers :many
-- SELECT user_id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date -- SELECT user_id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date
-- FROM users -- FROM users

View file

@ -41,6 +41,52 @@ func (q *Queries) GetImageByID(ctx context.Context, id int64) (Image, error) {
return i, err return i, err
} }
const getStudioByID = `-- name: GetStudioByID :one
SELECT id, studio_name, illust_id, studio_desc
FROM studios
WHERE id = $1::int
`
func (q *Queries) GetStudioByID(ctx context.Context, studioID int32) (Studio, error) {
row := q.db.QueryRow(ctx, getStudioByID, studioID)
var i Studio
err := row.Scan(
&i.ID,
&i.StudioName,
&i.IllustID,
&i.StudioDesc,
)
return i, err
}
const getTitleTags = `-- name: GetTitleTags :many
SELECT
tag_names
FROM tags as g
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) ([][]byte, error) {
rows, err := q.db.Query(ctx, getTitleTags, titleID)
if err != nil {
return nil, err
}
defer rows.Close()
var items [][]byte
for rows.Next() {
var tag_names []byte
if err := rows.Scan(&tag_names); err != nil {
return nil, err
}
items = append(items, tag_names)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUserByID = `-- name: GetUserByID :one const getUserByID = `-- name: GetUserByID :one
SELECT id, avatar_id, mail, nickname, disp_name, user_desc, creation_date SELECT id, avatar_id, mail, nickname, disp_name, user_desc, creation_date
FROM users FROM users
@ -72,6 +118,67 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (GetUserByIDRow, er
return i, err return i, err
} }
const insertStudio = `-- name: InsertStudio :one
INSERT INTO studios (studio_name, illust_id, studio_desc)
VALUES (
$1::text,
$2::bigint,
$3::text)
RETURNING id, studio_name, illust_id, studio_desc
`
type InsertStudioParams struct {
StudioName string `json:"studio_name"`
IllustID *int64 `json:"illust_id"`
StudioDesc *string `json:"studio_desc"`
}
func (q *Queries) InsertStudio(ctx context.Context, arg InsertStudioParams) (Studio, error) {
row := q.db.QueryRow(ctx, insertStudio, arg.StudioName, arg.IllustID, arg.StudioDesc)
var i Studio
err := row.Scan(
&i.ID,
&i.StudioName,
&i.IllustID,
&i.StudioDesc,
)
return i, err
}
const insertTag = `-- name: InsertTag :one
INSERT INTO tags (tag_names)
VALUES (
$1::jsonb)
RETURNING id, tag_names
`
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)
return i, err
}
const insertTitleTags = `-- name: InsertTitleTags :one
INSERT INTO title_tags (title_id, tag_id)
VALUES (
$1::bigint,
$2::bigint)
RETURNING title_id, tag_id
`
type InsertTitleTagsParams struct {
TitleID int64 `json:"title_id"`
TagID int64 `json:"tag_id"`
}
func (q *Queries) InsertTitleTags(ctx context.Context, arg InsertTitleTagsParams) (TitleTag, error) {
row := q.db.QueryRow(ctx, insertTitleTags, arg.TitleID, arg.TagID)
var i TitleTag
err := row.Scan(&i.TitleID, &i.TagID)
return i, err
}
const searchTitles = `-- name: SearchTitles :many const searchTitles = `-- name: SearchTitles :many