This doesn't work, but it's in the right direction

This commit is contained in:
Simon Brooke 2018-03-07 13:46:55 +00:00
parent 0be87038f0
commit 8af1ceaa38

View file

@ -1,7 +1,8 @@
(ns ^{:doc "A parser for SQL: generate Application Description Language." (ns ^{:doc "A parser for SQL: generate Application Description Language."
:author "Simon Brooke"} :author "Simon Brooke"}
squirrel-parse.to-adl squirrel-parse.to-adl
(:require [clojure.xml :refer [emit-element]] (:require [clojure.java.io :refer [file]]
[clojure.xml :refer [emit-element]]
[clj-time.core :refer [now]] [clj-time.core :refer [now]]
[clj-time.format :refer [formatters unparse]] [clj-time.format :refer [formatters unparse]]
[squirrel-parse.parser :refer [parse]] [squirrel-parse.parser :refer [parse]]
@ -42,27 +43,27 @@
(def sql-datatype-to-adl-datatype (def sql-datatype-to-adl-datatype
"Map to convert SQL datatypes to the nearest ADL equivalent." "Map to convert SQL datatypes to the nearest ADL equivalent."
{:DT-BIGINT :integer {:DT-BIGINT "integer"
:DT-BIGSERIAL :integer :DT-BIGSERIAL "integer"
:DT-BIT :integer :DT-BIT "integer"
:DT-BOOLEAN :boolean :DT-BOOLEAN "boolean"
:DT-BYTEA :unsupported :DT-BYTEA "unsupported"
:DT-DATE :date :DT-DATE "date"
:DT-DOUBLE-PRECISION :real :DT-DOUBLE-PRECISION "real"
:DT-FLOAT :real :DT-FLOAT "real"
:DT-INTEGER :integer :DT-INTEGER "integer"
:DT-MONEY :money :DT-MONEY "money"
:DT-NUMERIC :real :DT-NUMERIC "real"
:DT-REAL :real :DT-REAL "real"
:DT-SERIAL :integer :DT-SERIAL "integer"
:DT-TEXT :text :DT-TEXT "text"
:DT-CHAR :string :DT-CHAR "string"
:DT-CHARACTER :string :DT-CHARACTER "string"
:DT-CHARACTER-VARYING :string :DT-CHARACTER-VARYING "string"
:DT-VARCHAR :string :DT-VARCHAR "string"
:DT-TIME :string :DT-TIME "string"
:DT-TIMESTAMP :timestamp :DT-TIMESTAMP "timestamp"
:DT-INTERVAL :unsupported :DT-INTERVAL "unsupported"
}) })
(defn is-subtree-of-type? (defn is-subtree-of-type?
@ -136,12 +137,30 @@
(if (if
(is-subtree-of-type? (second column-spec) :COLUMN-SPEC) (is-subtree-of-type? (second column-spec) :COLUMN-SPEC)
(make-property (second column-spec)) (make-property (second column-spec))
(let [name (get-name column-spec)] (let [name (get-name column-spec)
size-spec (get-first-child-of-type column-spec :INT-VAL)
size (if size-spec (nth size-spec 1))
constraints (get-first-child-of-type column-spec :COLUMN-CONSTRAINTS)
required? (get-first-child-of-type constraints :NOT-NULL-CC)
default? (get-first-child-of-type constraints :DEFAULT-CC)
dflt-val (if default? (nth default? 2))]
{(keyword name) {(keyword name)
{:tag :property {:tag :property
:attrs :attrs
{:name (get-name column-spec) (merge
:type (get-column-datatype column-spec)}}}))) (if size {:size size} {})
(if required? {:required "true"} {})
(if default? {:default dflt-val})
{:name name
:column name
:type (get-column-datatype column-spec)})
:content
{:prompts
{:en-GB
{:tag :prompt
:attrs
{:prompt name
:local "en-GB"}}}}}})))
(defn make-entity-map [table-decl] (defn make-entity-map [table-decl]
@ -150,7 +169,8 @@
:attrs :attrs
{:name (get-name table-decl)} {:name (get-name table-decl)}
:content :content
{:properties {:key {:content {}}
:properties
(apply (apply
merge merge
(map (map
@ -177,6 +197,8 @@
(defn is-column-constraint-statement-of-type? (defn is-column-constraint-statement-of-type?
"Returns non-nil (actually the relevant fragment) if `statement` is an
'alter table... add column constraint' statement with the specified `key`"
[statement key] [statement key]
(and (and
(is-subtree-of-type? statement :ALTER-TABLE) (is-subtree-of-type? statement :ALTER-TABLE)
@ -192,8 +214,7 @@
(:COLUMN-CONSTRAINT (:COLUMN-CONSTRAINT
(:ADD-CONSTRAINT (:ADD-CONSTRAINT
(:ALTER-TABLE-ELEMENTS (:ALTER-TABLE-ELEMENTS
(:ALTER-TABLE sm)))))) (:ALTER-TABLE sm))))))))))
))))
(defn is-foreign-key-statement? (defn is-foreign-key-statement?
@ -237,25 +258,37 @@
(defn decorate-with-primary-key (defn decorate-with-primary-key
"If this `statement` is a primary key statement, return an entity-map like this `entity-map` "If this `statement` is a primary key statement, return an entity-map like this `entity-map`
but with the relevant property removed from the 'content' sub-element and added to the 'key' but with the relevant property moved into 'keys'."
sub-element."
[entity-map statement] [entity-map statement]
(if (if
(is-primary-key-statement? statement) (is-primary-key-statement? statement)
(let [sm (subtree-to-map (is-primary-key-statement? statement)) (let [sm (subtree-to-map (is-primary-key-statement? statement))
table (:name (:attrs entity-map)) em-table (:name (:attrs entity-map))
st-table (:NAME (:QUAL-NAME (:ALTER-TABLE (subtree-to-map statement)))) st-table (:NAME (:QUAL-NAME (:ALTER-TABLE (subtree-to-map statement))))
col (keyword (:NAME (:NAMES (:INDEX-PARAMS sm)))) col (keyword (:NAME (:NAMES (:INDEX-PARAMS sm))))
properties (:properties (:content entity-map))] properties (:properties (:content entity-map))
property (col properties)
remaining-properties (dissoc properties col)
pk (merge
(:content (:key (:content entity-map)))
{:content
{col
(merge
property
{:attrs
(merge (:attrs property)
{:distinct "system" :immutable "true" :required "true"})})}})]
(if (if
(= table st-table) (= em-table st-table)
(deep-merge (merge
entity-map entity-map
{:content {:content
{:properties {:key pk
{col {:tag :key}}}}))))) :properties remaining-properties }})))))
;; (merge electors {:content (merge (:content electors) {:properties (dissoc (:properties (:content electors)) :id :email)})})
(defn decorate-with-all (defn decorate-with-all
"Apply this `function` to this `entity-map` and each of these statements "Apply this `function` to this `entity-map` and each of these statements
in sequence, and return a merge of the map with each of the statements in sequence, and return a merge of the map with each of the statements
@ -307,6 +340,17 @@
(map (map
to-adl-xml to-adl-xml
(vals (:properties (:content object))))}) (vals (:properties (:content object))))})
(:property :key)
(merge
object
{:content
(map
to-adl-xml
(apply
concat
(map
#(vals (% (:content object)))
'(:permissions :options :prompts :helps :ifmissings))))})
(apply assoc (cons {} (interleave (keys object) (map to-adl-xml (vals object)))))) (apply assoc (cons {} (interleave (keys object) (map to-adl-xml (vals object))))))
true true
object)) object))
@ -329,3 +373,36 @@
(let [adl (to-adl input application-name version)] (let [adl (to-adl input application-name version)]
(spit output (str xml-header "\n" (with-out-str (emit-element adl)))) (spit output (str xml-header "\n" (with-out-str (emit-element adl))))
adl))) adl)))
(defn migrations-to-xml
"As above, but for all 'up' migrations in the migrations directory specified by
`migrations-path`. Writes XML to `output`, but returns, instead of the serialisable XML
structure, the intermediate mappy structure, because that is more tractable in Clojure."
([migrations-path application-name]
(migrations-to-xml migrations-path application-name (unparse (formatters :basic-date) (now))))
([migrations-path application-name version]
(migrations-to-xml migrations-path application-name version nil))
([migrations-path application-name version output]
(let
[filenames
(filter
#(re-matches #".*\.up\.sql" %)
(map
#(.getAbsolutePath %)
(filter
#(.isFile %)
(file-seq (file migrations-path)))))
statements (simplify
(apply concat (map #(parse (slurp %)) filenames)))
entities
(table-definitions-to-entities
statements)
adl {:tag :application
:attrs {:name application-name
:version version }
:content (to-adl-xml (vals entities))}]
(if
output
(spit output (str xml-header "\n" (with-out-str (emit-element adl)))))
entities)))