zoobzio January 15, 2026 3 mins Edit this page

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 needsSentinel provides
Valid column namesFieldMetadata.Tags["db"]
Column typesFieldMetadata.Type
Primary keysTags["constraints"] parsing
Unique constraintsTags["constraints"] parsing
Foreign keysTags["references"]
Check constraintsTags["check"]
Default valuesTags["default"]

Sentinel's one-time extraction means zero reflection on the query path—metadata is cached permanently.

Tag Reference

TagPurposeExample
dbColumn namedb:"user_id"
typeSQL type overridetype:"bigserial"
constraintsColumn constraintsconstraints:"primary_key"
referencesForeign keyreferences:"users(id)"
defaultDefault valuedefault:"now()"
checkCheck constraintcheck:"status IN ('a','b')"
indexIndex nameindex:"idx_email"

Learn More