fk-gen

0.3.3-alpha


Generates insert statements for a postgres table and all its foreign key dependencies.

dependencies

org.clojure/clojure
1.9.0
com.layerware/hugsql
0.4.8
drewverlee/table-spec
0.1.3
nilenso/honeysql-postgres
0.2.3
com.rpl/specter
1.1.1
org.clojure/test.check
0.9.0



(this space intentionally left almost blank)
 

Value proposition

You 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 short

This library helps generate sql statments that fulfill two constraints

  1. data types : e.g column's values are of type int
  2. foreign key : column's values match another value in another table's column

Using the library

It does this with only a little help from you. Mainly you have to tell it:

  1. How to connect to the database. So host, port, etc..
  2. Which table you wish to generate insert statments for.
  3. A function for how to handle the foreign key dependencies

Don't worry about the third point (the function) will provide one of those for you.

Example

Lets say you have a database with two tables

  1. dogs
  2. persons

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...

  1. "insert into persons (id) values (101)"
  2. "insert into dogs (id, person) values (1, 101)"

The clojure code for doing this looks like you might expect:

Contains all the functionality to get and transform the foreign key dependencies


(->sql-and-insert! {:table :dogs 
                    :db-info db-info)

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 works

What 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 dependencies

This is made possible by first extracting foreign key information from the database through a sql function which we can call called get-fk-deps which is brought into our namespace here

(hugsql/def-db-fns "fk-deps.sql")

Transform foreign key dependencies into a graph

Once 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 statments

Given the graph with the structure like:


{:dogs {:persons {:fk-column :owner 
                  :pk-column :id}}}

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))
 

Generators

These 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 table :keyword : the name of the table db-info :hashmap : a map describing the database connection information see https://github.com/clojure/java.jdbc. table-graph->insert-stmt-plan :fn : a two arity fn [table,graph] that returns a vector of honeysql formatted insert statements

(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 table :keyword : the name of the table db-info :hashmap : a map describing the database connection information see https://github.com/clojure/java.jdbc.

(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 table :keyword : the name of the table db-info :hashmap : a map describing the database connection information see https://github.com/clojure/java.jdbc.

(defn ->sql-and-insert!
  [{:keys [db-info table] :as db-args}]
  (->> (->sql db-args)
       (run! #(jdbc/execute! db-info %))))
 

Overcoming the constraints

on 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: table-graph->insert-stmt-plan

The functions in this namespace can be used to build a table-graph->insert-stmt-plan function which fulfills its contract of taking a graph and table and returning a vector of honeySQL insert statements.

(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 ->insert comes in. Which is really just a honeySQL code.

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