src/fiber_orm

  Source   Edit

Lightweight ORM supporting the Postgres and SQLite databases in Nim. It supports a simple, opinionated model mapper to generate SQL queries based on Nim objects. It also includes a simple connection pooling implementation.

Fiber ORM is not intended to be a 100% all-cases-covered ORM that handles every potential data access pattern one might wish to implement. It is best thought of as a collection of common SQL generation patterns. It is intended to cover 90% of the common queries and functions one might write when implementing an SQL-based access layer. It is expected that there may be a few more complicated queries that need to be implemented to handle specific access patterns.

The simple mapping pattern provided by Fiber ORM also works well on top of databases that encapsulate data access logic in SQL with, for example, views.

Basic Usage

Consider a simple TODO list application that keeps track of TODO items as well as time logged against those items.

Example DB Schema

You might have a schema such as:

create extension if not exists "pgcrypto";

create table todo_items columns (
  id uuid not null primary key default gen_random_uuid(),
  owner varchar not null,
  summary varchar not null,
  details varchar default null,
  priority integer not null default 0,
  related_todo_item_ids uuid[] not null default '{}'
);

create table time_entries columns (
  id uuid not null primary key default gen_random_uuid(),
  todo_item_id uuid not null references todo_items (id) on delete cascade,
  start timestamp with timezone not null default current_timestamp,
  stop timestamp with timezone default null,
);

Example Model Definitions

Models may be defined as:

# models.nim
import std/options, std/times
import uuids

type
  TodoItem* = object
    id*: UUID
    owner*: string
    summary*: string
    details*: Option[string]
    priority*: int
    relatedTodoItemIds*: seq[UUID]
  
  TimeEntry* = object
    id*: UUID
    todoItemId*: Option[UUID]
    start*: DateTime
    stop*: Option[DateTime]

Example Fiber ORM Usage

Using Fiber ORM we can generate a data access layer with:

# db.nim
import fiber_orm
import ./models.nim

type TodoDB* = DbConnPool

proc initDb*(connString: string): TodoDB =
  result = fiber_orm.initPool(
    connect = proc(): DbConn = open("", "", "", connString),
    poolSize = 20,
    hardCap = false)


generateProcsForModels(TodoDB, [TodoItem, TimeEntry])

generateLookup(TodoDB, TimeEntry, @["todoItemId"])

This will generate the following procedures:

proc getTodoItem*(db: TodoDB, id: UUID): TodoItem;
proc getAllTodoItems*(db: TodoDB): seq[TodoItem];
proc createTodoItem*(db: TodoDB, rec: TodoItem): TodoItem;
proc updateTodoItem*(db: TodoDB, rec: TodoItem): bool;
proc deleteTodoItem*(db: TodoDB, rec: TodoItem): bool;
proc deleteTodoItem*(db: TodoDB, id: UUID): bool;

proc findTodoItemsWhere*(db: TodoDB, whereClause: string,
  values: varargs[string, dbFormat]): seq[TodoItem];

proc getTimeEntry*(db: TodoDB, id: UUID): TimeEntry;
proc getAllTimeEntries*(db: TodoDB): seq[TimeEntry];
proc createTimeEntry*(db: TodoDB, rec: TimeEntry): TimeEntry;
proc updateTimeEntry*(db: TodoDB, rec: TimeEntry): bool;
proc deleteTimeEntry*(db: TodoDB, rec: TimeEntry): bool;
proc deleteTimeEntry*(db: TodoDB, id: UUID): bool;

proc findTimeEntriesWhere*(db: TodoDB, whereClause: string,
  values: varargs[string, dbFormat]): seq[TimeEntry];

proc findTimeEntriesByTodoItemId(db: TodoDB, todoItemId: UUID): seq[TimeEntry];

Object-Relational Modeling

Model Class

Fiber ORM uses simple Nim objects and ref objects as model classes. Fiber ORM expects there to be one table for each model class.

Name Mapping

Fiber ORM uses snake_case for database identifiers (column names, table names, etc.) and camelCase for Nim identifiers. We automatically convert model names to and from table names (TodoItem <-> todo_items), as well as column names (userId <-> user_id).

Notice that table names are automatically pluralized from model class names. In the above example, you have:

Model ClassTable Name
TodoItemtodo_items
TimeEntrytime_entries

Because Nim is style-insensitive, you can generall refer to model classes and fields using snake_case, camelCase, or PascalCase in your code and expect Fiber ORM to be able to map the names to DB identifier names properly (though FiberORM will always use camelCase internally).

See the identNameToDb, dbNameToIdent, tableName and dbFormat procedures in the fiber module for details.

ID Field

Fiber ORM expects every model class to have a field named id, with a corresponding id column in the model table. This field must be either a string, integer, or UUID.

When creating a new record the id field will be omitted if it is empty (Option.isNone, UUID.isZero, value of 0, or only whitespace). This is intended to allow for cases like the example where the database may generate an ID when a new record is inserted. If a non-zero value is provided, the create call will include the id field in the INSERT query.

For example, to allow the database to create the id:

let item = TodoItem(
  owner: "John Mann",
  summary: "Create a grocery list.",
  details: none[string](),
  priority: 0,
  relatedTodoItemIds: @[])

