Type-Safe Queries (soy)
What is soy?
soy is a type-safe SQL query builder. It validates queries against your schema at initialization—not execution—so invalid field names, operators, or constraints fail immediately. Queries return *T or []*T, never interface{}.
The Problem
Traditional query builders accept arbitrary strings:
// Field name comes from user input — SQL injection vector
db.Where(userInput + " = ?", value)
User input can inject query structure, not just values. Even with parameterized values, the field name itself is a vector.
How Sentinel Enables This
Sentinel extracts your schema once at initialization. soy converts that metadata into a validation layer—every query is checked against the struct definition before it runs.
Define types with database tags:
type User struct {
ID int64 `db:"id" constraints:"primary_key"`
Email string `db:"email" constraints:"unique,not_null"`
Name string `db:"name"`
}
Initialize soy with your type:
users, err := soy.New[User](db, "users", postgres.New())
Now, sentinel extracts the metadata:
// Internally, soy calls:
metadata := sentinel.Inspect[User]()
// metadata.Fields contains:
// - {Name: "ID", Tags: {"db": "id", "constraints": "primary_key"}}
// - {Name: "Email", Tags: {"db": "email", "constraints": "unique,not_null"}}
// - {Name: "Name", Tags: {"db": "name"}}
This metadata becomes the allowlist. Queries validate against it:
// Valid: "email" exists in schema
users.Select().Where("email", "=", "email_param").Exec(ctx, params)
// Rejected at initialization: "emai" doesn't exist
users.Select().Where("emai", "=", "email_param") // Error: unknown field
The struct definition fixes query shape. User input provides values only.
What Sentinel Provides
| soy needs | Sentinel provides |
|---|---|
| Valid column names | FieldMetadata.Tags["db"] |
| Column types | FieldMetadata.Type |
| Primary keys | Tags["constraints"] parsing |
| Unique constraints | Tags["constraints"] parsing |
| Foreign keys | Tags["references"] |
| Check constraints | Tags["check"] |
| Default values | Tags["default"] |
Sentinel's one-time extraction means zero reflection on the query path—metadata is cached permanently.
Tag Reference
| Tag | Purpose | Example |
|---|---|---|
db | Column name | db:"user_id" |
type | SQL type override | type:"bigserial" |
constraints | Column constraints | constraints:"primary_key" |
references | Foreign key | references:"users(id)" |
default | Default value | default:"now()" |
check | Check constraint | check:"status IN ('a','b')" |
index | Index name | index:"idx_email" |
Learn More
- soy repository
- dbml repository — schema representation