orm1

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 16, 2025 License: MIT Imports: 9 Imported by: 0

README

orm1

codecov

A lightweight, high-performance ORM for Go, designed to cleanly persist plain Go structs and support Domain-Driven Design.

Why orm1?

orm1 is built for developers who need the productivity of an ORM without sacrificing performance or architectural cleanliness. It provides a simple, powerful persistence engine for your domain models.

  • Supports DDD Aggregates: orm1 is designed to support the Aggregate pattern. It naturally handles loading and saving parent-child relationships, allowing you to focus on your business logic, not on persistence coordination.
  • Minimalist & Predictable: It's a persistence engine for plain Go structs. There is no complex boilerplate or code generation. Its behavior is explicit and its overhead is minimal and predictable.
  • High Performance: orm1 is designed to be fast. Its features add the minimum overhead necessary for their function. This has been verified by benchmarks against major Go libraries (GORM, Ent, Bun, pgx, sqlx), proving orm1 to be a highly performant and efficient choice.

Philosophy

orm1 lets you focus on your domain logic by providing straightforward persistence for your Go data structures.

The library is designed for:

  • Clean domain models: Your business entities are just plain Go structs.
  • DDD Aggregates: Parent-child relationships are a core concept.
  • Predictable performance: A lightweight design with minimal, predictable overhead.
  • Simplicity: No external dependencies in the core package.

Quick Start

import "github.com/hanpama/orm1"

// 1. Define your domain entities
type Order struct {
    ID         int64  `orm1:"auto"`  // Auto-increment primary key
    CustomerID int64
    Total      float64
    Items      []*OrderItem  // Aggregate children
}

type OrderItem struct {
    ID       int64   `orm1:"auto"`  // Auto-increment primary key
    OrderID  int64   `orm1:"parental"`  // Foreign key to parent
    Product  string
    Quantity int
    Price    float64
}

// 2. Register entities and create factory
factory := orm1.NewSessionFactory()
factory.RegisterEntity(&Order{})
factory.RegisterEntity(&OrderItem{})
factory.SetDriver(orm1.NewPostgreSQLDriver(db))

// 3. Use in your application
session := factory.CreateSession()

// Start a transaction
tx, err := session.Begin(ctx, nil)
if err != nil {
    log.Fatal(err)
}
// Defer rollback in case of error or panic
defer tx.Rollback(ctx)

// Load an aggregate root; children are loaded automatically
var order *Order
if err := session.Get(ctx, &order, orm1.NewKey(42)); err != nil {
    // ... handle "not found" or other errors
    return
}

// Modify the aggregate
order.Total = 159.99 // Update a value
order.Items = append(order.Items, &OrderItem{ // Add a new child
    Product:  "Widget",
    Quantity: 5,
    Price:    9.99,
})

// Save handles all changes to the aggregate
// (updates Order, inserts new OrderItem)
if err := session.Save(ctx, order); err != nil {
    // Rollback will be triggered by the defer
    return
}

// Commit the transaction
if err := tx.Commit(ctx); err != nil {
    log.Fatal(err)
}

Core Features

DDD Aggregate Support

orm1 treats aggregates as a first-class concept. An aggregate defines a transactional consistency boundary—all entities within it are loaded and saved together, maintaining invariants across the entire structure.

When you call session.Get() on an aggregate root, all its children are loaded. When you call session.Save(), orm1 cascades changes throughout the aggregate, handling inserts, updates, and deletes automatically.

// The Order aggregate represents a single transaction boundary.
// Order totals must stay consistent with line items—they belong in one aggregate.
type Order struct {
    ID         int64  `orm1:"auto"`
    CustomerID int64
    Status     string  // "pending", "paid", "shipped"
    Total      float64
    Items      []*OrderItem
}

type OrderItem struct {
    ID        int64   `orm1:"auto"`
    OrderID   int64   `orm1:"parental"`
    ProductID int64   // Reference to Product aggregate (different boundary)
    Quantity  int
    Price     float64  // Price at time of order (immutable)
}

// Load the entire order aggregate
var order *Order
if err := session.Get(ctx, &order, orm1.NewKey(123)); err != nil {
    log.Fatal(err)
}

// Business logic: Add an item and recalculate total
order.Items = append(order.Items, &OrderItem{
    ProductID: 456,
    Quantity:  2,
    Price:     29.99,
})

// Maintain invariant: total must equal sum of items
order.Total = 0
for _, item := range order.Items {
    order.Total += item.Price * float64(item.Quantity)
}

// Save persists all changes atomically
// (updates Order, inserts new OrderItem)
if err := session.Save(ctx, order); err != nil {
    log.Fatal(err)
}
Type-Safe Queries

