Architecture
Edamame is a semantic layer over soy. Understanding this relationship helps you use both effectively.
Layer Diagram
┌─────────────────────────────────────────────────────────────┐
│ Your Application │
│ │
│ exec.ExecQuery(ctx, ByStatus, params) │
└─────────────────────────┬───────────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────────┐
│ Edamame │
│ │
│ ┌──────────────┐ Typed statements │
│ │ Executor[T] │ Spec → Builder conversion │
│ │ │ Event emission (capitan) │
│ └──────┬───────┘ │
│ │ │
│ ┌──────▼───────────────────────────────────────────────┐ │
│ │ Statement Types │ │
│ │ QueryStatement SelectStatement UpdateStatement │ │
│ │ DeleteStatement AggregateStatement │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────┬───────────────────────────────────────────────────┘
│
┌─────────▼───────────────────────────────────────────────────┐
│ Soy │
│ │
│ Query builder API │
│ Field validation (via ASTQL) │
│ Parameterized SQL generation │
│ Render() → {SQL, Params} │
└─────────┬───────────────────────────────────────────────────┘
│
┌─────────▼───────────────────────────────────────────────────┐
│ sqlx │
│ │
│ Connection pooling │
│ Named parameter binding │
│ Struct scanning │
└─────────────────────────────────────────────────────────────┘
Responsibilities
Edamame
- Typed statements - Compile-time safe query definitions
- Spec-to-builder conversion - Transform declarative specs into soy builders
- Execution wrappers - Convenient
Exec*methods with params - Events - Emit executor lifecycle events via capitan
Soy
- Query building - Fluent API for constructing SQL
- Field validation - Validate field names against model metadata
- Operator validation - Ensure operators are safe and valid
- SQL generation - Render builders to parameterized SQL
- Execution - Execute queries via sqlx
When to Use Each
| Task | Use |
|---|---|
| Define named operations | Statement types |
| Execute named operations | exec.Exec* methods |
| Ad-hoc queries | exec.Soy().Query()... |
| Custom SQL construction | Soy builder API |
Spec-to-Builder Flow
When you call exec.ExecQuery(ctx, ByStatus, params):
- Extract spec - Statement contains the spec internally
- Convert - Spec is converted to a soy builder:
// QuerySpec → soy.Query[T] builder := exec.queryFromSpec(stmt.spec) - Render - Builder generates SQL:
result, err := builder.Render() // result.SQL = "SELECT ... FROM users WHERE status = $1" // result.Params = []any{"active"} - Execute - sqlx runs the parameterized query:
rows, err := db.QueryxContext(ctx, result.SQL, result.Params...) - Scan - Results are scanned into structs
Security Model
SQL injection protection flows through the entire stack:
- Edamame - Specs are data, not SQL strings
- Soy - Field names validated against model metadata
- Soy - Operators validated against allowlist
- Soy - All values become bound parameters
- sqlx - Parameters passed separately from SQL
// User input
params := map[string]any{"status": userInput}
// Never interpolated into SQL
exec.ExecQuery(ctx, ByStatus, params)
// Becomes:
// SQL: "SELECT ... WHERE status = $1"
// Args: [userInput] // Bound separately
Event Integration
Edamame emits events via capitan for observability:
| Signal | When | Fields |
|---|---|---|
ExecutorCreated | Executor initialized | table |
Hook for monitoring:
capitan.Hook(edamame.ExecutorCreated, func(ctx context.Context, e *capitan.Event) {
table, _ := edamame.KeyTable.From(e)
log.Printf("Executor created for table: %s", table)
})
Direct Soy Access
For operations not covered by statements, access soy directly:
// Get the underlying soy instance
s := exec.Soy()
// Use soy's fluent API
users, err := s.Query().
Where("age", ">=", "min_age").
Where("status", "=", "status").
OrderBy("name", "asc").
Limit(10).
Exec(ctx, params)
This bypasses statement types but still benefits from:
- Field validation
- Parameterized queries
- Type-safe results
Type Safety
Statements provide compile-time guarantees:
// Compiler enforces correct statement types
users, err := exec.ExecQuery(ctx, QueryAll, nil) // Must be QueryStatement
user, err := exec.ExecSelect(ctx, SelectByID, params) // Must be SelectStatement
count, err := exec.ExecAggregate(ctx, CountAll, nil) // Must be AggregateStatement
// This won't compile:
// exec.ExecQuery(ctx, SelectByID, nil) // SelectStatement not allowed
No magic strings, no runtime lookup failures.