Initial Commit

This commit is contained in:
Florian Sylvain
2025-05-02 01:30:37 +02:00
commit e6f9e96c8f
8 changed files with 369 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
tmp/
data/
+64
View File
@@ -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
+22
View File
@@ -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
)
+33
View File
@@ -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=
+39
View File
@@ -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)
}
}
+59
View File
@@ -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
View File
@@ -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
View File
@@ -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
}