Use EntityQuery for complex queries with compile-time type safety:

query := orm1.NewEntityQuery[User](session, "u")
users, err := query.
    Where("u.age > ?", 18).
    OrderBy(query.Desc("u.created_at")).
    FetchAll(ctx)
Raw SQL Escape Hatch

When you need full control, raw SQL is straightforward:

type Result struct {
    Category string
    Total    float64
}

var results []*Result
raw := orm1.NewRawQuery(session,
    "SELECT category, SUM(amount) as total FROM transactions GROUP BY category")
raw.ScanAll(ctx, &results)

Entity Mapping

orm1 uses struct tags and simple conventions to map structs to tables:

type User struct {
    ID        int64     `orm1:"auto"`             // Auto-managed (e.g., AUTOINCREMENT)
    Email     string    `orm1:"column:user_email"` // Custom column name
    CreatedAt time.Time `orm1:"auto"`             // DB-managed (e.g., DEFAULT NOW())
    UpdatedAt time.Time // Updated by application
    Internal  string    `orm1:"ignore"`           // Not persisted
}

Mapping Rules:

  • A field named ID is the primary key by default (no tag needed).
  • Use orm1:"auto" for auto-increment/database-managed columns (e.g., AUTOINCREMENT, DEFAULT NOW()).
  • Slice/pointer fields of registered types are treated as children.
  • Field names are automatically mapped to snake_case columns.
  • Parental (foreign) keys must be explicitly tagged with orm1:"parental".

See go doc orm1.SessionFactory.RegisterEntity for a complete tag reference.

Performance

orm1 is designed with performance as a primary goal. It adds minimal, predictable overhead on top of your database operations, ensuring your application remains fast.

This has been verified by benchmarks against other major Go database libraries (including GORM, Ent, Bun, and pgx), proving orm1 to be a highly efficient and performant choice for data access.

See the sample results for detailed performance comparisons.

Documentation

Complete documentation is available via go doc:

go doc -all github.com/hanpama/orm1

Or view online at pkg.go.dev/github.com/hanpama/orm1

Installation

go get github.com/hanpama/orm1

Requires Go 1.18 or later (uses generics).

License

MIT

Documentation

Overview

Package orm1 provides a lightweight, type-safe Object-Relational Mapping library.

Core Concepts

orm1 is built around three main ideas:

Entity Mapping: Define how Go structs map to database tables using RegisterEntity. Struct tags and naming conventions control the mapping behavior.

Session: All database operations are performed through a session that manages entity lifecycle. The session tracks which entities have been persisted to determine whether Save operations should INSERT or UPDATE.

Entity State: Entities loaded via Get or saved via Save are marked as persisted. This allows the session to perform UPDATEs for persisted entities and INSERTs for new ones.

Quick Start

// 1. Define your entity structs
type User struct {
    ID       int64  `orm1:"primary"`
    Name     string
    Email    string `orm1:"column:user_email"`
    Posts    []*Post
}

type Post struct {
    ID       int64 `orm1:"primary"`
    UserID   int64 `orm1:"parental"`
    Title    string
    Content  string
}

// 2. Register entities and create session factory
driver := orm1.NewSQLiteDriver(db)
factory := orm1.NewSessionFactoryWithDriver(driver)
factory.RegisterEntity(&User{})
factory.RegisterEntity(&Post{})

// 3. Create session
session := factory.CreateSession()

// 4. Perform operations
var user *User
session.Get(ctx, &user, orm1.NewKey(1))
user.Name = "Updated Name"
session.Save(ctx, user)

Entity Relationships

Parent-child relationships are automatically loaded and saved:

var user *User
session.Get(ctx, &user, orm1.NewKey(1))
// user.Posts is automatically populated

user.Posts = append(user.Posts, &Post{Title: "New Post"})
session.Save(ctx, user)
// New post is inserted with UserID set automatically

Type-Safe Queries

Use EntityQuery for complex queries with compile-time type safety:

query := orm1.NewEntityQuery[User](session, "u")
users, err := query.
    Where("u.email LIKE ?", "%@example.com").
    OrderBy(query.Desc("u.name")).
    FetchAll(ctx)

Raw SQL

For cases where you need raw SQL:

var results []*struct {
    Name  string
    Count int64
}
raw := orm1.NewRawQuery(session, "SELECT name, COUNT(*) as count FROM users GROUP BY name")
raw.ScanAll(ctx, &results)
Example (AggregateSupport)

Example from DDD Aggregate Support in README

package main

import (
	"context"
	"database/sql"
	"fmt"

	"github.com/hanpama/orm1"
	"github.com/hanpama/orm1/driver"

	_ "github.com/mattn/go-sqlite3"
)

