(ns clojure-sql.dsl (:refer-clojure :exclude [sort-by]) (:require [clojure.set :as set] [clojure.walk :as walk] [clojure-sql.query :as q] [clojure-sql.util :as u])) ;; ============================================================== ;; The DSL for making query maps ;; ============================================================== ;; important sections: ;; -PROJECTION- ;; -TABLES- ;; -FILTERS- ;; GROUPING ;; GROUPED FILTERS ;; -SORTING- ;; { ;; :tables => tablename -> table_alias ;; :fields => (table_alias, fieldname) -> field_alias ;; :joins => [left-table type right-table on?] ;; :where => expression ;; :sort-by => [(field, direction)] ;; } (defn table [arg] (into (q/->Query) (if (map? arg) {:tables (u/flip-map arg) :joins (vec (vals arg))} {:tables {arg arg} :joins [arg]}))) (defn ^:private ambiguous-error [field & [query]] (throw (ex-info (str "Ambiguous field " field " in query with more than one table") {:field field :query query}))) (defn ^:private resolve-field [table aliases field] (let [field-alias-lookup aliases] (or (field-alias-lookup field) (if table [table field] (ambiguous-error field))))) (defn ^:private process-expression [table aliases expression] (cond (vector? expression) (list 'quote expression) (sequential? expression) (map (partial process-expression table aliases) expression) (keyword? expression) (resolve-field table aliases expression) :else expression)) (defn project [query fields] (let [table (if (= (count (:tables query)) 1) (-> query :tables first key)) alias-lookup (or (:fields query) {}) get-real-name #(resolve-field table alias-lookup %)] (assoc query :fields (->> (for [[old-name new-name] (if (map? fields) fields (zipmap fields fields))] [new-name (get-real-name old-name)]) (into {}))))) (defn rename [query field-renames] {:pre [(map? field-renames) ;; the intersection of the new aliases with the old aliases NOT renamed by this operation (empty? (set/intersection (set (vals field-renames)) (set/difference (set (keys (:fields query))) (set (keys field-renames)))))]} (let [fields (:fields query) alias-lookup (or (:fields query) {}) fields-to-remove (set/difference (set (keys field-renames)) (set (vals field-renames))) original-name (fn [field] (cond (vector? field) field (contains? alias-lookup field) (get alias-lookup field) :else (throw (ex-info (str "Cannot rename field " (pr-str field) ". Field does not exist in query.") {:field field :query query :renames field-renames}))) (get alias-lookup field)) ;; do the renaming we want here: fields (->> (for [[key val] field-renames] [val (original-name key)]) (into fields)) ;; remove the things we no longer have here: fields (->> (remove (comp fields-to-remove key) fields) (into {}))] (assoc query :fields fields))) (defn ^:private combine-wheres [& wheres] (reduce (fn [acc where] (cond (nil? acc) where (nil? where) acc :else (or (if (and (sequential? where) (= (name (first where)) "and")) `(and ~acc ~@(next where))) (if (and (sequential? acc) (= (name (first acc)) "and")) `(and ~@(next acc) ~where)) `(and ~acc ~where)))) nil wheres)) (defn join [left right & {:keys [on type]}] (let [common-tables (set/intersection (set (keys (:tables left))) (set (keys (:tables right)))) ;;_ (assert (empty? common-tables) "Cannot join two tables with the same name") merged-tables (merge (:tables left) (:tables right)) common-fields (set/intersection (set (keys (:fields left))) (set (keys (:fields right)))) merged-fields (merge (:fields left) (:fields right)) join-condition (cond (nil? on) (let [implicit (map (fn [field] `(~'= ~(resolve-field (:table left) (:fields left) field) ~(resolve-field (:table right) (:fields right) field))) common-fields)] (if (next implicit) (seq (cons `and implicit)) ;; more than one, so add an "and" around them (first implicit))) (seq common-fields) (throw (ex-info "Cannot join with common fields unless natural join" {:left left :right right :common-fields common-fields})) :else (process-expression nil merged-fields on)) type (or type (if join-condition :inner) :cross)] (-> left (assoc :fields merged-fields :tables merged-tables :joins {:left (:joins left) :right (:joins right) :type type :on join-condition} :where (combine-wheres (:where left) (:where right)))))) (defn select [query expression] (let [table-name (if (= (count (:tables query)) 1) (-> query :tables first key)) old-where (:where query) resolved-expression (process-expression table-name (:fields query) expression) new-where (combine-wheres old-where resolved-expression)] (assoc query :where new-where))) (defn sort-by [query fields] (let [table-name (if (= (count (:tables query)) 1) (-> query :tables first key)) fields-seq (if (sequential? fields) fields [fields])] (assoc query :sort-by (for [field fields-seq] (if (vector? field) (resolve-field table-name (:fields query) field) [(resolve-field table-name (:fields query) field) :asc]))))) (let [id 10] (-> (table :x) (project [:x]) (select `(and (in :x [1 2 3 :y]) (= :x ~id))) (join (-> (table :y) (project [:y])) :on `(= :x :y)) (join (-> (table :z) (project [:x]))) (sort-by [:x])))