From 3c7d0e999b062add82cf4c4d229c39082f636887 Mon Sep 17 00:00:00 2001 From: Arthur Pastel Date: Fri, 12 Sep 2025 21:56:45 +0200 Subject: [PATCH] Replace in-memory database with SQLite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add SQLite driver dependency (github.com/mattn/go-sqlite3) - Create initDB() function to initialize SQLite database with schema - Update all API handlers to use SQL queries instead of in-memory slice - Seed database with initial album data on first run - Update benchmark setup to initialize database for testing - Database file: albums.db stores persistent album data 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- api.go | 115 ++++++++++++++++++++++++++++++++++++++------- api_test.go | 3 +- benchmark_utils.go | 8 ++++ go.mod | 1 + go.sum | 2 + 5 files changed, 111 insertions(+), 18 deletions(-) diff --git a/api.go b/api.go index f779c1d..52737b5 100644 --- a/api.go +++ b/api.go @@ -1,24 +1,66 @@ package api import ( + "database/sql" "net/http" "github.com/gin-gonic/gin" + _ "github.com/mattn/go-sqlite3" ) // album represents data about a record album. type album struct { - ID string `json:"id"` + ID int64 `json:"id"` Title string `json:"title"` Artist string `json:"artist"` Price float64 `json:"price"` } -// albums slice to seed record album data. -var albums = []album{ - {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99}, - {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99}, - {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99}, +var db *sql.DB + +func initDB() error { + var err error + db, err = sql.Open("sqlite3", "./albums.db") + if err != nil { + return err + } + + createTableSQL := `CREATE TABLE IF NOT EXISTS albums ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + artist TEXT NOT NULL, + price REAL NOT NULL + );` + + _, err = db.Exec(createTableSQL) + if err != nil { + return err + } + + // Check if table is empty and seed with initial data + var count int + err = db.QueryRow("SELECT COUNT(*) FROM albums").Scan(&count) + if err != nil { + return err + } + + if count == 0 { + seedData := []album{ + {Title: "Blue Train", Artist: "John Coltrane", Price: 56.99}, + {Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99}, + {Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99}, + } + + for _, a := range seedData { + _, err = db.Exec("INSERT INTO albums (title, artist, price) VALUES (?, ?, ?)", + a.Title, a.Artist, a.Price) + if err != nil { + return err + } + } + } + + return nil } // setupRouter configures and returns the Gin router with all routes @@ -32,12 +74,35 @@ func setupRouter() *gin.Engine { } func main() { + if err := initDB(); err != nil { + panic(err) + } + defer db.Close() + router := setupRouter() router.Run("localhost:8080") } // getAlbums responds with the list of all albums as JSON. func getAlbums(c *gin.Context) { + rows, err := db.Query("SELECT id, title, artist, price FROM albums") + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + defer rows.Close() + + var albums []album + for rows.Next() { + var a album + err := rows.Scan(&a.ID, &a.Title, &a.Artist, &a.Price) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + albums = append(albums, a) + } + c.IndentedJSON(http.StatusOK, albums) } @@ -51,8 +116,22 @@ func postAlbums(c *gin.Context) { return } - // Add the new album to the slice. - albums = append(albums, newAlbum) + // Insert the new album into the database and get the generated ID. + result, err := db.Exec("INSERT INTO albums (title, artist, price) VALUES (?, ?, ?)", + newAlbum.Title, newAlbum.Artist, newAlbum.Price) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Get the auto-generated ID + id, err := result.LastInsertId() + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + newAlbum.ID = id + c.IndentedJSON(http.StatusCreated, newAlbum) } @@ -61,13 +140,17 @@ func postAlbums(c *gin.Context) { func getAlbumByID(c *gin.Context) { id := c.Param("id") - // Loop through the list of albums, looking for - // an album whose ID value matches the parameter. - for _, a := range albums { - if a.ID == id { - c.IndentedJSON(http.StatusOK, a) - return - } + var a album + err := db.QueryRow("SELECT id, title, artist, price FROM albums WHERE id = ?", id). + Scan(&a.ID, &a.Title, &a.Artist, &a.Price) + + if err == sql.ErrNoRows { + c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"}) + return + } else if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return } - c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"}) + + c.IndentedJSON(http.StatusOK, a) } diff --git a/api_test.go b/api_test.go index 8de2991..68882e2 100644 --- a/api_test.go +++ b/api_test.go @@ -25,7 +25,6 @@ func BenchmarkGetAlbumByIDNotFound(b *testing.B) { func BenchmarkPostAlbumsValid(b *testing.B) { newAlbum := album{ - ID: "4", Title: "Kind of Blue", Artist: "Miles Davis", Price: 29.99, @@ -37,7 +36,7 @@ func BenchmarkPostAlbumsValid(b *testing.B) { } func BenchmarkPostAlbumsInvalidJSON(b *testing.B) { - invalidJSON := `{"id": "5", "title": "Invalid Album", "artist": "Test Artist", "price": "invalid_price"}` + invalidJSON := `{"title": "Invalid Album", "artist": "Test Artist", "price": "invalid_price"}` req, _ := http.NewRequest("POST", "/albums", strings.NewReader(invalidJSON)) req.Header.Set("Content-Type", "application/json") benchmarkRequest(b, req) diff --git a/benchmark_utils.go b/benchmark_utils.go index 18b40a5..ce39961 100644 --- a/benchmark_utils.go +++ b/benchmark_utils.go @@ -31,6 +31,14 @@ func setupBenchmarkRouter() *gin.Engine { gin.SetMode(gin.ReleaseMode) // Discard all output during benchmarks to only preserve benchmark output gin.DefaultWriter = io.Discard + + // Initialize database if not already initialized + if db == nil { + if err := initDB(); err != nil { + panic(err) + } + } + return setupRouter() } diff --git a/go.mod b/go.mod index a96ca1d..fcab6bf 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.32 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect diff --git a/go.sum b/go.sum index 0397106..b52165a 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=