diff --git a/deps.edn b/deps.edn index 2023d31..91522c2 100644 --- a/deps.edn +++ b/deps.edn @@ -6,14 +6,17 @@ com.github.seancorfield/next.jdbc {:mvn/version "1.2.780"} org.postgresql/postgresql {:mvn/version "42.3.6"} camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.2"} - com.github.vertical-blank/sql-formatter {:mvn/version "1.0.3"} - metosin/jsonista {:mvn/version "0.2.7"}} + com.github.vertical-blank/sql-formatter {:mvn/version "2.0.3"} + metosin/jsonista {:mvn/version "0.2.7"} + org.apache.commons/commons-lang3 {:mvn/version "3.12.0"}} :aliases {:test {:extra-paths ["test"] :extra-deps {org.clojure/test.check {:mvn/version "0.10.0"} clj-kondo/clj-kondo {:mvn/version "2022.05.31"} metosin/testit {:mvn/version "0.4.0"} - lambdaisland/kaocha {:mvn/version "1.0.700"}}} + same/ish {:mvn/version "0.1.4"} + lambdaisland/kaocha {:mvn/version "1.66.1034"} + lambdaisland/kaocha-cloverage {:mvn/version "1.0.75"}}} :runner {:extra-deps {com.cognitect/test-runner {:git/url "https://github.com/cognitect-labs/test-runner" diff --git a/project.clj b/project.clj index d0c0f4c..2cdbad0 100644 --- a/project.clj +++ b/project.clj @@ -9,6 +9,6 @@ [com.github.seancorfield/next.jdbc "1.2.780"] [org.postgresql/postgresql "42.3.6"] [camel-snake-kebab/camel-snake-kebab "0.4.2"] - [com.github.vertical-blank/sql-formatter "1.0.3"] + [com.github.vertical-blank/sql-formatter "2.0.3"] [metosin/jsonista "0.2.7"] [com.stuartsierra/dependency "1.0.0"]]) diff --git a/src/com/verybigthings/penkala/decomposition.clj b/src/com/verybigthings/penkala/decomposition.clj index 2103d2a..43675a6 100644 --- a/src/com/verybigthings/penkala/decomposition.clj +++ b/src/com/verybigthings/penkala/decomposition.clj @@ -2,7 +2,53 @@ (:require [clojure.spec.alpha :as s] [com.verybigthings.penkala.util :refer [as-vec path-prefix-join col->alias]] [clojure.set :as set] - [clojure.string :as str])) + [clojure.string :as str]) + (:import java.time.LocalDateTime + java.time.LocalTime + java.time.format.DateTimeFormatter + java.time.temporal.ChronoUnit)) + +(defmulti coerce-embedded-value + (fn [pg-type _] + pg-type)) + +(defmethod coerce-embedded-value :default [_ value] value) + +(defmethod coerce-embedded-value "numeric" [_ value] + (bigdec value)) + +(defmethod coerce-embedded-value "real" [_ value] + (float value)) + +(defmethod coerce-embedded-value "money" [_ value] + (let [[_ numeric] (re-matches #"\D*(\d*[\.|,]\d*)\D*" value) + numeric' (str/replace numeric #"," ".")] + (Double/parseDouble numeric'))) + +(let [formatter (DateTimeFormatter/ofPattern "yyyy-MM-dd'T'HH:mm:ss[.][SSSSSS][SSSSS][SSSS][SSS][SS][S][XXX][XX][X]")] + (defmethod coerce-embedded-value "timestamp with time zone" [_ value] + (LocalDateTime/parse value formatter))) + +(let [formatter (DateTimeFormatter/ofPattern "yyyy-MM-dd'T'HH:mm:ss[.][SSSSSS][SSSSS][SSSS][SSS][SS][S]")] + (defmethod coerce-embedded-value "timestamp without time zone" [_ value] + (LocalDateTime/parse value formatter))) + +(let [formatter (DateTimeFormatter/ofPattern "HH:mm:ss[.][SSSSSS][SSSSS][SSSS][SSS][SS][S][XXX][XX][X]")] + (defmethod coerce-embedded-value "time with time zone" [_ value] + (println "1>>>>>" value) + (LocalTime/parse value formatter))) + +(let [formatter (DateTimeFormatter/ofPattern "HH:mm:ss[.][SSSSSS][SSSSS][SSSS][SSS][SS][S]")] + (defmethod coerce-embedded-value "time without time zone" [_ value] + + (LocalTime/parse value formatter))) + +(defn coerce-embedded-row [heading row] + (->> heading + (map-indexed (fn [idx [col-name pg-type]] + (println col-name pg-type) + [(keyword col-name) (coerce-embedded-value pg-type (get row idx))])) + (into {}))) (defrecord DecompositionSchema [pk decompose-to namespace columns]) @@ -41,9 +87,12 @@ (s/def ::keep-nil? boolean?) +(s/def ::embedded? + boolean?) + (s/def ::schema (s/keys - :opt-un [::decompose-to ::pk ::schema ::keep-duplicates? ::keep-nil? ::namespace])) + :opt-un [::decompose-to ::pk ::schema ::keep-duplicates? ::keep-nil? ::embedded? ::namespace])) (declare process-schema) @@ -55,7 +104,7 @@ (reduce-kv (fn [acc k v] (if (map? v) - (assoc-in acc [:schemas (rename k)] (process-schema v)) + (assoc-in acc [:schemas (rename k)] (process-schema k v)) (assoc-in acc [:renames (rename k)] v))) schema columns))] @@ -69,9 +118,21 @@ schema columns)))) -(defn process-schema- [schema] +(defn process-embedded-schema [schema column] + (cond + (and (:embedded? schema) column) + (-> schema (dissoc :embedded?) (assoc :embedded column)) + + (and (:embedded? schema) (not column)) + (throw (ex-info "Root schema can't have :embedded? true" {:schema schema})) + + :else + schema)) + +(defn process-schema- [column schema] (-> schema process-schema-columns + (process-embedded-schema column) (update :pk as-vec))) (def process-schema (memoize process-schema-)) @@ -90,10 +151,20 @@ (defn assoc-descendants [acc schemas idx row] (reduce-kv (fn [m k v] - (let [descendant (build (get acc k) v idx row)] - (if descendant - (assoc m k descendant) - m))) + (if-let [embedded (:embedded v)] + (let [{:keys [heading body]} (get row embedded) + processed (reduce + (fn [acc [idx row]] + (->> row + (coerce-embedded-row heading) + (build acc v idx))) + {} + (map-indexed (fn [idx v] [idx v]) (as-vec body)))] + (assoc m k processed)) + (let [descendant (build (get acc k) v idx row)] + (if descendant + (assoc m k descendant) + m)))) acc schemas)) @@ -163,11 +234,13 @@ (expand-transformed-coll transformed) transformed))) + + (defn decompose "Decomposes the data based on the schema." [schema data] (when (and (seq schema) (seq data)) - (let [schema' (process-schema schema) + (let [schema' (process-schema nil schema) mapping (reduce (fn [acc [idx row]] (build acc schema' idx row)) @@ -280,12 +353,23 @@ (assoc acc alias join-schema))) acc))) columns - (:joins relation))] + (:joins relation)) + columns-with-joined-and-embeds (reduce + (fn [acc alias] + (let [col-id (get-in relation [:aliases->ids alias]) + {:keys [type relation]} (get-in relation [:columns col-id])] + (if (= :embed type) + (let [overrides (get-in overrides [:schema alias]) + schema (infer-schema relation overrides [])] + (assoc acc alias (assoc schema :embedded? true))) + acc))) + columns-with-joined + (:projection relation))] (map->DecompositionSchema {:pk pk :decompose-to decompose-to :namespace namespace - :schema columns-with-joined + :schema columns-with-joined-and-embeds :processor processor :keep-nil? keep-nil? :keep-duplicates? keep-duplicates?}))))) \ No newline at end of file diff --git a/src/com/verybigthings/penkala/next_jdbc.clj b/src/com/verybigthings/penkala/next_jdbc.clj index 360f38b..b27a006 100644 --- a/src/com/verybigthings/penkala/next_jdbc.clj +++ b/src/com/verybigthings/penkala/next_jdbc.clj @@ -125,7 +125,9 @@ (with-relations (concat tables views)))))) (defn prettify-sql [sql] - (SqlFormatter/format sql)) + (.. SqlFormatter + (of "postgresql") + (format sql))) (defn validate-relation [env rel] (let [rel' (get env rel)] @@ -142,6 +144,7 @@ relation' (if (keyword? relation) (validate-relation env relation) relation) sqlvec (r/get-select-query relation' env params) decomposition-schema (d/infer-schema relation' decomposition-schema-overrides)] + ;;(clojure.pprint/pprint decomposition-schema) ;;(-> sqlvec first prettify-sql println) ;;(println (rest sqlvec)) (->> (jdbc/execute! db sqlvec default-next-jdbc-options) @@ -246,4 +249,5 @@ (require '[com.verybigthings.penkala.helpers :refer [param]]) - (r/get-select-query (r/where posts-rel [:= :user-id (param :user/id)]) {} {:user/id 1})) \ No newline at end of file + (r/get-select-query (r/where posts-rel [:= :user-id (param :user/id)]) {} {:user/id 1})) + diff --git a/src/com/verybigthings/penkala/relation.clj b/src/com/verybigthings/penkala/relation.clj index d935aaa..0403256 100644 --- a/src/com/verybigthings/penkala/relation.clj +++ b/src/com/verybigthings/penkala/relation.clj @@ -32,6 +32,7 @@ (-extend [this col-name extend-expression]) (-extend-with-aggregate [this col-name agg-expression]) (-extend-with-window [this col-name window-expression partitions orders]) + (-extend-with-embedded [this col-name relation]) (-rename [this prev-col-name next-col-name]) (-select [this projection]) (-select-all-but [this projection]) @@ -904,6 +905,16 @@ (assoc-in [:ids->aliases id] col-name) (assoc-in [:aliases->ids col-name] id) (update :projection conj col-name)))) + (-extend-with-embedded [this col-name relation] + (when (contains? (:aliases->ids this) col-name) + (throw (ex-info (str "Column " col-name " already-exists") {:column col-name :relation this}))) + (let [id (keyword (gensym "column-"))] + (-> this + (assoc-in [:columns id] {:type :embed + :relation relation}) + (assoc-in [:ids->aliases id] col-name) + (assoc-in [:aliases->ids col-name] id) + (update :projection conj col-name)))) (-select [this projection] (let [processed-projection (process-projection this (s/conform ::column-list projection))] (assoc this :projection processed-projection))) @@ -1278,6 +1289,16 @@ :orders (s/? ::orders)) :ret ::relation) +(defn extend-with-embedded [rel col-name other-rel] + (-extend-with-embedded rel col-name other-rel)) + +(s/fdef extend-with-embedded + :args (s/cat + :rel ::relation + :col-name keyword? + :other-rel ::relation) + :ret ::relation) + (defn rename "Renames a column. If you rename a column, you must use a new name to reference it after that @@ -1787,4 +1808,7 @@ :args (s/cat :rel ::cte :is-materialized (s/or :nil nil? :boolean boolean?)) - :ret ::cte) \ No newline at end of file + :ret ::cte) + +(def empty-relation + (->Relation {:namespace false})) \ No newline at end of file diff --git a/src/com/verybigthings/penkala/statement.clj b/src/com/verybigthings/penkala/statement.clj index 2ec25fe..9188917 100644 --- a/src/com/verybigthings/penkala/statement.clj +++ b/src/com/verybigthings/penkala/statement.clj @@ -572,6 +572,57 @@ (update :query conj (str "DISTINCT ON" (-> query join-comma wrap-parens)))))) acc)) +(defn get-projection-for-embedded + ([rel] + (let [cursor (get-projection-for-embedded rel {:columns #{} :path-prefix []})] + (-> cursor :columns sort))) + ([{:keys [projection joins] :as rel} {:keys [columns path-prefix] :as cursor}] + (let [columns' (reduce + (fn [acc alias] + (let [col-id (get-in rel [:aliases->ids alias]) + col-def (get-in rel [:columns col-id]) + path-prefix-names (mapv name path-prefix) + col-path (conj path-prefix-names (name alias)) + col-alias (if (seq path-prefix) (path-prefix-join col-path) (name alias))] + (conj acc col-alias))) + columns + projection)] + (reduce-kv + (fn [acc alias {:keys [relation projection]}] + (get-projection-for-embedded (update relation :projection #(or projection %)) (update acc :path-prefix conj alias))) + (assoc cursor :columns columns') + (get-in rel [:joins]))))) + +(defn compile-embedded [acc env rel embedded-relation] + (let [projection (get-projection-for-embedded embedded-relation) + embedded-id (-> "embedded-" gensym name) + data-relation-name (str "t1-" embedded-id) + data-and-types-relation-name (str "t2" embedded-id) + [query & params] (binding [*scopes* (conj *scopes* {:env env :rel rel})] + (format-select-query-without-params-resolution env (assoc embedded-relation :parent rel))) + heading (->> projection + (map (fn [alias] + (str "array['" alias "', pg_typeof(" (q data-relation-name) "." (q alias) ")::text]"))) + join-comma) + body (->> projection + (map (fn [alias] + (str "to_json(" (q data-relation-name) "." (q alias) ")"))) + join-comma) + final-query ["SELECT json_build_object" + (wrap-parens "'heading', array_to_json" + (wrap-parens (q data-and-types-relation-name) "." (q "heading")) + "," + "'body', array_to_json" + (wrap-parens (q data-and-types-relation-name) "." (q "body"))) + "FROM" + (wrap-parens "SELECT array[" heading "] as heading, " + "array_agg(array[" body "]) AS body FROM" + (wrap-parens query) spc (q data-relation-name) " GROUP BY heading") + (q data-and-types-relation-name)]] + (-> acc + (update :params into params) + (update :query conj (join-space final-query))))) + (defn with-projection ([acc env rel] (let [{:keys [query params]} (with-projection empty-acc env rel [])] @@ -601,6 +652,12 @@ (update :params into params) (update :query conj (str (join-space query) spc "AS" spc (q col-alias))))) + (and (not (seq path-prefix)) (= :embed col-type)) + (let [{:keys [query params]} (compile-embedded empty-acc env rel (:relation col-def))] + (-> acc + (update :params into params) + (update :query conj (str (-> query join-space wrap-parens) spc "AS" spc (q col-alias))))) + (and (not (seq path-prefix)) (= :window col-type)) (let [{:keys [value-expression partition-by order-by]} col-def order-by-query-params (when order-by (compile-order-by empty-acc env rel order-by)) @@ -694,6 +751,9 @@ (update :params into params) (update :query conj (join-space ["FROM" query "AS" (q (get-rel-alias-with-prefix env rel-name))])))) + (nil? (get-in rel [:spec :name])) + acc + :else (let [rel-name (get-rel-alias rel)] (update acc :query into [(if (:only rel) "FROM ONLY" "FROM") @@ -891,7 +951,7 @@ (defn with-cte [env acc cte] (let [cte-query (get-in cte [:spec :query]) - materialized? (:materialized? cte-query) + materialized? (get-in cte [:spec :cte :materialized?]) [query & params] (cte-query env) cte-query (cond-> [] (get-in cte [:spec :cte :recursive?]) diff --git a/test/com/verybigthings/penkala/decomposition_test.clj b/test/com/verybigthings/penkala/decomposition_test.clj index 69b466c..ad1c25e 100644 --- a/test/com/verybigthings/penkala/decomposition_test.clj +++ b/test/com/verybigthings/penkala/decomposition_test.clj @@ -448,4 +448,35 @@ :processor (fn [val] (assoc val :child/val-upcased (-> val :child/val str/upper-case))) :schema {:id :children_id :val :children_val}}}} - data))))) \ No newline at end of file + data))))) + +(deftest it-should-decompose-embedded-data + (let [data [{:beta + {:heading [["val", "text"], ["j", "jsonb"], ["id", "integer"], ["alpha-id", "integer"]] + :body [["alpha three", nil, 3, 3], + ["alpha three again", nil, 4, 3]]}, + :id 3, + :val "three"}] + decomposed (d/decompose {:pk :id + :namespace :alpha + :schema {:id :id + :val :val + :beta {:embedded? true + :pk :id + :namespace :beta + :schema {:id :id + :val :val + :j :j + :alpha-id :alpha-id}}}} + data)] + (is (= [{:alpha/id 3 + :alpha/val "three" + :alpha/beta [{:beta/id 3 + :beta/alpha-id 3 + :beta/j nil + :beta/val "alpha three"} + {:beta/id 4 + :beta/alpha-id 3 + :beta/j nil + :beta/val "alpha three again"}]}] + decomposed)))) \ No newline at end of file diff --git a/test/com/verybigthings/penkala/postgresql_tutorial_test.clj b/test/com/verybigthings/penkala/postgresql_tutorial_test.clj index 5962ad7..4204b72 100644 --- a/test/com/verybigthings/penkala/postgresql_tutorial_test.clj +++ b/test/com/verybigthings/penkala/postgresql_tutorial_test.clj @@ -2981,4 +2981,204 @@ #:flights{:departure "New York", :arrival "Nicosia", :cyclic-data false, :cost 350, :connections 3, :itinerary "New York London Athens Nicosia"} #:flights{:departure "New York", :arrival "Hawaii", :cyclic-data false, :cost 330, :connections 3, :itinerary "New York Los Angeles Tokyo Hawaii"} #:flights{:departure "New York", :arrival "Paris", :cyclic-data true, :cost 400, :connections 3, :itinerary "New York Paris Cairo Paris"}])) - (.rollback tx)))) \ No newline at end of file + (.rollback tx)))) + +(deftest embedded-relations + ;; https://blog.jooq.org/jooq-3-15s-new-multiset-operator-will-change-how-you-think-about-sql/ + (let [{:keys [film film-actor film-category actor category]} *env* + film-actors (-> actor + (r/with-parent film) + (r/inner-join film-actor :film-actor + [:and + [:= [:parent-scope :film-id] :film-actor/film-id] + [:= :actor-id :film-actor/actor-id]] []) + (r/select [:first-name :last-name])) + film-categories (-> category + (r/with-parent film) + (r/inner-join film-category :film-category + [:and + [:= [:parent-scope :film-id] :film-category/film-id] + [:= :category-id :film-category/category-id]] []) + (r/select [:name])) + + films (-> film + (r/extend-with-embedded :actors film-actors) + (r/extend-with-embedded :categories film-categories) + (r/select [:title :actors :categories]) + (r/order-by [:title]) + (r/limit 5)) + res (select! *env* films)] + (fact + res => [#:film{:title "ACADEMY DINOSAUR", + :categories [#:category{:name "Documentary"}], + :actors + [#:actor{:last-name "GUINESS", :first-name "PENELOPE"} + #:actor{:last-name "GABLE", :first-name "CHRISTIAN"} + #:actor{:last-name "TRACY", :first-name "LUCILLE"} + #:actor{:last-name "PECK", :first-name "SANDRA"} + #:actor{:last-name "CAGE", :first-name "JOHNNY"} + #:actor{:last-name "TEMPLE", :first-name "MENA"} + #:actor{:last-name "NOLTE", :first-name "WARREN"} + #:actor{:last-name "KILMER", :first-name "OPRAH"} + #:actor{:last-name "DUKAKIS", :first-name "ROCK"} + #:actor{:last-name "KEITEL", :first-name "MARY"}]} + #:film{:title "ACE GOLDFINGER", + :categories [#:category{:name "Horror"}], + :actors + [#:actor{:last-name "FAWCETT", :first-name "BOB"} + #:actor{:last-name "ZELLWEGER", :first-name "MINNIE"} + #:actor{:last-name "GUINESS", :first-name "SEAN"} + #:actor{:last-name "DEPP", :first-name "CHRIS"}]} + #:film{:title "ADAPTATION HOLES", + :categories [#:category{:name "Documentary"}], + :actors + [#:actor{:last-name "WAHLBERG", :first-name "NICK"} + #:actor{:last-name "FAWCETT", :first-name "BOB"} + #:actor{:last-name "STREEP", :first-name "CAMERON"} + #:actor{:last-name "JOHANSSON", :first-name "RAY"} + #:actor{:last-name "DENCH", :first-name "JULIANNE"}]} + #:film{:title "AFFAIR PREJUDICE", + :categories [#:category{:name "Horror"}], + :actors + [#:actor{:last-name "DEGENERES", :first-name "JODIE"} + #:actor{:last-name "DAMON", :first-name "SCARLETT"} + #:actor{:last-name "PESCI", :first-name "KENNETH"} + #:actor{:last-name "WINSLET", :first-name "FAY"} + #:actor{:last-name "KILMER", :first-name "OPRAH"}]} + #:film{:title "AFRICAN EGG", + :categories [#:category{:name "Family"}], + :actors + [#:actor{:last-name "PHOENIX", :first-name "GARY"} + #:actor{:last-name "TAUTOU", :first-name "DUSTIN"} + #:actor{:last-name "LEIGH", :first-name "MATTHEW"} + #:actor{:last-name "CARREY", :first-name "MATTHEW"} + #:actor{:last-name "TEMPLE", :first-name "THORA"}]}]))) + +(deftest embedded-relations-onto-empty-relation + ;; This tests loads same dataset like `embedded-relations` test, but loads data in a different format + ;; with a probably better execution plan. It would require some lightweight processing to get structure + ;; like in `embedded-relations` test + (let [{:keys [film film-actor film-category actor category]} *env* + films (-> (r/as-cte + (-> film + (r/select [:film-id :title]) + (r/order-by [:title]) + (r/limit 5))) + (r/cte-materialized true)) + actors (-> actor + (r/inner-join film-actor :film-actor + [:and + [:in :film-actor/film-id (-> films (r/select [:film-id]))] + [:= :actor-id :film-actor/actor-id]] [:film-actor/film-id]) + (r/select [:first-name :last-name])) + categories (-> category + (r/inner-join film-category :film-category + [:and + [:in :film-category/film-id (-> films (r/select [:film-id]))] + [:= :category-id :film-category/category-id]] [:film-category/film-id]) + (r/select [:name])) + query (-> r/empty-relation + (r/extend-with-embedded :films films) + (r/extend-with-embedded :actors actors) + (r/extend-with-embedded :categories categories)) + res (select! *env* query)] + + (fact + res => [{:categories [#:category{:name "Documentary", + :film-category + [#:film-category{:film-id 1} + #:film-category{:film-id 3}]} + #:category{:name "Horror", + :film-category + [#:film-category{:film-id 2} + #:film-category{:film-id 4}]} + #:category{:name "Family", + :film-category [#:film-category{:film-id 5}]}], + :actors [#:actor{:last-name "GUINESS", + :first-name "PENELOPE", + :film-actor [#:film-actor{:film-id 1}]} + #:actor{:last-name "GABLE", + :first-name "CHRISTIAN", + :film-actor [#:film-actor{:film-id 1}]} + #:actor{:last-name "TRACY", + :first-name "LUCILLE", + :film-actor [#:film-actor{:film-id 1}]} + #:actor{:last-name "PECK", + :first-name "SANDRA", + :film-actor [#:film-actor{:film-id 1}]} + #:actor{:last-name "CAGE", + :first-name "JOHNNY", + :film-actor [#:film-actor{:film-id 1}]} + #:actor{:last-name "TEMPLE", + :first-name "MENA", + :film-actor [#:film-actor{:film-id 1}]} + #:actor{:last-name "NOLTE", + :first-name "WARREN", + :film-actor [#:film-actor{:film-id 1}]} + #:actor{:last-name "KILMER", + :first-name "OPRAH", + :film-actor + [#:film-actor{:film-id 1} #:film-actor{:film-id 4}]} + #:actor{:last-name "DUKAKIS", + :first-name "ROCK", + :film-actor [#:film-actor{:film-id 1}]} + #:actor{:last-name "KEITEL", + :first-name "MARY", + :film-actor [#:film-actor{:film-id 1}]} + #:actor{:last-name "WAHLBERG", + :first-name "NICK", + :film-actor [#:film-actor{:film-id 3}]} + #:actor{:last-name "FAWCETT", + :first-name "BOB", + :film-actor + [#:film-actor{:film-id 3} #:film-actor{:film-id 2}]} + #:actor{:last-name "STREEP", + :first-name "CAMERON", + :film-actor [#:film-actor{:film-id 3}]} + #:actor{:last-name "JOHANSSON", + :first-name "RAY", + :film-actor [#:film-actor{:film-id 3}]} + #:actor{:last-name "DENCH", + :first-name "JULIANNE", + :film-actor [#:film-actor{:film-id 3}]} + #:actor{:last-name "PHOENIX", + :first-name "GARY", + :film-actor [#:film-actor{:film-id 5}]} + #:actor{:last-name "TAUTOU", + :first-name "DUSTIN", + :film-actor [#:film-actor{:film-id 5}]} + #:actor{:last-name "LEIGH", + :first-name "MATTHEW", + :film-actor [#:film-actor{:film-id 5}]} + #:actor{:last-name "CARREY", + :first-name "MATTHEW", + :film-actor [#:film-actor{:film-id 5}]} + #:actor{:last-name "TEMPLE", + :first-name "THORA", + :film-actor [#:film-actor{:film-id 5}]} + #:actor{:last-name "DEGENERES", + :first-name "JODIE", + :film-actor [#:film-actor{:film-id 4}]} + #:actor{:last-name "DAMON", + :first-name "SCARLETT", + :film-actor [#:film-actor{:film-id 4}]} + #:actor{:last-name "PESCI", + :first-name "KENNETH", + :film-actor [#:film-actor{:film-id 4}]} + #:actor{:last-name "WINSLET", + :first-name "FAY", + :film-actor [#:film-actor{:film-id 4}]} + #:actor{:last-name "ZELLWEGER", + :first-name "MINNIE", + :film-actor [#:film-actor{:film-id 2}]} + #:actor{:last-name "GUINESS", + :first-name "SEAN", + :film-actor [#:film-actor{:film-id 2}]} + #:actor{:last-name "DEPP", + :first-name "CHRIS", + :film-actor [#:film-actor{:film-id 2}]}], + :films [#:film{:title "ACADEMY DINOSAUR", :film-id 1} + #:film{:title "ACE GOLDFINGER", :film-id 2} + #:film{:title "ADAPTATION HOLES", :film-id 3} + #:film{:title "AFFAIR PREJUDICE", :film-id 4} + #:film{:title "AFRICAN EGG", :film-id 5}]}]))) \ No newline at end of file diff --git a/test/com/verybigthings/penkala/relation/embedded_coercion_test.clj b/test/com/verybigthings/penkala/relation/embedded_coercion_test.clj new file mode 100644 index 0000000..5275ad9 --- /dev/null +++ b/test/com/verybigthings/penkala/relation/embedded_coercion_test.clj @@ -0,0 +1,110 @@ +(ns com.verybigthings.penkala.relation.embedded-coercion-test + (:require [clojure.test :refer [use-fixtures deftest is testing]] + [com.verybigthings.penkala.next-jdbc :refer [select! select-one!]] + [com.verybigthings.penkala.relation :as r] + [com.verybigthings.penkala.helpers :as h] + [com.verybigthings.penkala.test-helpers :as th :refer [*env*]] + [com.verybigthings.penkala.decomposition :refer [coerce-embedded-value]] + [same :refer [ish? zeroish?]])) + +(use-fixtures :once (partial th/reset-db-fixture "embedded_types")) + +(deftest it-correctly-coerces-values-in-embedded-relations + (let [{:keys [vals]} *env* + s (select-one! *env* vals) + embedded-res (select-one! *env* (-> r/empty-relation + (r/extend-with-embedded :vals vals))) + e (-> embedded-res :vals first)] + (clojure.pprint/pprint s) + (clojure.pprint/pprint e) + (testing "smallint" + (is (= (:vals/val-smallint s) (:vals/val-smallint e)))) + (testing "integer" + (is (= (:vals/val-integer s) (:vals/val-integer e)))) + (testing "bigint" + (is (= (:vals/val-bigint s) (:vals/val-bigint e)))) + (testing "decimal" + (is (= (type (:vals/val-decimal s)) (type (:vals/val-decimal e)))) + (is (ish? (float (:vals/val-decimal s)) (float (:vals/val-decimal e))))) + (testing "numeric" + (is (= (type (:vals/val-numeric s)) (type (:vals/val-numeric e)))) + (is (ish? (float (:vals/val-numeric s)) (float (:vals/val-numeric e))))) + (testing "numeric-precision" + (is (= (:vals/val-numeric-precision s) (:vals/val-numeric-precision e)))) + (testing "numeric-precision-scale" + (is (= (:vals/val-numeric-precision-scale s) (:vals/val-numeric-precision-scale e)))) + (testing "real" + (is (= (:vals/val-real s) (:vals/val-real e)))) + (testing "double-precision" + (is (= (:vals/val-double-precision s) (:vals/val-double-precision e)))) + (testing "smallserial" + (is (= (:vals/val-smallserial s) (:vals/val-smallserial e)))) + (testing "serial" + (is (= (:vals/val-serial s) (:vals/val-serial e)))) + (testing "bigserial" + (is (= (:vals/val-bigserial s) (:vals/val-bigserial e)))) + (testing "money" + (is (= (:vals/val-money s) (:vals/val-money e)))) + (testing "varchar" + (is (= (:vals/val-varchar s) (:vals/val-varchar e)))) + (testing "varchar-limit" + (is (= (:vals/val-varchar-limit s) (:vals/val-varchar-limit e)))) + (testing "char" + (is (= (:vals/val-char s) (:vals/val-char e)))) + (testing "text" + (is (= (:vals/val-text s) (:vals/val-text e)))) + + (testing "timestamp with time zone" + (is (= (:vals/val-timestamp-with-time-zone s) + (:vals/val-timestamp-with-time-zone e)))) + (testing "timestamp with time zone 0 precision" + (is (= (:vals/val-timestamp-with-time-zone-0-precision s) + (:vals/val-timestamp-with-time-zone-0-precision e)))) + (testing "timestamp with time zone 1 precision" + (is (= (:vals/val-timestamp-with-time-zone-1-precision s) + (:vals/val-timestamp-with-time-zone-1-precision e)))) + (testing "timestamp with time zone 2 precision" + (is (= (:vals/val-timestamp-with-time-zone-2-precision s) + (:vals/val-timestamp-with-time-zone-2-precision e)))) + (testing "timestamp with time zone 3 precision" + (is (= (:vals/val-timestamp-with-time-zone-3-precision s) + (:vals/val-timestamp-with-time-zone-3-precision e)))) + (testing "timestamp with time zone 4 precision" + (is (= (:vals/val-timestamp-with-time-zone-4-precision s) + (:vals/val-timestamp-with-time-zone-4-precision e)))) + (testing "timestamp with time zone 5 precision" + (is (= (:vals/val-timestamp-with-time-zone-5-precision s) + (:vals/val-timestamp-with-time-zone-5-precision e)))) + (testing "timestamp with time zone 6 precision" + (is (= (:vals/val-timestamp-with-time-zone-6-precision s) + (:vals/val-timestamp-with-time-zone-6-precision e)))) + (testing "timestamp with time zone" + (is (= (:vals/val-timestamp-with-time-zone s) + (:vals/val-timestamp-with-time-zone e)))) + (testing "timestamp without time zone 0 precision" + (is (= (:vals/val-timestamp-without-time-zone-0-precision s) + (:vals/val-timestamp-without-time-zone-0-precision e)))) + (testing "timestamp without time zone 1 precision" + (is (= (:vals/val-timestamp-without-time-zone-1-precision s) + (:vals/val-timestamp-without-time-zone-1-precision e)))) + (testing "timestamp without time zone 2 precision" + (is (= (:vals/val-timestamp-without-time-zone-2-precision s) + (:vals/val-timestamp-without-time-zone-2-precision e)))) + (testing "timestamp without time zone 3 precision" + (is (= (:vals/val-timestamp-without-time-zone-3-precision s) + (:vals/val-timestamp-without-time-zone-3-precision e)))) + (testing "timestamp without time zone 4 precision" + (is (= (:vals/val-timestamp-without-time-zone-4-precision s) + (:vals/val-timestamp-without-time-zone-4-precision e)))) + (testing "timestamp without time zone 5 precision" + (is (= (:vals/val-timestamp-without-time-zone-5-precision s) + (:vals/val-timestamp-without-time-zone-5-precision e)))) + (testing "timestamp without time zone 6 precision" + (is (= (:vals/val-timestamp-without-time-zone-6-precision s) + (:vals/val-timestamp-without-time-zone-6-precision e)))))) + +(deftest coerce-embedded-money + (is (= 99.99 (coerce-embedded-value "money" "$99.99"))) + (is (= 99.99 (coerce-embedded-value "money" "99.99€"))) + (is (= 99.99 (coerce-embedded-value "money" "$99.99€"))) + (is (= 99.99 (coerce-embedded-value "money" "99,99€")))) \ No newline at end of file diff --git a/test/com/verybigthings/penkala/relation/join_test.clj b/test/com/verybigthings/penkala/relation/join_test.clj index 610d614..1c1f648 100644 --- a/test/com/verybigthings/penkala/relation/join_test.clj +++ b/test/com/verybigthings/penkala/relation/join_test.clj @@ -813,4 +813,26 @@ :beta/sum-id 2}], :alpha/id 2, :alpha/sum-id 2}] + res)))) + +(deftest it-can-embed-other-relation + (let [alpha (:alpha *env*) + beta (:beta *env*) + gamma (:gamma *env*) + alpha-beta (-> alpha + (r/extend-with-embedded :beta (-> beta + (r/with-parent alpha) + (r/where [:= :alpha-id [:parent-scope :id]]))) + (r/where [:= :id 3])) + res (select! *env* alpha-beta)] + (is (= [{:alpha/id 3 + :alpha/val "three" + :alpha/beta [{:beta/id 3 + :beta/alpha-id 3 + :beta/j nil + :beta/val "alpha three"} + {:beta/id 4 + :beta/alpha-id 3 + :beta/j nil + :beta/val "alpha three again"}]}] res)))) \ No newline at end of file diff --git a/test/com/verybigthings/penkala/relation/querying_test.clj b/test/com/verybigthings/penkala/relation/querying_test.clj index 7112b19..82f105e 100644 --- a/test/com/verybigthings/penkala/relation/querying_test.clj +++ b/test/com/verybigthings/penkala/relation/querying_test.clj @@ -497,4 +497,10 @@ :products/description "Product 1 description", :products/in-stock true, :products/name "Product 1", - :products/price 12.00M} res)))) \ No newline at end of file + :products/price 12.00M} res)))) + +(deftest it-can-select-from-empty-relation + (let [rel (-> r/empty-relation + (r/extend :one 1)) + res (select! *env* rel)] + (is (= res [{:one 1}])))) \ No newline at end of file diff --git a/test/com/verybigthings/penkala/test_db_scripts/embedded_types/schema.sql b/test/com/verybigthings/penkala/test_db_scripts/embedded_types/schema.sql new file mode 100644 index 0000000..461be90 --- /dev/null +++ b/test/com/verybigthings/penkala/test_db_scripts/embedded_types/schema.sql @@ -0,0 +1,108 @@ +DROP SCHEMA IF EXISTS public; + +CREATE SCHEMA public; + +CREATE TABLE vals ( + val_smallint smallint NOT NULL, + val_integer integer NOT NULL, + val_bigint bigint NOT NULL, + val_decimal decimal NOT NULL, + val_numeric numeric NOT NULL, + val_numeric_precision numeric(2) NOT NULL, + val_numeric_precision_scale numeric(2, 1), + val_real real NOT NULL, + val_double_precision double precision NOT NULL, + val_smallserial smallserial NOT NULL, + val_serial serial NOT NULL, + val_bigserial bigserial NOT NULL, + val_money money NOT NULL, + val_varchar varchar NOT NULL, + val_varchar_limit varchar(2) NOT NULL, + val_char char(10) NOT NULL, + val_text text NOT NULL, + val_timestamp_without_time_zone timestamp without time zone NOT NULL, + val_timestamp_without_time_zone_0_precision timestamp(0) without time zone NOT NULL, + val_timestamp_without_time_zone_1_precision timestamp(1) without time zone NOT NULL, + val_timestamp_without_time_zone_2_precision timestamp(2) without time zone NOT NULL, + val_timestamp_without_time_zone_3_precision timestamp(3) without time zone NOT NULL, + val_timestamp_without_time_zone_4_precision timestamp(4) without time zone NOT NULL, + val_timestamp_without_time_zone_5_precision timestamp(5) without time zone NOT NULL, + val_timestamp_without_time_zone_6_precision timestamp(6) without time zone NOT NULL, + val_timestamp_with_time_zone timestamp with time zone NOT NULL, + val_timestamp_with_time_zone_0_precision timestamp(0) with time zone NOT NULL, + val_timestamp_with_time_zone_1_precision timestamp(1) with time zone NOT NULL, + val_timestamp_with_time_zone_2_precision timestamp(2) with time zone NOT NULL, + val_timestamp_with_time_zone_3_precision timestamp(3) with time zone NOT NULL, + val_timestamp_with_time_zone_4_precision timestamp(4) with time zone NOT NULL, + val_timestamp_with_time_zone_5_precision timestamp(5) with time zone NOT NULL, + val_timestamp_with_time_zone_6_precision timestamp(6) with time zone NOT NULL, + val_time_without_time_zone time without time zone NOT NULL, + val_time_without_time_zone_0_precision time(0) without time zone NOT NULL, + val_time_without_time_zone_1_precision time(1) without time zone NOT NULL, + val_time_without_time_zone_2_precision time(2) without time zone NOT NULL, + val_time_without_time_zone_3_precision time(3) without time zone NOT NULL, + val_time_without_time_zone_4_precision time(4) without time zone NOT NULL, + val_time_without_time_zone_5_precision time(5) without time zone NOT NULL, + val_time_without_time_zone_6_precision time(6) without time zone NOT NULL, + val_time_with_time_zone time with time zone NOT NULL, + val_time_with_time_zone_0_precision time(0) with time zone NOT NULL, + val_time_with_time_zone_1_precision time(1) with time zone NOT NULL, + val_time_with_time_zone_2_precision time(2) with time zone NOT NULL, + val_time_with_time_zone_3_precision time(3) with time zone NOT NULL, + val_time_with_time_zone_4_precision time(4) with time zone NOT NULL, + val_time_with_time_zone_5_precision time(5) with time zone NOT NULL, + val_time_with_time_zone_6_precision time(6) with time zone NOT NULL +); + +INSERT INTO vals + VALUES (1, -- smallint + 1, -- integer + 1, -- bigint + 1.2345678901234567890, -- decimal + 1.2345678901234567890, -- numeric + 1.2345678901234567890, -- numeric_precision + 1.2345678901234567890, -- numeric_precision_scale, + 1.2345678901234567890, -- real + 1.2345678901234567890, -- double_precision, + DEFAULT, -- smallserial + DEFAULT, -- serial + DEFAULT, -- bigserial, + 99.98996998, -- money + 'some text', -- varchar + 's', -- varchar limit + 'some text', -- char + 'some text', -- text, + NOW(), -- timestamp_without_time_zone + NOW(), -- timestamp_without_time_zone_0_precision + NOW(), -- timestamp_without_time_zone_1_precision + NOW(), -- timestamp_without_time_zone_2_precision + NOW(), -- timestamp_without_time_zone_3_precision + NOW(), -- timestamp_without_time_zone_4_precision + NOW(), -- timestamp_without_time_zone_5_precision + NOW(), -- timestamp_without_time_zone_6_precision + NOW(), -- timestamp_with_time_zone + NOW(), -- timestamp_with_time_zone_0_precision + NOW(), -- timestamp_with_time_zone_1_precision + NOW(), -- timestamp_with_time_zone_2_precision + NOW(), -- timestamp_with_time_zone_3_precision + NOW(), -- timestamp_with_time_zone_4_precision + NOW(), -- timestamp_with_time_zone_5_precision + NOW(), -- timestamp_with_time_zone_6_precision + NOW(), -- time_without_time_zone + NOW(), -- time_without_time_zone_0_precision + NOW(), -- time_without_time_zone_1_precision + NOW(), -- time_without_time_zone_2_precision + NOW(), -- time_without_time_zone_3_precision + NOW(), -- time_without_time_zone_4_precision + NOW(), -- time_without_time_zone_5_precision + NOW(), -- time_without_time_zone_6_precision + NOW(), -- time_with_time_zone + NOW(), -- time_with_time_zone_0_precision + NOW(), -- time_with_time_zone_1_precision + NOW(), -- time_with_time_zone_2_precision + NOW(), -- time_with_time_zone_3_precision + NOW(), -- time_with_time_zone_4_precision + NOW(), -- time_with_time_zone_5_precision + NOW() -- time_with_time_zone_6_precision +); + diff --git a/tests.edn b/tests.edn new file mode 100644 index 0000000..9fcc01a --- /dev/null +++ b/tests.edn @@ -0,0 +1,2 @@ +#kaocha/v1 + {:plugins [:kaocha.plugin/cloverage]} \ No newline at end of file