package model import ( "database/sql" "errors" "fmt" "log/slog" "time" ) type Snippet struct { ID int // Title string // Content string Title sql.NullString Content sql.NullString CreatedAt time.Time UpdatedAt time.Time ExpiresAt time.Time } func (s *Snippet) GetTitle() { return } type SnippetServiceInterface interface { Insert(title, content string, expiresAt int) (int, error) Get(id int) (Snippet, error) Lastest() ([]Snippet, error) } type SnippetService struct { DB *sql.DB } // Insert inserts a new SnippetModel into the database func (s *SnippetService) Insert(title, content string, expiresAt int) (int, error) { slog.Debug(fmt.Sprintf("Inserting new snippet. Title: %s", title)) // Really don't prepare statements. There are a lot of gotcha's where they exist on the connection objects they were created. They can potentially // recreate connections. It's an optimization you probably don't need at the moment. stmt, err := s.DB.Prepare("INSERT INTO snippets (title, content, expires_at) VALUES ($1, $2, DATETIME(CURRENT_TIMESTAMP, '+' || $3 || ' DAY'))") if err != nil { slog.Debug("The prepared statement has an error") return 0, err } defer stmt.Close() // stmt.Exec returns a sql.Result. That also has access to the statement metadata // use _ if you don't care about the result and only want to check the err. // Exec will NOT reserve a connection. unlike db.Query which returns a sql.Rows that // will hold on to a connection until .Close() is called. res, err := stmt.Exec(title, content, expiresAt) if err != nil { slog.Debug("SQL DML statement returned an error.") return 0, err } // Use the LastInsertId() method on the result to get the ID of our // newly inserted record in the snippets table. lastId, err := res.LastInsertId() if err != nil { slog.Debug("An error occured when retrieving insert result id.") return 0, err } // The ID returned has the type int64, so we convert it to an int type // before returning. slog.Debug(fmt.Sprintf("Inserted new snippet. Snippet pk: %d", int(lastId))) return int(lastId), nil } // Get retrieves a specific Snippet by ID ignoring the record if expired. func (s *SnippetService) Get(id int) (Snippet, error) { stmt := `SELECT id, title, content, created_at, updated_at, expires_at FROM snippets WHERE expires_at > CURRENT_TIMESTAMP AND id = $1` // errors from DB.QueryRow() are deferred until Scan() is called. // meaning you could also have used DB.QueryRow(...).Scan(...) row := s.DB.QueryRow(stmt, id) var snip Snippet err := row.Scan(&snip.ID, &snip.Title, &snip.Content, &snip.CreatedAt, &snip.UpdatedAt, &snip.ExpiresAt, ) if err != nil { slog.Debug("SQL DML statement returned an error.") // Loop up the difference between errors.Is and errors.As if errors.Is(err, sql.ErrNoRows) { return Snippet{}, ErrNoRecord } else { return Snippet{}, err } } return snip, nil } // Latest retrieves up to latest 10 Snippets from the database. func (s *SnippetService) Lastest() ([]Snippet, error) { stmt := `SELECT id, title, content, created_at, updated_at, expires_at FROM snippets WHERE expires_at > CURRENT_TIMESTAMP ORDER BY id DESC LIMIT 10` rows, err := s.DB.Query(stmt) if err != nil { return nil, err } // We defer rows.Close() to ensure the sql.Rows resultset is // always properly closed before the Latest() method returns. This defer // statement should come *after* you check for an error from the Query() // method. Otherwise, if Query() returns an error, you'll get a panic // trying to close a nil resultset. defer rows.Close() var snippets []Snippet // Use rows.Next to iterate through the rows in the resultset. This // prepares the first (and then each subsequent) row to be acted on by the // rows.Scan() method. If iteration over all the rows completes then the // resultset automatically closes itself and frees-up the underlying // database connection. for rows.Next() { var snip Snippet err := rows.Scan(&snip.ID, &snip.Title, &snip.Content, &snip.CreatedAt, &snip.UpdatedAt, &snip.ExpiresAt, ) if err != nil { return nil, err } snippets = append(snippets, snip) } // When the rows.Next() loop has finished we call rows.Err() to retrieve any // error that was encountered during the iteration. It's important to // call this - don't assume that a successful iteration was completed // over the whole resultset. if err = rows.Err(); err != nil { return nil, err } return snippets, nil }