# 🍥 Nigiri Delicious, hand-crafted in-memory database with JSON persistence. ## Installation ```bash go get github.com/Sharkk/Nigiri ``` ## Quick Start ### 1. Define Your Models ```go 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 ```go 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 ```go 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: ```go 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: ```go // 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: ```go 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: ```go 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 ```go 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 ```bash # 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: ```bash your-app migrate migrations/001_user_schema.txt ``` ### Custom Migration Commands Register domain-specific migration commands: ```go 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: ```go 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 ```go // 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: ```go // 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: ```go 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: ```json [ { "id": 1, "name": "John Doe", "email": "john@example.com", "age": 30 }, { "id": 2, "name": "Jane Smith", "email": "jane@example.com", "age": 25 } ] ``` ## Error Handling ```go // 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: ```go // 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. 🍣