This package is experimental. I am probably not qualified to write an ORM!
Rekor
is a greek noun for “record”
Rekor.el is an ORM (object relational mapper) for Emacs Lisp and SQLite.
It does:
- allow you to easily define db-backed model classes
- create/drop the tables backing model classes
- save model instances as rows in the database
- execute queries resulting in model instances
- everything in the most naive unoptimized way possible
It does not (yet?):
- generate or run migrations
- automatically resolve foreign-key relationships
- automatically safe-quote string-values
- defer or lazy evaluate anything
- support transactions
(defmodel person
(first-name string :not-null)
(last-name string :not-null)
(age integer :not-null :check (> age 0)))
(rekor:db:connect "/tmp/data.sql")
(rekor:db:migrate-all)
(let ((kant (person:new :first-name "Immanuel"
:last-name "Kant"
:age 296)))
(::first-name kant "Mack Daddy")
(rekor:save kant)
(dolist (obj (:? person (> :age 100)))
(with-slots (first-name last-name age) obj
(message "%s %s is %s years old." first-name last-name age))))
;; Mack Daddy Kant is 296 years old.
(el-get-bundle rekor
:url "https://github.com/apoptosis/rekor.el.git"
:features rekor)
(use-package rekor
:straight (rekor :type git :host github :repo "apoptosis/rekor.el")
Models are defined with the (defmodel MODEL-NAME &rest FIELDS)
macro:
(defmodel person
(first-name string :not-null)
(last-name string :not-null)
(age integer :not-null :check (> age 0)))
MODEL-NAME
is used to determine:
- the SQL table name
- the underlying EIEIO class
Each field form in FIELDS
is:
- a column in the SQL table
- an attribute of the EIEIO class
Field forms have the following structure:
(NAME TYPE &rest CONSTRAINTS)
NAME
is used to determine:
- column name in the SQL table
- attribute name in the EIEIO class
TYPE
can be any of the following:
- integer
- float
- number
- string
CONSTRAINTS
are passed directly to EmacSQL as column constraints.
Rekor maintains a single database cursor. To set it, call
(rekor:db:connect FILENAME)
with the filename of your database.
All subsequent calls to Rekor will use that database until rekor:db:connect
is
called with a different filename.
(rekor:db:connect "/tmp/data.sql")
Rekor doesn’t really support migrations. But it will create tables.
Call (rekor:db:migrate-all)
to create tables for any defined models.
During development it may be handy to drop the tables for all models.
Call (rekor:db:drop-all)
to do so.
Of course, if the only tables in the database are your Rekor models, you can also just delete the database file. :)
To introduce working with model objects we’ll use an example.
First, let’s create a person
model:
(defmodel person
(first-name string :not-null)
(last-name string :not-null)
(age integer :not-null :check (> age 0)))
The person
model has fields for first name, last name, and age. We’ve used some
column constraints which are used when constructing the underlying SQL
table. In this case, all three fields are constrained to be NOT NULL.
The age field additionally is constrained to only unsigned, or positive values.
Each call to defmodel
generates a corresponding constructor that can be used to
create instances of the model:
(setq person-obj ((person:new :first-name "Immanuel"
:last-name "Kant"
:age 296)))
A generic getter method is created for each field:
(:first-name person-obj) ; "Immanuel"
A generic setter method is created for each field:
(::first-name person-obj "Mack Daddy")
(format "%s %s" (:first-name person-obj)
(:last-name person-obj))
; "Mack Daddy Kant"
To save an instance to the database call (rekor:save OBJ)
.
(rekor:save person-obj)
(:? MODEL-NAME WHERE &rest VALUES)
can be used to search for existing objects in the
database. It returns a list of the results or nil.
The WHERE
clause is passed directly to EmacSQL as a where clause.
(dolist (obj (:? person (> age 100)))
(with-slots (first-name last-name age) obj
(message "%s %s is %s years old" first-name last-name age)))
;; "Mack Daddy Kant is 296 years old"
If the WHERE
clause contains templates, you can provide &rest VALUES
with
their values. This is necessary if you have the value in a variable:
(let ((minimum-age 100))
(dolist (obj (:? person (> age $s1) minimum-age))
(with-slots (first-name last-name age) obj
(message "%s %s is %s years old" first-name last-name age))))
;; "Mack Daddy Kant is 296 years old"