let itemWithId = db.createTodoItem(item)
echo $itemWithId.id # generated in the database

And to create it in code:

import uuids

let item = TodoItem(
  id: genUUID(),
  owner: "John Mann",
  summary: "Create a grocery list.",
  details: none[string](),
  priority: 0,
  relatedTodoItemIds: @[])

let itemInDb = db.createTodoItem(item)
echo $itemInDb.id # will be the same as what was provided

Supported Data Types

The following Nim data types are supported by Fiber ORM:

Nim TypePostgres TypeSQLite Type
stringvarchar
intinteger
floatdouble
boolboolean
DateTimetimestamp
seq[]array
UUIDuuid (pg)
Optionallows NULL [1]
JsonNodejsonb

  Note that this implies that all NULL-able fields should be typed as optional using Option[fieldType]. Conversely, any fields with non-optional types should also be constrained to be NOT NULL in the database schema.

Database Object

Many of the Fiber ORM macros expect a database object type to be passed. In the example above the pool.DbConnPool object is used as database object type (aliased as TodoDB). This is the intended usage pattern, but anything can be passed as the database object type so long as there is a defined withConn template that provides an injected conn: DbConn object to the provided statement body.

For example, a valid database object implementation that opens a new connection for every request might look like this:

import std/db_postgres

type TodoDB* = object
  connString: string

template withConn*(db: TodoDB, stmt: untyped): untyped =
  let conn {.inject.} = open("", "", "", db.connString)
  try: stmt
  finally: close(conn)

See Also

fiber_orm/pool

Types

NotFoundError = object of CatchableError
Error type raised when no record matches a given ID   Source   Edit

Procs

proc createRecord[T](db: DbConn; rec: T): T

Create a new record. rec is expected to be a model class. The id field is only set if it is non-empty (see ID Field for details).

Returns the newly created record.

  Source   Edit
proc deleteRecord[T](db: DbConn; rec: T): bool
Delete a record by id.   Source   Edit
proc initPool(connect: proc (): DbConn; poolSize = 10; hardCap = false;
              healthCheckQuery = "SELECT \'true\' AS alive"): DbConnPool {.
    ...raises: [Exception], tags: [RootEffect].}
Initialize a new DbConnPool. See the initDb procedure in the Example Fiber ORM Usage for an example
  • connect must be a factory which creates a new DbConn.
  • poolSize sets the desired capacity of the connection pool.
  • hardCap defaults to false. When false, the pool can grow beyond the configured capacity, but will release connections down to the its capacity (no less than poolSize).

    When true the pool will not create more than its configured capacity. It a connection is requested, none are free, and the pool is at capacity, this will result in an Error being raised.

  • healthCheckQuery should be a simple and fast SQL query that the pool can use to test the liveliness of pooled connections.
  Source   Edit
proc updateRecord[T](db: DbConn; rec: T): bool
Update a record by id. rec is expected to be a model class.   Source   Edit

Macros

macro generateLookup(dbType: type; modelType: type; fields: seq[string]): untyped
Create a lookup procedure for a given set of field names. For example, given the TODO database demostrated above,
generateLookup(TodoDB, TodoItem, ["owner", "priority"])

will generate the following procedure:

proc findTodoItemsByOwnerAndPriority*(db: SampleDB,
  owner: string, priority: int): seq[TodoItem]
  Source   Edit
macro generateProcsForFieldLookups(dbType: type; modelsAndFields: openArray[
    tuple[t: type, fields: seq[string]]]): untyped
  Source   Edit
macro generateProcsForModels(dbType: type; modelTypes: openArray[type]): untyped
Generate all standard access procedures for the given model types. For a model class named TodoItem, this will generate the following procedures:
proc getTodoItem*(db: TodoDB, id: idType): TodoItem;
proc getAllTodoItems*(db: TodoDB): TodoItem;
proc createTodoItem*(db: TodoDB, rec: TodoItem): TodoItem;
proc deleteTodoItem*(db: TodoDB, rec: TodoItem): bool;
proc deleteTodoItem*(db: TodoDB, id: idType): bool;
proc updateTodoItem*(db: TodoDB, rec: TodoItem): bool;

proc findTodoItemsWhere*(
  db: TodoDB, whereClause: string, values: varargs[string]): TodoItem;

dbType is expected to be some type that has a defined withConn procedure (see Database Object for details).

  Source   Edit

Templates

template deleteRecord(db: DbConn; modelType: type; id: typed): untyped
Delete a record by id.   Source   Edit
template findRecordsBy(db: DbConn; modelType: type;
                       lookups: seq[tuple[field: string, value: string]]): untyped
Find all records matching the provided lookup values.   Source   Edit
template findRecordsWhere(db: DbConn; modelType: type; whereClause: string;
                          values: varargs[string, dbFormat]): untyped
Find all records matching a given WHERE clause. The number of elements in the values array must match the number of placeholders (?) in the provided WHERE clause.   Source   Edit
template getAllRecords(db: DbConn; modelType: type): untyped
Fetch all records of the given type.   Source   Edit
template getRecord(db: DbConn; modelType: type; id: typed): untyped
Fetch a record by id.   Source   Edit
template inTransaction(db: DbConnPool; body: untyped)
  Source   Edit