ponairi

Search:
Group by:
Source   Edit  

Pónairí can be used when all you need is a simple ORM for CRUD tasks.

  • Create: insert can be used for insertion
  • Read: find is used with a type based API to perform selections on your data
  • Update: upsert will either insert or update your data
  • Delete: delete does what it says on the tin, deletes

Currently there is not support for auto migrations and so you'll need to perform those yourself if modifying the schema

Getting started

After installing the library through nimble (or any other means) you'll want to open a connection with newConn which will be used for all interactions with the database. While this library does just use the connection object from lowdb, it is best to use this since it configures certain settings to make things like foreign keys work correctly

let db = newConn(":memory:") # Or pass a path to a file

After that your first step will be creating your schema through objects and then using create to build them in the database

type
  # Create your objects like any other object.
  # You then use pragmas to control aspects of the columns
  Person = object
    name {.primary.}: string
    age: int
  
  Item = object
    ## An item is just something owned by a person
    id {.autoIncrement, primary.}: int
    name: string
    # Add a one-to-many relation (one person owns many objects)
    owner {.references: Person.name.}: string

# We can also run db.drop(Type) if we want to drop a table
db.create(Person)
db.create(Item)

Now you'll probably want to start doing some CRUD tasks which is very easy to do

Create

Just call insert with an object of your choice

db.insert(Person(name: "Jake", age: 42))

Read

find is used for all operations relating to getting objects from a database. It uses a type based API where the first parameter (after the db connection) determines the return type. Currently most tasks require you to write SQL yourself but this will hopefully change in the future

# Gets the Object we created before
assert db.find(Person, sql"SELECT * FROM Person WHERE name = 'Jake'").age == 42

# We can use Option[T] to handle when a query might not return a value
# It would return an exception otherwise
import std/options
assert db.find(Option[Person], sql"SELECT * FROM Person WHERE name = 'John Doe'").isNone

# We can use seq[T] to return all rows that match the query
for person in db.find(seq[Person], sql"SELECT * FROM Person WHERE age > 1"):
  echo person
# This can also be used to get every row in a table
for person in db.find(seq[Person]):
  echo person

Update

Updating is done with the upsert proc. This only works for tables with primary keys since it needs to be able to find the old object to be able to update it. If object doesn't exist then this acts like a normal insert

# Lets use the person we had before, but make them travel back in time
let newPerson = Person(name: "Jake", age: 25)
db.upsert(newPerson)

Delete

Deleting is done via delete and requires passing the object that should be deleted. It finds the row to delete by either matching the primary keys or comparing all the values (If there is no primary keys defined)

db.delete(Person(name: "Jake"))

Custom types

Custom types can be added by implementing three functions

  • sqlType: Returns a string that will be the type to use in the SQL table
  • dbValue: For converting from the type to a value the database can read (See lowdb DbValue)
  • to: For converting from the database value back to the Nim type

Here is an example of implementing these for SecureHash. This code isn't very performant (performs unneeded copies) but is more of an example

Example:

import ponairi
import std/sha1

# Its just an array of bytes so blob is the best storage type
proc sqlType(t: typedesc[SecureHash]): string = "BLOB"

proc dbValue(s: SecureHash): DbValue =
  # We need to convert it into a blob for the database
  # SHA1 hashes are 20 bytes in length
  var blob = newString(20)
  for i in 0..<20:
    blob[i] = char(Sha1Digest(s)[i])
  DbValue(kind: dvkBlob, b: DbBlob(blob))

proc to(src: DbValue, dest: var SecureHash) =
  for i in 0..<20:
    Sha1Digest(dest)[i] = uint8(string(src.b)[i])

type
  User = object
    # Usually you would add some salt and pepper and use a more cryptographic hash.
    # But once again, this is an example
    username {.primary.}: string
    password: SecureHash

let db = newConn(":memory:")
db.create(User)

let user = User(
  username: "coolDude",
  password: secureHash("laptop")
)
# We will now show that we can send the user to the DB and get the same values back
db.insert user
assert db.find(User, sql"SELECT * FROM User") == user

Types

SomeTable = ref [object] | object
Supported types for reprsenting table schema Source   Edit  

Procs

proc commit(db: DbConn) {....raises: [DbError],
                          tags: [ReadDbEffect, WriteDbEffect], forbids: [].}
Commits a transaction Source   Edit  
proc create[T: SomeTable](db: DbConn; table: typedesc[T])
Creates a table in the database that reflects an object

Example:

let db = newConn(":memory:")
# Create object
type Something = object
  foo, bar: int
# Use `create` to make a table named 'something' with field reflecting `Something`
db.create Something
Source   Edit  
proc dbValue(b: bool): DbValue {....raises: [], tags: [], forbids: [].}
Source   Edit  
proc dbValue(d: DateTime): DbValue {....raises: [], tags: [], forbids: [].}
Source   Edit  
func dbValue(e: enum): DbValue
Source   Edit  
proc dbValue(t: Time): DbValue {....raises: [], tags: [], forbids: [].}
Source   Edit  
proc delete[T: SomeTable](db: DbConn; item: T)
Tries to delete item from table. Does nothing if it doesn't exist Source   Edit  
proc drop[T: object](db: DbConn; table: typedesc[T])
Drops a table from the database Source   Edit  
proc exists[T: SomeTable](db: DbConn; item: T): bool
Returns true if item already exists in the database Source   Edit  
proc explain(db: DbConn; query: SqlQuery): string {....raises: [DbError],
    tags: [ReadDbEffect], forbids: [].}
