zoobzio January 3, 2026 Edit this page

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

TaskUse
Define named operationsStatement types
Execute named operationsexec.Exec* methods
Ad-hoc queriesexec.Soy().Query()...
Custom SQL constructionSoy builder API

Spec-to-Builder Flow

When you call exec.ExecQuery(ctx, ByStatus, params):

  1. Extract spec - Statement contains the spec internally
  2. Convert - Spec is converted to a soy builder:
    // QuerySpec → soy.Query[T]
    builder := exec.queryFromSpec(stmt.spec)
    
  3. Render - Builder generates SQL:
    result, err := builder.Render()
    // result.SQL = "SELECT ... FROM users WHERE status = $1"
    // result.Params = []any{"active"}
    
  4. Execute - sqlx runs the parameterized query:
    rows, err := db.QueryxContext(ctx, result.SQL, result.Params...)
    
  5. Scan - Results are scanned into structs

Security Model

SQL injection protection flows through the entire stack:

  1. Edamame - Specs are data, not SQL strings
  2. Soy - Field names validated against model metadata
  3. Soy - Operators validated against allowlist
  4. Soy - All values become bound parameters
  5. 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:

SignalWhenFields
ExecutorCreatedExecutor initializedtable

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.