-
Notifications
You must be signed in to change notification settings - Fork 2
database integration
Datapotato is written in a database-agnostic way so that you can use it with your standard relational dbs, or something more esoteric like datomic or xtdb, or anything you want.
It comes with helper libraries that should handle the majority of use cases if you're using one of the following:
This doc covers using the helper libraries, as well as writing your own integration. It also shows how you can specify how to insert records per-ent-type and per-record. It assumes you've gone through the Getting Started doc.
This doc also assumes you've added the matcher combinators library to your dependencies. This isn't necessary for working with datapotato, but it's generally useful for testing random data.
Example namespace showing how you can use the next-jdbc helper in tests
To use the next-jdbc helper, first add the library
party.donut/datapotato-next-jdbc
to your dependencies. Its version number
should match datapotato's version number.
Next, you'll want to include the following namespaces in your test namespace:
[donut.datapotato.core :as dc]
[donut.datapotato.next-jdbc :as dnj]
[matcher-combinators.test] ;; optional
[next.jdbc :as jdbc]
[next.jdbc.sql :as sql]
Then define a potato-db
:
(def potato-db
{:schema potato-schema ;; <1>
:generate {:generator mg/generate} ;; <2>
:fixtures (merge dnj/config ;; <3>
{:dbspec {:dbtype "sqlite"
:connection-uri "jdbc:sqlite::memory:"} ;; <4>
:setup (fn [_]
(create-tables dc/*connection*))})}) ;; <5>
-
Your
potato-schema
should be defined. See Getting Started for an explanation of potato-schemas. -
See Getting Started for data generation config details
-
Here we're constructing the configuration for
:fixtures
by mergingdnj/config
with some project-specific values.dnj/config
includes the keys:insert
,:get-connection
, and:close-connection
.-
:insert
is a function used to actually insert a generated record in a database. See Writing your own integration below for more details. -
:get-connection
is a function that takes the potato-db as its argument and that should return a JDBC connection. The function provided indnj/config
is:(fn next-jdbc-get-connection [potato-db] (jdbc/get-connection (get-in potato-db [:fixtures :dbspec])))
The
with-fixtures
macro binds thedc/*connection*
dynamic var to the value returned by:get-connection
. -
If
:close-connection
is set, it gets called after everything else in the body ofdc/with-fixtures
-
-
The
:fixtures
map should include a:dbspec
key. this is passed directly tonext.jdbc/get-connection
. Check out next-jdbc's Datasources and Connections docs for more info on the keys that:dbspec
can take. -
The
:setup
function is called when you use thedc/with-fixtures
macro before anything in the macro body is evaluated. This is a good place to empty your database or do whatever tidying you need for your tests.
NOTE: If you use a popular database like postgres, mysql, or sqlite, this section probably isn't relevant to you. If things aren't working as expected, this is a good place to start.
Datapotato and its database-specific helper libraries have to handle a very specific use case: setting foreign key values based on the data that actually exists in the database. Datapotato must handle this scenario because sometimes the database is responsible for generating values, like when it autogenerates primary keys.
For example, if you're inserting a DreamJournal
that references a User
via
:created-by-id
, the DreamJournal
record's :created-by-id
field must be set
to the User
's actual :id
in the database. If you don't do this, then you'll
likely violate a foreign key constraint and encounter an exception.
To deal with this, the next-jdbc helper library defines the get-inserted
multimethod, which is used to retrieve a record after it has been inserted in
the database. The retrieved record is then used to set foreign keys.
get-inserted
is a multimethod because the method for retrieving a record after
insertion varies across database vendors (postgres, sqlite, etc).
The default method for get-inserted
will return the return value from
next.jdbc.sql/insert!
. For some databases, like postgres, this is the full
record that exists in the database.
Consult the
source
to see how get-inserted
is dispatched and what arguments it takes.
Now you're ready to use datapotato in tests:
(deftest inserts-simple-generated-data
(dc/with-fixtures potato-db
(dc/insert-fixtures {:user [{:count 2}]})
(is (match? [#:users{:id 1 :username string?}
#:users{:id 2 :username string?}]
(sql/query dc/*connection* ["SELECT * FROM users"])))))
Now you're cooking with potatoes!
To use the datomic helper, first add the library
party.donut/datapotato-datomic
to your dependencies. Its version number should
match datapotato's version number.
Next, you'll want to include the following namespaces in your test namespace:
[datomic.api :as d]
[donut.datapotato.core :as dc]
[donut.datapotato.datomic :as dd]
[matcher-combinators.test] ;; optional
Then define a potato-db
:
(def uri "datomic:mem://datapotato-test") ;; <1>
(def potato-db
{:schema schema ;; <2>
:generate {:generator mg/generate} ;; <3>
:fixtures {:insert
dd/insert ;; <4>
:get-connection ;; <5>
(fn get-connection [_]
(d/delete-database uri)
(d/create-database uri)
(d/connect uri))
:setup ;; <6>
(fn setup [{:keys [fixtures]}]
;; set up database
@(d/transact
dc/*connection*
[]))}})
- In this example we connect to an in-memory datomic database
- Your
potato-schema
should be defined. See Getting Started for an explanation of potato-schemas. - See Getting Started for data generation config details
- We call the helper
donut.datapotato.datomic/insert
to insert records -
:get-connection
is a function that takes the potato-db as its argument and that should return a datomic connection. Thewith-fixtures
macro binds thedc/*connection*
dynamic var to the value returned by:get-connection
. - You can write a
:setup
function to set up your datomic db.
Now you're ready to use datapotato in tests:
(defn q
"lil helper helpin out"
[connection query]
(->> (d/q query (d/db connection))
(map first)
(sort-by :db/id)))
(deftest inserts-simple-generated-data
(dc/with-fixtures potato-db
(dc/insert-fixtures {:user [{:count 2}]})
(is (match? [{:db/id 17592186045418 :user/username string?}
{:db/id 17592186045420 :user/username string?}]
(q dc/*connection*
'{:find [(pull ?u [*])]
:where [[?u :user/username]]})))))
To use the xtdb helper, first add the library party.donut/datapotato-xtdb
to your
dependencies. Its version number should match datapotato's version number.
Next, you'll want to include the following namespaces in your test namespace:
[donut.datapotato.core :as dc]
[donut.datapotato.xtdb :as dxtdb]
[matcher-combinators.test] ;; optional
[xtdb.api :as xt]
Then define a potato-db
:
(def node-atom (atom nil)) <1>
(def potato-db
{:schema potato-schema ;; <2>
:generate {:generator mg/generate} ;; <3>
:fixtures {:insert dxtdb/insert <4>
:get-connection (fn get-connection [_]
(when-let [node @node-atom]
(.close node))
(reset! node-atom (xt/start-node {}))
@node-atom)}}) ;; <5>
- This atom stores the connection to an xtdb node
- Your
potato-schema
should be defined. See Getting Started for an explanation of potato-schemas. - See Getting Started for data generation config details
- We call the helper
donut.datapotato.xtdb/insert
to insert records -
:get-connection
is a function that takes the potato-db as its argument and that should return an XTDB node. Thewith-fixtures
macro binds thedc/*connection*
dynamic var to the value returned by:get-connection
. Here, the "connection" is an XTDB in-memory node.
Now you're ready to use datapotato in tests:
(defn q
"query helper for tests"
[query]
(->> (xt/q (xt/db dc/*connection*) query)
(map first)
(sort-by :xt/id)))
(deftest inserts-simple-generated-data
(dc/with-fixtures potato-db
(dc/insert-fixtures {:user [{:count 2}]})
(is (match? [{:xt/id 1 :user/username string?}
{:xt/id 2 :user/username string?}]
(q '{:find [(pull ?u [*])]
:where [[?u :user/username]]})))))
Now you're cooking with potatoes!
Writing your own integration means configuring your potato-db so that
datapotato.core/with-fixtures
and datapotato.core/insert-fixtures
will work.
The with-fixtures
macro is responsible for:
- Calling the function under
[:fixtures :get-connection]
to get a connection and binding the connection todatapotato.core/*connection*
- Calling the function under
[:fixtures :setup]
insert-fixtures
relies on a function defined at [:fixtures :insert]
, calling
it for every record that needs to get inserted. It takes two arguments:
To do that, you need to add a map under the :fixtures
key of your potato-db
with the following keys:
-
potato-db
- the first argument is the potato-db itself. You can use this to look up values under:fixtures
, including a:connection
key gets populated by thewith-fixtures
macro. -
visit-data
- this is a map with the following keys:-
:visit-val
- this is the data to be inserted -
:query-opts
- explained more below -
:visit-visit-query-opts
- explained more below
-
For reference, here's how the datomic insert
function is defined:
(defn insert
[{{:keys [connection]} dc/fixtures-visit-key :as _potato-db}
{:keys [visit-val] :as _visit-data}]
(let [result @(d/transact connection [visit-val])
dbid (first (vals (:tempids result)))]
(assoc (d/pull (d/db connection) '[*] dbid)
:db/id dbid)))
:query-opts
and :visit-query-opts
allow you refine how your data is inserted
and generated. They refer to the values passed in to your datapotato query:
(dc/insert {:users {:fixtures {:option-a "whatever"}
:other-opts {:option-b "bloop"}}})
For the query above, :query-opts
would be the map
{:fixtures {:option-a "whatever"}
:other-opts {:option-b "bloop"}}
and :visit-query-opts
would be the map
{:option-a "whatever"}
If for some reason you need to use different databases per entity type or even
per entity, you can override the potato-db-wide configuration in your
potato-schema
and even in your queries. Here's what that could look like:
(def potato-db
{:schema {:user {:prefix :u
:generate {:schema User}
:fixtures {:insert custom-insert}}
:dream-journal {:prefix :dj
:generate {:schema DreamJournal}}
:entry {:prefix :e
:generate {:schema Entry}}}
:fixtures {:insert donut.datapotato.datomic/insert}})
(dc/with-fixtures
(dc/insert {:entry [{:fixtures {:insert custom-dream-journal-insert}}]}))
In this example, a custom insert is defined for users at the potato-schema level, and a custom insert function is defined for entries at the query level.