package postgres import ( "database/sql" "gardomatic.kleiax.de/internal/storage" ) // PlantModel stores garden-scoped plant instances in PostgreSQL. // PlantModel implements storage.PlantModelInterface for PostgreSQL and scopes // every concrete plant operation to its garden. type PlantModel struct{ DB *sql.DB } const plantColumns = `p.id, p.garden_id, p.species_id, p.name, p.notes, p.acquired_at, p.status, p.removed_at, p.attributes, COALESCE('data:'||i.media_type||';base64,'||replace(encode(i.data,'base64'), E'\n', ''), ''), p.image_id, p.created_at, p.updated_at, p.version, COALESCE(p.created_by, 0), COALESCE(p.updated_by, 0), COALESCE(p.planted_by, 0), COALESCE(u.name, '')` func scanPlant(s scanner) (storage.Plant, error) { var plant storage.Plant err := s.Scan( &plant.ID, &plant.GardenID, &plant.SpeciesID, &plant.Name, &plant.Notes, &plant.AcquiredAt, &plant.Status, &plant.RemovedAt, &plant.Attributes, &plant.ImageData, &plant.ImageID, &plant.CreatedAt, &plant.UpdatedAt, &plant.Version, &plant.CreatedBy, &plant.UpdatedBy, &plant.PlantedBy, &plant.PlantedByName, ) return plant, err } // Insert creates a plant in its garden. func (m PlantModel) Insert(plant storage.Plant) (storage.Plant, error) { ctx, cancel := contextWithTimeout() defer cancel() err := m.DB.QueryRowContext(ctx, ` INSERT INTO plants (garden_id, species_id, name, notes, acquired_at, status, removed_at, attributes, image_data, image_id, created_by, updated_by, planted_by) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, '', $9, $10, $11, $12) RETURNING id, created_at, updated_at, version`, plant.GardenID, plant.SpeciesID, plant.Name, plant.Notes, plant.AcquiredAt, plant.Status, plant.RemovedAt, jsonValue(plant.Attributes), plant.ImageID, nullableUserID(plant.CreatedBy), nullableUserID(plant.UpdatedBy), nullableUserID(plant.PlantedBy), ).Scan(&plant.ID, &plant.CreatedAt, &plant.UpdatedAt, &plant.Version) if err != nil { return storage.Plant{}, recordError(err) } return plant, nil } // Get returns a plant within its garden. func (m PlantModel) Get(gardenID, id int) (storage.Plant, error) { ctx, cancel := contextWithTimeout() defer cancel() plant, err := scanPlant(m.DB.QueryRowContext(ctx, `SELECT `+plantColumns+` FROM plants p LEFT JOIN users u ON u.id=p.planted_by LEFT JOIN images i ON i.id=p.image_id WHERE p.garden_id = $1 AND p.id = $2`, gardenID, id)) if err != nil { return storage.Plant{}, recordError(err) } return plant, nil } // GetAllForGarden lists plants in a garden. func (m PlantModel) GetAllForGarden(gardenID int) ([]storage.Plant, error) { ctx, cancel := contextWithTimeout() defer cancel() rows, err := m.DB.QueryContext(ctx, `SELECT `+plantColumns+` FROM plants p LEFT JOIN users u ON u.id=p.planted_by LEFT JOIN images i ON i.id=p.image_id WHERE p.garden_id = $1 ORDER BY p.name, p.id`, gardenID) if err != nil { return nil, err } defer rows.Close() plants := []storage.Plant{} for rows.Next() { plant, err := scanPlant(rows) if err != nil { return nil, err } plants = append(plants, plant) } return plants, rows.Err() } // Update changes a plant using optimistic locking. func (m PlantModel) Update(gardenID int, plant storage.Plant) (storage.Plant, error) { ctx, cancel := contextWithTimeout() defer cancel() err := m.DB.QueryRowContext(ctx, ` UPDATE plants SET species_id = $1, name = $2, notes = $3, acquired_at = $4, status = $5, removed_at = $6, attributes = $7, image_data = '', image_id = $8, updated_by = $9, updated_at = CURRENT_TIMESTAMP, version = version + 1 WHERE garden_id = $10 AND id = $11 AND version = $12 RETURNING updated_at, version`, plant.SpeciesID, plant.Name, plant.Notes, plant.AcquiredAt, plant.Status, plant.RemovedAt, jsonValue(plant.Attributes), plant.ImageID, nullableUserID(plant.UpdatedBy), gardenID, plant.ID, plant.Version, ).Scan(&plant.UpdatedAt, &plant.Version) if err != nil { if err == sql.ErrNoRows { return storage.Plant{}, storage.ErrEditConflict } return storage.Plant{}, recordError(err) } return plant, nil } // Delete removes a plant within its garden. func (m PlantModel) Delete(gardenID, id int) error { return deleteByGardenID(m.DB, `DELETE FROM plants WHERE garden_id = $1 AND id = $2`, gardenID, id) }