fk-gen0.3.3-alphaGenerates insert statements for a postgres table and all its foreign key dependencies. dependencies
| (this space intentionally left almost blank) | ||||||||||||||||||
Value propositionYou should only have to care about the data in the database that you ... care about. But often, you have to care about more because the database has some constraints that you have to meet. This library handles some of those constraints for you by generating data that satisfies them. | |||||||||||||||||||
Introducing "Foreign Key Generator" or fk-gen for shortThis library helps generate sql statments that fulfill two constraints
| |||||||||||||||||||
Using the library | |||||||||||||||||||
It does this with only a little help from you. Mainly you have to tell it:
Don't worry about the third point (the function) will provide one of those for you. | |||||||||||||||||||
ExampleLets say you have a database with two tables
| |||||||||||||||||||
where a dog has to have a owner. A constraint that is enforced through a foreign key dependency dogs -> persons | |||||||||||||||||||
When you use this library you can expect to get sql that full fills this constraint. E.g a list of sql insert statments like... | |||||||||||||||||||
| |||||||||||||||||||
The clojure code for doing this looks like you might expect: | |||||||||||||||||||
Contains all the functionality to get and transform the foreign key dependencies
Inserts sql insert statements that full fill all the foreign key dependencies of the table. I suggest jumping to either the test (core_test.clj) for a working example or the fk-gen.generate namespace for the public facing functionality | (ns fk-gen.core (:require [clojure.set :as set] [hugsql.core :as hugsql] [com.rpl.specter :refer [transform MAP-VALS ALL]])) | ||||||||||||||||||
Understanding how the library worksWhat follows is a overview of how this library works internally and so can be happily ignored if your just a consumer of the functionality. | |||||||||||||||||||
Get the foreign key dependenciesThis is made possible by first extracting foreign key information from the database
through a sql function which we can call called | (hugsql/def-db-fns "fk-deps.sql") | ||||||||||||||||||
Transform foreign key dependencies into a graphOnce we have the foreign key dependencies we need to put them into a structure that is easy to traverse so we turn it into a graph. | (defn- fk-deps->graph [fk-deps] (reduce-kv (fn [c fk v] (assoc c fk (reduce (fn [x {:keys [fk-table pk-table fk-column pk-column]}] (assoc x pk-table {:fk-column fk-column :pk-column pk-column})) {} v))) {} (group-by :fk-table (transform [ALL MAP-VALS] keyword fk-deps)))) | ||||||||||||||||||
Transform the graph into our sql insert statmentsGiven the graph with the structure like:
| |||||||||||||||||||
we can do a depth first search walk on it and on each node (table) use our table-graph->sql->insert-stmt-plan function which takes the current table and the graph and produces a sql insert statments | |||||||||||||||||||
Sense these sql insert statments are ordered by walking our dependency graph, we can simply reverse that ordering and insert them to full fill the database constraints. | (defn- graph->dfs-path ([n f g] (graph->dfs-path [n] f #{} g)) ([nxs f v g] (let [n (peek nxs) v (conj v n)] (when n (cons (f n g) (graph->dfs-path (filterv #(not (v %)) (concat (pop nxs) (keys (g n)))) f v g)))))) | ||||||||||||||||||
We wrap that functionality together into a side effect free function | (defn fk-deps->sql-plan [{:keys [table table-graph->insert-stmt-plan fk-deps]}] (->> fk-deps fk-deps->graph (graph->dfs-path table table-graph->insert-stmt-plan) reverse)) | ||||||||||||||||||
GeneratorsThese functions generate SQL insert statements in honeySQL format, regular SQL and also just insert the generated data for you. This namespace brings together all the functionality and is here users of library should go for public facing functions. | (ns fk-gen.generate (:require [fk-gen.core :refer [get-fk-deps fk-deps->sql-plan]] [clojure.spec.alpha :as s] [table-spec.core :as t] [honeysql.core :as sql] [clojure.java.jdbc :as jdbc] [fk-gen.table-graph-to-sql :refer [->select-any ->insert]] [com.rpl.specter :refer [transform MAP-KEYS ALL]] [clojure.spec.gen.alpha :as gen])) | ||||||||||||||||||
Returns a vector of sql insert statement (honeysql format) necessary to fulfill all the foreign key constraints of the given table
| (defn- ->sql-plan [{:keys [db-info table table-graph->insert-stmt-plan]}] (fk-deps->sql-plan {:table table :table-graph->insert-stmt-plan table-graph->insert-stmt-plan :fk-deps (get-fk-deps db-info)})) | ||||||||||||||||||
Returns a collection of sql insert Statements that full fill all the foreign key dependencies of the table
| (defn ->sql [{:keys [db-info table] :as db-args}] (let [db-info->connection-uri (fn [{:keys [subprotocol subname user schema password]}] {:connection-uri (str "jdbc:" subprotocol ":" subname "?user=" user "&password=" password) :schema schema}) gen-values (fn [table] (transform [MAP-KEYS] #(keyword (name %)) (last (gen/sample (s/gen (keyword (str "table/" (name table))) 30))))) table-graph->insert-stmt-plan (partial ->insert (fn [table graph] [(merge (gen-values table) (->select-any table graph))]))] (do (-> (db-info->connection-uri db-info) (t/tables) (t/register)) (map sql/format (flatten (->sql-plan (assoc db-args :table-graph->insert-stmt-plan table-graph->insert-stmt-plan))))))) | ||||||||||||||||||
Inserts sql insert statements that full fill all the foreign key dependencies of the table
| (defn ->sql-and-insert! [{:keys [db-info table] :as db-args}] (->> (->sql db-args) (run! #(jdbc/execute! db-info %)))) | ||||||||||||||||||
Overcoming the constraintson the last leg of our journey we need to find a way to actually create sql insert statments.
What follows will describe how to do this in general while creating some functions you can use to get
you started or serve as a default. We call the function that produces these insert statements: | |||||||||||||||||||
The functions in this namespace can be used to build a
| (ns fk-gen.table-graph-to-sql (:require [honeysql.helpers :refer [insert-into values]])) | ||||||||||||||||||
One sensible way to implement the table-graph->insert-stmt-plan fn is to generate a sql select statement (honeySQL format) that will be used to create a value that meets the foreign key dependency constraint. This function is used to achieve just that. | |||||||||||||||||||
Returns a vector of select any sql statements | (defn ->select-any [table graph] (reduce-kv (fn [values pk-table {:keys [fk-column pk-column]}] (assoc values fk-column {:select [pk-column] :from [pk-table] :limit 1})) {} (graph table))) | ||||||||||||||||||
Now that you have a way to generate select any statements you still need a way to create the actual sql insert statments.
thats where | |||||||||||||||||||
returns a vector of insert sql statements | (defn ->insert [table->values table graph] (-> (insert-into table) (values (table->values table graph)))) | ||||||||||||||||||
If your paying attention you have probably noticed we never showed how to generate values that fullfil the data type constraint. Thats because most of that is done by another library called table-spec. Examples of how to use the functionality in fk-gen can be seen in fk-gen-example | |||||||||||||||||||