🍥 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 value
  • unique - Field value must be unique across all records
  • index - Create an index for fast lookups
  • index:custom_name - Create named index
  • fkey: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

  1. Use indices for frequently queried fields
  2. Batch operations with unsafe methods + rebuild indices
  3. Lazy load relationships to avoid circular references
  4. Pre-allocate maps for large datasets
  5. 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

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass
  5. Submit a pull request

License

MIT License - see LICENSE file for details.


Nigiri - Simple, type-safe JSON persistence for Go applications. 🍣

Description
Delicious, hand-crafted in-memory database with JSON persistence.
Readme 75 KiB
First release Latest
2025-08-16 20:04:02 -05:00
Languages
Go 100%