zoobzio January 3, 2026 Edit this page

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 *Tx method 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"`
}
TagPurposeExample
dbColumn namedb:"user_id"
typeSQL typetype:"text", type:"integer"
constraintsColumn constraintsconstraints:"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

TypeConstructorReturnsUse Case
QueryStatementNewQueryStatement[]*TMulti-record retrieval
SelectStatementNewSelectStatement*TSingle-record retrieval
UpdateStatementNewUpdateStatement*TModify and return record
DeleteStatementNewDeleteStatementint64Remove records, return count
AggregateStatementNewAggregateStatementfloat64COUNT, 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

OperatorDescription
=Equal
!=, <>Not equal
<, <=Less than
>, >=Greater than
LIKE, ILIKEPattern matching
INValue in list
IS NULLNULL check
IS NOT NULLNOT 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},
})