🍥 Nigiri
Delicious, hand-crafted in-memory database with JSON persistence.
Installation
go get github.com/Sharkk/Nigiri
Quick Start
1. Define Your Models
package main
import "your-app/nigiri"
type User struct {
ID int `json:"id"`
Name string `json:"name" db:"required,unique"`
Email string `json:"email" db:"unique"`
Age int `json:"age"`
Posts []*Post `json:"posts,omitempty"`
}
type Post struct {
ID int `json:"id"`
Title string `json:"title" db:"required"`
Content string `json:"content"`
AuthorID int `json:"author_id" db:"fkey:user"`
Author *User `json:"author,omitempty"`
}
func (u *User) Validate() error {
if u.Age < 0 || u.Age > 150 {
return fmt.Errorf("invalid age: %d", u.Age)
}
return nil
}
2. Set Up Your Database
func main() {
// Create collection (database)
db := nigiri.NewCollection("./data")
// Create and register stores (tables)
userStore := nigiri.NewBaseStore[User]()
postStore := nigiri.NewBaseStore[Post]()
db.Add("users", userStore)
db.Add("posts", postStore)
// Load existing data
if err := db.Load(); err != nil {
log.Fatal(err)
}
// Your app logic here...
createUser(db)
// Save all changes
if err := db.Save(); err != nil {
log.Fatal(err)
}
}
3. Basic CRUD Operations
func createUser(db *nigiri.Collection) {
users := nigiri.Get[User](db, "users")
// Create new user
user := &User{
Name: "John Doe",
Email: "john@example.com",
Age: 30,
}
id, err := users.Create(user)
if err != nil {
log.Printf("Error creating user: %v", err)
return
}
fmt.Printf("Created user with ID: %d\n", id)
// Find user
foundUser, exists := users.Find(id)
if exists {
fmt.Printf("Found: %s\n", foundUser.Name)
}
// Update user
foundUser.Age = 31
if err := users.Update(id, foundUser); err != nil {
log.Printf("Error updating user: %v", err)
}
// List all users
allUsers := users.GetAll()
for id, user := range allUsers {
fmt.Printf("User %d: %s\n", id, user.Name)
}
}
Schema Constraints
Use db
tags to define constraints:
type Product struct {
ID int `json:"id"`
SKU string `json:"sku" db:"unique,required"`
Name string `json:"name" db:"required"`
Price float64 `json:"price"`
CategoryID int `json:"category_id" db:"fkey:category"`
Description string `json:"description" db:"index"`
}
Available Constraints
required
- Field cannot be empty/zero valueunique
- Field value must be unique across all recordsindex
- Create an index for fast lookupsindex:custom_name
- Create named indexfkey:target
- Foreign key reference to another store
Indexing System
Automatic Indices
Indices are automatically created for fields with unique
and index
constraints:
// This creates an automatic index
type User struct {
Email string `json:"email" db:"unique"` // Creates email_idx
Name string `json:"name" db:"index"` // Creates name_idx
}
// Use the index for fast lookups
user, found := users.LookupByIndex("email_idx", "john@example.com")
Custom Indices
Register custom indices for complex queries:
func setupIndices(users *nigiri.BaseStore[User]) {
// Group users by age
users.RegisterIndex("by_age", nigiri.BuildIntGroupIndex(func(u *User) int {
return u.Age
}))
// Case-insensitive name lookup
users.RegisterIndex("name_lower", nigiri.BuildCaseInsensitiveLookupIndex(func(u *User) string {
return u.Name
}))
// Sorted by creation date
users.RegisterIndex("by_date", nigiri.BuildSortedListIndex(func(a, b *User) bool {
return a.ID < b.ID // Assuming ID represents creation order
}))
}
// Use custom indices
thirtyYearOlds := users.GroupByIndex("by_age", 30)
user, found := users.LookupByIndex("name_lower", "john doe")
sortedUsers := users.AllSorted("by_date")
Relationships
Nigiri automatically detects and validates relationships:
type User struct {
ID int `json:"id"`
Posts []*Post `json:"posts"` // One-to-many: detected automatically
}
type Post struct {
ID int `json:"id"`
AuthorID int `json:"author_id"`
Author *User `json:"author"` // Many-to-one: detected automatically
}
Relationship Types:
*EntityType
- Many-to-one relationship[]*EntityType
- One-to-many relationship
Foreign key validation ensures referenced entities exist.
Migrations
Nigiri includes a powerful migration system for schema evolution.
CLI Integration
func main() {
db := nigiri.NewCollection("./data")
// ... setup stores ...
// CLI migration support
if len(os.Args) > 1 && os.Args[1] == "migrate" {
cli := db.CreateMigrationCLI()
if err := cli.Run(os.Args[2:]); err != nil {
log.Fatal(err)
}
return
}
// Normal app logic...
}
Built-in Migration Commands
# Rename a field
your-app migrate users.json 'rename Name to FullName'
# Add new field with default value
your-app migrate users.json 'add Email string to user'
# Remove deprecated field
your-app migrate users.json 'remove LegacyField from user'
# Change field type
your-app migrate users.json 'change Age to int'
Migration Scripts
Create migrations/001_user_schema.txt
:
# User schema updates
users.json: rename Name to FullName
users.json: add Email string to user
users.json: add CreatedAt time to user
posts.json: rename AuthorName to Author
posts.json: add Status string to post
Run the script:
your-app migrate migrations/001_user_schema.txt
Custom Migration Commands
Register domain-specific migration commands:
func setupMigrations(db *nigiri.Collection) {
// Custom slugify command
slugPattern := regexp.MustCompile(`^slugify\s+(\w+)\s+from\s+(\w+)$`)
db.RegisterMigrationCommand("slugify", slugPattern, func(items []map[string]any, cmd *nigiri.MigrationCommand) error {
for _, item := range items {
if sourceVal, exists := item[cmd.To]; exists {
if sourceStr, ok := sourceVal.(string); ok {
slug := strings.ToLower(strings.ReplaceAll(sourceStr, " ", "-"))
item[cmd.Field] = slug
}
}
}
return nil
})
}
// Usage: your-app migrate posts.json 'slugify slug from Title'
Advanced Features
Custom Validation
Implement the Validatable
interface:
func (u *User) Validate() error {
if u.Age < 13 {
return fmt.Errorf("user must be at least 13 years old")
}
if !strings.Contains(u.Email, "@") {
return fmt.Errorf("invalid email format")
}
return nil
}
Filtering and Querying
// Filter with custom function
activeUsers := users.FilterByIndex("by_date", func(u *User) bool {
return u.Active && u.Age >= 18
})
// Complex queries with multiple indices
recentPosts := posts.GroupByIndex("by_author", userID)
popularPosts := posts.FilterByIndex("by_popularity", func(p *Post) bool {
return p.Views > 1000
})
Unsafe Operations
For performance-critical bulk operations:
// Disable validation for bulk inserts
for i, user := range users {
users.AddUnsafe(i, user)
}
// Rebuild indices once after bulk operations
users.RebuildIndices()
Singleton Pattern
Create singleton stores for configuration:
var GetConfig = nigiri.NewSingleton(func() *ConfigStore {
return nigiri.NewBaseStore[Config]()
})
// Usage
config := GetConfig()
File Structure
your-app/
├── data/
│ ├── users.json
│ ├── posts.json
│ └── categories.json
├── migrations/
│ ├── 001_initial_schema.txt
│ └── 002_add_timestamps.txt
└── main.go
JSON Format
Data is stored as JSON arrays with consistent formatting:
[
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"age": 30
},
{
"id": 2,
"name": "Jane Smith",
"email": "jane@example.com",
"age": 25
}
]
Error Handling
// Constraint violations
user := &User{Email: "duplicate@example.com"}
if err := users.Add(1, user); err != nil {
if strings.Contains(err.Error(), "already exists") {
// Handle duplicate email
}
}
// Validation errors
user := &User{Age: -5}
if err := users.Add(2, user); err != nil {
// Handle validation failure
}
// Relationship violations
post := &Post{AuthorID: 999} // Non-existent user
if err := posts.Add(1, post); err != nil {
if strings.Contains(err.Error(), "foreign key violation") {
// Handle missing reference
}
}
Performance Tips
- Use indices for frequently queried fields
- Batch operations with unsafe methods + rebuild indices
- Lazy load relationships to avoid circular references
- Pre-allocate maps for large datasets
- Use filtered indices to avoid full scans
Thread Safety
All operations are thread-safe with read-write mutexes:
// Safe concurrent access
go func() {
users.Create(&User{Name: "User 1"})
}()
go func() {
users.Create(&User{Name: "User 2"})
}()
Migration Safety
- Automatic backups (.backup files) before each migration
- Atomic operations with temporary files
- Validation ensures migrated data loads correctly
- Rollback support by restoring from backups
Contributing
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
License
MIT License - see LICENSE file for details.
Nigiri - Simple, type-safe JSON persistence for Go applications. 🍣