Generic Database Interface
(import :std/db/dbi)
sql-connect
First things first we must connect to a database. Though the actual function to
connect differs between databases, sql-connect
also makes a will
so that the
connection is automagically closed using sql-close
when garbage collected.
(import :std/db/postgresql)
(def db (sql-connect postgresql-connect host: "localhost" user: "foo" passwd: "bar"))
;; => #<postgresql-connection #36>
connection?
This predicate asks if the thing passed is in fact a connection.
(connection? db) ;; => #t
(connection? car) ;; => #f
sql-eval
Often when interacting with the DB we do not actually need a result.
sql-eval
returns unspecified when run. In fact, three letters out of four,
(C)reate, (U)pdate and (D)elete, often need nothing at all.
(sql-eval db "CREATE TEMPORARY TABLE foo (bar text)") ;; => #!void
(sql-eval db "INSERT INTO foo VALUES ('huh?')") ;; => #!void
(sql-eval db "UPDATE foo SET bar = 'bar' WHERE bar = 'huh?'") ;; => #!void
(sql-eval db "DELETE FROM foo WHERE bar = 'bar'") ;; => #!void
It can take arguments.
(sql-eval db "INSERT INTO foo VALUES ($1)" "yay!") ;; => #!void
sql-eval-query
The R in CRUD is likely what is most often used.
(sql-eval-query db "SELECT * from foo") ;; => ("yay!")
Of course SELECT
is not the only query that returns things. It can take
arguments as well.
(sql-eval-query
db "INSERT INTO foo
VALUES ('huh?') returning foo.*") ;; => ("huh?")
(sql-eval-query
db "INSERT INTO foo
VALUES ('huh?, again?') returning foo.*") ;; => ("huh?, again?")
(sql-eval-query db "WITH q AS (
UPDATE foo SET bar = 'bar'
WHERE bar != 'huh?' returning true)
SELECT count(*) FROM q") ;; => (2)
(sql-eval-query db "DELETE FROM foo returning foo.*")
;; => ("huh?" "bar" "bar")
sql-prepare
Often an evaluation of a query is not enough.
There’s the simple matter of column names as we only return a list of results.
We may want to pass arguments.
For that purpose there’s a prepared statement. They are willed to run
sql-finalize
before taking out the trash.
(def istmt (sql-prepare db "INSERT INTO foo VALUES ('bar') RETURNING foo.*;"))
(def bind-istmt (sql-prepare db "INSERT INTO foo VALUES ($1) returning 'hahaha' AS baz"))
(def stmt (sql-prepare db "SELECT foo AS bat FROM foo"))
(def bind-stmt (sql-prepare db "SELECT true AS funnyColumn, * FROM foo WHERE bar = $1"))
statement?
Is this an SQL Statement?
(statement? istmt) ;; => #t
(statement? 'foobar) ;; =? #f
sql-columns
We’ve got some predicative-ly confirmed prepared statements. sql-columns
gives
us the column names.
(map sql-columns [ istmt bind-istmt stmt bind-stmt ])
;; => (("bar") ("baz") ("bat") ("funnycolumn" "bar"))
sql-exec
Like the name says this function executes a prepared statement from
sql-prepare
. Like sql-eval
, which in fact uses this after it prepares a
statement for you, it returns a useless value.
(sql-exec istmt)
A statement can be executed many times.
[ (sql-exec istmt) (sql-exec istmt) ] ;; => (#!void #!void)
(sql-eval-query db "SELECT * FROM foo")
("bar" "bar" "bar")
Unlike sql-eval
, sql-exec
does not take arguments.
(sql-exec bind-istmt "yay!")
; Evaluation aborted on Wrong number of arguments passed to procedure
; (std/db/dbi#sql-exec '#<postgresql-statement #77> "yay!")
Not only that, calling it with a statement that requires arguments is an error as well.
(sql-exec bind-istmt)
; Evaluation aborted on postgresql-exec!: [sql-error] bind message supplies 0
; parameters, but prepared statement "stmt539" requires 1 --- irritants: (S .
; ERROR) (V . ERROR) (C . 08P01) (M . bind message supplies 0 parameters, but
; prepared statement "stmt539" requires 1) (F . postgres.c) (L . 1665) (R .
; exec_bind_message)
That’s because arguments need to be bound with sql-bind
.
sql-query
Similar to sql-eval-query
sql-query
returns the results of the
sql-prepare
’d statement in list form.
(sql-query stmt) => ("(bar)" "(bar)" "(bar)")
Like sql-exec
it does not take arguments because they need to be bound with
sql-bind
.
in-sql-query
For more advanced uses in-sql-query
takes a statement and returns an iterator.
(import :std/iter)
(for ((r (in-sql-query stmt)))
(displayln r))
;(bar)
;(bar)
;(bar)
;; => #!void
Being able to take rows from the DB one at a time has some advantages, especially with reset, or even reset/clear.
sql-bind
For sql-prepare
’d statements that take arguments sql-bind
sets them to the
value before we run the statement.
(sql-bind bind-istmt "yay!") ;; => #!void
(sql-bind bind-stmt "yay!") ;; => #!void
It does not run it, just sets up the specified environment.
(sql-query stmt) ;; => ("(bar)" "(bar)" "(bar)")
To run it we of course use sql-exec
or sql-query
.
(sql-exec bind-istmt) ;; => #!void
(sql-query bind-stmt) ;; => (#(#t "yay!"))
A bound statement can run many times.
(sql-exec bind-istmt) ;; => #!void
(sql-query bind-stmt) ;; => (#(#t "yay!") #(#t "yay!"))
A statement can be rebound.
(sql-bind bind-istmt "huh?") ;; => ("hahaha")
(sql-query bind-istmt) ;; => #!void
(sql-query stmt)
;; => ("(bar)" "(bar)" "(bar)" "(yay!)" "(yay!)" "(huh?)")
It can also be cleared, reset, or even reset/clear’d.
sql-clear
Simply clear the local variables bound to a statement.
(sql-clear bind-stmt) ;; => #<postgresql-statement>
(sql-query bind-stmt)
; Evaluation aborted on postgresql-query!: [sql-error] bind message supplies 0
; parameters, but prepared statement "stmt149" requires 1 --- irritants: (S .
; ERROR) (V . ERROR) (C . 08P01) (M . bind message supplies 0 parameters, but
; prepared statement "stmt149" requires 1) (F . postgres.c) (L . 1665) (R .
; exec_bind_message)
sql-reset
When interacting with the DB a stored proc, after being run, has a reference to the results and can be considered “set”.
Imagine a statement that returns a certain amount of rows.
(for ((r (in-sql-query stmt)))
(displayln r))
;(bar)
;(bar)
;(bar)
;(yay!)
;(yay!)
;(huh?)
;; => #!void
For some reason we only want a few. We can so do so and reset it so the database knows we are done with it.
(def count 0)
(for ((r (in-sql-query stmt)))
(set! count (1+ count))
(displayln r count)
(when (= count 3) (sql-reset stmt)))
;(bar)1
;(bar)2
;(bar)3
;; => #!void
sql-reset/clear
This clears, and resets, a stored procedure.
(sql-query bind-stmt) ;; => (#(#t "yay!") #(#t "yay!"))
(for ((r (in-sql-query bind-stmt)))
(displayln r) (sql-reset/clear bind-stmt))
;#(#t yay!)
;; => #!void
(sql-query bind-stmt)
; Evaluation aborted on postgresql-query!: [sql-error] bind message supplies 0
; parameters, but prepared statement "stmt137" requires 1 --- irritants: (S .
; ERROR) (V . ERROR) (C . 08P01) (M . bind message supplies 0 parameters, but
; prepared statement "stmt137" requires 1) (F . postgres.c) (L . 1665) (R .
; exec_bind_message)
sql-finalize
When a stored procedure is finalized it is gone for good. While this is done by the garbage collection there may be times when we want to get rid of it now.
(map sql-finalize [stmt istmt bind-stmt bind-istmt])
;; => (#!void #!void #!void #!void)
(sql-query stmt)
; Evaluation aborted on Invalid operation; statement finalized
; #<postgresql-statement>
sql-txn-begin
Begins a transaction. See sql-error?
for a transaction if you do not know what
one is.
(sql-txn-begin db) ;; => #!void
sql-txn-commit
Commits a transaction if there’s one that can be commited. It errors if the transaction cannot be commited and otherwise, if there is no transaction, does nothing at all.
See sql-error?
for a transaction that can and cannot be commited.
(sql-txn-commit db) ;; => #!void
(sql-txn-commit db) ;; => #!void
(sql-txn-commit db) ;; => #!void
(sql-txn-commit db) ;; => #!void
(sql-txn-commit db) ;; => #!void
sql-txn-abort
Aborts a transaction. Sometimes we choose to abort, sometimes it is needed.
(sql-txn-abort db)
sql-error?
Is this error a database error or something else? This predicate tells us so.
Using it with try, we can for example we can make a connection not error if we have a transaction that error yet leave other errors thrown.
To start with, no catcher.
(sql-txn-begin db) ;; => #!void
(sql-eval-query db "SELECT 1") ;; => (1)
(sql-eval-query db "SELECT asd")
; Evaluation aborted on postgresql-prepare!: [sql-error] column "asd" does not
; exist --- irritants: (S . ERROR) (V . ERROR) (C . 42703) (M . column "asd"
; does not exist) (P . 8) (F . parse_relation.c) (L . 3349) (R .
; errorMissingColumn)
(sql-txn-commit db))
; Evaluation aborted on postgresql-exec!: [sql-error] current transaction is
; aborted, commands ignored until end of transaction block --- irritants: (S .
; ERROR) (V . ERROR) (C . 25P02) (M . current transaction is aborted, commands
; ignored until end of transaction block) (F . postgres.c) (L . 1682) (R .
; exec_bind_message)
(sql-eval-query db "SELECT 1")
; Evaluation aborted on postgresql-prepare!: [sql-error] current transaction is
; aborted, commands ignored until end of transaction block --- irritants: (S .
; ERROR) (V . ERROR) (C . 25P02) (M . current transaction is aborted, commands
; ignored until end of transaction block) (F . postgres.c) (L . 1424) (R .
; exec_parse_message)
Now a better try
.
(import :std/sugar)
(try
(sql-txn-begin db)
(sql-eval-query db "SELECT 1")
(sql-eval-query db "SELECT asd")
(catch sql-error? => (lambda _ (sql-txn-abort db)))
(finally (sql-txn-commit db))) ;; => #!void
(sql-eval-query db "SELECT 1") ;; => (1)
sql-close
Close a database connection.
(sql-eval-query db "SELECT 1") ;; => (1)
(sql-close db) ;; => #!void
(sql-eval-query db "SELECT 1")
; Evaluation aborted on Invalid operation; connection closed
; #<postgresql-connection>
(sql-close db) ;; => #!void
(sql-close db) ;; => #!void