Core Concepts
Edamame has three primitives: executors, statements, and specs. Understanding these unlocks the full API.
Executor
An executor is the execution context for a single model type. It wraps soy and provides methods to execute typed statements.
exec, err := edamame.New[User](db, "users", renderer)
The executor:
- Wraps a soy instance for SQL building
- Provides execution methods that accept typed statements
- Supports transactions via
*Txmethod variants
Struct Tags
Edamame uses struct tags to understand 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"`
}
| Tag | Purpose | Example |
|---|---|---|
db | Column name | db:"user_id" |
type | SQL type | type:"text", type:"integer" |
constraints | Column constraints | constraints:"primarykey,notnull" |
Statements
A statement is a typed, named database operation. 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"}},
})
)
Each statement has:
- Name - Human-readable identifier
- Description - What the statement does
- ID - Auto-generated UUID for uniqueness
- Spec - Declarative definition of the operation
- Params - Required parameters (auto-derived from spec)
- Tags - Optional metadata for categorization
Statement Types
| Type | Constructor | Returns | Use Case |
|---|---|---|---|
QueryStatement | NewQueryStatement | []*T | Multi-record retrieval |
SelectStatement | NewSelectStatement | *T | Single-record retrieval |
UpdateStatement | NewUpdateStatement | *T | Modify and return record |
DeleteStatement | NewDeleteStatement | int64 | Remove records, return count |
AggregateStatement | NewAggregateStatement | float64 | COUNT, SUM, AVG, MIN, MAX |
Defining Statements
// Query statement
var ByStatus = edamame.NewQueryStatement("by-status", "Find users by status", edamame.QuerySpec{
Where: []edamame.ConditionSpec{
{Field: "status", Operator: "=", Param: "status"},
},
}, "user", "filter") // Optional tags
// Select statement
var ByEmail = edamame.NewSelectStatement("by-email", "Find user by email", edamame.SelectSpec{
Where: []edamame.ConditionSpec{
{Field: "email", Operator: "=", Param: "email"},
},
})
// Update statement
var Activate = edamame.NewUpdateStatement("activate", "Activate a user by ID", edamame.UpdateSpec{
Set: map[string]string{"active": "active"},
Where: []edamame.ConditionSpec{
{Field: "id", Operator: "=", Param: "id"},
},
})
// Delete statement
var DeleteByID = edamame.NewDeleteStatement("delete-by-id", "Delete user by ID", edamame.DeleteSpec{
Where: []edamame.ConditionSpec{
{Field: "id", Operator: "=", Param: "id"},
},
})
// Aggregate statement
var AvgAge = edamame.NewAggregateStatement("avg-age", "Average age of all users", edamame.AggAvg, edamame.AggregateSpec{
Field: "age",
})
Statement Metadata
Inspect statement properties:
fmt.Println(ByStatus.Name()) // "by-status"
fmt.Println(ByStatus.Description()) // "Find users by status"
fmt.Println(ByStatus.ID()) // UUID
fmt.Println(ByStatus.Tags()) // ["user", "filter"]
for _, p := range ByStatus.Params() {
fmt.Printf("Param: %s (type: %s, required: %v)\n", p.Name, p.Type, p.Required)
}
Specs
A spec is a declarative definition of a database operation. Specs are pure data—no SQL strings, no builder calls.
QuerySpec
For multi-record retrieval:
spec := edamame.QuerySpec{
Fields: []string{"id", "name", "email"}, // SELECT columns (empty = all)
Where: []edamame.ConditionSpec{...}, // WHERE clauses
OrderBy: []edamame.OrderBySpec{...}, // ORDER BY clauses
GroupBy: []string{"status"}, // GROUP BY columns
Having: []edamame.ConditionSpec{...}, // HAVING clauses
Limit: &limit, // LIMIT
Offset: &offset, // OFFSET
Distinct: true, // SELECT DISTINCT
ForLocking: "update", // FOR UPDATE/SHARE
}
SelectSpec
For single-record retrieval (same structure as QuerySpec):
spec := edamame.SelectSpec{
Fields: []string{"id", "name"},
Where: []edamame.ConditionSpec{...},
ForLocking: "share",
}
UpdateSpec
For modifications:
spec := edamame.UpdateSpec{
Set: map[string]string{
"name": "new_name", // field -> param mapping
"status": "new_status",
},
Where: []edamame.ConditionSpec{...},
}
DeleteSpec
For deletions:
spec := edamame.DeleteSpec{
Where: []edamame.ConditionSpec{...},
}
AggregateSpec
For aggregate functions:
spec := edamame.AggregateSpec{
Field: "age", // Field to aggregate
Where: []edamame.ConditionSpec{...}, // Optional filter
}
Use with AggCount, AggSum, AggAvg, AggMin, or AggMax.
Conditions
Conditions define WHERE clauses. They can be simple or grouped.
Simple Conditions
cond := edamame.ConditionSpec{
Field: "age",
Operator: ">=",
Param: "min_age",
}
NULL Conditions
// IS NULL
cond := edamame.ConditionSpec{
Field: "deleted_at",
IsNull: true,
Operator: "IS NULL",
}
// IS NOT NULL
cond := edamame.ConditionSpec{
Field: "email",
IsNull: true,
Operator: "IS NOT NULL",
}
Grouped Conditions (OR)
cond := edamame.ConditionSpec{
Logic: "OR",
Group: []edamame.ConditionSpec{
{Field: "status", Operator: "=", Param: "status1"},
{Field: "status", Operator: "=", Param: "status2"},
},
}
Supported Operators
| Operator | Description |
|---|---|
= | Equal |
!=, <> | Not equal |
<, <= | Less than |
>, >= | Greater than |
LIKE, ILIKE | Pattern matching |
IN | Value in list |
IS NULL | NULL check |
IS NOT NULL | NOT NULL check |
Ordering
order := edamame.OrderBySpec{
Field: "created_at",
Direction: "desc", // "asc" or "desc"
Nulls: "last", // "first" or "last" (optional)
}
Expression-Based Ordering
For vector similarity or computed distances:
order := edamame.OrderBySpec{
Field: "embedding",
Operator: "<->", // pgvector distance operator
Param: "query_vector",
Direction: "asc",
}
Execution
Execute statements with params:
// Query (multiple records)
users, err := exec.ExecQuery(ctx, ByStatus, map[string]any{
"status": "active",
})
// Select (single record)
user, err := exec.ExecSelect(ctx, ByEmail, map[string]any{
"email": "alice@example.com",
})
// Update
updated, err := exec.ExecUpdate(ctx, Activate, map[string]any{
"id": 123,
"active": true,
})
// Delete
count, err := exec.ExecDelete(ctx, DeleteByID, map[string]any{
"id": 123,
})
// Aggregate
avg, err := exec.ExecAggregate(ctx, AvgAge, nil)
// Insert (no statement needed)
inserted, err := exec.ExecInsert(ctx, &user)
Transaction Support
All execution methods have *Tx variants:
tx, err := db.BeginTxx(ctx, nil)
user, err := exec.ExecSelectTx(ctx, tx, SelectByID, params)
_, err = exec.ExecUpdateTx(ctx, tx, Activate, params)
tx.Commit()
Batch Operations
// Batch insert
count, err := exec.ExecInsertBatch(ctx, users)
// Batch update
count, err := exec.ExecUpdateBatch(ctx, Activate, []map[string]any{
{"id": 1, "active": true},
{"id": 2, "active": true},
})