Nesso ORM

A lightweight Object-Relational Mapper for Strada, built on DBI.

Overview

Nesso provides a thin, practical model layer over DBI. It offers:

Schema Definition

Define models with tables, columns, and primary keys.

CRUD Operations

Create, read, update, and delete records with simple method calls.

Relationships

Define has_many, belongs_to, and has_one relationships between models.

Two Styles

Use Data Mapper style or ActiveRecord style based on preference.

Quick Start

use lib "lib";
use DBI;
use Nesso;
use Nesso::Record;    # For ActiveRecord-style methods

# Connect to database and create Nesso instance
my scalar $db = DBI::connect("dbi:SQLite:app.db", "", "");
my scalar $n = Nesso::new($db);

# Define a model
$n->define("users", {
    "table"   => "users",
    "columns" => ["id", "username", "email"],
    "primary" => "id"
});

# Create and save a record
my scalar $user = $n->create("users", { "username" => "alice" });
$user->save();    # ActiveRecord style

# Query records
my scalar $found = $n->find("users", 1);
say($found->{"username"});

Defining Models

Use define() to register a model with its schema:

$n->define("users", {
    "table"   => "users",           # Database table name
    "columns" => ["id", "username", "email", "created_at"],
    "primary" => "id"              # Primary key column
});

$n->define("posts", {
    "table"   => "posts",
    "columns" => ["id", "user_id", "title", "body"],
    "primary" => "id"
});

CRUD Operations

Create

Create a new record object (not saved until you call save()):

my scalar $user = $n->create("users", {
    "username" => "alice",
    "email"    => "alice@example.com"
});

# Data Mapper style
$n->save($user);

# Or ActiveRecord style (requires Nesso::Record)
$user->save();

Read

# Find by primary key
my scalar $user = $n->find("users", 42);

# Find first matching record
my scalar $alice = $n->find_by("users", { "username" => "alice" });

# Find all matching records
my scalar $active = $n->where("users", { "status" => "active" });

# Get all records
my scalar $all = $n->all("users");

# Count records
my int $count = $n->count("users", { "status" => "active" });

Update

# Modify and save
$user->{"email"} = "newemail@example.com";
$user->save();

# Or use update() for multiple fields at once
$user->update({
    "email"    => "new@example.com",
    "username" => "alice2"
});

Delete

# Data Mapper style
$n->delete($user);

# Or ActiveRecord style
$user->delete();

Query Options

Use special directives in conditions for ordering, limiting, and pagination:

# Order by column
my scalar $recent = $n->where("posts", {
    "status"  => "published",
    "_order"  => "created_at DESC"
});

# Limit results
my scalar $top10 = $n->where("posts", {
    "_order" => "views DESC",
    "_limit" => 10
});

# Pagination
my scalar $page2 = $n->where("posts", {
    "_order"  => "created_at DESC",
    "_limit"  => 20,
    "_offset" => 20
});

Relationships

Nesso supports three types of relationships:

has_many

One-to-many relationship (e.g., a user has many posts):

# A user has many posts (posts.user_id references users.id)
$n->has_many("users", "posts", "user_id");

# Fetch user's posts
my scalar $user = $n->find("users", 1);
my scalar $posts = $n->related($user, "posts");   # Returns array
# Or: $posts = $user->related("posts");

foreach my scalar $post (@{$posts}) {
    say($post->{"title"});
}

belongs_to

Many-to-one relationship (e.g., a post belongs to an author):

# A post belongs to an author (posts.user_id references users.id)
$n->belongs_to("posts", "author", "users", "user_id");

# Fetch post's author
my scalar $post = $n->find("posts", 1);
my scalar $author = $post->related("author");   # Returns single record
say("Written by: " . $author->{"username"});

has_one

One-to-one relationship (e.g., a user has one profile):

# A user has one profile (profiles.user_id references users.id)
$n->has_one("users", "profile", "profiles", "user_id");

# Fetch user's profile
my scalar $profile = $user->related("profile");  # Returns single record or undef
if (defined($profile)) {
    say($profile->{"bio"});
}

Transactions

Wrap multiple operations in a transaction for atomicity. The closure passed to transaction() runs within a database transaction — if it succeeds, changes are committed; if it throws, changes are rolled back:

$n->transaction(func () {
    # All operations here are atomic
    my scalar $user = $n->create("users", { "username" => "bob" });
    $user->save();

    my scalar $post = $n->create("posts", {
        "user_id" => $user->id(),
        "title"   => "First post"
    });
    $post->save();

    # If anything throws, both operations are rolled back
});

ActiveRecord Methods

When you use Nesso::Record, records gain these instance methods:

