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
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 delete[T: SomeTable](db; item: T)
- Tries to delete item from table. Does nothing if it doesn't exist Source Edit
proc exists[T: SomeTable](db; item: T): bool
- Returns true if item already exists in the database Source Edit
proc explain(db; query: SqlQuery): string {....raises: [DbError], tags: [ReadDbEffect].}
- Returns the query plan for a query 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 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
proc startTransaction(db) {....raises: [DbError], tags: [ReadDbEffect, WriteDbEffect].}
- Starts a transaction context Source Edit
Macros
macro create(db; tables: varargs[typed])
-
Creates multiple classes at once
- See [create(db, table)]
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: untyped; 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 keysSource 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