Specs
Specs are JSON-serializable query definitions. They enable query construction from external sources: LLM-generated queries, configuration files, or API request bodies.
See API Reference for complete type definitions.
Use Cases
LLM Integration - Let language models generate query specifications that your application executes safely.
Configuration - Define queries in config files for report generation or scheduled jobs.
API Endpoints - Accept query parameters as JSON and convert to type-safe queries.
Dynamic Filters - Build complex filter UIs that serialise to specs.
Query Specs
QuerySpec
Multi-record SELECT queries:
spec := soy.QuerySpec{
Fields: []string{"id", "email", "name"},
Where: []soy.ConditionSpec{...},
OrderBy: []soy.OrderBySpec{...},
Limit: intPtr(10),
Offset: intPtr(0),
Distinct: true,
}
query := users.QueryFromSpec(spec)
results, err := query.Exec(ctx, params)
SelectSpec
Single-record SELECT:
spec := soy.SelectSpec{
Fields: []string{"id", "email"},
Where: []soy.ConditionSpec{
{Field: "id", Operator: "=", Param: "user_id"},
},
ForUpdate: true,
}
sel := users.SelectFromSpec(spec)
user, err := sel.Exec(ctx, map[string]any{"user_id": 123})
Condition Specs
Simple Conditions
condition := soy.ConditionSpec{
Field: "age",
Operator: ">=",
Param: "min_age",
}
NULL Conditions
// IS NULL
condition := soy.ConditionSpec{
Field: "deleted_at",
Operator: "IS NULL",
IsNull: true,
}
// IS NOT NULL
condition := soy.ConditionSpec{
Field: "email",
Operator: "IS NOT NULL",
IsNull: true,
}
Grouped Conditions
AND groups:
condition := soy.ConditionSpec{
Logic: "AND",
Group: []soy.ConditionSpec{
{Field: "age", Operator: ">=", Param: "min"},
{Field: "age", Operator: "<=", Param: "max"},
{Field: "status", Operator: "=", Param: "status"},
},
}
OR groups:
condition := soy.ConditionSpec{
Logic: "OR",
Group: []soy.ConditionSpec{
{Field: "role", Operator: "=", Param: "admin"},
{Field: "role", Operator: "=", Param: "mod"},
},
}
Nested groups:
// (status = 'active' AND (role = 'admin' OR role = 'mod'))
condition := soy.ConditionSpec{
Logic: "AND",
Group: []soy.ConditionSpec{
{Field: "status", Operator: "=", Param: "status"},
{
Logic: "OR",
Group: []soy.ConditionSpec{
{Field: "role", Operator: "=", Param: "admin"},
{Field: "role", Operator: "=", Param: "mod"},
},
},
},
}
OrderBy Specs
Basic Ordering
order := soy.OrderBySpec{
Field: "created_at",
Direction: "desc",
}
NULL Handling
order := soy.OrderBySpec{
Field: "score",
Direction: "desc",
NullsFirst: true,
}
order := soy.OrderBySpec{
Field: "score",
Direction: "asc",
NullsLast: true,
}
Expression Ordering
For pgvector or computed values:
order := soy.OrderBySpec{
Expression: "embedding <-> :query_vec",
Direction: "asc",
}
Mutation Specs
CreateSpec
INSERT with conflict handling:
spec := soy.CreateSpec{
OnConflict: []string{"email"},
ConflictAction: "nothing",
}
// Or with upsert
spec := soy.CreateSpec{
OnConflict: []string{"email"},
ConflictAction: "update",
ConflictSet: map[string]string{"name": "name", "age": "age"},
}
create := users.InsertFromSpec(spec)
result, err := create.Exec(ctx, user)
UpdateSpec
spec := soy.UpdateSpec{
Set: map[string]string{
"name": "new_name",
"status": "new_status",
},
Where: []soy.ConditionSpec{
{Field: "id", Operator: "=", Param: "user_id"},
},
}
update := users.ModifyFromSpec(spec)
result, err := update.Exec(ctx, params)
DeleteSpec
spec := soy.DeleteSpec{
Where: []soy.ConditionSpec{
{Field: "status", Operator: "=", Param: "status"},
{Field: "created_at", Operator: "<", Param: "cutoff"},
},
}
del := users.RemoveFromSpec(spec)
count, err := del.Exec(ctx, params)
Aggregate Specs
The aggregate function is determined by which method you call:
// COUNT (Field is ignored)
countSpec := soy.AggregateSpec{
Where: []soy.ConditionSpec{
{Field: "status", Operator: "=", Param: "status"},
},
}
count, err := users.CountFromSpec(countSpec).Exec(ctx, params)
// SUM/AVG/MIN/MAX (Field is required)
sumSpec := soy.AggregateSpec{
Field: "amount",
Where: []soy.ConditionSpec{
{Field: "status", Operator: "=", Param: "paid"},
},
}
total, err := users.SumFromSpec(sumSpec).Exec(ctx, params)
avg, err := users.AvgFromSpec(sumSpec).Exec(ctx, params)
min, err := users.MinFromSpec(sumSpec).Exec(ctx, params)
max, err := users.MaxFromSpec(sumSpec).Exec(ctx, params)
Compound Query Specs
Combine queries with set operations:
spec := soy.CompoundQuerySpec{
Base: soy.QuerySpec{
Where: []soy.ConditionSpec{
{Field: "status", Operator: "=", Param: "active"},
},
},
Operands: []soy.SetOperandSpec{
{
Operation: "union",
Query: soy.QuerySpec{
Where: []soy.ConditionSpec{
{Field: "status", Operator: "=", Param: "pending"},
},
},
},
},
OrderBy: []soy.OrderBySpec{
{Field: "name", Direction: "asc"},
},
Limit: intPtr(100),
}
compound := users.CompoundFromSpec(spec)
results, err := compound.Exec(ctx, params)
JSON Serialisation
Specs marshal to JSON for storage or transmission:
spec := soy.QuerySpec{
Fields: []string{"id", "email"},
Where: []soy.ConditionSpec{
{Field: "status", Operator: "=", Param: "status"},
},
OrderBy: []soy.OrderBySpec{
{Field: "name", Direction: "asc"},
},
Limit: intPtr(10),
}
data, _ := json.Marshal(spec)
{
"fields": ["id", "email"],
"where": [
{"field": "status", "operator": "=", "param": "status"}
],
"order_by": [
{"field": "name", "direction": "asc"}
],
"limit": 10
}
API Example
Accept query specs from HTTP requests:
func HandleSearch(w http.ResponseWriter, r *http.Request) {
var spec soy.QuerySpec
if err := json.NewDecoder(r.Body).Decode(&spec); err != nil {
http.Error(w, "invalid spec", http.StatusBadRequest)
return
}
// Validate and sanitise spec
if spec.Limit == nil || *spec.Limit > 100 {
limit := 100
spec.Limit = &limit
}
// Extract params from request (e.g., query string)
params := extractParams(r)
query := users.QueryFromSpec(spec)
results, err := query.Exec(r.Context(), params)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(results)
}
LLM Integration
Generate specs from natural language:
// Example: User asks "Find active users over 30, sorted by name"
// LLM generates:
specJSON := `{
"where": [
{"field": "status", "operator": "=", "param": "status"},
{"field": "age", "operator": ">", "param": "min_age"}
],
"orderBy": [{"field": "name", "direction": "asc"}]
}`
var spec soy.QuerySpec
json.Unmarshal([]byte(specJSON), &spec)
// Safe execution with validated params
params := map[string]any{
"status": "active",
"min_age": 30,
}
results, err := users.QueryFromSpec(spec).Exec(ctx, params)
The spec system ensures that:
- Only valid field names are used (schema validation)
- SQL injection is impossible (named parameters)
- Query structure is predictable (type-safe builders)