MethodDescriptionExample
save()INSERT or UPDATE the record$user->save();
update($attrs)Update attributes and save$user->update({"email" => "..."})
delete()DELETE the record$user->delete();
reload()Refresh from database$user->reload();
related($name)Fetch related records$user->related("posts")
id()Get primary key value$user->id()
model()Get model name$user->model()
is_new()Check if unsaved$user->is_new()

Custom Model Classes

Create custom classes that inherit from Nesso::Record to add model-specific methods:

package User;
inherit Nesso::Record;

func set_password(scalar $self, str $password) void {
    $self->{"password_hash"} = crypt::hash_password($password);
    $self->save();
}

func authenticate(scalar $self, str $password) int {
    return crypt::check_password($password, $self->{"password_hash"});
}

func full_name(scalar $self) str {
    return $self->{"first_name"} . " " . $self->{"last_name"};
}

Specify the custom class in the model definition:

$n->define("users", {
    "table"   => "users",
    "columns" => ["id", "username", "password_hash", "first_name", "last_name"],
    "primary" => "id",
    "class"   => "User"   # Use custom User class
});

# Now records are blessed as User objects
my scalar $user = $n->find("users", 1);
$user->set_password("secret123");    # Custom method
say($user->full_name());              # Custom method
$user->save();                        # Inherited method

Debug Mode

Enable SQL logging to see all queries:

$n->set_debug(1);

# Now all queries are logged:
# [Nesso] SQL: SELECT * FROM users WHERE id = ?
# [Nesso] PARAMS: ["1"]
my scalar $user = $n->find("users", 1);

$n->set_debug(0);  # Disable logging

Raw SQL Queries

For complex queries, use query():

my scalar $stats = $n->query(
    "SELECT status, COUNT(*) as count FROM users GROUP BY status",
    []
);

foreach my scalar $row (@{$stats}) {
    say($row->{"status"} . ": " . $row->{"count"});
}

Database Swapping

Change the database handle at runtime:

# Switch to a different database
my scalar $test_db = DBI::connect("dbi:SQLite:test.db", "", "");
$n->set_db($test_db);

# Or clone the Nesso instance with a different database
my scalar $test_n = $n->clone($test_db);
# $test_n has same models and relationships, different database

Complete Example

use lib "lib";
use DBI;
use Nesso;
use Nesso::Record;

func main() int {
    # Connect
    my scalar $db = DBI::connect("dbi:SQLite:blog.db", "", "");
    my scalar $n = Nesso::new($db);

    # Create tables
    DBI::do_sql($db, "CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY, username TEXT, email TEXT
    )");
    DBI::do_sql($db, "CREATE TABLE IF NOT EXISTS posts (
        id INTEGER PRIMARY KEY, user_id INTEGER, title TEXT, body TEXT
    )");

    # Define models
    $n->define("users", {
        "table" => "users", "columns" => ["id", "username", "email"], "primary" => "id"
    });
    $n->define("posts", {
        "table" => "posts", "columns" => ["id", "user_id", "title", "body"], "primary" => "id"
    });

    # Define relationships
    $n->has_many("users", "posts", "user_id");
    $n->belongs_to("posts", "author", "users", "user_id");

    # Create user and posts
    my scalar $user = $n->create("users", {
        "username" => "alice", "email" => "alice@example.com"
    });
    $user->save();

    my scalar $post = $n->create("posts", {
        "user_id" => $user->id(),
        "title"   => "Hello World",
        "body"    => "My first blog post!"
    });
    $post->save();

    # Query with relationships
    my scalar $all_users = $n->all("users");
    foreach my scalar $u (@{$all_users}) {
        say("User: " . $u->{"username"});

        my scalar $posts = $u->related("posts");
        foreach my scalar $p (@{$posts}) {
            say("  - " . $p->{"title"});
        }
    }

    DBI::disconnect($db);
    return 0;
}

Function Reference

CategoryMethodDescription
Setupnew($dbh)Create Nesso instance
define($name, $schema)Register a model
set_db($dbh)Change database handle
clone($dbh)Clone with different DB
CRUDcreate($model, $attrs)Create record (unsaved)
save($record)INSERT or UPDATE
find($model, $id)Find by primary key
find_by($model, $conds)Find first match
where($model, $conds)Find all matches
delete($record)Delete record
Queryall($model)Get all records
count($model, $conds)Count matches
Relationshas_many($owner, $name, $fk)Define 1:N relation
belongs_to($owner, $name, $target, $fk)Define N:1 relation
has_one($owner, $name, $target, $fk)Define 1:1 relation
related($record, $name)Fetch related records
Othertransaction($callback)Atomic operations
query($sql, $params)Raw SQL query
Tip: Nesso is built on DBI. For low-level database operations or features not covered by Nesso, you can always use DBI directly with $n->get_db().