Much improved query generation.

This commit is contained in:
Simon Brooke 2018-05-13 17:49:15 +01:00
parent e3dfbb5343
commit 481743ff2d
2 changed files with 276 additions and 169 deletions

View file

@ -6,7 +6,7 @@
[clojure.string :as s]
[clj-time.core :as t]
[clj-time.format :as f]
[adl.utils :refer [has-non-key-properties? has-primary-key? link-table? key-names singularise]]))
[adl.utils :refer :all]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
@ -32,75 +32,96 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn where-clause [entity-map]
(def ^:dynamic *output-path*
"The path to which generated files will be written."
"resources/auto/")
(defn where-clause
"Generate an appropriate `where` clause for queries on this `entity`"
[entity]
(let
[entity-name (:name (:attrs entity-map))]
[entity-name (:name (:attrs entity))]
(str
"WHERE " entity-name "."
(s/join
(str " AND\n\t" entity-name ".")
(map #(str % " = " (keyword %)) (key-names entity-map))))))
(map #(str % " = " (keyword %)) (key-names entity))))))
(defn order-by-clause [entity-map]
(defn order-by-clause
"Generate an appropriate `order by` clause for queries on this `entity`"
[entity]
(let
[entity-name (:name (:attrs entity-map))
[entity-name (:name (:attrs entity))
preferred (map
#(:name (:attrs %))
(filter #(= (-> % :attrs :distinct) "user")
(-> entity-map :content :properties vals)))]
(filter #(#{"user" "all"} (-> % :attrs :distinct))
(children entity #(= (:tag %) :property))))]
(if
(empty? preferred)
""
(str
"ORDER BY " entity-name "."
(s/join
(str ",\n\t" entity-name ".")
(doall (flatten (cons preferred (key-names entity-map))))))))
(flatten (cons preferred (key-names entity))))))))
(defn insert-query [entity-map]
(let [entity-name (:name (:attrs entity-map))
(defn insert-query
"Generate an appropriate `insert` query for this `entity`.
TODO: this depends on the idea that system-unique properties
are not insertable, which is... dodgy."
[entity]
(let [entity-name (:name (:attrs entity))
pretty-name (singularise entity-name)
all-property-names (map #(:name (:attrs %)) (vals (:properties (:content entity-map))))
insertable-property-names (map
#(:name (:attrs %))
(filter
#(not (= (:distinct (:attrs %)) "system"))
(all-properties entity)))
query-name (str "create-" pretty-name "!")
signature " :! :n"]
(hash-map
(keyword query-name)
{:name query-name
:signature signature
:entity entity-map
:entity entity
:type :insert-1
:query
(str "-- :name " query-name " " signature "\n"
"-- :doc creates a new " pretty-name " record\n"
"INSERT INTO " entity-name " ("
(s/join ",\n\t" all-property-names)
(s/join ",\n\t" insertable-property-names)
")\nVALUES ("
(s/join ",\n\t" (map keyword all-property-names))
(s/join ",\n\t" (map keyword insertable-property-names))
")"
(if
(has-primary-key? entity-map)
(str "\nreturning " (s/join ",\n\t" (key-names entity-map))))
"\n\n")})))
(has-primary-key? entity)
(str "\nreturning " (s/join ",\n\t" (key-names entity)))))})))
(defn update-query [entity-map]
(defn update-query
"Generate an appropriate `update` query for this `entity`"
[entity]
(if
(and
(has-primary-key? entity-map)
(has-non-key-properties? entity-map))
(let [entity-name (:name (:attrs entity-map))
(has-primary-key? entity)
(has-non-key-properties? entity))
(let [entity-name (:name (:attrs entity))
pretty-name (singularise entity-name)
property-names (remove
nil?
(map
#(if (= (:tag %) :property) (:name (:attrs %)))
(vals (:properties (:content entity-map)))))
(vals (:properties (:content entity)))))
query-name (str "update-" pretty-name "!")
signature ":! :n"]
(hash-map
(keyword query-name)
{:name query-name
:signature signature
:entity entity-map
:entity entity
:type :update-1
:query
(str "-- :name " query-name " " signature "\n"
@ -109,19 +130,19 @@
"SET "
(s/join ",\n\t" (map #(str % " = " (keyword %)) property-names))
"\n"
(where-clause entity-map)
"\n\n")}))
(where-clause entity))}))
{}))
(defn search-query [entity-map]
(let [entity-name (:name (:attrs entity-map))
(defn search-query [entity]
"Generate an appropriate search query for this `entity`"
(let [entity-name (:name (:attrs entity))
pretty-name (singularise entity-name)
query-name (str "search-strings-" pretty-name)
signature ":? :1"
string-fields (filter
#(= (-> % :attrs :type) "string")
(-> entity-map :content :properties vals))]
(children entity #(= (:tag %) :property)))]
(if
(empty? string-fields)
{}
@ -129,30 +150,36 @@
(keyword query-name)
{:name query-name
:signature signature
:entity entity-map
:entity entity
:type :text-search
:query
(str "-- :name " query-name " " signature "\n"
"-- :doc selects existing " entity-name " records having any string field matching `:pattern` by substring match\n"
"SELECT * FROM " entity-name "\n"
(s/join
"\n"
(remove
empty?
(list
(str "-- :name " query-name " " signature)
(str
"-- :doc selects existing "
pretty-name
" records having any string field matching `:pattern` by substring match")
(str "SELECT * FROM " entity-name)
"WHERE "
(s/join
"\n\tOR "
(map
#(str (-> % :attrs :name) " LIKE '%:pattern%'")
string-fields))
"\n"
(order-by-clause entity-map)
"\n"
"--~ (if (:offset params) \"OFFSET :offset \") \n"
"--~ (if (:limit params) \"LIMIT :limit\" \"LIMIT 100\")"
"\n\n")}))))
(order-by-clause entity)
"--~ (if (:offset params) \"OFFSET :offset \")"
"--~ (if (:limit params) \"LIMIT :limit\" \"LIMIT 100\")")))}))))
(defn select-query [entity-map]
(defn select-query [entity]
"Generate an appropriate `select` query for this `entity`"
(if
(has-primary-key? entity-map)
(let [entity-name (:name (:attrs entity-map))
(has-primary-key? entity)
(let [entity-name (:name (:attrs entity))
pretty-name (singularise entity-name)
query-name (str "get-" pretty-name)
signature ":? :1"]
@ -160,25 +187,28 @@
(keyword query-name)
{:name query-name
:signature signature
:entity entity-map
:entity entity
:type :select-1
:query
(str "-- :name " query-name " " signature "\n"
"-- :doc selects an existing " pretty-name " record\n"
"SELECT * FROM " entity-name "\n"
(where-clause entity-map)
(s/join
"\n"
(order-by-clause entity-map)
"\n\n")}))
(remove
empty?
(list
(str "-- :name " query-name " " signature)
(str "-- :doc selects an existing " pretty-name " record")
(str "SELECT * FROM " entity-name)
(where-clause entity)
(order-by-clause entity))))}))
{}))
(defn list-query
"Generate a query to list records in the table represented by this `entity-map`.
"Generate a query to list records in the table represented by this `entity`.
Parameters `:limit` and `:offset` may be supplied. If not present limit defaults
to 100 and offset to 0."
[entity-map]
(let [entity-name (:name (:attrs entity-map))
[entity]
(let [entity-name (:name (:attrs entity))
pretty-name (singularise entity-name)
query-name (str "list-" entity-name)
signature ":? :*"]
@ -186,28 +216,40 @@
(keyword query-name)
{:name query-name
:signature signature
:entity entity-map
:entity entity
:type :select-many
:query
(str "-- :name " query-name " " signature "\n"
"-- :doc lists all existing " pretty-name " records\n"
"SELECT * FROM " entity-name "\n"
(order-by-clause entity-map) "\n"
"--~ (if (:offset params) \"OFFSET :offset \") \n"
"--~ (if (:limit params) \"LIMIT :limit\" \"LIMIT 100\")"
"\n\n")})))
(s/join
"\n"
(remove
empty?
(list
(str "-- :name " query-name " " signature)
(str "-- :doc lists all existing " pretty-name " records")
(str "SELECT * FROM " entity-name)
(order-by-clause entity)
"--~ (if (:offset params) \"OFFSET :offset \")"
"--~ (if (:limit params) \"LIMIT :limit\" \"LIMIT 100\")")))})))
(defn foreign-queries [entity-map entities-map]
(let [entity-name (:name (:attrs entity-map))
(defn foreign-queries
[entity application]
(let [entity-name (:name (:attrs entity))
pretty-name (singularise entity-name)
links (filter #(-> % :attrs :entity) (-> entity-map :content :properties vals))]
links (filter #(-> % :attrs :entity) (children entity #(= (:tag %) :property)))]
(apply
merge
(map
#(let [far-name (-> % :attrs :entity)
far-entity ((keyword far-name) entities-map)
pretty-far (s/replace (s/replace far-name #"_" "-") #"s$" "")
far-entity (first
(children
application
(fn [x]
(and
(= (:tag x) :entity)
(= (:name (:attrs x)) far-name)))))
pretty-far (singularise far-name)
farkey (-> % :attrs :farkey)
link-field (-> % :attrs :name)
query-name (str "list-" entity-name "-by-" pretty-far)
@ -216,20 +258,33 @@
(keyword query-name)
{:name query-name
:signature signature
:entity entity-map
:entity entity
:type :select-one-to-many
:far-entity far-entity
:query
(str "-- :name " query-name " " signature "\n"
"-- :doc lists all existing " pretty-name " records related to a given " pretty-far "\n"
"SELECT * \nFROM " entity-name "\n"
"WHERE " entity-name "." link-field " = :id\n"
(order-by-clause entity-map)
"\n\n")}))
(s/join
"\n"
(remove
empty?
(list
(str "-- :name " query-name " " signature)
(str "-- :doc lists all existing " pretty-name " records related to a given " pretty-far)
(str "SELECT * \nFROM " entity-name)
(str "WHERE " entity-name "." link-field " = :id")
(order-by-clause entity))))}))
links))))
(defn link-table-query [near link far]
(defn link-table-query
"Generate a query which links across the entity passed as `link`
from the entity passed as `near` to the entity passed as `far`.
TODO: not working?"
[near link far]
(if
(and
(entity? near)
(entity? link)
(entity? far))
(let [properties (-> link :content :properties vals)
links (apply
merge
@ -251,36 +306,55 @@
:near-entity near
:far-entity far
:query
(str "-- :name " query-name " " signature " \n"
"-- :doc lists all existing " near-name " records related through " link-name " to a given " pretty-far "\n"
"SELECT "near-name ".*\n"
"FROM " near-name ", " link-name "\n"
"WHERE " near-name "." (first (key-names near)) " = " link-name "." (-> (links (keyword near-name)) :attrs :name) "\n\t"
"AND " link-name "." (-> (links (keyword far-name)) :attrs :name) " = :id\n"
(order-by-clause near)
"\n\n")})))
(s/join
"\n"
(remove
empty?
(list
(str "-- :name " query-name " " signature)
(str "-- :doc lists all existing " near-name " records related through " link-name " to a given " pretty-far )
(str "SELECT "near-name ".*")
(str "FROM " near-name ", " link-name )
(str "WHERE " near-name "." (first (key-names near)) " = " link-name "." (-> (links (keyword near-name)) :attrs :name) )
("\tAND " link-name "." (-> (links (keyword far-name)) :attrs :name) " = :id")
(order-by-clause near))))}))))
(defn link-table-queries [entity-map entities-map]
(defn link-table-queries [entity application]
"Generate all the link queries in this `application` which link via this `entity`."
(let
[entities (map
#((keyword %) entities-map)
(remove nil? (map #(-> % :attrs :entity) (-> entity-map :content :properties vals))))
;; find the far-side entities
(fn
[far-name]
(children
application
(fn [x]
(and
(= (:tag x) :entity)
(= (:name (:attrs x)) far-name)))))
;; of those properties of this `entity` which are of type `entity`
(remove
nil?
(map
#(-> % :attrs :entity)
(children entity #(= (:tag %) :property)))))
pairs (combinations entities 2)]
(apply
merge
(map
#(merge
(link-table-query (nth % 0) entity-map (nth % 1))
(link-table-query (nth % 1) entity-map (nth % 0)))
(link-table-query (nth % 0) entity (nth % 1))
(link-table-query (nth % 1) entity (nth % 0)))
pairs))))
(defn delete-query [entity-map]
(defn delete-query [entity]
"Generate an appropriate `delete` query for this `entity`"
(if
(has-primary-key? entity-map)
(let [entity-name (:name (:attrs entity-map))
(has-primary-key? entity)
(let [entity-name (:name (:attrs entity))
pretty-name (singularise entity-name)
query-name (str "delete-" pretty-name "!")
signature ":! :n"]
@ -288,57 +362,58 @@
(keyword query-name)
{:name query-name
:signature signature
:entity entity-map
:entity entity
:type :delete-1
:query
(str "-- :name " query-name " " signature "\n"
"-- :doc updates an existing " pretty-name " record\n"
"DELETE FROM " entity-name "\n"
(where-clause entity-map)
"\n\n")}))))
(where-clause entity))}))))
(defn queries
[entity-map entities-map]
"Generate all standard queries for this `entity` in this `application`."
[entity application]
(merge
{}
(insert-query entity-map)
(update-query entity-map)
(delete-query entity-map)
(insert-query entity)
(update-query entity)
(delete-query entity)
(if
(link-table? entity-map)
(link-table-queries entity-map entities-map)
(link-table? entity)
(link-table-queries entity application)
(merge
(select-query entity-map)
(list-query entity-map)
(search-query entity-map)
(foreign-queries entity-map entities-map)))))
(select-query entity)
(list-query entity)
(search-query entity)
(foreign-queries entity application)))))
;; (defn migrations-to-queries-sql
;; ([migrations-path]
;; (migrations-to-queries-sql migrations-path "queries.auto.sql"))
;; ([migrations-path output]
;; (let
;; [adl-struct (migrations-to-xml migrations-path "Ignored")
;; file-content (apply
;; str
;; (cons
;; (str "-- "
;; output
;; " autogenerated by \n-- [squirrel-parse](https://github.com/simon-brooke/squirrel-parse)\n-- at "
;; (f/unparse (f/formatters :basic-date-time) (t/now))
;; "\n\n")
;; (doall
;; (map
;; #(:query %)
;; (sort
;; #(compare (:name %1) (:name %2))
;; (vals
;; (apply
;; merge
;; (map
;; #(queries % adl-struct)
;; (vals adl-struct)))))))))]
;; (spit output file-content)
;; file-content)))
(defn to-hugsql-queries
"Generate all [HugSQL](https://www.hugsql.org/) queries implied by this ADL `application` spec."
[application]
(spit
(str *output-path* "queries.sql")
(s/join
"\n\n"
(cons
(s/join
"\n-- "
(list
"-- File queries.sql"
"autogenerated by adl.to-hugsql-queries at"
(t/now)
"See [Application Description Language](https://github.com/simon-brooke/adl).\n\n"))
(map
#(:query %)
(sort
#(compare (:name %1) (:name %2))
(vals
(apply
merge
(map
#(queries % application)
(children
application
(fn [child] (= (:tag child) :entity))))))))))))

