package store import ( "context" "database/sql" "errors" "fmt" "gitea.stevedudenhoeffer.com/steve/pansy/internal/domain" ) // plantingColumns lists plantings columns in the order scanPlanting expects. // Used unqualified for direct selects; the /full read below qualifies with pl. const plantingColumns = `id, object_id, plant_id, x_cm, y_cm, radius_cm, count, label, planted_at, removed_at, version, created_at, updated_at` func scanPlanting(s scanner) (*domain.Planting, error) { var p domain.Planting if err := s.Scan( &p.ID, &p.ObjectID, &p.PlantID, &p.XCM, &p.YCM, &p.RadiusCM, &p.Count, &p.Label, &p.PlantedAt, &p.RemovedAt, &p.Version, &p.CreatedAt, &p.UpdatedAt, ); err != nil { return nil, err } return &p, nil } // ListActivePlantingsForGarden returns every currently-planted plop (removed_at // IS NULL) across all objects in a garden — the editor's one-shot load. Always a // non-nil slice. The service fills each row's DerivedCount; this is the raw read. func (d *DB) ListActivePlantingsForGarden(ctx context.Context, gardenID int64) ([]domain.Planting, error) { rows, err := d.sql.QueryContext(ctx, `SELECT `+qualifyColumns("pl", plantingColumns)+` FROM plantings pl JOIN garden_objects o ON o.id = pl.object_id WHERE o.garden_id = ? AND pl.removed_at IS NULL ORDER BY pl.id`, gardenID, ) if err != nil { return nil, fmt.Errorf("store: list plantings: %w", err) } defer rows.Close() plantings := []domain.Planting{} for rows.Next() { p, err := scanPlanting(rows) if err != nil { return nil, fmt.Errorf("store: scan planting: %w", err) } plantings = append(plantings, *p) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("store: iterate plantings: %w", err) } return plantings, nil } // GetPlanting returns the planting with the given id, or domain.ErrNotFound. func (d *DB) GetPlanting(ctx context.Context, id int64) (*domain.Planting, error) { p, err := scanPlanting(d.sql.QueryRowContext(ctx, `SELECT `+plantingColumns+` FROM plantings WHERE id = ?`, id)) if errors.Is(err, sql.ErrNoRows) { return nil, domain.ErrNotFound } if err != nil { return nil, fmt.Errorf("store: get planting: %w", err) } return p, nil } // CreatePlanting inserts a plop (fields already validated by the service) and // returns the stored row. removed_at is always NULL on create — a new plop is // active; "clear bed" sets removed_at later via UpdatePlanting. func (d *DB) CreatePlanting(ctx context.Context, p *domain.Planting) (*domain.Planting, error) { created, err := scanPlanting(d.sql.QueryRowContext(ctx, `INSERT INTO plantings (object_id, plant_id, x_cm, y_cm, radius_cm, count, label, planted_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING `+plantingColumns, p.ObjectID, p.PlantID, p.XCM, p.YCM, p.RadiusCM, p.Count, p.Label, p.PlantedAt, )) if err != nil { return nil, fmt.Errorf("store: insert planting: %w", err) } return created, nil } // UpdatePlanting applies a version-guarded update of all mutable columns (the // service merges partial patches first). Returns the updated row, or // (current row, ErrVersionConflict) / ErrNotFound — the same contract as the // other mutable resources. func (d *DB) UpdatePlanting(ctx context.Context, p *domain.Planting) (*domain.Planting, error) { updated, err := scanPlanting(d.sql.QueryRowContext(ctx, `UPDATE plantings SET plant_id = ?, x_cm = ?, y_cm = ?, radius_cm = ?, count = ?, label = ?, planted_at = ?, removed_at = ?, version = version + 1, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ? AND version = ? RETURNING `+plantingColumns, p.PlantID, p.XCM, p.YCM, p.RadiusCM, p.Count, p.Label, p.PlantedAt, p.RemovedAt, p.ID, p.Version, )) if errors.Is(err, sql.ErrNoRows) { current, gerr := d.GetPlanting(ctx, p.ID) if gerr != nil { return nil, gerr } return current, domain.ErrVersionConflict } if err != nil { return nil, fmt.Errorf("store: update planting: %w", err) } return updated, nil } // DeletePlanting hard-deletes a plop (for mistakes; "removed/harvested" flows set // removed_at instead). Returns domain.ErrNotFound if no row was deleted. func (d *DB) DeletePlanting(ctx context.Context, id int64) error { res, err := d.sql.ExecContext(ctx, `DELETE FROM plantings WHERE id = ?`, id) if err != nil { return fmt.Errorf("store: delete planting: %w", err) } n, err := res.RowsAffected() if err != nil { return fmt.Errorf("store: planting delete rows: %w", err) } if n == 0 { return domain.ErrNotFound } return nil }