Overview
Database operations in Go often mean choosing between raw SQL and heavy ORMs.
Edamame offers a third path: a statement-driven query exec that stays out of your way while providing type-safe, declarative query definitions.
// Define your model
type User struct {
ID int `db:"id" type:"integer" constraints:"primarykey"`
Email string `db:"email" type:"text" constraints:"notnull,unique"`
Name string `db:"name" type:"text"`
Age *int `db:"age" type:"integer"`
}
// Define statements as package-level variables
var (
QueryAll = edamame.NewQueryStatement("query-all", "Query all users", edamame.QuerySpec{})
SelectByID = edamame.NewSelectStatement("select-by-id", "Select user by ID", edamame.SelectSpec{
Where: []edamame.ConditionSpec{{Field: "id", Operator: "=", Param: "id"}},
})
DeleteByID = edamame.NewDeleteStatement("delete-by-id", "Delete user by ID", edamame.DeleteSpec{
Where: []edamame.ConditionSpec{{Field: "id", Operator: "=", Param: "id"}},
})
Adults = edamame.NewQueryStatement("adults", "Find users over a minimum age", edamame.QuerySpec{
Where: []edamame.ConditionSpec{
{Field: "age", Operator: ">=", Param: "min_age"},
},
OrderBy: []edamame.OrderBySpec{
{Field: "name", Direction: "asc"},
},
})
)
// Create a exec and execute
exec, err := edamame.New[User](db, "users", renderer)
users, err := exec.ExecQuery(ctx, QueryAll, nil)
user, err := exec.ExecSelect(ctx, SelectByID, map[string]any{"id": 123})
inserted, err := exec.ExecInsert(ctx, &user)
deleted, err := exec.ExecDelete(ctx, DeleteByID, map[string]any{"id": 123})
adults, err := exec.ExecQuery(ctx, Adults, map[string]any{"min_age": 18})
Type-safe, injection-protected, declarative.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Edamame │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Executor[T] │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────┐ │ │
│ │ │ Statement Types │ │ │
│ │ │ │ │ │
│ │ │ QueryStatement SelectStatement │ │ │
│ │ │ UpdateStatement DeleteStatement │ │ │
│ │ │ AggregateStatement │ │ │
│ │ └────────────────────┬───────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌─────▼─────┐ │ │
│ │ │ Soy │ │ │
│ │ │ (Builder) │ │ │
│ │ └─────┬─────┘ │ │
│ │ │ │ │
│ └───────────────────────┼──────────────────────────────┘ │
│ │ │
│ ┌─────▼─────┐ │
│ │ sqlx │ │
│ │ DB │ │
│ └───────────┘ │
└─────────────────────────────────────────────────────────────┘
Edamame provides typed statements over soy's query builder. Each statement type encapsulates a declarative spec that maps to a parameterized SQL builder.
Philosophy
Edamame bridges two worlds: the declarative simplicity of specs and the type safety of Go generics. Define what you want, get SQL that's validated at build time and parameterized at runtime.
// Define statements as package-level variables
var ActiveByRole = edamame.NewQueryStatement("active-by-role", "Find active users by role", edamame.QuerySpec{
Where: []edamame.ConditionSpec{
{Field: "active", Operator: "=", Param: "active"},
{Field: "role", Operator: "=", Param: "role"},
},
})
// In your API handler
users, err := exec.ExecQuery(ctx, ActiveByRole, map[string]any{
"active": true,
"role": "admin",
})
Typed statements, compile-time safety, no magic strings.
Statement Types
Edamame provides five statement types for different operations:
| Statement Type | Description |
|---|---|
QueryStatement | Multi-record retrieval with filtering |
SelectStatement | Single-record retrieval |
UpdateStatement | Targeted updates with SET/WHERE |
DeleteStatement | Conditional deletion with WHERE |
AggregateStatement | COUNT, SUM, AVG, MIN, MAX operations |
Each statement type accepts a spec that defines the query behavior:
QuerySpec - Filtering, sorting, pagination, grouping, locking
SelectSpec - Single-record filtering with optional locking
UpdateSpec - SET clauses and WHERE conditions
DeleteSpec - WHERE conditions for targeted deletion
AggregateSpec - Field to aggregate and optional filtering
Priorities
Type Safety
Fields, operators, and params are validated at query build time. No runtime SQL injection, no magic strings.
// Spec-based: field names validated against model metadata
var Adults = edamame.NewQueryStatement("adults", "Find adults", edamame.QuerySpec{
Where: []edamame.ConditionSpec{
{Field: "age", Operator: ">=", Param: "min_age"},
},
})
// Execution: params bound safely via sqlx
users, err := exec.ExecQuery(ctx, Adults, map[string]any{
"min_age": 18, // Parameterized, never interpolated
})
Compile-Time Guarantees
Statements are typed. Pass a QueryStatement to ExecQuery, a SelectStatement to ExecSelect. The compiler catches mismatches.
// Compiler ensures correct statement types
users, err := exec.ExecQuery(ctx, QueryAll, nil) // QueryStatement
user, err := exec.ExecSelect(ctx, SelectByID, params) // SelectStatement
count, err := exec.ExecAggregate(ctx, CountAll, nil) // AggregateStatement
Security
All SQL generation flows through soy's validated builder:
- Field names validated against model metadata
- Operators validated against allowlist
- All values bound as parameters, never interpolated
- No raw SQL construction from user input
Performance
- Lazy initialization - Builders created on demand
- Minimal allocations - Spec-to-builder conversion is lightweight
- Batch operations - Insert, update, delete batches in single transactions