Returns the query plan for a query Source   Edit  
proc find[T: SomeTable | tuple](db: DbConn; table: typedesc[seq[T]];
                                query: SqlQuery; args): seq[T]
Source   Edit  
proc find[T: SomeTable | tuple](db: DbConn; table: typedesc[T]; query: SqlQuery;
                                args): T
Returns first row that matches query Source   Edit  
proc find[T: SomeTable](db: DbConn; table: typedesc[Option[T]]; query: SqlQuery;
                        args): Option[T]
Returns first row that matches query. If nothing matches then it returns none(T) Source   Edit  
proc find[T: SomeTable](db: DbConn; table: typedesc[seq[T]]): seq[T]
Source   Edit  
proc insert[T: SomeTable](db: DbConn; item: T)
Inserts an object into the database Source   Edit  
proc insert[T: SomeTable](db: DbConn; items: openArray[T])
Inserts the list of items into the database. This gets ran in a transaction so if an error happens then none of the items are saved to the database Source   Edit  
proc insertID[T: SomeTable](db: DbConn; item: T): int64
Inserts an object and returns the auto generated ID Source   Edit  
proc newConn(file: string): DbConn {....raises: [DbError], tags: [DbEffect,
    ReadDbEffect, WriteDbEffect], forbids: [].}
Sets up a new connection with needed configuration. File is just a normal sqlite file string Source   Edit  
proc rollback(db: DbConn) {....raises: [DbError],
                            tags: [ReadDbEffect, WriteDbEffect], forbids: [].}
Runs a rollback on the current transaction Source   Edit  
func sqlType(T: typedesc[bool]): string {.inline.}
Source   Edit  
func sqlType(T: typedesc[DateTime]): string {.inline.}
Source   Edit  
func sqlType(T: typedesc[SomeFloat]): string {.inline.}
Source   Edit  
func sqlType(T: typedesc[SomeOrdinal]): string {.inline.}
Source   Edit  
func sqlType(T: typedesc[string]): string {.inline.}
Source   Edit  
func sqlType(T: typedesc[Time]): string {.inline.}
Source   Edit  
func sqlType[V](T: typedesc[Option[V]]): string {.inline.}
Source   Edit  
proc startTransaction(db: DbConn) {....raises: [DbError],
                                    tags: [ReadDbEffect, WriteDbEffect],
                                    forbids: [].}
Starts a transaction context Source   Edit  
func to(src: DbValue; dest: var bool) {.inline, ...raises: [], tags: [],
                                        forbids: [].}
Source   Edit  
proc to(src: DbValue; dest: var DateTime) {.inline, ...raises: [TimeParseError],
    tags: [TimeEffect], forbids: [].}
Source   Edit  
func to(src: DbValue; dest: var string) {.inline, ...raises: [], tags: [],
    forbids: [].}
Source   Edit  
func to(src: DbValue; dest: var Time) {.inline, ...raises: [], tags: [],
                                        forbids: [].}
Source   Edit  
func to[T: SomeFloat](src: DbValue; dest: var T) {.inline.}
Source   Edit  
func to[T: SomeOrdinal](src: DbValue; dest: var T) {.inline.}
Source   Edit  
proc to[T: SomeTable | tuple](row: Row; dest: var T)
Source   Edit  
proc to[T](src: DbValue; dest: var Option[T])
Source   Edit  

Iterators

iterator find[T: SomeTable | tuple](db: DbConn; table: typedesc[seq[T]]): T
Returns all rows that belong to table Source   Edit  
iterator find[T: SomeTable | tuple](db: DbConn; table: typedesc[seq[T]];
                                    query: SqlQuery; args): T
Source   Edit  

Macros

macro create(db; tables: varargs[typed])
Creates multiple classes at once Source   Edit  
macro load[C: SomeTable](db; child: C; field: untyped): object
Loads parent from child using field

Example:

let db = newConn(":memory:")

type
  User = object
    id {.primary, autoIncrement.}: int64
    name: string
  Item = object
    id {.primary, autoIncrement.}: int64
    owner {.references: User.id.}: int64
    name: string

db.create(User, Item)

let
  ownerID = db.insertID(User(name: "Jake"))
  item = Item(owner: ownerID, name: "Lamp")
db.insert(item)
# We can now load the parent object that is referenced in the owner field
assert db.load(item, owner).name == "Jake"
Source   Edit  
macro upsert(db; item: typed; excludes: varargs[untyped])

Trys to insert an item (or items) into the database. If it conflicts with an existing item then it insteads updates the values to reflect item. If inserting a list of items then it is ran in a transaction

If you don't want fields to be excluded then you can pass a list of fields to exclude in.

# Using the person example we can show how to update
var jake = db.find(Person, sql"SELECT * WHERE name = 'Jake'")
jake.age = 100
# We have now updated Jake in the database to be 100 years old
db.upsert(jake)
# If we want other fields untouched then we can exclude them.
# This is handy if constructing the object yourself and not initialising all the fields.
# If we didn't exclude age then Jake would become 0 years old
db.upsert(Person(name: "Jake"), age)

Note: This checks for conflicts on primary keys only and so won't work if your object has no primary keys
Source   Edit  

Templates

template transaction(db; body: untyped)
Runs the body in a transaction. If any error happens then it rolls back the transaction Source   Edit