Sphire Hydra is a Go library for hydrating Go structs from database rows using reflection and hydra tags.
It supports:
- MySQL
- MariaDB
- SQLite
- PostgreSQL
- CockroachDB
- Microsoft SQL Server
- Oracle
Warning
Hydra is still a small and evolving project. Treat the API as stabilizing rather than fully mature.
go get github.com/sphireinc/Hydrapackage main
import (
"database/sql"
"errors"
"fmt"
"log"
"github.com/sphireinc/Hydra/hydra"
_ "github.com/mattn/go-sqlite3"
)
type Person struct {
ID int `hydra:"id,pk"`
Email string `hydra:"email,lookup"`
Name string `hydra:"name"`
hydra.Hydratable
}
func (Person) HydraTableName() string {
return "person"
}
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
defer db.Close()
_, err = db.Exec(`
CREATE TABLE person (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL,
name TEXT NOT NULL
);
INSERT INTO person (id, email, name)
VALUES (1, 'alice@example.com', 'Alice');
`)
if err != nil {
log.Fatal(err)
}
person := &Person{}
person.Init(person)
person.XDBTypeOverride = "sqlite"
if err := person.HydrateByPrimaryKey(db, 1); err != nil {
log.Fatal(err)
}
fmt.Printf("loaded by pk: %+v\n", person)
byEmail := &Person{
Email: "alice@example.com",
}
byEmail.Init(byEmail)
byEmail.XDBTypeOverride = "sqlite"
if err := byEmail.HydrateByLookup(db); err != nil {
if errors.Is(err, hydra.ErrNotFound) {
log.Fatal("row not found")
}
log.Fatal(err)
}
fmt.Printf("loaded by lookup: %+v\n", byEmail)
}Hydra requires an addressable struct pointer.
Use:
p := &Person{}
p.Init(p)Do not rely on calling Init on a non-pointer struct value.
Hydra resolves the table name in this order:
- Hydratable.XTableNameOverride
- HydraTableName() string on the struct
- lowercase struct type name
Example:
type Person struct {
ID int `hydra:"id,pk"`
hydra.Hydratable
}
func (Person) HydraTableName() string {
return "people"
}Hydra currently supports these handle types:
- *sql.DB
- MySQL
- MariaDB
- SQLite
- MSSQL
- Oracle
- *pgx.Conn
- PostgreSQL
- CockroachDB
Database routing is controlled by XDBTypeOverride.
Examples:
obj.XDBTypeOverride = "sqlite"
obj.XDBTypeOverride = "mysql"
obj.XDBTypeOverride = "postgres"
obj.XDBTypeOverride = "cockroachdb"If no row matches, Hydra returns:
hydra.ErrNotFoundHydra does not silently leaves the struct at zero values (earlier versions of Hydra did)
If hydration is attempted with an empty where map, Hydra returns:
hydra.ErrEmptyWhereClauseHydra validates table names and column names before building SQL.
Only simple identifiers are allowed:
- letters
- numbers
- underscore
- must not start with a number
This intentionally rejects raw SQL fragments in identifiers.
type Person struct {
ID int `hydra:"id"`
Name string `hydra:"name"`
Email string `hydra:"email"`
hydra.Hydratable
}Use pk to mark the field used by HydrateByPrimaryKey(...)
type Person struct {
ID int `hydra:"id,pk"`
hydra.Hydratable
}Use lookup for fields that should be used by HydrateByLookup()
type Person struct {
Email string `hydra:"email,lookup"`
hydra.Hydratable
}Hydra supports these built-in conversions:
- string from string or []byte
- bool from bool, numeric values, "true" / "false", or byte equivalents
- signed integers from common numeric values and parseable strings/bytes
- unsigned integers from common numeric values and parseable strings/bytes
- floats from numeric values and parseable strings/bytes
- pointers to supported destination types
- interfaces
- direct assignable / convertible values for matching struct, slice, array, or map types
NULL values are supported for:
- pointers
- slices
- maps
- interfaces
NULL into non-nullable concrete value fields returns an error.
Hydra supports two converter styles.
Implement HydraConvert(src any) error
Exaple:
type RFC3339Time struct {
time.Time
}
func (t *RFC3339Time) HydraConvert(src any) error {
switch v := src.(type) {
case string:
parsed, err := time.Parse(time.RFC3339, v)
if err != nil {
return err
}
t.Time = parsed
return nil
case []byte:
parsed, err := time.Parse(time.RFC3339, string(v))
if err != nil {
return err
}
t.Time = parsed
return nil
default:
return fmt.Errorf("unsupported time input %T", src)
}
}Implement HydraConverters() map[string]hydra.HydraFieldConverter
Keys can be either:
- field name
- column name
Hydra supports context-aware calls:
HydrateContext(ctx, db, whereClauses)HydrateByPrimaryKeyContext(ctx, db, value)HydrateByLookupContext(ctx, db)FetchContext(ctx, db, tableName, columns, whereClauses)
Use these when cancellation or deadlines matter.
person := &Person{}
person.Init(person)
person.XDBTypeOverride = "sqlite"
err := person.Hydrate(db, map[string]interface{}{
"id": 1,
})person := &Person{}
person.Init(person)
person.XDBTypeOverride = "sqlite"
err := person.HydrateByPrimaryKey(db, 1)person := &Person{
Email: "alice@example.com",
}
person.Init(person)
person.XDBTypeOverride = "sqlite"
err := person.HydrateByLookup(db)Fast local/unit checks:
make test
make test-hydraFull Docker-backed functional suite:
make test-funcOr directly:
docker compose -f functional_tests/docker-compose.yml up --build --abort-on-container-exit --exit-code-from test-runnerContributions are welcome. Please run the relevant test targets before opening a PR.