type Post struct {
	ID       int64 `orm1:"auto"`
	Title    string
	Comments []*Comment
}

type Comment struct {
	ID     int64 `orm1:"auto"`
	PostID int64 `orm1:"parental"`
	Text   string
}

func main() {
	// Setup test database
	db, _ := sql.Open("sqlite3", ":memory:")
	defer db.Close()

	db.Exec(`CREATE TABLE posts (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		title TEXT
	)`)
	db.Exec(`CREATE TABLE comments (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		post_id INTEGER,
		text TEXT
	)`)

	registry := orm1.NewRegistry()
	registry.Register(&Post{}, orm1.WithTable("posts"))
	registry.Register(&Comment{}, orm1.WithTable("comments"))
	factory := orm1.NewSessionFactory(registry, driver.NewSQLite(db))

	// Setup: Create a post
	setupSession := factory.CreateSession()
	setupTx, _ := setupSession.Begin(context.Background(), nil)
	post := &Post{Title: "Hello World"}
	setupSession.Save(context.Background(), post)
	setupTx.Commit(context.Background())

	session := factory.CreateSession()
	ctx := context.Background()
	tx, _ := session.Begin(ctx, nil)
	defer tx.Rollback(ctx)

	// Load post with all comments
	var loadedPost *Post
	if err := session.Get(ctx, &loadedPost, orm1.NewKey(post.ID)); err != nil {
		return
	}

	// Add a new comment
	loadedPost.Comments = append(loadedPost.Comments, &Comment{Text: "Great!"})

	// Save cascades changes to the Comments slice
	if err := session.Save(ctx, loadedPost); err != nil {
		return
	}
	if err := tx.Commit(ctx); err != nil {
		return
	}

	// Verify
	verifySession := factory.CreateSession()
	var verifyPost *Post
	verifySession.Get(context.Background(), &verifyPost, orm1.NewKey(post.ID))
	fmt.Printf("Post has %d comment(s)\n", len(verifyPost.Comments))

}
Output:

Post has 1 comment(s)
Example (QuickStart)

Example from Quick Start in README

package main

import (
	"context"
	"database/sql"
	"fmt"

	"github.com/hanpama/orm1"
	"github.com/hanpama/orm1/driver"

	_ "github.com/mattn/go-sqlite3"
)

// Domain entities for examples
type Order struct {
	ID         int64 `orm1:"auto"`
	CustomerID int64
	Total      float64
	Items      []*OrderItem
}

type OrderItem struct {
	ID       int64 `orm1:"auto"`
	OrderID  int64 `orm1:"parental"`
	Product  string
	Quantity int
	Price    float64
}

func main() {
	// Setup test database
	db, _ := sql.Open("sqlite3", ":memory:")
	defer db.Close()

	// Create tables
	db.Exec(`CREATE TABLE orders (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		customer_id INTEGER,
		total REAL
	)`)
	db.Exec(`CREATE TABLE order_items (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		order_id INTEGER,
		product TEXT,
		quantity INTEGER,
		price REAL
	)`)

	// 2. Register entities and create factory
	registry := orm1.NewRegistry()
	registry.Register(&Order{}, orm1.WithTable("orders"))
	registry.Register(&OrderItem{}, orm1.WithTable("order_items"))
	factory := orm1.NewSessionFactory(registry, driver.NewSQLite(db))

	// Setup: Create an order with items
	setupSession := factory.CreateSession()
	setupTx, _ := setupSession.Begin(context.Background(), nil)
	order := &Order{
		CustomerID: 1,
		Total:      99.99,
		Items: []*OrderItem{
			{Product: "Gadget", Quantity: 2, Price: 49.99},
		},
	}
	setupSession.Save(context.Background(), order)
	setupTx.Commit(context.Background())

	// 3. Use in your application
	session := factory.CreateSession()
	ctx := context.Background()

	// Start a transaction
	tx, err := session.Begin(ctx, nil)
	if err != nil {
		return
	}
	// Defer rollback in case of error or panic
	defer tx.Rollback(ctx)

	// Load an aggregate root; children are loaded automatically
	var loadedOrder *Order
	if err := session.Get(ctx, &loadedOrder, orm1.NewKey(order.ID)); err != nil {
		// ... handle "not found" or other errors
		return
	}

	// Modify the aggregate
	loadedOrder.Total = 159.99                                // Update a value
	loadedOrder.Items = append(loadedOrder.Items, &OrderItem{ // Add a new child
		Product:  "Widget",
		Quantity: 5,
		Price:    9.99,
	})

	// Save handles all changes to the aggregate
	// (updates Order, inserts new OrderItem)
	if err := session.Save(ctx, loadedOrder); err != nil {
		// Rollback will be triggered by the defer
		return
	}

	// Commit the transaction
	if err := tx.Commit(ctx); err != nil {
		return
	}

	// Verify the result
	verifySession := factory.CreateSession()
	var verifyOrder *Order
	verifySession.Get(context.Background(), &verifyOrder, orm1.NewKey(order.ID))
	fmt.Printf("Order total: %.2f, Items: %d\n", verifyOrder.Total, len(verifyOrder.Items))

}
Output:

