From 79b70390a1e4aebdc5f6ac0405f6204ef6180064 Mon Sep 17 00:00:00 2001 From: gered Date: Sun, 22 May 2016 15:26:20 -0400 Subject: [PATCH] initial commit --- .gitignore | 18 ++++++++ LICENSE | 21 ++++++++++ README.md | 21 ++++++++++ project.clj | 15 +++++++ src/views/sql/core.clj | 95 ++++++++++++++++++++++++++++++++++++++++++ src/views/sql/view.clj | 38 +++++++++++++++++ 6 files changed, 208 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 project.clj create mode 100644 src/views/sql/core.clj create mode 100644 src/views/sql/view.clj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b8f73f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +.DS_Store +/target +/classes +/checkouts +/out +pom.xml +pom.xml.asc +*.jar +*.class +.lein-* +.nrepl-port +/*.project +/*.classpath +/.settings/ +*.iml +*.ipr +*.iws +.idea \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0781dfc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Gered King + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c98254a --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# views-sql + +SQL plugin for the [views][1] library. Allows for plain SQL strings to +be used. + +[1]: https://github.com/gered/views + +Implementation is largely based on the views-honeysql library. + +Note that this library leverages [JSqlParser][2] for parsing SQL +queries and extracting the view system hint information needed. +JSqlParser is not perfect and will not be able to parse some more +complex queries and/or queries using some vendor-specific extensions. + +[2]: https://github.com/JSQLParser/JSqlParser + +## License + +Copyright © 2016 Gered King + +Distributed under the MIT License. diff --git a/project.clj b/project.clj new file mode 100644 index 0000000..a2b5420 --- /dev/null +++ b/project.clj @@ -0,0 +1,15 @@ +(defproject gered/views-sql "0.1.0-SNAPSHOT" + :description "Plain SQL view implementation for views" + :url "https://github.com/gered/views-honeysql" + + :license {:name "MIT License" + :url "http://opensource.org/licenses/MIT"} + + :dependencies [[org.clojure/tools.logging "0.3.1"] + [com.github.jsqlparser/jsqlparser "0.9.5"]] + + :profiles {:provided + {:dependencies + [[org.clojure/clojure "1.8.0"] + [org.clojure/java.jdbc "0.6.1"] + [gered/views "1.5-SNAPSHOT"]]}}) diff --git a/src/views/sql/core.clj b/src/views/sql/core.clj new file mode 100644 index 0000000..dea96ad --- /dev/null +++ b/src/views/sql/core.clj @@ -0,0 +1,95 @@ +(ns views.sql.core + (:import + (net.sf.jsqlparser.parser CCJSqlParserUtil) + (net.sf.jsqlparser.util TablesNamesFinder) + (net.sf.jsqlparser.statement Statement) + (net.sf.jsqlparser.statement.update Update) + (net.sf.jsqlparser.statement.delete Delete) + (net.sf.jsqlparser.statement.insert Insert) + (net.sf.jsqlparser.statement.select Select)) + (:require + [clojure.java.jdbc :as jdbc] + [views.core :as views])) + +(def hint-type :sql-table-name) + +(def ^:private query-types + {Select :select + Insert :insert + Update :update + Delete :delete}) + +(defn- get-query-tables-set + [^Statement stmt] + (as-> (TablesNamesFinder.) x + (.getTableList x stmt) + (map keyword x) + (set x))) + +(defn- sql-stmt-returning? + [^Statement stmt stmt-type] + (condp = stmt-type + :select true + :insert (let [return-expr (.getReturningExpressionList ^Insert stmt) + returning-all? (.isReturningAllColumns ^Insert stmt)] + (or returning-all? + (and return-expr + (not (.isEmpty return-expr))))) + ; TODO: JSqlParser doesn't currently support PostgreSQL's RETURNING clause + ; support in UPDATE and DELETE queries + :update false + :delete false)) + +(defn- get-query-info + [^String sql] + (let [stmt (CCJSqlParserUtil/parse sql) + stmt-type (get query-types (type stmt))] + (if-not stmt-type + (throw (new Exception "Unsupported SQL query. Only SELECT, INSERT, UPDATE and DELETE queries are supported!")) + {:type stmt-type + :returning? (sql-stmt-returning? stmt stmt-type) + :tables (get-query-tables-set stmt)}))) + +(defn query-info + [^String sql] + (if-let [info (get-in @views/view-system [:views-sql :cache sql])] + info + (let [info (get-query-info sql)] + (swap! views/view-system assoc-in [:views-sql :cache sql] info) + info))) + +(defn query-tables + [^String sql] + (:tables (query-info sql))) + +(defmacro with-view-transaction + [binding & forms] + (let [tvar (first binding) + db (second binding) + args (drop 2 binding)] + `(if (:views-sql/hints ~db) ;; check if we are in a nested transaction + (let [~tvar ~db] ~@forms) + (let [hints# (atom []) + result# (jdbc/with-db-transaction [t# ~db ~@args] + (let [~tvar (assoc ~db :views-sql/hints hints#)] + ~@forms))] + (views/put-hints! @hints#) + result#)))) + +(defn- execute-sql! + [db [sql & params :as sqlvec]] + (let [info (query-info sql)] + (if (:returning? info) + (jdbc/query db sqlvec) + (jdbc/execute! db sqlvec)))) + +(defn vexec! + ([db namespace [sql & params :as sqlvec]] + (let [results (execute-sql! db sqlvec) + hint (views/hint namespace (query-tables sql) hint-type)] + (if-let [tx-hints (:views-sql/hints db)] + (swap! tx-hints conj hint) + (views/put-hints! [hint])) + results)) + ([db [sql & params :as sqlvec]] + (vexec! db nil sqlvec))) diff --git a/src/views/sql/view.clj b/src/views/sql/view.clj new file mode 100644 index 0000000..569daaa --- /dev/null +++ b/src/views/sql/view.clj @@ -0,0 +1,38 @@ +(ns views.sql.view + (:require + [views.protocols :refer [IView]] + [views.sql.core :refer [hint-type query-tables]] + [clojure.set :refer [intersection]] + [clojure.java.jdbc :as jdbc] + [clojure.tools.logging :refer [warn]])) + +; this implementation based on views-honeysql + +(defrecord SQLView [id db-or-db-fn query-fn row-fn] + IView + (id [_] id) + (data [_ namespace parameters] + (let [db (if (fn? db-or-db-fn) + (db-or-db-fn namespace) + db-or-db-fn) + start (System/currentTimeMillis) + data (jdbc/query db (apply query-fn parameters) {:row-fn row-fn}) + time (- (System/currentTimeMillis) start)] + (when (>= time 1000) (warn id "took" time "msecs")) + data)) + (relevant? [_ namespace parameters hints] + (let [[sql & sqlparams] (apply query-fn parameters) + tables (query-tables sql) + nhints (filter #(and (= namespace (:namespace %)) + (= hint-type (:type %))) hints)] + (boolean (some #(not-empty (intersection (:hint %) tables)) nhints))))) + +(defn view + "Creates a SQL view that uses a JDBC database configuration. + + db-or-db-fn - either a database connection map, or a function that will get passed + a namespace and should return a database connection map + sql-fn - a function that returns a sqlvec format SELECT query to be run when + this view is refreshed" + [id db-or-db-fn sql-fn & {:keys [row-fn]}] + (SQLView. id db-or-db-fn sql-fn (or row-fn identity))) \ No newline at end of file