zoobzio January 3, 2026 Edit this page

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 TypeDescription
QueryStatementMulti-record retrieval with filtering
SelectStatementSingle-record retrieval
UpdateStatementTargeted updates with SET/WHERE
DeleteStatementConditional deletion with WHERE
AggregateStatementCOUNT, 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