mirror of
https://github.com/Floriansylvain/GoAntiSqlPOC.git
synced 2026-08-19 11:43:24 +02:00
Initial Commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
tmp/
|
||||||
|
data/
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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=
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+35
@@ -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
|
||||||
|
}
|
||||||
+115
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user