mirror of
https://github.com/Floriansylvain/GoAntiSqlPOC.git
synced 2026-08-19 11:43:24 +02:00
feat: update methods & improved demo
This commit is contained in:
@@ -1,2 +1 @@
|
|||||||
tmp/
|
tmp/
|
||||||
data/
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
+1
-2
@@ -10,12 +10,11 @@ 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
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user