451 lines
9.3 KiB
Markdown
451 lines
9.3 KiB
Markdown
# 🍥 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. 🍣 |