feat: update methods & improved demo

This commit is contained in:
Florian Sylvain
2025-05-02 02:23:47 +02:00
parent dde8487382
commit 72eacc461e
8 changed files with 123 additions and 27 deletions
-1
View File
@@ -1,2 +1 @@
tmp/ tmp/
data/
+26 -9
View File
@@ -4,7 +4,7 @@ This project demonstrates a proof of concept for a database solution in Go that
## Disclaimer ⚠️ ## Disclaimer ⚠️
This is not clean architecture friendly yet. This project is not clean architecture friendly, yet.
## Core Features ## Core Features
@@ -15,23 +15,23 @@ This is not clean architecture friendly yet.
## Project Structure ## Project Structure
- **main.go**: Example usage demonstrating user creation and address management - **main.go**: Example usage demonstrating CRUD operations and relationships
- **store/db.go**: Database connection management - **store/db.go**: Database connection management with in-memory storage
- **store/user.go**: User entity operations (create, find, list) - **store/user.go**: User entity operations (create, find, update, list)
- **store/address.go**: Address entity operations with user relationships - **store/address.go**: Address entity operations with user relationships
## How It Works ## How It Works
### Database Management ### 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 ### Data Models
The project defines strongly typed Go structs to enforce schema: The project defines strongly typed Go structs to enforce schema:
- `User`: Stores user information with ID, name, email, and password - `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 ### Data Access Pattern
@@ -46,6 +46,10 @@ addr, _ := store.AddAddressToUser(user.ID, "123 Main St", "Paris", "75001")
// List addresses for a user // List addresses for a user
addresses, _ := store.ListAddressesByUserID(user.ID) addresses, _ := store.ListAddressesByUserID(user.ID)
// Update user data
user.Name = "Bobby"
store.UpdateUser(user)
``` ```
### "Relational" Modeling ### "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 1. Each address has a `UserID` field linking it to its owner
2. The `ListAddressesByUserID` function retrieves all addresses for a specific user 2. The `ListAddressesByUserID` function retrieves all addresses for a specific user
3. Updates to either entity maintain relationship integrity
### Data Consistency ### Data Consistency
@@ -61,8 +66,20 @@ The project implements data consistency features:
- Email uniqueness enforcement using an email-to-ID index - Email uniqueness enforcement using an email-to-ID index
- Transaction support for atomic operations - 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 The main.go file provides a demonstration that showcases:
- [Google UUID](https://github.com/google/uuid) - For generating unique identifiers
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
```
+3
View File
@@ -7,11 +7,14 @@ require (
github.com/dgraph-io/badger/v4 v4.7.0 // indirect github.com/dgraph-io/badger/v4 v4.7.0 // indirect
github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // 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/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.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/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect
+9
View File
@@ -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/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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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.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 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 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/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 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 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 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 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= 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 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 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 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+62 -12
View File
@@ -2,38 +2,88 @@ package main
import ( import (
"fmt" "fmt"
"strings"
"github.com/Floriansylvain/GoAntiSqlPOC/store" "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() { func main() {
store.InitDB() store.InitDB()
defer store.CloseDB() defer store.CloseDB()
// Create user printSection("USER CREATION")
user, err := store.CreateUser("Bob", "bob@example.com", "hunter2") user, err := store.CreateUser("Bob", "bob@example.com", "hunter2")
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Printf("Created user: %+v\n", user) printInfo(fmt.Sprintf("Created: %s | %s | %s", user.ID, user.Name, user.Email))
// Add addresses printSection("ADDRESS MANAGEMENT")
_, _ = store.AddAddressToUser(user.ID, "123 Main St", "Paris", "75001") addr1, err := store.AddAddressToUser(user.ID, "123 Main St", "Paris", "75001")
_, _ = store.AddAddressToUser(user.ID, "456 Side Rd", "Lyon", "69000") 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) addrs, err := store.ListAddressesByUserID(user.ID)
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Println("Addresses for", user.Name) printInfo(fmt.Sprintf("Retrieved: %d addresses for %s", len(addrs), user.Name))
for _, a := range addrs {
fmt.Printf("- %s, %s %s\n", a.Line1, a.City, a.ZipCode) 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 addrs, _ = store.ListAddressesByUserID(user.ID)
_, err = store.CreateUser("Alice", "bob@example.com", "oops") 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 { if err != nil {
fmt.Println("Expected error:", err) printSuccess("Email uniqueness enforced: " + err.Error())
} else {
printWarning("Failed email uniqueness check!")
} }
} }
+10
View File
@@ -57,3 +57,13 @@ func ListAddressesByUserID(userID string) ([]*Address, error) {
}) })
return addresses, err 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)
})
}
+3 -4
View File
@@ -8,14 +8,13 @@ import (
) )
var ( var (
db *badger.DB db *badger.DB
once sync.Once once sync.Once
dbPath = "./data"
) )
func InitDB() { func InitDB() {
once.Do(func() { once.Do(func() {
opts := badger.DefaultOptions(dbPath).WithLogger(nil) opts := badger.DefaultOptions("").WithInMemory(true).WithLogger(nil)
var err error var err error
db, err = badger.Open(opts) db, err = badger.Open(opts)
if err != nil { if err != nil {
+10 -1
View File
@@ -25,7 +25,6 @@ var (
) )
func CreateUser(name, email, password string) (*User, error) { func CreateUser(name, email, password string) (*User, error) {
// Enforce uniqueness
if _, err := findUserIDByEmail(email); err == nil { if _, err := findUserIDByEmail(email); err == nil {
return nil, ErrEmailExists return nil, ErrEmailExists
} }
@@ -113,3 +112,13 @@ func ListUsers() ([]*User, error) {
}) })
return users, err 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)
})
}