350 lines
8.5 KiB
Go
350 lines
8.5 KiB
Go
/**
|
|
* file: database/product.go
|
|
* author: Theo Technicguy
|
|
* license: Apache 2.0
|
|
*
|
|
* Product database table connector
|
|
*/
|
|
|
|
package database
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
|
|
"git.licolas.net/hoffman/server/types"
|
|
)
|
|
|
|
const (
|
|
sqlDropProducts = "DROP TABLE Product;"
|
|
sqlCreateProducts = `CREATE TABLE Product (
|
|
ProductID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
EAN INTEGER UNIQUE,
|
|
Name VARCHAR(64) NOT NULL,
|
|
TTL INTEGER,
|
|
TTLFridge INTEGER,
|
|
TTLFreezer INTEGER
|
|
);`
|
|
sqlSelectAllProducts = "SELECT * FROM Product;"
|
|
sqlSelectProductById = "SELECT * FROM Product WHERE ProductID = ?;"
|
|
sqlSelectProductByEAN = "SELECT * FROM Product WHERE EAN = ?;"
|
|
sqlInsertProduct = `INSERT INTO Product
|
|
(EAN, Name, TTL, TTLFridge, TTLFreezer)
|
|
VALUES
|
|
(?, ?, ?, ?, ?);`
|
|
sqlUpdateProduct = `UPDATE Product
|
|
SET EAN = ?, Name = ?, TTL = ?, TTLFridge = ?, TTLFreezer = ?
|
|
WHERE ProductID = ?;`
|
|
sqlDeleteProduct = "DELETE FROM Product WHERE ProductID = ?"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidEANType = errors.New("invalid ean type")
|
|
ErrNegativeEAN = errors.New("negative ean")
|
|
ErrNonUniqueIdentifier = errors.New("found non-unique identifier")
|
|
)
|
|
|
|
// convert uint EAN to correct SQL number
|
|
// 0 EANs are mapped to <nil> to allow multiple EANless products
|
|
// other uints stay the same
|
|
func convertEANtoSQL(ean uint) *uint {
|
|
if ean == 0 {
|
|
return nil
|
|
} else {
|
|
return &ean
|
|
}
|
|
}
|
|
|
|
// convert uint EAN from SQL number
|
|
// NULL SQL EANs are mapped to 0 to allow multiple EANless products
|
|
// other uints stay the same
|
|
func convertEANfromSQL(ean any) (e uint, err error) {
|
|
logger.Trace().Any("ean", ean).Msg("converting ean from sql equivalent")
|
|
switch v := ean.(type) {
|
|
case nil:
|
|
e, err = 0, nil
|
|
case int:
|
|
if v < 0 {
|
|
e, err = 0, ErrNegativeEAN
|
|
} else {
|
|
e, err = uint(v), nil
|
|
}
|
|
case int8:
|
|
if v < 0 {
|
|
e, err = 0, ErrNegativeEAN
|
|
} else {
|
|
e, err = uint(v), nil
|
|
}
|
|
case int16:
|
|
if v < 0 {
|
|
e, err = 0, ErrNegativeEAN
|
|
} else {
|
|
e, err = uint(v), nil
|
|
}
|
|
case int32:
|
|
if v < 0 {
|
|
e, err = 0, ErrNegativeEAN
|
|
} else {
|
|
e, err = uint(v), nil
|
|
}
|
|
case int64:
|
|
if v < 0 {
|
|
e, err = 0, ErrNegativeEAN
|
|
} else {
|
|
e, err = uint(v), nil
|
|
}
|
|
case uint:
|
|
e, err = uint(v), nil
|
|
case uint8:
|
|
e, err = uint(v), nil
|
|
case uint16:
|
|
e, err = uint(v), nil
|
|
case uint32:
|
|
e, err = uint(v), nil
|
|
case uint64:
|
|
e, err = uint(v), nil
|
|
default:
|
|
e, err = 0, ErrInvalidEANType
|
|
}
|
|
|
|
logger.Trace().Uint("ean", e).Msg("done converting ean")
|
|
return
|
|
}
|
|
|
|
// Scan products from SQL rows into product structures
|
|
// Given an SQL query and arguments, it returns an array of complete products.
|
|
// The first argument is a SQL Transaction pointer. If none (nil) is passed,
|
|
// then one is created. The other two arguments are directly passed to tx.Query
|
|
func (r *R) scanProducts(tx *sql.Tx, query string, args ...any) ([]*types.Product, error) {
|
|
logger.Trace().Bool("transaction", tx != nil).Str("query", query).Any("args", args).Msg("excuting query and scanning products")
|
|
if tx == nil {
|
|
var err error
|
|
tx, err = r.db.Begin()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer tx.Rollback()
|
|
}
|
|
|
|
rows, err := tx.Query(query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var products []*types.Product = []*types.Product{}
|
|
for rows.Next() {
|
|
var product types.Product
|
|
var ttl types.TTL
|
|
var ean any
|
|
|
|
product.TTL = ttl
|
|
|
|
if err = rows.Scan(
|
|
&product.Id,
|
|
&ean,
|
|
&product.Name,
|
|
&product.TTL.Normal,
|
|
&product.TTL.Fridge,
|
|
&product.TTL.Freezer,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
logger.Trace().Any("ean", ean).Msg("converting ean")
|
|
if product.EAN, err = convertEANfromSQL(ean); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
logger.Trace().Uint("product-id", product.Id).Msg("adding tags")
|
|
if product.Tags, err = r.SelectProductTaggingByPid(product.Id); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
logger.Trace().Any("product", product).Msg("adding found product")
|
|
|
|
products = append(products, &product)
|
|
}
|
|
|
|
// This is only a query, no data is modified so there is no need to commit.
|
|
logger.Trace().Int("products", len(products)).Msg("done querying and scanning products")
|
|
return products, nil
|
|
}
|
|
|
|
// Scan products and assert uniqueness
|
|
func (r *R) scanUniqueProduct(tx *sql.Tx, query string, args ...any) (product *types.Product, err error) {
|
|
logger.Trace().Bool("transaction", tx != nil).Str("query", query).Any("args", args).Msg("excuting query and scanning one product")
|
|
|
|
products, err := r.scanProducts(tx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch len(products) {
|
|
case 0:
|
|
product, err = new(types.Product), nil
|
|
case 1:
|
|
product, err = products[0], nil
|
|
default:
|
|
product, err = nil, ErrNonUniqueIdentifier
|
|
}
|
|
|
|
logger.Trace().Any("product", product).Msg("found product")
|
|
return
|
|
}
|
|
|
|
func (r *R) CreateProductTable() error {
|
|
logger.Debug().Msg("creating product table")
|
|
_, err := r.db.Exec(sqlCreateProducts)
|
|
return err
|
|
}
|
|
|
|
func (r *R) SelectAllProducts() ([]*types.Product, error) {
|
|
logger.Trace().Msg("selecting all products")
|
|
return r.scanProducts(nil, sqlSelectAllProducts)
|
|
}
|
|
|
|
func (r *R) SelectProductById(id uint) (*types.Product, error) {
|
|
logger.Trace().Uint("id", id).Msg("selecting product by id")
|
|
return r.scanUniqueProduct(nil, sqlSelectProductById, id)
|
|
}
|
|
|
|
func (r *R) SelectProductByEAN(ean uint) (*types.Product, error) {
|
|
return r.scanUniqueProduct(nil, sqlSelectProductByEAN, ean)
|
|
}
|
|
|
|
func (r *R) SelectProductsLike(ps *types.ProductSearch) ([]*types.Product, error) {
|
|
logger.Trace().Any("search", ps).Msg("selecting product like search")
|
|
if !ps.Valid() {
|
|
return nil, types.ErrInvalidSearch
|
|
}
|
|
|
|
return r.scanProducts(nil, ps.BuildSearch())
|
|
}
|
|
|
|
func (r *R) InsertProduct(product *types.Product) error {
|
|
logger.Trace().Any("product", product).Msg("inserting new product")
|
|
tx, err := r.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
result, err := tx.Exec(
|
|
sqlInsertProduct,
|
|
convertEANtoSQL(product.EAN),
|
|
product.Name,
|
|
product.TTL.Normal,
|
|
product.TTL.Fridge,
|
|
product.TTL.Freezer,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
id, err := result.LastInsertId()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
product.Id = uint(id)
|
|
logger.Trace().Any("product", product).Int64("new-id", id).Msg("new product id")
|
|
|
|
for _, tag := range product.Tags {
|
|
logger.Trace().Any("product", product).Int64("new-id", id).Uint("tag", tag.Id).Msg("adding tag")
|
|
if _, err := tx.Exec(sqlInsertProductTagging, id, tag.Id); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
logger.Trace().Any("product", product).Int64("new-id", id).Msg("selecting insertion")
|
|
newProduct, err := r.scanUniqueProduct(tx, sqlSelectProductById, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
logger.Trace().Any("product", product).Int64("new-id", id).Msg("updating product")
|
|
product.Tags = newProduct.Tags
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (r *R) UpdateProduct(product *types.Product) (*types.Product, error) {
|
|
logger.Trace().Any("product", product).Msg("updating product")
|
|
tx, err := r.db.Begin()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
if _, err := tx.Exec(
|
|
sqlUpdateProduct,
|
|
convertEANtoSQL(product.EAN),
|
|
product.Name,
|
|
product.TTL.Normal,
|
|
product.TTL.Fridge,
|
|
product.TTL.Freezer,
|
|
product.Id,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
logger.Trace().Any("product", product).Msg("deleting taggings")
|
|
if _, err := tx.Exec(sqlDeleteProductProductTagging, product.Id); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stmt, err := tx.Prepare(sqlInsertProductTagging)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, tag := range product.Tags {
|
|
logger.Trace().Any("product", product).Uint("tag-id", tag.Id).Msg("inserting tagging")
|
|
if _, err := stmt.Exec(product.Id, tag.Id); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
logger.Trace().Any("product", product).Msg("returning selection")
|
|
return r.scanUniqueProduct(nil, sqlSelectProductById, product.Id)
|
|
}
|
|
|
|
func (r *R) DeleteProduct(id uint) (*types.Product, error) {
|
|
logger := logger.With().Uint("product-id", id).Logger()
|
|
logger.Trace().Msg("deleting product")
|
|
tx, err := r.db.Begin()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
logger.Trace().Msg("selecting product")
|
|
product, err := r.scanUniqueProduct(tx, sqlSelectProductById, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
logger.Trace().Msg("deleting taggings")
|
|
if _, err := tx.Exec(sqlDeleteProductProductTagging, id); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
logger.Trace().Msg("deleting product")
|
|
if _, err := tx.Exec(sqlDeleteProduct, id); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return product, tx.Commit()
|
|
}
|
|
|
|
func (r *R) DropProductTable() error {
|
|
logger.Info().Msg("dropping product table")
|
|
_, err := r.db.Exec(sqlDropProducts)
|
|
return err
|
|
}
|