zoobzio January 5, 2025 Edit this page

Core Concepts

Understanding the building blocks of atom.

Atoms

An Atom is the decomposed representation of a struct. It contains type-segregated maps where each field is stored under its name in the appropriate map for its type.

type Atom struct {
    Spec    Spec                    // Type metadata
    Strings map[string]string       // String fields
    Ints    map[string]int64        // Integer fields (all sizes)
    Uints   map[string]uint64       // Unsigned integer fields
    Floats  map[string]float64      // Float fields
    Bools   map[string]bool         // Boolean fields
    Times   map[string]time.Time    // Time fields
    Bytes   map[string][]byte       // Byte slice fields
    // ... pointers, slices, nested
}

Why Type Segregation?

Type segregation enables:

  1. Type-native storage: Store integers in Redis sorted sets, strings in search indices
  2. Efficient serialization: Each map can use optimal encoding for its type
  3. Partial access: Read only the fields you need without deserializing everything

Tables

A Table identifies which map in an Atom stores a particular field type.

type Table string

const (
    TableStrings      Table = "strings"
    TableInts         Table = "ints"
    TableUints        Table = "uints"
    TableFloats       Table = "floats"
    TableBools        Table = "bools"
    TableTimes        Table = "times"
    TableBytes        Table = "bytes"
    // ... plus pointers and slices
)

Table Categories

CategoryTablesGo Types
Scalarsstrings, ints, uints, floats, bools, times, bytesPrimitive types
Pointersstring_ptrs, int_ptrs, etc.*T where T is scalar
Slicesstring_slices, int_slices, etc.[]T where T is scalar

Querying Tables

atomizer, _ := atom.Use[User]()

// Get all fields
fields := atomizer.Fields()
// [{Name: "ID", Table: "ints"}, {Name: "Name", Table: "strings"}, ...]

// Get fields in a specific table
stringFields := atomizer.FieldsIn(atom.TableStrings)
// ["Name", "Email"]

// Get table for a field
table, ok := atomizer.TableFor("Age")
// table = "ints", ok = true

Specs

A Spec contains metadata about a type, including its name and package path. It is an alias for sentinel.Metadata:

type Spec = sentinel.Metadata

Key fields:

spec.TypeName    // e.g., "User"
spec.PackageName // e.g., "github.com/example/app"

Specs are automatically populated from the sentinel library and can be accessed:

atomizer, _ := atom.Use[User]()
spec := atomizer.Spec()
fmt.Println(spec.TypeName)    // "User"
fmt.Println(spec.PackageName) // "github.com/example/app"

Fields

A Field describes a single struct field and its storage location.

type Field struct {
    Name  string // Field name (e.g., "Age")
    Table Table  // Storage table (e.g., "ints")
}

Atomizers

An AtomizerT is the typed interface for converting between structs and atoms.

type Atomizer[T any] struct {
    // internal
}

func (a *Atomizer[T]) Atomize(obj *T) *Atom
func (a *Atomizer[T]) Deatomize(atom *Atom) (*T, error)
func (a *Atomizer[T]) NewAtom() *Atom
func (a *Atomizer[T]) Spec() Spec
func (a *Atomizer[T]) Fields() []Field
func (a *Atomizer[T]) FieldsIn(table Table) []string
func (a *Atomizer[T]) TableFor(field string) (Table, bool)

Atomizers are obtained via the Use[T]() function and are cached in a global registry.

The Registry

The registry maintains a cache of atomizers indexed by type. When you call Use[T]():

  1. Check if an atomizer exists for type T
  2. If yes, return the cached instance
  3. If no, build an execution plan via reflection and cache it
// First call: builds atomizer (~microseconds)
atomizer1, _ := atom.Use[User]()

// Subsequent calls: returns cached (~nanoseconds)
atomizer2, _ := atom.Use[User]()

// atomizer1 and atomizer2 share the same internal state

Type Width Conversion

Atom normalizes integer and float types to their widest representation:

Go TypeAtom Storage
int8, int16, int32, int, int64int64
uint8, uint16, uint32, uint, uint64uint64
float32, float64float64

Overflow detection occurs during deatomization:

type Small struct {
    Value int8 // Range: -128 to 127
}

atom := &Atom{Ints: map[string]int64{"Value": 200}}
_, err := atomizer.Deatomize(atom)
// err: "value 200 overflows int8 (range -128 to 127)"

Named Types

Named types (type aliases) are fully supported:

type UserID string
type Status int

const (
    StatusActive Status = iota
    StatusInactive
)

type User struct {
    ID     UserID
    Status Status
}

// UserID stored in Strings table
// Status stored in Ints table

Next Steps