diff --git a/src/adl/to_hugsql_queries.clj b/src/adl/to_hugsql_queries.clj index 074978e..de23de7 100644 --- a/src/adl/to_hugsql_queries.clj +++ b/src/adl/to_hugsql_queries.clj @@ -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)))] - (str - "ORDER BY " entity-name "." - (s/join - (str ",\n\t" entity-name ".") - (doall (flatten (cons preferred (key-names entity-map)))))))) + (filter #(#{"user" "all"} (-> % :attrs :distinct)) + (children entity #(= (:tag %) :property))))] + (if + (empty? preferred) + "" + (str + "ORDER BY " entity-name "." + (s/join + (str ",\n\t" entity-name ".") + (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,50 +130,56 @@ "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))] + #(= (-> % :attrs :type) "string") + (children entity #(= (:tag %) :property)))] (if (empty? string-fields) {} (hash-map - (keyword query-name) - {:name query-name - :signature signature - :entity entity-map - :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" - "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")})))) + (keyword query-name) + {:name query-name + :signature signature + :entity entity + :type :text-search + :query + (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)) + (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) - "\n" - (order-by-clause entity-map) - "\n\n")})) + (s/join + "\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,71 +258,103 @@ (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] - (let [properties (-> link :content :properties vals) - links (apply - merge - (map - #(hash-map (keyword (-> % :attrs :entity)) %) - (filter #(-> % :attrs :entity) properties))) - near-name (-> near :attrs :name) - link-name (-> link :attrs :name) - far-name (-> far :attrs :name) - pretty-far (singularise far-name) - query-name (str "list-" link-name "-" near-name "-by-" pretty-far) - signature ":? :*"] - (hash-map - (keyword query-name) - {:name query-name - :signature signature - :entity link - :type :select-many-to-many - :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")}))) +(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 + (map + #(hash-map (keyword (-> % :attrs :entity)) %) + (filter #(-> % :attrs :entity) properties))) + near-name (-> near :attrs :name) + link-name (-> link :attrs :name) + far-name (-> far :attrs :name) + pretty-far (singularise far-name) + query-name (str "list-" link-name "-" near-name "-by-" pretty-far) + signature ":? :*"] + (hash-map + (keyword query-name) + {:name query-name + :signature signature + :entity link + :type :select-many-to-many + :near-entity near + :far-entity far + :query + (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)))))))))))) + diff --git a/src/adl/utils.clj b/src/adl/utils.clj index ca30560..de0cc44 100644 --- a/src/adl/utils.clj +++ b/src/adl/utils.clj @@ -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)) +