Sphire Hydra Documentation

Sphire Hydra Documentation

View the Project on GitHub sphireinc/Hydra

Hydra package guide

Import

import "github.com/sphireinc/Hydra/hydra"

Overview

Hydra populates tagged struct fields from a single database row.

Typical flow:

  1. define a struct with hydra tags
  2. embed hydra.Hydratable
  3. initialize with Init
  4. hydrate using one of the supported APIs

Basic example

package main

import (
	"database/sql"
	"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)
	}
}

Core contract

Initialization

Hydra expects an addressable struct pointer.

person := &Person{}
person.Init(person)

Calling Init on a non-pointer struct value is not the intended usage.

Table name resolution

Hydra resolves the table name in this order:

  1. XTableNameOverride
  2. HydraTableName() string
  3. lowercase struct type name

Supported handle types

Hydra supports these database handle types:

Use XDBTypeOverride to route the fetcher.

Examples:

obj.XDBTypeOverride = "sqlite"
obj.XDBTypeOverride = "mysql"
obj.XDBTypeOverride = "postgres"
obj.XDBTypeOverride = "cockroachdb"

No matching row

If no matching row exists, Hydra returns:

hydra.ErrNotFound

Empty filters

If hydration is attempted with an empty where map, Hydra returns:

hydra.ErrEmptyWhereClause

Identifier safety

Hydra validates table names and column names before building SQL.

Only simple identifiers are accepted.

Tags

Standard mapping

type Person struct {
	ID   int    `hydra:"id"`
	Name string `hydra:"name"`
}

Primary key tags

type Person struct {
	ID int `hydra:"id,pk"`
}

This enables:

err := person.HydrateByPrimaryKey(db, 1)

Lookup tags

type Person struct {
	Email string `hydra:"email,lookup"`
}

This enables:

person := &Person{Email: "alice@example.com"}
person.Init(person)
err := person.HydrateByLookup(db)

Supported field conversion behavior

Hydra supports built-in conversion for:

NULL is supported for:

Attempting to place NULL into a non-nullable scalar field returns an error.

Custom converters

Hydra supports two converter models.

Field-level converter

If the field implements:

HydraConvert(src any) error

Hydra uses it before built-in primitive conversion.

Example:

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 value %T", src)
	}
}

Parent-level converters

A struct can provide converters keyed by field name or column name:

HydraConverters() map[string]hydra.HydraFieldConverter

This is useful for:

Context-aware APIs

Hydra exposes context-aware calls:

HydrateContext(ctx, db, whereClauses)
HydrateByPrimaryKeyContext(ctx, db, value)
HydrateByLookupContext(ctx, db)
FetchContext(ctx, db, tableName, columns, whereClauses)

Use them when the caller needs cancellation or deadlines.

Error handling

Common errors include:

Example:

if err := person.HydrateByPrimaryKey(db, 42); err != nil {
	switch {
	case errors.Is(err, hydra.ErrNotFound):
		// row missing
	case errors.Is(err, hydra.ErrNotInitialized):
		// Init was not called
	default:
		// other conversion, validation, or database error
	}
}

Explicit where clauses

person := &Person{}
person.Init(person)
person.XDBTypeOverride = "sqlite"

err := person.Hydrate(db, map[string]interface{}{
	"id": 1,
})

Primary key lookup

person := &Person{}
person.Init(person)
person.XDBTypeOverride = "sqlite"

err := person.HydrateByPrimaryKey(db, 1)

Lookup-field hydration

person := &Person{
	Email: "alice@example.com",
}
person.Init(person)
person.XDBTypeOverride = "sqlite"

err := person.HydrateByLookup(db)

Testing flow

Unit tests:

make test
make test-hydra

Full Docker-backed functional suite:

make test-func