Order total: 159.99, Items: 2
Example (RawSQL)

Example from Raw SQL in README

package main

import (
	"context"
	"database/sql"
	"fmt"

	"github.com/hanpama/orm1"
	"github.com/hanpama/orm1/driver"

	_ "github.com/mattn/go-sqlite3"
)

func main() {
	// Setup test database
	db, _ := sql.Open("sqlite3", ":memory:")
	defer db.Close()

	db.Exec(`CREATE TABLE transactions (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		category TEXT,
		amount REAL
	)`)
	db.Exec(`INSERT INTO transactions (category, amount) VALUES
		('Food', 50.00),
		('Food', 30.00),
		('Transport', 20.00)`)

	registry := orm1.NewRegistry()
	factory := orm1.NewSessionFactory(registry, driver.NewSQLite(db))

	session := factory.CreateSession()
	ctx := context.Background()

	type Result struct {
		Category string
		Total    float64
	}

	var results []*Result
	raw := orm1.NewRawQuery(session,
		"SELECT category, SUM(amount) as total FROM transactions GROUP BY category")
	raw.ScanAll(ctx, &results)

	for _, r := range results {
		fmt.Printf("%s: %.2f\n", r.Category, r.Total)
	}

}
Output:

Food: 80.00
Transport: 20.00
Example (TypeSafeQueries)

Example from Type-Safe Queries in README

package main

import (
	"context"
	"database/sql"
	"fmt"

	"github.com/hanpama/orm1"
	"github.com/hanpama/orm1/driver"

	_ "github.com/mattn/go-sqlite3"
)

type User struct {
	ID        int64 `orm1:"auto"`
	Age       int
	CreatedAt string `orm1:"auto"`
}

func main() {
	// Setup test database
	db, _ := sql.Open("sqlite3", ":memory:")
	defer db.Close()

	db.Exec(`CREATE TABLE users (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		age INTEGER,
		created_at DATETIME DEFAULT CURRENT_TIMESTAMP
	)`)
	db.Exec(`INSERT INTO users (age) VALUES (25), (30), (15)`)

	registry := orm1.NewRegistry()
	registry.Register(&User{}, orm1.WithTable("users"))
	factory := orm1.NewSessionFactory(registry, driver.NewSQLite(db))

	session := factory.CreateSession()
	ctx := context.Background()

	query := orm1.NewEntityQuery[User](session, "u")
	users, err := query.
		Where("u.age > ?", 18).
		OrderBy(query.Desc("u.created_at")).
		FetchAll(ctx)

	if err != nil {
		return
	}

	fmt.Printf("Found %d users over 18\n", len(users))

}
Output:

Found 2 users over 18

Index

Examples

Constants

This section is empty.

Variables

View Source
var DefaultRegistry = NewRegistry()

DefaultRegistry is the global registry for entity types.

View Source
var NewKey = key.NewKey

NewKey creates a new Key from the given values. Supports up to 9 column values.

Functions

This section is empty.

Types

type Driver

type Driver = driver.Driver

Driver creates Backend instances for database operations. This is a type alias for driver.Driver, allowing users to reference the Driver interface from the orm1 package without importing driver.

func NewPostgreSQLDriver

func NewPostgreSQLDriver(db *sql.DB) Driver

NewPostgreSQLDriver creates a driver for PostgreSQL databases.

This is a convenience wrapper around driver.NewPostgres. The driver creates a new backend instance for each session.

Example:

db, _ := sql.Open("postgres", "...")
driver := orm1.NewPostgreSQLDriver(db)
factory := orm1.NewSessionFactoryWithDriver(driver)

func NewSQLiteDriver

func NewSQLiteDriver(db *sql.DB) Driver

NewSQLiteDriver creates a driver for SQLite databases.

This is a convenience wrapper around driver.NewSQLite. The driver creates a new backend instance for each session.

Example:

db, _ := sql.Open("sqlite3", "...")
driver := orm1.NewSQLiteDriver(db)
factory := orm1.NewSessionFactoryWithDriver(driver)

type EntityQuery

type EntityQuery[T any] struct {
	// contains filtered or unexported fields
}

