server/database/product.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
}