From 72eacc461ed03595cd6a445f138ebf538012fbce Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Fri, 2 May 2025 02:23:47 +0200 Subject: [PATCH] feat: update methods & improved demo --- .gitignore | 1 - README.md | 35 +++++++++++++++++------ go.mod | 3 ++ go.sum | 9 ++++++ main.go | 74 ++++++++++++++++++++++++++++++++++++++++-------- store/address.go | 10 +++++++ store/db.go | 7 ++--- store/user.go | 11 ++++++- 8 files changed, 123 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index 60cd341..3fec32c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ tmp/ -data/ diff --git a/README.md b/README.md index bab6e22..85562b9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This project demonstrates a proof of concept for a database solution in Go that ## Disclaimer ⚠️ -This is not clean architecture friendly yet. +This project is not clean architecture friendly, yet. ## Core Features @@ -15,23 +15,23 @@ This is not clean architecture friendly yet. ## Project Structure -- **main.go**: Example usage demonstrating user creation and address management -- **store/db.go**: Database connection management -- **store/user.go**: User entity operations (create, find, list) +- **main.go**: Example usage demonstrating CRUD operations and relationships +- **store/db.go**: Database connection management with in-memory storage +- **store/user.go**: User entity operations (create, find, update, list) - **store/address.go**: Address entity operations with user relationships ## How It Works ### Database Management -The project uses BadgerDB as the underlying storage engine. The `store.InitDB()` function sets up the database connection with a singleton pattern using `sync.Once` to ensure it's only initialized once. +The project uses BadgerDB as the underlying storage engine. The `store.InitDB()` function sets up an in-memory database connection with a singleton pattern using `sync.Once` to ensure it's only initialized once. ### Data Models The project defines strongly typed Go structs to enforce schema: - `User`: Stores user information with ID, name, email, and password -- `Address`: Stores address information with reference to a user +- `Address`: Stores address information with reference to a user via UserID ### Data Access Pattern @@ -46,6 +46,10 @@ addr, _ := store.AddAddressToUser(user.ID, "123 Main St", "Paris", "75001") // List addresses for a user addresses, _ := store.ListAddressesByUserID(user.ID) + +// Update user data +user.Name = "Bobby" +store.UpdateUser(user) ``` ### "Relational" Modeling @@ -54,6 +58,7 @@ The project demonstrates relationship modeling between users and addresses: 1. Each address has a `UserID` field linking it to its owner 2. The `ListAddressesByUserID` function retrieves all addresses for a specific user +3. Updates to either entity maintain relationship integrity ### Data Consistency @@ -61,8 +66,20 @@ The project implements data consistency features: - Email uniqueness enforcement using an email-to-ID index - Transaction support for atomic operations +- Error handling for constraint violations -## Dependencies +### Terminal Demo -- [BadgerDB v4](https://github.com/dgraph-io/badger) - Embedded key-value database -- [Google UUID](https://github.com/google/uuid) - For generating unique identifiers +The main.go file provides a demonstration that showcases: + +1. User creation with validation +2. Address management (create, read, update) +3. Relationship handling (users have multiple addresses) +4. Constraint enforcement (unique emails) +5. Entity retrieval by ID and other fields + +## Running the Demo + +```bash +go run main.go +``` diff --git a/go.mod b/go.mod index 37fdd1f..ab950d1 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,14 @@ require ( github.com/dgraph-io/badger/v4 v4.7.0 // indirect github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect diff --git a/go.sum b/go.sum index fe4a518..d7e5098 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINA github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -17,6 +19,11 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +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= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= @@ -27,6 +34,8 @@ go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= diff --git a/main.go b/main.go index 6847df5..52eed9e 100644 --- a/main.go +++ b/main.go @@ -2,38 +2,88 @@ package main import ( "fmt" + "strings" "github.com/Floriansylvain/GoAntiSqlPOC/store" + "github.com/fatih/color" ) +func printSection(title string) { + color.Cyan("\n" + strings.Repeat("-", 40)) + color.Cyan(" " + title) +} + +func printSuccess(message string) { + color.Green("✅ " + message) +} + +func printInfo(message string) { + color.White(" " + message) +} + +func printWarning(message string) { + color.Yellow("⚠️ " + message) +} + func main() { store.InitDB() defer store.CloseDB() - // Create user + printSection("USER CREATION") user, err := store.CreateUser("Bob", "bob@example.com", "hunter2") if err != nil { panic(err) } - fmt.Printf("Created user: %+v\n", user) + printInfo(fmt.Sprintf("Created: %s | %s | %s", user.ID, user.Name, user.Email)) - // Add addresses - _, _ = store.AddAddressToUser(user.ID, "123 Main St", "Paris", "75001") - _, _ = store.AddAddressToUser(user.ID, "456 Side Rd", "Lyon", "69000") + printSection("ADDRESS MANAGEMENT") + addr1, err := store.AddAddressToUser(user.ID, "123 Main St", "Paris", "75001") + if err != nil { + panic(err) + } + printInfo(fmt.Sprintf("Added: %s | %s, %s %s", addr1.ID, addr1.Line1, addr1.City, addr1.ZipCode)) + + addr2, err := store.AddAddressToUser(user.ID, "456 Side Rd", "Lyon", "69000") + if err != nil { + panic(err) + } + printInfo(fmt.Sprintf("Added: %s | %s, %s %s", addr2.ID, addr2.Line1, addr2.City, addr2.ZipCode)) - // Retrieve addresses addrs, err := store.ListAddressesByUserID(user.ID) if err != nil { panic(err) } - fmt.Println("Addresses for", user.Name) - for _, a := range addrs { - fmt.Printf("- %s, %s %s\n", a.Line1, a.City, a.ZipCode) + printInfo(fmt.Sprintf("Retrieved: %d addresses for %s", len(addrs), user.Name)) + + printSection("DATA UPDATES") + if len(addrs) > 0 { + printInfo(fmt.Sprintf("Original city: %s → Changing to Marseille", addrs[0].City)) + addrs[0].City = "Marseille" + err := store.UpdateAddress(addrs[0]) + if err != nil { + panic(err) + } } - // Attempt duplicate email - _, err = store.CreateUser("Alice", "bob@example.com", "oops") + addrs, _ = store.ListAddressesByUserID(user.ID) + printInfo(fmt.Sprintf("Address now: %s, %s %s", addrs[0].Line1, addrs[0].City, addrs[0].ZipCode)) + + printInfo(fmt.Sprintf("User name: %s → Changing to Bobby", user.Name)) + user.Name = "Bobby" + if err := store.UpdateUser(user); err != nil { + panic(err) + } + newUser, _ := store.FindUserByID(user.ID) + printInfo(fmt.Sprintf("Name now: %s", newUser.Name)) + + printSection("SEARCH & CONSTRAINTS") + foundUser, _ := store.FindUserByEmail("bob@example.com") + printInfo(fmt.Sprintf("Found by email: %s | %s", foundUser.ID, foundUser.Name)) + + _, err = store.CreateUser("Alice", "bob@example.com", "password123") if err != nil { - fmt.Println("Expected error:", err) + printSuccess("Email uniqueness enforced: " + err.Error()) + } else { + printWarning("Failed email uniqueness check!") } } diff --git a/store/address.go b/store/address.go index f7af8be..cc694dd 100644 --- a/store/address.go +++ b/store/address.go @@ -57,3 +57,13 @@ func ListAddressesByUserID(userID string) ([]*Address, error) { }) return addresses, err } + +func UpdateAddress(a *Address) error { + data, err := json.Marshal(a) + if err != nil { + return err + } + return GetDB().Update(func(txn *badger.Txn) error { + return txn.Set([]byte(addressPrefix+a.ID), data) + }) +} diff --git a/store/db.go b/store/db.go index 29bf05d..70ea86e 100644 --- a/store/db.go +++ b/store/db.go @@ -8,14 +8,13 @@ import ( ) var ( - db *badger.DB - once sync.Once - dbPath = "./data" + db *badger.DB + once sync.Once ) func InitDB() { once.Do(func() { - opts := badger.DefaultOptions(dbPath).WithLogger(nil) + opts := badger.DefaultOptions("").WithInMemory(true).WithLogger(nil) var err error db, err = badger.Open(opts) if err != nil { diff --git a/store/user.go b/store/user.go index f9d9834..4b1b3c6 100644 --- a/store/user.go +++ b/store/user.go @@ -25,7 +25,6 @@ var ( ) func CreateUser(name, email, password string) (*User, error) { - // Enforce uniqueness if _, err := findUserIDByEmail(email); err == nil { return nil, ErrEmailExists } @@ -113,3 +112,13 @@ func ListUsers() ([]*User, error) { }) return users, err } + +func UpdateUser(u *User) error { + data, err := json.Marshal(u) + if err != nil { + return err + } + return GetDB().Update(func(txn *badger.Txn) error { + return txn.Set([]byte(userPrefix+u.ID), data) + }) +}