EntityQuery provides a type-safe query builder for entities. It uses generics to ensure compile-time type safety for query results. Supports WHERE, JOIN, ORDER BY, GROUP BY, HAVING, and pagination.

func NewEntityQuery

func NewEntityQuery[T any](session *Session, alias string) *EntityQuery[T]

NewEntityQuery creates a new query builder for entity type T. The alias is used as the table alias in the generated SQL.

func (*EntityQuery[T]) Asc

func (q *EntityQuery[T]) Asc(expr string, params ...any) sql.OrderBy

Asc creates an ascending OrderBy clause with NULLS LAST (PostgreSQL/Oracle standard). This ensures consistent behavior across all databases (PostgreSQL, SQLite, Oracle).

func (*EntityQuery[T]) AscNullsFirst

func (q *EntityQuery[T]) AscNullsFirst(expr string, params ...any) sql.OrderBy

AscNullsFirst creates an ascending OrderBy clause with NULL values sorted first.

func (*EntityQuery[T]) AscNullsLast

func (q *EntityQuery[T]) AscNullsLast(expr string, params ...any) sql.OrderBy

AscNullsLast creates an ascending OrderBy clause with NULL values sorted last.

func (*EntityQuery[T]) Count

func (q *EntityQuery[T]) Count(ctx context.Context) (int64, error)

Count returns the number of entities matching the query.

func (*EntityQuery[T]) Desc

func (q *EntityQuery[T]) Desc(expr string, params ...any) sql.OrderBy

Desc creates a descending OrderBy clause with NULLS FIRST (PostgreSQL/Oracle standard). This ensures consistent behavior across all databases (PostgreSQL, SQLite, Oracle).

func (*EntityQuery[T]) DescNullsFirst

func (q *EntityQuery[T]) DescNullsFirst(expr string, params ...any) sql.OrderBy

DescNullsFirst creates a descending OrderBy clause with NULL values sorted first.

func (*EntityQuery[T]) DescNullsLast

func (q *EntityQuery[T]) DescNullsLast(expr string, params ...any) sql.OrderBy

DescNullsLast creates a descending OrderBy clause with NULL values sorted last.

func (*EntityQuery[T]) FetchAll

func (q *EntityQuery[T]) FetchAll(ctx context.Context) ([]*T, error)

FetchAll executes the query and returns all matching entities. All returned entities are marked as persisted.

func (*EntityQuery[T]) FetchMany

func (q *EntityQuery[T]) FetchMany(ctx context.Context, limit int) ([]*T, error)

FetchMany executes the query and returns up to limit matching entities. All returned entities are marked as persisted.

func (*EntityQuery[T]) FetchOne

func (q *EntityQuery[T]) FetchOne(ctx context.Context) (*T, error)

FetchOne executes the query and returns the first matching entity. Returns nil if no entity matches the query.

func (*EntityQuery[T]) GroupByPrimaryKey

func (q *EntityQuery[T]) GroupByPrimaryKey() *EntityQuery[T]

GroupByPrimaryKey adds a GROUP BY clause using the entity's primary key columns.

func (*EntityQuery[T]) Having

func (q *EntityQuery[T]) Having(condition string, params ...any) *EntityQuery[T]

Having adds a HAVING condition to the query. Multiple Having calls are combined with AND.

func (*EntityQuery[T]) Join

func (q *EntityQuery[T]) Join(table string, alias string, on string, params ...any) *EntityQuery[T]

Join adds an INNER JOIN to the query. table is the table name, alias is its query alias, on is the join condition.

func (*EntityQuery[T]) LeftJoin

func (q *EntityQuery[T]) LeftJoin(table string, alias string, on string, params ...any) *EntityQuery[T]

LeftJoin adds a LEFT OUTER JOIN to the query. table is the table name, alias is its query alias, on is the join condition.

func (*EntityQuery[T]) Offset

func (q *EntityQuery[T]) Offset(n int) *EntityQuery[T]

Offset sets the offset for the query.

func (*EntityQuery[T]) OrderBy

func (q *EntityQuery[T]) OrderBy(orderBys ...sql.OrderBy) *EntityQuery[T]

OrderBy sets the ORDER BY clause for the query.

func (*EntityQuery[T]) Paginate

func (q *EntityQuery[T]) Paginate(ctx context.Context, after Key, first *int, before Key, last *int) (*Page, error)

Paginate performs cursor-based pagination on the query. after/before are cursors (primary keys) for pagination boundaries. first/last control the number of results (use nil for no limit). Offset set via Offset() method is applied after cursor filtering.

func (*EntityQuery[T]) Where

func (q *EntityQuery[T]) Where(condition string, params ...any) *EntityQuery[T]

