-
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-schemashould 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
:fixturesby mergingdnj/configwith some project-specific values.dnj/configincludes the keys:insert,:get-connection, and:close-connection.-
:insertis a function used to actually insert a generated record in a database. See Writing your own integration below for more details. -
:get-connectionis a function that takes the potato-db as its argument and that should return a JDBC connection. The function provided indnj/configis:(fn next-jdbc-get-connection [potato-db] (jdbc/get-connection (get-in potato-db [:fixtures :dbspec])))
The
with-fixturesmacro binds thedc/*connection*dynamic var to the value returned by:get-connection. -
If
:close-connectionis set, it gets called after everything else in the body ofdc/with-fixtures
-
-
The
:fixturesmap should include a:dbspeckey. this is passed directly tonext.jdbc/get-connection. Check out next-jdbc's Datasources and Connections docs for more info on the keys that:dbspeccan take. -
The
:setupfunction is called when you use thedc/with-fixturesmacro 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.client.api :as d]
[donut.datapotato.core :as dc]
[donut.datapotato.datomic :as dd]
[matcher-combinators.test] ;; optionalThen define a potato-db:
;; <1>
(def client (d/client {:server-type :datomic-local
:storage-dir :mem
:system "dev"}))
(def db-arg-map {:db-name "datapotato-test"})
(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 client db-arg-map)
(d/create-database client db-arg-map)
(d/connect client db-arg-map))
: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-schemashould 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/insertto insert records -
:get-connectionis a function that takes the potato-db as its argument and that should return a datomic connection. Thewith-fixturesmacro binds thedc/*connection*dynamic var to the value returned by:get-connection. - You can write a
:setupfunction 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-schemashould 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/insertto insert records -
:get-connectionis a function that takes the potato-db as its argument and that should return an XTDB node. Thewith-fixturesmacro 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:connectionkey gets populated by thewith-fixturesmacro. -
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.