ponairi

Search:
  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 ndb, 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 ndb 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) {....raises: [DbError], tags: [ReadDbEffect, WriteDbEffect].}
Commits a transaction   Source   Edit
proc create[T: SomeTable](db; 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: [].}
  Source   Edit
proc dbValue(d: DateTime): DbValue {....raises: [], tags: [].}
  Source   Edit
func dbValue(e: enum): DbValue
  Source   Edit
proc dbValue(t: Time): DbValue {....raises: [], tags: [].}
  Source   Edit
proc delete[T: SomeTable](db; item: T)
Tries to delete item from table. Does nothing if it doesn't exist   Source   Edit
proc drop[T: object](db; table: typedesc[T])
Drops a table from the database   Source   Edit
proc exists[T: SomeTable](db; item: T): bool
Returns true if item already exists in the database   Source   Edit
proc find[T: SomeTable | tuple](db; table: typedesc[seq[T]]; query: SqlQuery;
                                args): seq[T]
  Source   Edit
proc find[T: SomeTable | tuple](db; table: typedesc[T]; query: SqlQuery; args): T
Returns first row that matches query   Source   Edit
proc find[T: SomeTable](db; 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; table: typedesc[seq[T]]): seq[T]
  Source   Edit
proc insert[T: SomeTable](db; item: T)
Inserts an object into the database   Source   Edit
proc insert[T: SomeTable](db; 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; 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].}
Sets up a new connection with needed configuration. File is just a normal sqlite file string   Source   Edit
proc rollback(db) {....raises: [DbError], tags: [ReadDbEffect, WriteDbEffect].}
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[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) {....raises: [DbError],
                            tags: [ReadDbEffect, WriteDbEffect].}
Starts a transaction context   Source   Edit
func to(src: DbValue; dest: var bool) {.inline, ...raises: [], tags: [].}
  Source   Edit
proc to(src: DbValue; dest: var DateTime) {.inline, ...raises: [TimeParseError],
    tags: [TimeEffect].}
  Source   Edit
func to(src: DbValue; dest: var string) {.inline, ...raises: [], tags: [].}
  Source   Edit
func to(src: DbValue; dest: var Time) {.inline, ...raises: [], tags: [].}
  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
func to[T](src: DbValue; dest: var Option[T])
  Source   Edit
proc upsert[T: SomeTable](db; item: T)
Trys to insert an item into the database. If it conflicts with an existing item then it insteads updates the values to reflect item.
Note: This checks for conflicts on primary keys only and so won't work if your object has no primary keys
  Source   Edit
proc upsert[T: SomeTable](db; items: openArray[T])
Upsets a list of items into the database
  • See [upsert(db, item)]
  • See [insert(db, items)]
  Source   Edit

Iterators

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

Macros

macro create(db; tables: varargs[typed])
Creates multiple classes at once
  • See [create(db, table)]
  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