Where adds a WHERE condition to the query. Multiple Where calls are combined with AND.

type IsolationLevel added in v0.2.0

type IsolationLevel int

IsolationLevel represents the isolation level for a transaction.

const (
	LevelDefault IsolationLevel = iota
	LevelReadUncommitted
	LevelReadCommitted
	LevelWriteCommitted
	LevelRepeatableRead
	LevelSnapshot
	LevelSerializable
	LevelLinearizable
)

type Key

type Key = key.Key

Key represents an entity's primary or foreign key value(s). It is a comparable interface that can be used as a map key.

Examples:

session.Get(ctx, &user, orm1.NewKey(1))              // single-column key
session.Get(ctx, &item, orm1.NewKey(orderId, itemId)) // composite key

type MappingOption added in v0.2.0

type MappingOption func(*mapping.EntityMetadata)

MappingOption is a function that configures an EntityMetadata.

func WithSchema

func WithSchema(schema string) MappingOption

WithSchema sets the database schema for the entity's table.

func WithTable

func WithTable(table string) MappingOption

WithTable sets the table name for the entity. If not specified, defaults to snake_case of the struct name.

type Page

type Page struct {
	Cursors         []Key
	HasPreviousPage bool
	HasNextPage     bool
}

Page represents pagination metadata

type RawQuery

type RawQuery struct {
	// contains filtered or unexported fields
}

RawQuery represents a raw SQL query with parameter interpolation.

func NewRawQuery

func NewRawQuery(session *Session, query string, params ...any) *RawQuery

NewRawQuery creates a new raw query with the given SQL and parameters.

func (*RawQuery) Exec

func (r *RawQuery) Exec(ctx context.Context) (int64, error)

Exec executes the raw query and returns the number of rows affected. Use this for INSERT, UPDATE, DELETE, and other non-SELECT queries.

Example:

affected, err := orm1.NewRawQuery(session, "UPDATE users SET age = ? WHERE id = ?", 30, 1).Exec(ctx)

func (*RawQuery) Rows

func (r *RawQuery) Rows(ctx context.Context) (Rows, error)

Rows executes the raw query and returns the result rows. The caller is responsible for calling Close() on the returned Rows.

func (*RawQuery) ScanAll

func (r *RawQuery) ScanAll(ctx context.Context, dest any) error

ScanAll executes the raw query and scans all rows into dest. dest must be a pointer to a slice of struct pointers (*[]*Struct).

Columns are mapped to struct fields by name. By default, field names are converted to snake_case for matching (e.g., UserID matches user_id column).

Use the `orm1:"column:name"` tag to specify custom column names, and `orm1:"ignore"` to exclude fields. See ScanOne for tag examples.

func (*RawQuery) ScanOne

func (r *RawQuery) ScanOne(ctx context.Context, dest any) error

ScanOne executes the raw query and scans the first row into dest. dest must be a pointer to a struct. Returns nil if no rows are found.

Columns are mapped to struct fields by name. By default, field names are converted to snake_case for matching (e.g., UserID matches user_id column).

The `orm1:"column:name"` tag can specify a custom column name:

type Result struct {
    UserEmail string `orm1:"column:email"`  // Maps to "email" column
    PostCount int64  `orm1:"column:count"`  // Maps to "count" column
}

The `orm1:"ignore"` tag excludes fields from scanning:

type Result struct {
    Name     string
    Computed string `orm1:"ignore"`  // Not populated from query
}

Unmapped columns in the result set are silently ignored.

type Registry added in v0.2.0

type Registry struct {
	// contains filtered or unexported fields
}

Registry is a storage for entity type metadata. It collects struct field metadata through registration and provides this metadata to SessionFactory for building EntityMapping instances.

func NewRegistry added in v0.2.0

func NewRegistry() *Registry

NewRegistry creates a new registry.

func (*Registry) GetMetadata added in v0.2.0

func (r *Registry) GetMetadata() map[reflect.Type]mapping.EntityMetadata

GetMetadata returns the collected metadata for building. This is used by SessionFactory to build EntityMapping instances.

func (*Registry) GetRegistered added in v0.2.0

func (r *Registry) GetRegistered() map[reflect.Type]bool

GetRegistered returns the set of registered entity types. This is used by SessionFactory to detect child relationships.

func (*Registry) Register added in v0.2.0

func (r *Registry) Register(entityPtr any, opts ...MappingOption)

Register adds an entity type to the registry with optional configuration.

Entity Model

An entity represents a database table row and its relationships. Entities are defined as Go structs with fields mapped to database columns. orm1 uses struct tags and naming conventions to determine how fields map to the database.

Parameters

