feat: Phase 2 — database layer

- PostgreSQL schema: sources, manga, chapters, pages, source_meta with indexes
- golang-migrate runner with embedded SQL via go:embed (pgx5:// scheme)
- sqlc-generated type-safe queries for all tables (pgx/v5 native)
- Config package with all env vars including TTL durations
- Wire DB init and config into cmd/server/main.go
This commit is contained in:
achmad
2026-05-10 21:32:40 +07:00
parent 85d2ea6143
commit 95cab106d8
20 changed files with 1291 additions and 139 deletions
+47
View File
@@ -0,0 +1,47 @@
package config
import (
"os"
"strconv"
"time"
)
type Config struct {
DatabaseURL string
FlareSolverrURL string
Addr string
MangaListTTL time.Duration
MangaDetailTTL time.Duration
ChapterListTTL time.Duration
DBMaxConns int
DBMinConns int
}
func Load() Config {
return Config{
DatabaseURL: os.Getenv("DATABASE_URL"),
FlareSolverrURL: os.Getenv("FLARESOLVERR_URL"),
Addr: envStr("ADDR", ":8080"),
MangaListTTL: time.Duration(envInt("MANGA_LIST_TTL_SECONDS", 600)) * time.Second,
MangaDetailTTL: time.Duration(envInt("MANGA_DETAIL_TTL_SECONDS", 3600)) * time.Second,
ChapterListTTL: time.Duration(envInt("CHAPTER_LIST_TTL_SECONDS", 600)) * time.Second,
DBMaxConns: envInt("DB_MAX_CONNS", 10),
DBMinConns: envInt("DB_MIN_CONNS", 2),
}
}
func envStr(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func envInt(key string, def int) int {
if v := os.Getenv(key); v != "" {
if n, err := strconv.Atoi(v); err == nil {
return n
}
}
return def
}
+113
View File
@@ -0,0 +1,113 @@
package db
import (
"context"
"embed"
"fmt"
"log"
"os"
"strconv"
"time"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/pgx/v5"
"github.com/golang-migrate/migrate/v4/source/iofs"
"github.com/jackc/pgx/v5/pgxpool"
"goyomi/internal/db/queries"
)
//go:embed migrations/*.sql
var migrationFS embed.FS
// DB holds the connection pool and sqlc query clients.
type DB struct {
Pool *pgxpool.Pool
Queries *queries.Queries
}
func Open(ctx context.Context) (*DB, error) {
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
return nil, fmt.Errorf("DATABASE_URL not set")
}
cfg, err := pgxpool.ParseConfig(dsn)
if err != nil {
return nil, fmt.Errorf("parse DATABASE_URL: %w", err)
}
cfg.MaxConns = int32(envInt("DB_MAX_CONNS", 10))
cfg.MinConns = int32(envInt("DB_MIN_CONNS", 2))
cfg.HealthCheckPeriod = 30 * time.Second
pool, err := pgxpool.NewWithConfig(ctx, cfg)
if err != nil {
return nil, fmt.Errorf("open pool: %w", err)
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, fmt.Errorf("ping db: %w", err)
}
if err := runMigrations(dsn); err != nil {
pool.Close()
return nil, err
}
return &DB{
Pool: pool,
Queries: queries.New(pool),
}, nil
}
func (d *DB) Close() {
d.Pool.Close()
}
func runMigrations(dsn string) error {
src, err := iofs.New(migrationFS, "migrations")
if err != nil {
return fmt.Errorf("migration source: %w", err)
}
// golang-migrate pgx/v5 driver expects pgx5:// scheme
driverDSN := dsnToMigrateScheme(dsn)
m, err := migrate.NewWithSourceInstance("iofs", src, driverDSN)
if err != nil {
return fmt.Errorf("migrate init: %w", err)
}
defer m.Close()
before, _, _ := m.Version()
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
return fmt.Errorf("migrate up: %w", err)
}
after, _, _ := m.Version()
if before != after {
log.Printf("db: migrated from version %d to %d", before, after)
} else {
log.Printf("db: schema up to date at version %d", after)
}
return nil
}
// dsnToMigrateScheme converts a postgres:// or postgresql:// DSN to pgx5://
// as required by the golang-migrate pgx/v5 driver.
func dsnToMigrateScheme(dsn string) string {
for _, prefix := range []string{"postgresql://", "postgres://"} {
if len(dsn) >= len(prefix) && dsn[:len(prefix)] == prefix {
return "pgx5://" + dsn[len(prefix):]
}
}
return dsn
}
func envInt(key string, def int) int {
if v := os.Getenv(key); v != "" {
if n, err := strconv.Atoi(v); err == nil {
return n
}
}
return def
}
@@ -0,0 +1,5 @@
DROP TABLE IF EXISTS source_meta;
DROP TABLE IF EXISTS pages;
DROP TABLE IF EXISTS chapters;
DROP TABLE IF EXISTS manga;
DROP TABLE IF EXISTS sources;
+71
View File
@@ -0,0 +1,71 @@
CREATE TABLE sources (
id BIGINT PRIMARY KEY,
name VARCHAR(128) NOT NULL,
lang VARCHAR(32) NOT NULL,
is_nsfw BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE TABLE manga (
id SERIAL PRIMARY KEY,
source_id BIGINT NOT NULL REFERENCES sources(id),
url VARCHAR(2048) NOT NULL,
title VARCHAR(512) NOT NULL,
initialized BOOLEAN NOT NULL DEFAULT FALSE,
artist TEXT,
author TEXT,
description TEXT,
genre TEXT,
status INTEGER NOT NULL DEFAULT 0,
thumbnail_url VARCHAR(2048),
thumbnail_last_fetched BIGINT NOT NULL DEFAULT 0,
in_library BOOLEAN NOT NULL DEFAULT FALSE,
in_library_at BIGINT NOT NULL DEFAULT 0,
real_url VARCHAR(2048),
last_fetched_at BIGINT NOT NULL DEFAULT 0,
chapters_last_fetched_at BIGINT NOT NULL DEFAULT 0,
update_strategy VARCHAR(64) NOT NULL DEFAULT 'ALWAYS_UPDATE',
UNIQUE (source_id, url)
);
CREATE INDEX ON manga (source_id);
CREATE INDEX ON manga (last_fetched_at);
CREATE TABLE chapters (
id SERIAL PRIMARY KEY,
manga_id INTEGER NOT NULL REFERENCES manga(id) ON DELETE CASCADE,
url VARCHAR(2048) NOT NULL,
name VARCHAR(512) NOT NULL,
date_upload BIGINT NOT NULL DEFAULT 0,
chapter_number REAL NOT NULL DEFAULT -1,
scanlator VARCHAR(256),
source_order INTEGER NOT NULL,
is_read BOOLEAN NOT NULL DEFAULT FALSE,
is_bookmarked BOOLEAN NOT NULL DEFAULT FALSE,
last_page_read INTEGER NOT NULL DEFAULT 0,
last_read_at BIGINT NOT NULL DEFAULT 0,
fetched_at BIGINT NOT NULL DEFAULT 0,
real_url VARCHAR(2048),
is_downloaded BOOLEAN NOT NULL DEFAULT FALSE,
page_count INTEGER NOT NULL DEFAULT -1,
UNIQUE (manga_id, url)
);
CREATE INDEX ON chapters (manga_id);
CREATE TABLE pages (
id SERIAL PRIMARY KEY,
chapter_id INTEGER NOT NULL REFERENCES chapters(id) ON DELETE CASCADE,
"index" INTEGER NOT NULL,
url VARCHAR(2048) NOT NULL,
image_url TEXT,
UNIQUE (chapter_id, "index")
);
CREATE INDEX ON pages (chapter_id);
CREATE TABLE source_meta (
source_id BIGINT NOT NULL REFERENCES sources(id),
key VARCHAR(256) NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (source_id, key)
);
+23
View File
@@ -0,0 +1,23 @@
-- name: UpsertChapter :one
INSERT INTO chapters (
manga_id, url, name, date_upload, chapter_number, scanlator, source_order, fetched_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
)
ON CONFLICT (manga_id, url) DO UPDATE
SET name = EXCLUDED.name,
date_upload = EXCLUDED.date_upload,
chapter_number = EXCLUDED.chapter_number,
scanlator = EXCLUDED.scanlator,
source_order = EXCLUDED.source_order,
fetched_at = EXCLUDED.fetched_at
RETURNING *;
-- name: GetChapterByID :one
SELECT * FROM chapters WHERE id = $1;
-- name: ListChaptersByManga :many
SELECT * FROM chapters WHERE manga_id = $1 ORDER BY source_order;
-- name: UpdateChapterFetchedAt :exec
UPDATE chapters SET fetched_at = $2 WHERE id = $1;
+153
View File
@@ -0,0 +1,153 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: chapter.sql
package queries
import (
"context"
)
const getChapterByID = `-- name: GetChapterByID :one
SELECT id, manga_id, url, name, date_upload, chapter_number, scanlator, source_order, is_read, is_bookmarked, last_page_read, last_read_at, fetched_at, real_url, is_downloaded, page_count FROM chapters WHERE id = $1
`
func (q *Queries) GetChapterByID(ctx context.Context, id int32) (Chapter, error) {
row := q.db.QueryRow(ctx, getChapterByID, id)
var i Chapter
err := row.Scan(
&i.ID,
&i.MangaID,
&i.Url,
&i.Name,
&i.DateUpload,
&i.ChapterNumber,
&i.Scanlator,
&i.SourceOrder,
&i.IsRead,
&i.IsBookmarked,
&i.LastPageRead,
&i.LastReadAt,
&i.FetchedAt,
&i.RealUrl,
&i.IsDownloaded,
&i.PageCount,
)
return i, err
}
const listChaptersByManga = `-- name: ListChaptersByManga :many
SELECT id, manga_id, url, name, date_upload, chapter_number, scanlator, source_order, is_read, is_bookmarked, last_page_read, last_read_at, fetched_at, real_url, is_downloaded, page_count FROM chapters WHERE manga_id = $1 ORDER BY source_order
`
func (q *Queries) ListChaptersByManga(ctx context.Context, mangaID int32) ([]Chapter, error) {
rows, err := q.db.Query(ctx, listChaptersByManga, mangaID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Chapter
for rows.Next() {
var i Chapter
if err := rows.Scan(
&i.ID,
&i.MangaID,
&i.Url,
&i.Name,
&i.DateUpload,
&i.ChapterNumber,
&i.Scanlator,
&i.SourceOrder,
&i.IsRead,
&i.IsBookmarked,
&i.LastPageRead,
&i.LastReadAt,
&i.FetchedAt,
&i.RealUrl,
&i.IsDownloaded,
&i.PageCount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateChapterFetchedAt = `-- name: UpdateChapterFetchedAt :exec
UPDATE chapters SET fetched_at = $2 WHERE id = $1
`
type UpdateChapterFetchedAtParams struct {
ID int32 `db:"id" json:"id"`
FetchedAt int64 `db:"fetched_at" json:"fetched_at"`
}
func (q *Queries) UpdateChapterFetchedAt(ctx context.Context, arg UpdateChapterFetchedAtParams) error {
_, err := q.db.Exec(ctx, updateChapterFetchedAt, arg.ID, arg.FetchedAt)
return err
}
const upsertChapter = `-- name: UpsertChapter :one
INSERT INTO chapters (
manga_id, url, name, date_upload, chapter_number, scanlator, source_order, fetched_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
)
ON CONFLICT (manga_id, url) DO UPDATE
SET name = EXCLUDED.name,
date_upload = EXCLUDED.date_upload,
chapter_number = EXCLUDED.chapter_number,
scanlator = EXCLUDED.scanlator,
source_order = EXCLUDED.source_order,
fetched_at = EXCLUDED.fetched_at
RETURNING id, manga_id, url, name, date_upload, chapter_number, scanlator, source_order, is_read, is_bookmarked, last_page_read, last_read_at, fetched_at, real_url, is_downloaded, page_count
`
type UpsertChapterParams struct {
MangaID int32 `db:"manga_id" json:"manga_id"`
Url string `db:"url" json:"url"`
Name string `db:"name" json:"name"`
DateUpload int64 `db:"date_upload" json:"date_upload"`
ChapterNumber float32 `db:"chapter_number" json:"chapter_number"`
Scanlator *string `db:"scanlator" json:"scanlator"`
SourceOrder int32 `db:"source_order" json:"source_order"`
FetchedAt int64 `db:"fetched_at" json:"fetched_at"`
}
func (q *Queries) UpsertChapter(ctx context.Context, arg UpsertChapterParams) (Chapter, error) {
row := q.db.QueryRow(ctx, upsertChapter,
arg.MangaID,
arg.Url,
arg.Name,
arg.DateUpload,
arg.ChapterNumber,
arg.Scanlator,
arg.SourceOrder,
arg.FetchedAt,
)
var i Chapter
err := row.Scan(
&i.ID,
&i.MangaID,
&i.Url,
&i.Name,
&i.DateUpload,
&i.ChapterNumber,
&i.Scanlator,
&i.SourceOrder,
&i.IsRead,
&i.IsBookmarked,
&i.LastPageRead,
&i.LastReadAt,
&i.FetchedAt,
&i.RealUrl,
&i.IsDownloaded,
&i.PageCount,
)
return i, err
}
+32
View File
@@ -0,0 +1,32 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package queries
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}
+46
View File
@@ -0,0 +1,46 @@
-- name: UpsertManga :one
INSERT INTO manga (
source_id, url, title, artist, author, description, genre,
status, thumbnail_url, initialized, last_fetched_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
)
ON CONFLICT (source_id, url) DO UPDATE
SET title = EXCLUDED.title,
artist = EXCLUDED.artist,
author = EXCLUDED.author,
description = EXCLUDED.description,
genre = EXCLUDED.genre,
status = EXCLUDED.status,
thumbnail_url = EXCLUDED.thumbnail_url,
initialized = EXCLUDED.initialized,
last_fetched_at = EXCLUDED.last_fetched_at
RETURNING *;
-- name: GetMangaBySourceURL :one
SELECT * FROM manga WHERE source_id = $1 AND url = $2;
-- name: GetMangaByID :one
SELECT * FROM manga WHERE id = $1;
-- name: ListMangaBySource :many
SELECT * FROM manga
WHERE source_id = $1
ORDER BY last_fetched_at DESC;
-- name: UpdateMangaDetails :exec
UPDATE manga
SET artist = $2,
author = $3,
description = $4,
genre = $5,
status = $6,
thumbnail_url = $7,
initialized = TRUE
WHERE id = $1;
-- name: UpdateMangaFetchedAt :exec
UPDATE manga SET last_fetched_at = $2 WHERE id = $1;
-- name: UpdateChaptersFetchedAt :exec
UPDATE manga SET chapters_last_fetched_at = $2 WHERE id = $1;
+255
View File
@@ -0,0 +1,255 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: manga.sql
package queries
import (
"context"
)
const getMangaByID = `-- name: GetMangaByID :one
SELECT id, source_id, url, title, initialized, artist, author, description, genre, status, thumbnail_url, thumbnail_last_fetched, in_library, in_library_at, real_url, last_fetched_at, chapters_last_fetched_at, update_strategy FROM manga WHERE id = $1
`
func (q *Queries) GetMangaByID(ctx context.Context, id int32) (Manga, error) {
row := q.db.QueryRow(ctx, getMangaByID, id)
var i Manga
err := row.Scan(
&i.ID,
&i.SourceID,
&i.Url,
&i.Title,
&i.Initialized,
&i.Artist,
&i.Author,
&i.Description,
&i.Genre,
&i.Status,
&i.ThumbnailUrl,
&i.ThumbnailLastFetched,
&i.InLibrary,
&i.InLibraryAt,
&i.RealUrl,
&i.LastFetchedAt,
&i.ChaptersLastFetchedAt,
&i.UpdateStrategy,
)
return i, err
}
const getMangaBySourceURL = `-- name: GetMangaBySourceURL :one
SELECT id, source_id, url, title, initialized, artist, author, description, genre, status, thumbnail_url, thumbnail_last_fetched, in_library, in_library_at, real_url, last_fetched_at, chapters_last_fetched_at, update_strategy FROM manga WHERE source_id = $1 AND url = $2
`
type GetMangaBySourceURLParams struct {
SourceID int64 `db:"source_id" json:"source_id"`
Url string `db:"url" json:"url"`
}
func (q *Queries) GetMangaBySourceURL(ctx context.Context, arg GetMangaBySourceURLParams) (Manga, error) {
row := q.db.QueryRow(ctx, getMangaBySourceURL, arg.SourceID, arg.Url)
var i Manga
err := row.Scan(
&i.ID,
&i.SourceID,
&i.Url,
&i.Title,
&i.Initialized,
&i.Artist,
&i.Author,
&i.Description,
&i.Genre,
&i.Status,
&i.ThumbnailUrl,
&i.ThumbnailLastFetched,
&i.InLibrary,
&i.InLibraryAt,
&i.RealUrl,
&i.LastFetchedAt,
&i.ChaptersLastFetchedAt,
&i.UpdateStrategy,
)
return i, err
}
const listMangaBySource = `-- name: ListMangaBySource :many
SELECT id, source_id, url, title, initialized, artist, author, description, genre, status, thumbnail_url, thumbnail_last_fetched, in_library, in_library_at, real_url, last_fetched_at, chapters_last_fetched_at, update_strategy FROM manga
WHERE source_id = $1
ORDER BY last_fetched_at DESC
`
func (q *Queries) ListMangaBySource(ctx context.Context, sourceID int64) ([]Manga, error) {
rows, err := q.db.Query(ctx, listMangaBySource, sourceID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Manga
for rows.Next() {
var i Manga
if err := rows.Scan(
&i.ID,
&i.SourceID,
&i.Url,
&i.Title,
&i.Initialized,
&i.Artist,
&i.Author,
&i.Description,
&i.Genre,
&i.Status,
&i.ThumbnailUrl,
&i.ThumbnailLastFetched,
&i.InLibrary,
&i.InLibraryAt,
&i.RealUrl,
&i.LastFetchedAt,
&i.ChaptersLastFetchedAt,
&i.UpdateStrategy,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateChaptersFetchedAt = `-- name: UpdateChaptersFetchedAt :exec
UPDATE manga SET chapters_last_fetched_at = $2 WHERE id = $1
`
type UpdateChaptersFetchedAtParams struct {
ID int32 `db:"id" json:"id"`
ChaptersLastFetchedAt int64 `db:"chapters_last_fetched_at" json:"chapters_last_fetched_at"`
}
func (q *Queries) UpdateChaptersFetchedAt(ctx context.Context, arg UpdateChaptersFetchedAtParams) error {
_, err := q.db.Exec(ctx, updateChaptersFetchedAt, arg.ID, arg.ChaptersLastFetchedAt)
return err
}
const updateMangaDetails = `-- name: UpdateMangaDetails :exec
UPDATE manga
SET artist = $2,
author = $3,
description = $4,
genre = $5,
status = $6,
thumbnail_url = $7,
initialized = TRUE
WHERE id = $1
`
type UpdateMangaDetailsParams struct {
ID int32 `db:"id" json:"id"`
Artist *string `db:"artist" json:"artist"`
Author *string `db:"author" json:"author"`
Description *string `db:"description" json:"description"`
Genre *string `db:"genre" json:"genre"`
Status int32 `db:"status" json:"status"`
ThumbnailUrl *string `db:"thumbnail_url" json:"thumbnail_url"`
}
func (q *Queries) UpdateMangaDetails(ctx context.Context, arg UpdateMangaDetailsParams) error {
_, err := q.db.Exec(ctx, updateMangaDetails,
arg.ID,
arg.Artist,
arg.Author,
arg.Description,
arg.Genre,
arg.Status,
arg.ThumbnailUrl,
)
return err
}
const updateMangaFetchedAt = `-- name: UpdateMangaFetchedAt :exec
UPDATE manga SET last_fetched_at = $2 WHERE id = $1
`
type UpdateMangaFetchedAtParams struct {
ID int32 `db:"id" json:"id"`
LastFetchedAt int64 `db:"last_fetched_at" json:"last_fetched_at"`
}
func (q *Queries) UpdateMangaFetchedAt(ctx context.Context, arg UpdateMangaFetchedAtParams) error {
_, err := q.db.Exec(ctx, updateMangaFetchedAt, arg.ID, arg.LastFetchedAt)
return err
}
const upsertManga = `-- name: UpsertManga :one
INSERT INTO manga (
source_id, url, title, artist, author, description, genre,
status, thumbnail_url, initialized, last_fetched_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
)
ON CONFLICT (source_id, url) DO UPDATE
SET title = EXCLUDED.title,
artist = EXCLUDED.artist,
author = EXCLUDED.author,
description = EXCLUDED.description,
genre = EXCLUDED.genre,
status = EXCLUDED.status,
thumbnail_url = EXCLUDED.thumbnail_url,
initialized = EXCLUDED.initialized,
last_fetched_at = EXCLUDED.last_fetched_at
RETURNING id, source_id, url, title, initialized, artist, author, description, genre, status, thumbnail_url, thumbnail_last_fetched, in_library, in_library_at, real_url, last_fetched_at, chapters_last_fetched_at, update_strategy
`
type UpsertMangaParams struct {
SourceID int64 `db:"source_id" json:"source_id"`
Url string `db:"url" json:"url"`
Title string `db:"title" json:"title"`
Artist *string `db:"artist" json:"artist"`
Author *string `db:"author" json:"author"`
Description *string `db:"description" json:"description"`
Genre *string `db:"genre" json:"genre"`
Status int32 `db:"status" json:"status"`
ThumbnailUrl *string `db:"thumbnail_url" json:"thumbnail_url"`
Initialized bool `db:"initialized" json:"initialized"`
LastFetchedAt int64 `db:"last_fetched_at" json:"last_fetched_at"`
}
func (q *Queries) UpsertManga(ctx context.Context, arg UpsertMangaParams) (Manga, error) {
row := q.db.QueryRow(ctx, upsertManga,
arg.SourceID,
arg.Url,
arg.Title,
arg.Artist,
arg.Author,
arg.Description,
arg.Genre,
arg.Status,
arg.ThumbnailUrl,
arg.Initialized,
arg.LastFetchedAt,
)
var i Manga
err := row.Scan(
&i.ID,
&i.SourceID,
&i.Url,
&i.Title,
&i.Initialized,
&i.Artist,
&i.Author,
&i.Description,
&i.Genre,
&i.Status,
&i.ThumbnailUrl,
&i.ThumbnailLastFetched,
&i.InLibrary,
&i.InLibraryAt,
&i.RealUrl,
&i.LastFetchedAt,
&i.ChaptersLastFetchedAt,
&i.UpdateStrategy,
)
return i, err
}
+66
View File
@@ -0,0 +1,66 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package queries
type Chapter struct {
ID int32 `db:"id" json:"id"`
MangaID int32 `db:"manga_id" json:"manga_id"`
Url string `db:"url" json:"url"`
Name string `db:"name" json:"name"`
DateUpload int64 `db:"date_upload" json:"date_upload"`
ChapterNumber float32 `db:"chapter_number" json:"chapter_number"`
Scanlator *string `db:"scanlator" json:"scanlator"`
SourceOrder int32 `db:"source_order" json:"source_order"`
IsRead bool `db:"is_read" json:"is_read"`
IsBookmarked bool `db:"is_bookmarked" json:"is_bookmarked"`
LastPageRead int32 `db:"last_page_read" json:"last_page_read"`
LastReadAt int64 `db:"last_read_at" json:"last_read_at"`
FetchedAt int64 `db:"fetched_at" json:"fetched_at"`
RealUrl *string `db:"real_url" json:"real_url"`
IsDownloaded bool `db:"is_downloaded" json:"is_downloaded"`
PageCount int32 `db:"page_count" json:"page_count"`
}
type Manga struct {
ID int32 `db:"id" json:"id"`
SourceID int64 `db:"source_id" json:"source_id"`
Url string `db:"url" json:"url"`
Title string `db:"title" json:"title"`
Initialized bool `db:"initialized" json:"initialized"`
Artist *string `db:"artist" json:"artist"`
Author *string `db:"author" json:"author"`
Description *string `db:"description" json:"description"`
Genre *string `db:"genre" json:"genre"`
Status int32 `db:"status" json:"status"`
ThumbnailUrl *string `db:"thumbnail_url" json:"thumbnail_url"`
ThumbnailLastFetched int64 `db:"thumbnail_last_fetched" json:"thumbnail_last_fetched"`
InLibrary bool `db:"in_library" json:"in_library"`
InLibraryAt int64 `db:"in_library_at" json:"in_library_at"`
RealUrl *string `db:"real_url" json:"real_url"`
LastFetchedAt int64 `db:"last_fetched_at" json:"last_fetched_at"`
ChaptersLastFetchedAt int64 `db:"chapters_last_fetched_at" json:"chapters_last_fetched_at"`
UpdateStrategy string `db:"update_strategy" json:"update_strategy"`
}
type Page struct {
ID int32 `db:"id" json:"id"`
ChapterID int32 `db:"chapter_id" json:"chapter_id"`
Index int32 `db:"index" json:"index"`
Url string `db:"url" json:"url"`
ImageUrl *string `db:"image_url" json:"image_url"`
}
type Source struct {
ID int64 `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Lang string `db:"lang" json:"lang"`
IsNsfw bool `db:"is_nsfw" json:"is_nsfw"`
}
type SourceMetum struct {
SourceID int64 `db:"source_id" json:"source_id"`
Key string `db:"key" json:"key"`
Value string `db:"value" json:"value"`
}
+13
View File
@@ -0,0 +1,13 @@
-- name: UpsertPage :one
INSERT INTO pages (chapter_id, "index", url, image_url)
VALUES ($1, $2, $3, $4)
ON CONFLICT (chapter_id, "index") DO UPDATE
SET url = EXCLUDED.url,
image_url = EXCLUDED.image_url
RETURNING *;
-- name: ListPagesByChapter :many
SELECT * FROM pages WHERE chapter_id = $1 ORDER BY "index";
-- name: UpdatePageImageURL :exec
UPDATE pages SET image_url = $2 WHERE id = $1;
+88
View File
@@ -0,0 +1,88 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: page.sql
package queries
import (
"context"
)
const listPagesByChapter = `-- name: ListPagesByChapter :many
SELECT id, chapter_id, index, url, image_url FROM pages WHERE chapter_id = $1 ORDER BY "index"
`
func (q *Queries) ListPagesByChapter(ctx context.Context, chapterID int32) ([]Page, error) {
rows, err := q.db.Query(ctx, listPagesByChapter, chapterID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Page
for rows.Next() {
var i Page
if err := rows.Scan(
&i.ID,
&i.ChapterID,
&i.Index,
&i.Url,
&i.ImageUrl,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updatePageImageURL = `-- name: UpdatePageImageURL :exec
UPDATE pages SET image_url = $2 WHERE id = $1
`
type UpdatePageImageURLParams struct {
ID int32 `db:"id" json:"id"`
ImageUrl *string `db:"image_url" json:"image_url"`
}
func (q *Queries) UpdatePageImageURL(ctx context.Context, arg UpdatePageImageURLParams) error {
_, err := q.db.Exec(ctx, updatePageImageURL, arg.ID, arg.ImageUrl)
return err
}
const upsertPage = `-- name: UpsertPage :one
INSERT INTO pages (chapter_id, "index", url, image_url)
VALUES ($1, $2, $3, $4)
ON CONFLICT (chapter_id, "index") DO UPDATE
SET url = EXCLUDED.url,
image_url = EXCLUDED.image_url
RETURNING id, chapter_id, index, url, image_url
`
type UpsertPageParams struct {
ChapterID int32 `db:"chapter_id" json:"chapter_id"`
Index int32 `db:"index" json:"index"`
Url string `db:"url" json:"url"`
ImageUrl *string `db:"image_url" json:"image_url"`
}
func (q *Queries) UpsertPage(ctx context.Context, arg UpsertPageParams) (Page, error) {
row := q.db.QueryRow(ctx, upsertPage,
arg.ChapterID,
arg.Index,
arg.Url,
arg.ImageUrl,
)
var i Page
err := row.Scan(
&i.ID,
&i.ChapterID,
&i.Index,
&i.Url,
&i.ImageUrl,
)
return i, err
}
+24
View File
@@ -0,0 +1,24 @@
-- name: UpsertSource :one
INSERT INTO sources (id, name, lang, is_nsfw)
VALUES ($1, $2, $3, $4)
ON CONFLICT (id) DO UPDATE
SET name = EXCLUDED.name,
lang = EXCLUDED.lang,
is_nsfw = EXCLUDED.is_nsfw
RETURNING *;
-- name: ListSources :many
SELECT * FROM sources ORDER BY id;
-- name: GetSourceByID :one
SELECT * FROM sources WHERE id = $1;
-- name: GetSourceMeta :one
SELECT value FROM source_meta
WHERE source_id = $1 AND key = $2;
-- name: SetSourceMeta :exec
INSERT INTO source_meta (source_id, key, value)
VALUES ($1, $2, $3)
ON CONFLICT (source_id, key) DO UPDATE
SET value = EXCLUDED.value;
+124
View File
@@ -0,0 +1,124 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: source.sql
package queries
import (
"context"
)
const getSourceByID = `-- name: GetSourceByID :one
SELECT id, name, lang, is_nsfw FROM sources WHERE id = $1
`
func (q *Queries) GetSourceByID(ctx context.Context, id int64) (Source, error) {
row := q.db.QueryRow(ctx, getSourceByID, id)
var i Source
err := row.Scan(
&i.ID,
&i.Name,
&i.Lang,
&i.IsNsfw,
)
return i, err
}
const getSourceMeta = `-- name: GetSourceMeta :one
SELECT value FROM source_meta
WHERE source_id = $1 AND key = $2
`
type GetSourceMetaParams struct {
SourceID int64 `db:"source_id" json:"source_id"`
Key string `db:"key" json:"key"`
}
func (q *Queries) GetSourceMeta(ctx context.Context, arg GetSourceMetaParams) (string, error) {
row := q.db.QueryRow(ctx, getSourceMeta, arg.SourceID, arg.Key)
var value string
err := row.Scan(&value)
return value, err
}
const listSources = `-- name: ListSources :many
SELECT id, name, lang, is_nsfw FROM sources ORDER BY id
`
func (q *Queries) ListSources(ctx context.Context) ([]Source, error) {
rows, err := q.db.Query(ctx, listSources)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Source
for rows.Next() {
var i Source
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Lang,
&i.IsNsfw,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const setSourceMeta = `-- name: SetSourceMeta :exec
INSERT INTO source_meta (source_id, key, value)
VALUES ($1, $2, $3)
ON CONFLICT (source_id, key) DO UPDATE
SET value = EXCLUDED.value
`
type SetSourceMetaParams struct {
SourceID int64 `db:"source_id" json:"source_id"`
Key string `db:"key" json:"key"`
Value string `db:"value" json:"value"`
}
func (q *Queries) SetSourceMeta(ctx context.Context, arg SetSourceMetaParams) error {
_, err := q.db.Exec(ctx, setSourceMeta, arg.SourceID, arg.Key, arg.Value)
return err
}
const upsertSource = `-- name: UpsertSource :one
INSERT INTO sources (id, name, lang, is_nsfw)
VALUES ($1, $2, $3, $4)
ON CONFLICT (id) DO UPDATE
SET name = EXCLUDED.name,
lang = EXCLUDED.lang,
is_nsfw = EXCLUDED.is_nsfw
RETURNING id, name, lang, is_nsfw
`
type UpsertSourceParams struct {
ID int64 `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Lang string `db:"lang" json:"lang"`
IsNsfw bool `db:"is_nsfw" json:"is_nsfw"`
}
func (q *Queries) UpsertSource(ctx context.Context, arg UpsertSourceParams) (Source, error) {
row := q.db.QueryRow(ctx, upsertSource,
arg.ID,
arg.Name,
arg.Lang,
arg.IsNsfw,
)
var i Source
err := row.Scan(
&i.ID,
&i.Name,
&i.Lang,
&i.IsNsfw,
)
return i, err
}