diff --git a/internal/merch/repository.go b/internal/merch/repository.go index 8940d86..e5d10ab 100644 --- a/internal/merch/repository.go +++ b/internal/merch/repository.go @@ -1,57 +1,16 @@ package merch import ( - "context" - "database/sql" - "errors" - "fmt" "github.com/jackc/pgx/v5/pgxpool" - "strings" - "time" ) type Repository interface { - // createMerch creates new merch record and inserts extra data if given - createMerch(ctx context.Context, merch *Merch, extra []ExtraData) error - - // getMany returns list of only main merch record, without origins extra data - getMany(ctx context.Context, userId int64) ([]merchDTO, error) - - getMerchIdByUuid(ctx context.Context, userId int64, uuid string) (int64, error) - getMerchUuidMap(ctx context.Context, merchUuids []string) (map[string]int64, error) - getAllUserMerch(ctx context.Context, userId int64) ([]Merch, error) - - updateMerch(ctx context.Context, userId int64, merch *updateMerchDTO) (*merchDTO, error) - updateExtraData(ctx context.Context, merchId int64, insertData []ExtraData) ([]ExtraData, error) - - // deleteOneMerchRecord sets deleted_at in merch + extra tables - deleteOneMerchRecord(ctx context.Context, userId int64, merchUuid string, delTime time.Time) error - + MerchRepo Origins Prices Tasks } -type Origins interface { - createOrigin(ctx context.Context, origin *Origin) error - getOrigins(ctx context.Context) ([]Origin, error) - deleteOriginByName(ctx context.Context, name string, deletedAt sql.NullTime) error -} - -type Prices interface { - insertPrices(ctx context.Context, prices []Price) error - getPricesWithDays(ctx context.Context, userId int64, days time.Time) ([]Price, error) - getDistinctPrices(ctx context.Context, userId int64, merchUuid string, days time.Time) ([]Price, error) - - getZeroPrices(ctx context.Context, userId int64) ([]ZeroPrice, error) - deleteZeroPricesPeriod(ctx context.Context, userId int64, start, end time.Time, now sql.NullTime) error - deleteZeroPrices(ctx context.Context, now sql.NullTime, list []int64) error -} - -type Tasks interface { - getTaskData(ctx context.Context) ([]taskData, error) -} - type repo struct { db *pgxpool.Pool } @@ -61,494 +20,3 @@ func newRepo(db *pgxpool.Pool) Repository { db: db, } } - -func (r *repo) createOrigin(ctx context.Context, origin *Origin) error { - q := `INSERT INTO merch_origins (created_at, deleted_at, name) VALUES ($1, $2, $3)` - - _, err := r.db.Exec(ctx, q, origin.CreatedAt, origin.DeletedAt, origin.Name) - if err != nil { - return err - } - - return nil -} - -func (r *repo) getOrigins(ctx context.Context) ([]Origin, error) { - q := `SELECT * FROM merch_origins WHERE deleted_at IS NULL` - - rows, err := r.db.Query(ctx, q) - if err != nil { - return nil, err - } - defer rows.Close() - - var origins []Origin - for rows.Next() { - var o Origin - if err = rows.Scan(&o.Id, &o.CreatedAt, &o.DeletedAt, &o.Name); err != nil { - return nil, err - } - origins = append(origins, o) - } - - if err = rows.Err(); err != nil { - return nil, err - } - - return origins, nil -} - -func (r *repo) deleteOriginByName(ctx context.Context, name string, deletedAt sql.NullTime) error { - q := `UPDATE merch_origins SET deleted_at = $1 WHERE name = $2` - - _, err := r.db.Exec(ctx, q, deletedAt.Time, name) - if err != nil { - return err - } - - return nil -} - -//Merch crud - -func (r *repo) createMerch(ctx context.Context, merch *Merch, extra []ExtraData) error { - tx, err := r.db.Begin(ctx) - if err != nil { - return err - } - - qMerch := `INSERT INTO merch (created_at, updated_at, deleted_at, merch_uuid, user_id, name) - VALUES ($1, $2, $3, $4, $5, $6) RETURNING id` - - var merchId int64 - err = tx. - QueryRow(ctx, qMerch, merch.CreatedAt, merch.UpdatedAt, merch.DeletedAt, merch.MerchUuid, merch.UserId, merch.Name). - Scan(&merchId) - if err != nil { - tx.Rollback(ctx) - return err - } - - if extra == nil { - return tx.Commit(ctx) - } - - countArgs := 1 - var insertFields []string - var insertArgs []interface{} - - for _, item := range extra { - insertFields = append(insertFields, fmt.Sprintf("($%v, $%v, $%v, $%v, $%v, $%v)", - countArgs, countArgs+1, countArgs+2, countArgs+3, countArgs+4, countArgs+5)) - - insertArgs = append(insertArgs, item.CreatedAt, item.UpdatedAt, item.DeletedAt, merchId, item.OriginId, item.URL) - - countArgs += 6 - } - - qExtra := fmt.Sprintf( - "INSERT INTO merch_extra_data (created_at, updated_at, deleted_at, merch_id, origin_id, url) VALUES %v", - strings.Join(insertFields, ",")) - - _, err = tx.Exec(ctx, qExtra, insertArgs...) - if err != nil { - tx.Rollback(ctx) - return err - } - - return tx.Commit(ctx) -} - -func (r *repo) getMany(ctx context.Context, userId int64) ([]merchDTO, error) { - q := `SELECT created_at, updated_at, merch_uuid, name FROM merch WHERE deleted_at IS NULL AND user_id = $1` - - rows, err := r.db.Query(ctx, q, userId) - if err != nil { - return nil, err - } - defer rows.Close() - - var result []merchDTO - for rows.Next() { - var m merchDTO - if err = rows.Scan(&m.CreatedAt, &m.UpdatedAt, &m.MerchUuid, &m.Name); err != nil { - return nil, err - } - result = append(result, m) - } - if err = rows.Err(); err != nil { - return nil, err - } - return result, nil -} - -func (r *repo) getMerchIdByUuid(ctx context.Context, userId int64, uuid string) (int64, error) { - q := `SELECT id FROM merch WHERE deleted_at IS NULL AND merch_uuid = $1 AND user_id = $2` - - var merchId int64 - if err := r.db.QueryRow(ctx, q, uuid, userId).Scan(&merchId); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return 0, nil - } else { - return 0, err - } - } - - return merchId, nil -} - -func (r *repo) getMerchUuidMap(ctx context.Context, merchUuids []string) (map[string]int64, error) { - q := `SELECT merch_uuid, id FROM merch WHERE deleted_at IS NULL AND merch_uuid = ANY($1)` - - rows, err := r.db.Query(ctx, q, merchUuids) - if err != nil { - return nil, err - } - - var merchUuidMap map[string]int64 - for rows.Next() { - var ( - uuid string - id int64 - ) - if err = rows.Scan(&uuid, &id); err != nil { - rows.Close() - return nil, err - } - merchUuidMap[uuid] = id - } - - rows.Close() - if err = rows.Err(); err != nil { - return nil, err - } - - return merchUuidMap, nil -} - -func (r *repo) getAllUserMerch(ctx context.Context, userId int64) ([]Merch, error) { - var userMerch []Merch - - q := `SELECT id, merch_uuid, name FROM merch WHERE user_id = $1 AND deleted_at IS NULL` - - rows, err := r.db.Query(ctx, q, userId) - if err != nil { - return nil, err - } - - for rows.Next() { - var m Merch - if err = rows.Scan(&m.Id, &m.MerchUuid, &m.Name); err != nil { - rows.Close() - return nil, err - } - userMerch = append(userMerch, m) - } - - rows.Close() - if err = rows.Err(); err != nil { - return nil, err - } - return userMerch, nil -} - -func (r *repo) deleteOneMerchRecord(ctx context.Context, userId int64, merchUuid string, delTime time.Time) error { - tx, err := r.db.Begin(ctx) - if err != nil { - return err - } - - var merch_id int64 - qMerch := `UPDATE merch SET deleted_at = $1 WHERE merch_uuid = $2 AND user_id = $3 RETURNING id` - if err = tx.QueryRow(ctx, qMerch, delTime, merchUuid, userId).Scan(&merch_id); err != nil { - tx.Rollback(ctx) - return err - } - - if merch_id != 0 { - qExtra := `UPDATE merch_extra_data SET deleted_at = $1 WHERE merch_id = $2` - _, err = tx.Exec(ctx, qExtra, delTime, merch_id) - } - - return tx.Commit(ctx) -} - -func (r *repo) updateMerch(ctx context.Context, userId int64, merch *updateMerchDTO) (*merchDTO, error) { - q := `UPDATE merch SET name = $1 WHERE merch_uuid = $2 AND user_id = $3 - RETURNING created_at, updated_at, merch_uuid, name` - - var result merchDTO - if err := r.db. - QueryRow(ctx, q, merch.Name, merch.MerchUuid, userId). - Scan(&result.CreatedAt, &result.UpdatedAt, &result.MerchUuid, &result.Name); err != nil { - return nil, err - } - - return &result, nil -} - -func (r *repo) updateExtraData(ctx context.Context, merchId int64, insertData []ExtraData) ([]ExtraData, error) { - q := ` - INSERT INTO merch_extra_data (merch_id, origin_id, url, created_at, updated_at) - SELECT $1, src.origin_id, src.new_url, src.created_at, src.updated_at - FROM UNNEST( - $2::text[], - $3::bigint[], - $4::timestamptz[], - $5::timestamptz[]) - AS src(new_url, origin_id, created_at, updated_at) - ON CONFLICT (merch_id, origin_id) DO UPDATE SET url = COALESCE(NULLIF(EXCLUDED.url, ''), merch_extra_data.url), updated_at = NOW() - RETURNING created_at, updated_at, merch_id, origin_id, url; - ` - - var ( - urls []string - origins []int64 - createdAt []time.Time - updatedAt []sql.NullTime - ) - - for _, data := range insertData { - urls = append(urls, data.URL) - origins = append(origins, data.OriginId) - createdAt = append(createdAt, data.CreatedAt) - updatedAt = append(updatedAt, data.UpdatedAt) - } - - rows, err := r.db.Query(ctx, q, merchId, urls, origins, createdAt, updatedAt) - if err != nil { - return nil, err - } - - var result []ExtraData - for rows.Next() { - var m ExtraData - if err = rows.Scan(&m.CreatedAt, &m.UpdatedAt, &m.MerchId, &m.OriginId, &m.URL); err != nil { - return nil, err - } - result = append(result, m) - } - - rows.Close() //must be before rows.Err check! - if err = rows.Err(); err != nil { - return nil, err - } - - return result, nil -} - -func (r *repo) insertPrices(ctx context.Context, prices []Price) error { - q := ` - INSERT INTO merch_prices (created_at, updated_at, merch_id, origin_id, price) - SELECT $1, $2, src.merch_id, src.origin_id, src.price - FROM UNNEST( - $3::bigint[], - $4::bigint[], - $5::int[] - ) AS src (merch_id, origin_id, price) - ` - - var ( - merchIds []int64 - originIds []int64 - priceValues []int - ) - - for _, price := range prices { - merchIds = append(merchIds, price.MerchId) - originIds = append(originIds, price.OriginId) - priceValues = append(priceValues, price.Price) - } - - _, err := r.db.Exec(ctx, q, merchIds, originIds, priceValues) - if err != nil { - return err - } - - return nil -} - -func (r *repo) getTaskData(ctx context.Context) ([]taskData, error) { - q := `SELECT m.merch_uuid, med.url, mo.name - FROM merch AS m - JOIN merch_extra_data AS med ON m.id = med.merch_id - JOIN merch_origins AS mo ON mo.id = med.origin_id - WHERE m.deleted_at IS NULL - AND med.deleted_at IS NULL - AND mo.deleted_at IS NULL - ` - rows, err := r.db.Query(ctx, q) - if err != nil { - return nil, err - } - - var result []taskData - for rows.Next() { - var t taskData - if err = rows.Scan(&t.MerchUuid, &t.Url, &t.OriginName); err != nil { - return nil, err - } - result = append(result, t) - } - - rows.Close() - if err = rows.Err(); err != nil { - return nil, err - } - - return result, nil -} - -func (r *repo) getPricesWithDays(ctx context.Context, userId int64, days time.Time) ([]Price, error) { - q := ` - SELECT mp.created_at, mp.merch_id, mp.price, mp.origin_id - FROM merch_prices AS mp - JOIN merch AS m ON m.id = mp.merch_id - WHERE m.user_id = $1 - AND mp.created_at > $2 - AND mp.deleted_at IS NULL - AND m.deleted_at IS NULL - ` - - rows, err := r.db.Query(ctx, q, userId, days) - if err != nil { - return nil, err - } - - var result []Price - for rows.Next() { - var p Price - if err = rows.Scan(&p.CreatedAt, &p.MerchId, &p.Price, &p.OriginId); err != nil { - rows.Close() - return nil, err - } - result = append(result, p) - } - rows.Close() - - if err = rows.Err(); err != nil { - return nil, err - } - - return result, nil -} - -func (r *repo) getDistinctPrices(ctx context.Context, userId int64, merchUuid string, days time.Time) ([]Price, error) { - q := ` - SELECT price, created_at, origin_id - FROM ( - SELECT DISTINCT ON (price) price, created_at, origin_id - FROM merch_prices - WHERE merch_id = ( - SELECT id - FROM merch - WHERE merch_uuid = $1 - AND user_id = $2 - AND deleted_at IS NULL - ) - AND deleted_at IS NULL - AND created_at > $3 - ) - ORDER BY created_at; - ` - fmt.Println(merchUuid, userId, days) - rows, err := r.db.Query(ctx, q, merchUuid, userId, days) - if err != nil { - return nil, err - } - - var result []Price - for rows.Next() { - var p Price - if err = rows.Scan(&p.Price, &p.CreatedAt, &p.OriginId); err != nil { - rows.Close() - return nil, err - } - result = append(result, p) - } - - rows.Close() - if err = rows.Err(); err != nil { - return nil, err - } - return result, nil -} - -func (r *repo) getZeroPrices(ctx context.Context, userId int64) ([]ZeroPrice, error) { - q := ` - WITH price_with_neighbors AS ( - SELECT - p.id, p.created_at, p.merch_id, p.price, p.origin_id, m.name, - LAG(price) OVER (PARTITION BY p.merch_id, p.origin_id ORDER BY p.created_at, p.id) AS prev_price, - LEAD(price) OVER (PARTITION BY p.merch_id, p.origin_id ORDER BY p.created_at, p.id) AS next_price - FROM merch_prices AS p - JOIN merch as m ON m.id = p.merch_id - WHERE p.deleted_at IS NULL - AND m.deleted_at IS NULL - AND m.user_id = 3) - - SELECT - pwn.id, pwn.created_at, pwn.merch_uuid, pwn.name, mo.name - FROM price_with_neighbors AS pwn - JOIN merch_origins AS mo ON mo.id = pwn.origin_id - WHERE - pwn.price = 0 - AND pwn.prev_price IS NOT NULL - AND pwn.prev_price > 0 - AND pwn.next_price IS NOT NULL - AND pwn.next_price > 0 - ORDER BY pwn.created_at DESC - ` - - rows, err := r.db.Query(ctx, q, userId) - if err != nil { - return nil, err - } - - var zeroPrices []ZeroPrice - for rows.Next() { - var p ZeroPrice - if err = rows.Scan(&p.Id, &p.CreatedAt, &p.MerchUuid, &p.Name, &p.Origin); err != nil { - rows.Close() - return nil, err - } - zeroPrices = append(zeroPrices, p) - } - - rows.Close() - if err = rows.Err(); err != nil { - return nil, err - } - - return zeroPrices, nil -} - -func (r *repo) deleteZeroPricesPeriod(ctx context.Context, userId int64, start, end time.Time, now sql.NullTime) error { - q := ` - UPDATE merch_prices - SET deleted_at = $1 - FROM merch - WHERE merch_prices.merch_id = merch.id - AND merch.user_id = $2 - AND merch_prices.price = 0 - AND merch_prices.deleted_at IS NULL - AND merch_prices.created_at BETWEEN $3 AND $4; - ` - - _, err := r.db.Exec(ctx, q, now, userId, start, end) - if err != nil { - return err - } - - return nil -} - -func (r *repo) deleteZeroPrices(ctx context.Context, now sql.NullTime, list []int64) error { - q := `UPDATE merch_prices SET deleted_at = $1 WHERE id IN $2` - - _, err := r.db.Exec(ctx, q, now, list) - if err != nil { - return err - } - return nil -} diff --git a/internal/merch/repository_merch.go b/internal/merch/repository_merch.go new file mode 100644 index 0000000..c29af6d --- /dev/null +++ b/internal/merch/repository_merch.go @@ -0,0 +1,254 @@ +package merch + +import ( + "context" + "database/sql" + "errors" + "fmt" + "strings" + "time" +) + +type MerchRepo interface { + // createMerch creates new merch record and inserts extra data if given + createMerch(ctx context.Context, merch *Merch, extra []ExtraData) error + + // getMany returns list of only main merch record, without origins extra data + getMany(ctx context.Context, userId int64) ([]merchDTO, error) + + getMerchIdByUuid(ctx context.Context, userId int64, uuid string) (int64, error) + getMerchUuidMap(ctx context.Context, merchUuids []string) (map[string]int64, error) + getAllUserMerch(ctx context.Context, userId int64) ([]Merch, error) + + updateMerch(ctx context.Context, userId int64, merch *updateMerchDTO) (*merchDTO, error) + updateExtraData(ctx context.Context, merchId int64, insertData []ExtraData) ([]ExtraData, error) + + // deleteOneMerchRecord sets deleted_at in merch + extra tables + deleteOneMerchRecord(ctx context.Context, userId int64, merchUuid string, delTime time.Time) error +} + +func (r *repo) createMerch(ctx context.Context, merch *Merch, extra []ExtraData) error { + tx, err := r.db.Begin(ctx) + if err != nil { + return err + } + + qMerch := `INSERT INTO merch (created_at, updated_at, deleted_at, merch_uuid, user_id, name) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING id` + + var merchId int64 + err = tx. + QueryRow(ctx, qMerch, merch.CreatedAt, merch.UpdatedAt, merch.DeletedAt, merch.MerchUuid, merch.UserId, merch.Name). + Scan(&merchId) + if err != nil { + tx.Rollback(ctx) + return err + } + + if extra == nil { + return tx.Commit(ctx) + } + + countArgs := 1 + var insertFields []string + var insertArgs []interface{} + + for _, item := range extra { + insertFields = append(insertFields, fmt.Sprintf("($%v, $%v, $%v, $%v, $%v, $%v)", + countArgs, countArgs+1, countArgs+2, countArgs+3, countArgs+4, countArgs+5)) + + insertArgs = append(insertArgs, item.CreatedAt, item.UpdatedAt, item.DeletedAt, merchId, item.OriginId, item.URL) + + countArgs += 6 + } + + qExtra := fmt.Sprintf( + "INSERT INTO merch_extra_data (created_at, updated_at, deleted_at, merch_id, origin_id, url) VALUES %v", + strings.Join(insertFields, ",")) + + _, err = tx.Exec(ctx, qExtra, insertArgs...) + if err != nil { + tx.Rollback(ctx) + return err + } + + return tx.Commit(ctx) +} + +func (r *repo) getMany(ctx context.Context, userId int64) ([]merchDTO, error) { + q := `SELECT created_at, updated_at, merch_uuid, name FROM merch WHERE deleted_at IS NULL AND user_id = $1` + + rows, err := r.db.Query(ctx, q, userId) + if err != nil { + return nil, err + } + defer rows.Close() + + var result []merchDTO + for rows.Next() { + var m merchDTO + if err = rows.Scan(&m.CreatedAt, &m.UpdatedAt, &m.MerchUuid, &m.Name); err != nil { + return nil, err + } + result = append(result, m) + } + if err = rows.Err(); err != nil { + return nil, err + } + return result, nil +} + +func (r *repo) getMerchIdByUuid(ctx context.Context, userId int64, uuid string) (int64, error) { + q := `SELECT id FROM merch WHERE deleted_at IS NULL AND merch_uuid = $1 AND user_id = $2` + + var merchId int64 + if err := r.db.QueryRow(ctx, q, uuid, userId).Scan(&merchId); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return 0, nil + } else { + return 0, err + } + } + + return merchId, nil +} + +func (r *repo) getMerchUuidMap(ctx context.Context, merchUuids []string) (map[string]int64, error) { + q := `SELECT merch_uuid, id FROM merch WHERE deleted_at IS NULL AND merch_uuid = ANY($1)` + + rows, err := r.db.Query(ctx, q, merchUuids) + if err != nil { + return nil, err + } + + var merchUuidMap map[string]int64 + for rows.Next() { + var ( + uuid string + id int64 + ) + if err = rows.Scan(&uuid, &id); err != nil { + rows.Close() + return nil, err + } + merchUuidMap[uuid] = id + } + + rows.Close() + if err = rows.Err(); err != nil { + return nil, err + } + + return merchUuidMap, nil +} + +func (r *repo) getAllUserMerch(ctx context.Context, userId int64) ([]Merch, error) { + var userMerch []Merch + + q := `SELECT id, merch_uuid, name FROM merch WHERE user_id = $1 AND deleted_at IS NULL` + + rows, err := r.db.Query(ctx, q, userId) + if err != nil { + return nil, err + } + + for rows.Next() { + var m Merch + if err = rows.Scan(&m.Id, &m.MerchUuid, &m.Name); err != nil { + rows.Close() + return nil, err + } + userMerch = append(userMerch, m) + } + + rows.Close() + if err = rows.Err(); err != nil { + return nil, err + } + return userMerch, nil +} + +func (r *repo) deleteOneMerchRecord(ctx context.Context, userId int64, merchUuid string, delTime time.Time) error { + tx, err := r.db.Begin(ctx) + if err != nil { + return err + } + + var merch_id int64 + qMerch := `UPDATE merch SET deleted_at = $1 WHERE merch_uuid = $2 AND user_id = $3 RETURNING id` + if err = tx.QueryRow(ctx, qMerch, delTime, merchUuid, userId).Scan(&merch_id); err != nil { + tx.Rollback(ctx) + return err + } + + if merch_id != 0 { + qExtra := `UPDATE merch_extra_data SET deleted_at = $1 WHERE merch_id = $2` + _, err = tx.Exec(ctx, qExtra, delTime, merch_id) + } + + return tx.Commit(ctx) +} + +func (r *repo) updateMerch(ctx context.Context, userId int64, merch *updateMerchDTO) (*merchDTO, error) { + q := `UPDATE merch SET name = $1 WHERE merch_uuid = $2 AND user_id = $3 + RETURNING created_at, updated_at, merch_uuid, name` + + var result merchDTO + if err := r.db. + QueryRow(ctx, q, merch.Name, merch.MerchUuid, userId). + Scan(&result.CreatedAt, &result.UpdatedAt, &result.MerchUuid, &result.Name); err != nil { + return nil, err + } + + return &result, nil +} + +func (r *repo) updateExtraData(ctx context.Context, merchId int64, insertData []ExtraData) ([]ExtraData, error) { + q := ` + INSERT INTO merch_extra_data (merch_id, origin_id, url, created_at, updated_at) + SELECT $1, src.origin_id, src.new_url, src.created_at, src.updated_at + FROM UNNEST( + $2::text[], + $3::bigint[], + $4::timestamptz[], + $5::timestamptz[]) + AS src(new_url, origin_id, created_at, updated_at) + ON CONFLICT (merch_id, origin_id) DO UPDATE SET url = COALESCE(NULLIF(EXCLUDED.url, ''), merch_extra_data.url), updated_at = NOW() + RETURNING created_at, updated_at, merch_id, origin_id, url; + ` + + var ( + urls []string + origins []int64 + createdAt []time.Time + updatedAt []sql.NullTime + ) + + for _, data := range insertData { + urls = append(urls, data.URL) + origins = append(origins, data.OriginId) + createdAt = append(createdAt, data.CreatedAt) + updatedAt = append(updatedAt, data.UpdatedAt) + } + + rows, err := r.db.Query(ctx, q, merchId, urls, origins, createdAt, updatedAt) + if err != nil { + return nil, err + } + + var result []ExtraData + for rows.Next() { + var m ExtraData + if err = rows.Scan(&m.CreatedAt, &m.UpdatedAt, &m.MerchId, &m.OriginId, &m.URL); err != nil { + return nil, err + } + result = append(result, m) + } + + rows.Close() //must be before rows.Err check! + if err = rows.Err(); err != nil { + return nil, err + } + + return result, nil +} diff --git a/internal/merch/repository_origins.go b/internal/merch/repository_origins.go new file mode 100644 index 0000000..464d6c3 --- /dev/null +++ b/internal/merch/repository_origins.go @@ -0,0 +1,59 @@ +package merch + +import ( + "context" + "database/sql" +) + +type Origins interface { + createOrigin(ctx context.Context, origin *Origin) error + getOrigins(ctx context.Context) ([]Origin, error) + deleteOriginByName(ctx context.Context, name string, deletedAt sql.NullTime) error +} + +func (r *repo) createOrigin(ctx context.Context, origin *Origin) error { + q := `INSERT INTO merch_origins (created_at, deleted_at, name) VALUES ($1, $2, $3)` + + _, err := r.db.Exec(ctx, q, origin.CreatedAt, origin.DeletedAt, origin.Name) + if err != nil { + return err + } + + return nil +} + +func (r *repo) getOrigins(ctx context.Context) ([]Origin, error) { + q := `SELECT * FROM merch_origins WHERE deleted_at IS NULL` + + rows, err := r.db.Query(ctx, q) + if err != nil { + return nil, err + } + defer rows.Close() + + var origins []Origin + for rows.Next() { + var o Origin + if err = rows.Scan(&o.Id, &o.CreatedAt, &o.DeletedAt, &o.Name); err != nil { + return nil, err + } + origins = append(origins, o) + } + + if err = rows.Err(); err != nil { + return nil, err + } + + return origins, nil +} + +func (r *repo) deleteOriginByName(ctx context.Context, name string, deletedAt sql.NullTime) error { + q := `UPDATE merch_origins SET deleted_at = $1 WHERE name = $2` + + _, err := r.db.Exec(ctx, q, deletedAt.Time, name) + if err != nil { + return err + } + + return nil +} diff --git a/internal/merch/repository_prices.go b/internal/merch/repository_prices.go new file mode 100644 index 0000000..a9310d0 --- /dev/null +++ b/internal/merch/repository_prices.go @@ -0,0 +1,203 @@ +package merch + +import ( + "context" + "database/sql" + "fmt" + "time" +) + +type Prices interface { + insertPrices(ctx context.Context, prices []Price) error + getPricesWithDays(ctx context.Context, userId int64, days time.Time) ([]Price, error) + getDistinctPrices(ctx context.Context, userId int64, merchUuid string, days time.Time) ([]Price, error) + + getZeroPrices(ctx context.Context, userId int64) ([]ZeroPrice, error) + deleteZeroPricesPeriod(ctx context.Context, userId int64, start, end time.Time, now sql.NullTime) error + deleteZeroPrices(ctx context.Context, now sql.NullTime, list []int64) error +} + +func (r *repo) insertPrices(ctx context.Context, prices []Price) error { + q := ` + INSERT INTO merch_prices (created_at, updated_at, merch_id, origin_id, price) + SELECT $1, $2, src.merch_id, src.origin_id, src.price + FROM UNNEST( + $3::bigint[], + $4::bigint[], + $5::int[] + ) AS src (merch_id, origin_id, price) + ` + + var ( + merchIds []int64 + originIds []int64 + priceValues []int + ) + + for _, price := range prices { + merchIds = append(merchIds, price.MerchId) + originIds = append(originIds, price.OriginId) + priceValues = append(priceValues, price.Price) + } + + _, err := r.db.Exec(ctx, q, merchIds, originIds, priceValues) + if err != nil { + return err + } + + return nil +} + +func (r *repo) getPricesWithDays(ctx context.Context, userId int64, days time.Time) ([]Price, error) { + q := ` + SELECT mp.created_at, mp.merch_id, mp.price, mp.origin_id + FROM merch_prices AS mp + JOIN merch AS m ON m.id = mp.merch_id + WHERE m.user_id = $1 + AND mp.created_at > $2 + AND mp.deleted_at IS NULL + AND m.deleted_at IS NULL + ` + + rows, err := r.db.Query(ctx, q, userId, days) + if err != nil { + return nil, err + } + + var result []Price + for rows.Next() { + var p Price + if err = rows.Scan(&p.CreatedAt, &p.MerchId, &p.Price, &p.OriginId); err != nil { + rows.Close() + return nil, err + } + result = append(result, p) + } + rows.Close() + + if err = rows.Err(); err != nil { + return nil, err + } + + return result, nil +} + +func (r *repo) getDistinctPrices(ctx context.Context, userId int64, merchUuid string, days time.Time) ([]Price, error) { + q := ` + SELECT price, created_at, origin_id + FROM ( + SELECT DISTINCT ON (price) price, created_at, origin_id + FROM merch_prices + WHERE merch_id = ( + SELECT id + FROM merch + WHERE merch_uuid = $1 + AND user_id = $2 + AND deleted_at IS NULL + ) + AND deleted_at IS NULL + AND created_at > $3 + ) + ORDER BY created_at; + ` + fmt.Println(merchUuid, userId, days) + rows, err := r.db.Query(ctx, q, merchUuid, userId, days) + if err != nil { + return nil, err + } + + var result []Price + for rows.Next() { + var p Price + if err = rows.Scan(&p.Price, &p.CreatedAt, &p.OriginId); err != nil { + rows.Close() + return nil, err + } + result = append(result, p) + } + + rows.Close() + if err = rows.Err(); err != nil { + return nil, err + } + return result, nil +} + +func (r *repo) getZeroPrices(ctx context.Context, userId int64) ([]ZeroPrice, error) { + q := ` + WITH price_with_neighbors AS ( + SELECT + p.id, p.created_at, p.merch_id, p.price, p.origin_id, m.name, + LAG(price) OVER (PARTITION BY p.merch_id, p.origin_id ORDER BY p.created_at, p.id) AS prev_price, + LEAD(price) OVER (PARTITION BY p.merch_id, p.origin_id ORDER BY p.created_at, p.id) AS next_price + FROM merch_prices AS p + JOIN merch as m ON m.id = p.merch_id + WHERE p.deleted_at IS NULL + AND m.deleted_at IS NULL + AND m.user_id = 3) + + SELECT + pwn.id, pwn.created_at, pwn.merch_uuid, pwn.name, mo.name + FROM price_with_neighbors AS pwn + JOIN merch_origins AS mo ON mo.id = pwn.origin_id + WHERE + pwn.price = 0 + AND pwn.prev_price IS NOT NULL + AND pwn.prev_price > 0 + AND pwn.next_price IS NOT NULL + AND pwn.next_price > 0 + ORDER BY pwn.created_at DESC + ` + + rows, err := r.db.Query(ctx, q, userId) + if err != nil { + return nil, err + } + + var zeroPrices []ZeroPrice + for rows.Next() { + var p ZeroPrice + if err = rows.Scan(&p.Id, &p.CreatedAt, &p.MerchUuid, &p.Name, &p.Origin); err != nil { + rows.Close() + return nil, err + } + zeroPrices = append(zeroPrices, p) + } + + rows.Close() + if err = rows.Err(); err != nil { + return nil, err + } + + return zeroPrices, nil +} + +func (r *repo) deleteZeroPricesPeriod(ctx context.Context, userId int64, start, end time.Time, now sql.NullTime) error { + q := ` + UPDATE merch_prices + SET deleted_at = $1 + FROM merch + WHERE merch_prices.merch_id = merch.id + AND merch.user_id = $2 + AND merch_prices.price = 0 + AND merch_prices.deleted_at IS NULL + AND merch_prices.created_at BETWEEN $3 AND $4; + ` + + _, err := r.db.Exec(ctx, q, now, userId, start, end) + if err != nil { + return err + } + + return nil +} + +func (r *repo) deleteZeroPrices(ctx context.Context, now sql.NullTime, list []int64) error { + q := `UPDATE merch_prices SET deleted_at = $1 WHERE id IN $2` + + _, err := r.db.Exec(ctx, q, now, list) + if err != nil { + return err + } + return nil +} diff --git a/internal/merch/repository_tasks.go b/internal/merch/repository_tasks.go new file mode 100644 index 0000000..b42444f --- /dev/null +++ b/internal/merch/repository_tasks.go @@ -0,0 +1,38 @@ +package merch + +import "context" + +type Tasks interface { + getTaskData(ctx context.Context) ([]taskData, error) +} + +func (r *repo) getTaskData(ctx context.Context) ([]taskData, error) { + q := `SELECT m.merch_uuid, med.url, mo.name + FROM merch AS m + JOIN merch_extra_data AS med ON m.id = med.merch_id + JOIN merch_origins AS mo ON mo.id = med.origin_id + WHERE m.deleted_at IS NULL + AND med.deleted_at IS NULL + AND mo.deleted_at IS NULL + ` + rows, err := r.db.Query(ctx, q) + if err != nil { + return nil, err + } + + var result []taskData + for rows.Next() { + var t taskData + if err = rows.Scan(&t.MerchUuid, &t.Url, &t.OriginName); err != nil { + return nil, err + } + result = append(result, t) + } + + rows.Close() + if err = rows.Err(); err != nil { + return nil, err + } + + return result, nil +}