entityPtr must be a pointer to a struct instance (e.g., &User{}). The struct type defines the entity's schema.

Options can customize the mapping:

  • WithTable("table_name"): Set table name (default: snake_case of struct name)
  • WithSchema("schema_name"): Set database schema
  • WithPrimaryKey("Field1", "Field2"): Define composite primary key

Struct Tags

Fields can be annotated with `orm1` struct tags to control mapping behavior:

type User struct {
    ID        int64  `orm1:"auto"`              // Auto-managed (AUTOINCREMENT, etc.)
    ParentID  int64  `orm1:"parental"`          // Foreign key to parent
    Email     string `orm1:"column:user_email"` // Custom column name
    CreatedAt time.Time `orm1:"skip_insert"`    // Not inserted (e.g., trigger-managed)
    UpdatedAt time.Time `orm1:"skip_update"`    // Not updated (e.g., immutable)
    Internal  string `orm1:"ignore"`            // Not persisted at all
    Posts     []*Post `orm1:"child"`            // One-to-many relationship
    Profile   *Profile `orm1:"child"`           // One-to-one relationship
}

Available tags:

  • auto: Exclude from both INSERT and UPDATE (for DB-managed values like AUTOINCREMENT)
  • primary: Mark as primary key field
  • parental: Mark as foreign key to parent entity
  • column:name: Specify database column name (default: snake_case of field name)
  • skip_insert: Exclude from INSERT statements (for DB-generated values)
  • skip_update: Exclude from UPDATE statements (for immutable columns)
  • ignore: Exclude field from persistence entirely
  • child: Mark as relationship field (auto-detected for registered entity types)

Auto-Detection Rules

When tags are not specified, orm1 applies these conventions:

  • Field named "ID" is the primary key
  • Field names map to snake_case column names
  • Fields of registered entity types ([]*T or *T) are children
  • Parental keys must be explicitly tagged with `orm1:"parental"`

Relationships

Child entities are loaded and saved cascading from their parents. Define relationships as fields with registered entity types:

  • []*ChildType: One-to-many (plural children)
  • *ChildType: One-to-one (singular child)

Child entities must have parental key fields matching the parent's primary key.

Example

registry := orm1.NewRegistry()

// Register parent entity
registry.Register(&User{},
    orm1.WithTable("users"),
    orm1.WithPrimaryKey("ID"))

// Register child entity
registry.Register(&Post{},
    orm1.WithTable("posts"))

// Create factory with registry
factory := orm1.NewSessionFactory(registry, driver)
session := factory.CreateSession()

type Rows

type Rows = driver.Rows

Rows is the interface for iterating over query results. This is a type alias for driver.Rows.

type Session

type Session struct {
	// contains filtered or unexported fields
}

Session manages database operations and tracks entity state. It tracks which entities have been persisted to the database, enabling Save to determine whether to INSERT (for new entities) or UPDATE (for persisted entities). All persistence operations (Get, Save, Delete) are performed through a Session.

func (*Session) BatchDelete

func (s *Session) BatchDelete(ctx context.Context, entities ...any) error

BatchDelete removes multiple entities from the database. Only deletes entities that are marked as persisted. After deletion, entities are unmarked from the persisted state. entities must be entity pointers (*Entity).

func (*Session) BatchGet

func (s *Session) BatchGet(ctx context.Context, dests any, ids []Key) error

BatchGet loads multiple entities by their primary keys in a single query. dests must be a pointer to a slice of entity pointers (*[]*Entity). Results are returned in the same order as ids.

BatchGet always performs a fresh database query. After loading, entities are marked as persisted, so subsequent Save calls will perform UPDATE.

For non-existent keys, nil is placed at the corresponding position in the result slice. For example, if ids = [key1, key2, key3] and only key1 and key3 exist in the database, the result will be [entity1, nil, entity3].

func (*Session) BatchSave

func (s *Session) BatchSave(ctx context.Context, entities ...any) error

BatchSave persists multiple entities in a single batch operation. Performs UPDATE for entities marked as persisted, INSERT for new entities. After saving, all entities are marked as persisted. entities must be entity pointers (*Entity).

func (*Session) Begin

func (s *Session) Begin(ctx context.Context, opts *TxOptions) (*Transaction, error)

Begin starts a transaction with optional transaction options. For the first transaction, it calls backend.BeginTx(). For nested transactions, it creates a SAVEPOINT. It backs up the current session state onto a stack for potential restoration on Rollback.

Default isolation level: READ COMMITTED SQLite: Only supports SERIALIZABLE (ignores other levels) PostgreSQL: Supports all standard isolation levels

Nested transactions inherit the isolation level from the parent and cannot change it.

func (*Session) Clear

func (s *Session) Clear()