View file

@ -117,6 +117,12 @@
(= (:distinct (:attrs property)) "system"))))
(defn entity?
"Return true if `x` is an ADL entity."
[x]
(= (:tag x) :entity))
(defn visible-to
"Return a list of names of groups to which are granted read access,
given these `permissions`, else nil."
@ -134,7 +140,13 @@
(defn singularise
"Attempt to construct an idiomatic English-language singular of this string."
[string]
(s/replace (s/replace (s/replace string #"_" "-") #"s$" "") #"ie$" "y"))
(s/replace
(s/replace
(s/replace
(s/replace string #"_" "-")
#"s$" "")
#"se$" "s")
#"ie$" "y"))
(defn link-table?
@ -169,6 +181,26 @@
(count (key-names entity-map))))
(defn children-with-tag
"Return all children of this `element` which have this `tag`."
[element tag]
(children element #(= (:tag %) tag)))
(defn descendants-with-tag
"Return all descendants of this `element`, recursively, which have this `tag`."
[element tag]
(flatten
(remove
empty?
(cons
(children element #(= (:tag %) tag))
(map
#(descendants-with-tag % tag)
(children element))))))
;; (read-adl "../youyesyet/stripped.adl.xml")
(defn all-properties
"Return all properties of this entity (including key properties)."
[entity]
(descendants-with-tag entity :property))