From e6f9e96c8f86b46812ecae78fa737fa06c1dfd58 Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Fri, 2 May 2025 01:30:37 +0200 Subject: [PATCH] Initial Commit --- .gitignore | 2 + README.md | 64 ++++++++++++++++++++++++++ go.mod | 22 +++++++++ go.sum | 33 ++++++++++++++ main.go | 39 ++++++++++++++++ store/address.go | 59 ++++++++++++++++++++++++ store/db.go | 35 +++++++++++++++ store/user.go | 115 +++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 369 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 store/address.go create mode 100644 store/db.go create mode 100644 store/user.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..60cd341 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +tmp/ +data/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e587400 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# GoAntiSqlPOC - SQL-Free Database Management in Go + +This project demonstrates a proof of concept for a database solution in Go that uses zero SQL or text-based query languages. Instead, it leverages [BadgerDB](https://github.com/dgraph-io/badger), a fast key-value store written in Go, to provide structured data storage with API-style access patterns. + +## Core Features + +- **No SQL or Query Languages** - Pure programmatic data access through Go functions +- **Strong Schema Enforcement** - Type safety via Go structs +- **Relational-Like Data Modeling** - Links between entities using IDs and indexes +- **Clean API-Style Access** - Simple function calls instead of writing queries + +## 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) +- **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. + +### 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 + +### Data Access Pattern + +Instead of SQL queries, all data access is done through simple function calls: + +```go +// Create a user +user, _ := store.CreateUser("Bob", "bob@example.com", "hunter2") + +// Add an address +addr, _ := store.AddAddressToUser(user.ID, "123 Main St", "Paris", "75001") + +// List addresses for a user +addresses, _ := store.ListAddressesByUserID(user.ID) +``` + +### "Relational" Modeling + +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 + +### Data Consistency + +The project implements data consistency features: + +- Email uniqueness enforcement using an email-to-ID index +- Transaction support for atomic operations + +## Dependencies + +- [BadgerDB v4](https://github.com/dgraph-io/badger) - Embedded key-value database +- [Google UUID](https://github.com/google/uuid) - For generating unique identifiers diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..37fdd1f --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +module github.com/Floriansylvain/GoAntiSqlPOC + +go 1.24.1 + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + 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/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 + 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 + go.opentelemetry.io/otel/trace v1.35.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sys v0.31.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fe4a518 --- /dev/null +++ b/go.sum @@ -0,0 +1,33 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dgraph-io/badger/v4 v4.7.0 h1:Q+J8HApYAY7UMpL8d9owqiB+odzEc0zn/aqOD9jhc6Y= +github.com/dgraph-io/badger/v4 v4.7.0/go.mod h1:He7TzG3YBy3j4f5baj5B7Zl2XyfNe5bl4Udl0aPemVA= +github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= +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/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= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +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= +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= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +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.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= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= diff --git a/main.go b/main.go new file mode 100644 index 0000000..6847df5 --- /dev/null +++ b/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + + "github.com/Floriansylvain/GoAntiSqlPOC/store" +) + +func main() { + store.InitDB() + defer store.CloseDB() + + // Create user + user, err := store.CreateUser("Bob", "bob@example.com", "hunter2") + if err != nil { + panic(err) + } + fmt.Printf("Created user: %+v\n", user) + + // Add addresses + _, _ = store.AddAddressToUser(user.ID, "123 Main St", "Paris", "75001") + _, _ = store.AddAddressToUser(user.ID, "456 Side Rd", "Lyon", "69000") + + // 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) + } + + // Attempt duplicate email + _, err = store.CreateUser("Alice", "bob@example.com", "oops") + if err != nil { + fmt.Println("Expected error:", err) + } +} diff --git a/store/address.go b/store/address.go new file mode 100644 index 0000000..f7af8be --- /dev/null +++ b/store/address.go @@ -0,0 +1,59 @@ +package store + +import ( + "encoding/json" + + "github.com/dgraph-io/badger/v4" + "github.com/google/uuid" +) + +type Address struct { + ID string + UserID string + Line1 string + City string + ZipCode string +} + +const addressPrefix = "addr:" + +func AddAddressToUser(userID, line1, city, zip string) (*Address, error) { + addr := &Address{ + ID: uuid.NewString(), + UserID: userID, + Line1: line1, + City: city, + ZipCode: zip, + } + data, err := json.Marshal(addr) + if err != nil { + return nil, err + } + err = GetDB().Update(func(txn *badger.Txn) error { + return txn.Set([]byte(addressPrefix+addr.ID), data) + }) + return addr, err +} + +func ListAddressesByUserID(userID string) ([]*Address, error) { + addresses := []*Address{} + err := GetDB().View(func(txn *badger.Txn) error { + it := txn.NewIterator(badger.DefaultIteratorOptions) + defer it.Close() + for it.Seek([]byte(addressPrefix)); it.ValidForPrefix([]byte(addressPrefix)); it.Next() { + item := it.Item() + err := item.Value(func(v []byte) error { + var a Address + if err := json.Unmarshal(v, &a); err == nil && a.UserID == userID { + addresses = append(addresses, &a) + } + return nil + }) + if err != nil { + return err + } + } + return nil + }) + return addresses, err +} diff --git a/store/db.go b/store/db.go new file mode 100644 index 0000000..29bf05d --- /dev/null +++ b/store/db.go @@ -0,0 +1,35 @@ +package store + +import ( + "log" + "sync" + + "github.com/dgraph-io/badger/v4" +) + +var ( + db *badger.DB + once sync.Once + dbPath = "./data" +) + +func InitDB() { + once.Do(func() { + opts := badger.DefaultOptions(dbPath).WithLogger(nil) + var err error + db, err = badger.Open(opts) + if err != nil { + log.Fatal(err) + } + }) +} + +func CloseDB() { + if db != nil { + db.Close() + } +} + +func GetDB() *badger.DB { + return db +} diff --git a/store/user.go b/store/user.go new file mode 100644 index 0000000..f9d9834 --- /dev/null +++ b/store/user.go @@ -0,0 +1,115 @@ +package store + +import ( + "encoding/json" + "errors" + + "github.com/dgraph-io/badger/v4" + "github.com/google/uuid" +) + +type User struct { + ID string + Name string + Email string + Password string +} + +const ( + userPrefix = "user:" + emailIndexPref = "email:" +) + +var ( + ErrEmailExists = errors.New("email already registered") +) + +func CreateUser(name, email, password string) (*User, error) { + // Enforce uniqueness + if _, err := findUserIDByEmail(email); err == nil { + return nil, ErrEmailExists + } + + user := &User{ + ID: uuid.NewString(), + Name: name, + Email: email, + Password: password, + } + data, err := json.Marshal(user) + if err != nil { + return nil, err + } + + err = GetDB().Update(func(txn *badger.Txn) error { + if err := txn.Set([]byte(userPrefix+user.ID), data); err != nil { + return err + } + return txn.Set([]byte(emailIndexPref+email), []byte(user.ID)) + }) + + return user, err +} + +func FindUserByID(id string) (*User, error) { + var user User + err := GetDB().View(func(txn *badger.Txn) error { + item, err := txn.Get([]byte(userPrefix + id)) + if err != nil { + return err + } + return item.Value(func(val []byte) error { + return json.Unmarshal(val, &user) + }) + }) + if err != nil { + return nil, err + } + return &user, nil +} + +func findUserIDByEmail(email string) (string, error) { + var id string + err := GetDB().View(func(txn *badger.Txn) error { + item, err := txn.Get([]byte(emailIndexPref + email)) + if err != nil { + return err + } + return item.Value(func(v []byte) error { + id = string(v) + return nil + }) + }) + return id, err +} + +func FindUserByEmail(email string) (*User, error) { + id, err := findUserIDByEmail(email) + if err != nil { + return nil, err + } + return FindUserByID(id) +} + +func ListUsers() ([]*User, error) { + users := []*User{} + err := GetDB().View(func(txn *badger.Txn) error { + it := txn.NewIterator(badger.DefaultIteratorOptions) + defer it.Close() + for it.Seek([]byte(userPrefix)); it.ValidForPrefix([]byte(userPrefix)); it.Next() { + item := it.Item() + err := item.Value(func(v []byte) error { + var u User + if err := json.Unmarshal(v, &u); err == nil { + users = append(users, &u) + } + return nil + }) + if err != nil { + return err + } + } + return nil + }) + return users, err +}