Clear clears the session state, removing all tracked entities and cached relationships. This is useful for long-lived sessions that process multiple logical units of work and need to free memory or reset tracking state between operations.

Example usage in a batch processing scenario:

session := factory.CreateSession()
for batch := range batches {
    for _, item := range batch {
        session.Save(ctx, item)
    }
    session.Clear() // Clear session state between batches
}

Note: After calling Clear(), previously loaded entities are no longer tracked as persisted. Calling Save on them again will perform INSERT instead of UPDATE.

func (*Session) Delete

func (s *Session) Delete(ctx context.Context, entity any) error

Delete removes an entity from the database. Only deletes entities that are marked as persisted. After deletion, the entity is unmarked from the persisted state. entity must be a pointer to a struct (*Entity).

func (*Session) Get

func (s *Session) Get(ctx context.Context, dest any, id Key) error

Get loads a single entity by primary key. dest must be a pointer to an entity pointer (**Entity).

Get always performs a fresh database query. After loading, the entity is marked as persisted, so subsequent Save calls will perform UPDATE.

func (*Session) Save

func (s *Session) Save(ctx context.Context, entity any) error

Save persists an entity to the database. Performs UPDATE for entities marked as persisted, INSERT for new entities. After saving, the entity is marked as persisted. entity must be a pointer to a struct (*Entity).

type SessionBackend

type SessionBackend = driver.Backend

SessionBackend defines the interface for database-specific implementations. This is a type alias for driver.Backend.

type SessionFactory

type SessionFactory struct {
	// contains filtered or unexported fields
}

SessionFactory builds entity mappings and creates sessions. It builds EntityMapping instances from Registry metadata at creation time and reuses them for all sessions.

func NewSessionFactory

func NewSessionFactory(registry *Registry, driver Driver) *SessionFactory

NewSessionFactory creates a new session factory with a registry and driver. The factory builds entity mappings immediately from the registry metadata and caches them for efficient session creation.

Example:

registry := orm1.NewRegistry()
registry.Register(&User{})
registry.Register(&Post{})

driver := orm1.NewPostgreSQLDriver(db)
factory := orm1.NewSessionFactory(registry, driver)

session := factory.CreateSession()

func (*SessionFactory) CreateSession

func (sf *SessionFactory) CreateSession() *Session

CreateSession creates a new Session with a fresh backend instance. The backend is created using the Driver that was configured.

Each call to CreateSession creates a new backend instance, as backends are not safe for concurrent use across multiple sessions.

Entity mappings are already built at factory creation time, making session creation very efficient.

type Transaction added in v0.2.0

type Transaction struct {
	// contains filtered or unexported fields
}

Transaction represents a database transaction that can be committed or rolled back. It is safe to call Rollback in a defer statement even after Commit has been called.

func (*Transaction) Commit added in v0.2.0

func (tx *Transaction) Commit(ctx context.Context) error

Commit commits the transaction. For the outermost transaction, it calls backend.Commit(). For nested transactions, it releases the SAVEPOINT. After Commit succeeds, subsequent calls to Commit or Rollback are no-ops (safe for defer).

func (*Transaction) Rollback added in v0.2.0

func (tx *Transaction) Rollback(ctx context.Context) error

Rollback rolls back the transaction. For the outermost transaction, it calls backend.Rollback(). For nested transactions, it rolls back to the SAVEPOINT. It restores the session state to what it was before Begin was called. After Rollback succeeds or if already committed, subsequent calls are no-ops (safe for defer).

type TxOptions added in v0.2.0

type TxOptions struct {
	Isolation IsolationLevel
	ReadOnly  bool
}

TxOptions holds transaction options for Begin.

func ReadOnlyTxOptions added in v0.2.0

func ReadOnlyTxOptions() *TxOptions

ReadOnlyTxOptions returns TxOptions for a read-only transaction.

func RepeatableReadTxOptions added in v0.2.0

func RepeatableReadTxOptions() *TxOptions

RepeatableReadTxOptions returns TxOptions for a repeatable read transaction.

func SerializableTxOptions added in v0.2.0

func SerializableTxOptions() *TxOptions

SerializableTxOptions returns TxOptions for a serializable transaction.

Directories

Path Synopsis
Package driver provides database driver interfaces and implementations for orm1.
Package driver provides database driver interfaces and implementations for orm1.
Package key provides the Key type for representing entity keys.
Package key provides the Key type for representing entity keys.
Package mapping provides the entity mapping metadata types used by orm1.
Package mapping provides the entity mapping metadata types used by orm1.
Package sql provides SQL AST types for implementing custom database backends.
Package sql provides SQL AST types for implementing custom database backends.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL