├── .env ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── deps.edn ├── doc └── intro.md ├── docker-compose.yaml ├── project.clj ├── runme ├── src └── ql │ ├── core.cljc │ ├── insert.cljc │ ├── method.cljc │ ├── pg │ ├── core.cljc │ ├── jsonb.clj │ └── string.cljc │ ├── pretty_sql.cljc │ ├── select.cljc │ └── update.cljc └── test ├── ql ├── core_test.clj ├── insert_test.clj ├── method_test.clj ├── pg │ ├── core_test.clj │ └── jsonb_test.clj └── select_test.clj ├── scratchpad_test.clj └── testdb.clj /.env: -------------------------------------------------------------------------------- 1 | export PGPASSWORD=verysecret 2 | export PGHOST=localhost 3 | export PGDATABASE=ql 4 | export PGPORT=5447 5 | export PGHOST=localhost 6 | export PGUSER=postgres 7 | 8 | export DATABASE_URL="jdbc:postgresql://$PGHOST:$PGPORT/$PGDATABASE?user=$PGUSER&password=$PGPASSWORD" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | .cpcache/ 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | addons: 3 | postgresql: "10" 4 | apt: 5 | packages: 6 | - postgresql-10 7 | - postgresql-client-10 8 | # services: 9 | # - postgresql 10 | env: 11 | global: 12 | - PGPORT=5433 13 | before_script: 14 | - sudo -u postgres psql -c "CREATE USER test WITH PASSWORD 'test'" 15 | - sudo -u postgres psql -c 'ALTER ROLE test WITH SUPERUSER' 16 | - sudo -u postgres psql -c 'CREATE DATABASE test_db;' -U postgres 17 | script: env DATABASE_URL="jdbc:postgresql://localhost:5433/test_db?user=test&password=test" lein test 18 | jdk: 19 | - oraclejdk8 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [Unreleased] 5 | ### Changed 6 | - Add a new arity to `make-widget-async` to provide a different widget shape. 7 | 8 | ## [0.1.1] - 2018-03-26 9 | ### Changed 10 | - Documentation on how to make the widgets. 11 | 12 | ### Removed 13 | - `make-widget-sync` - we're all async, all the time. 14 | 15 | ### Fixed 16 | - Fixed widget maker to keep working when daylight savings switches over. 17 | 18 | ## 0.1.0 - 2018-03-26 19 | ### Added 20 | - Files from the new template. 21 | - Widget maker public API - `make-widget-sync`. 22 | 23 | [Unreleased]: https://github.com/your-name/ql/compare/0.1.1...HEAD 24 | [0.1.1]: https://github.com/your-name/ql/compare/0.1.0...0.1.1 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor to control, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ql 2 | 3 | ## data ast for sql, aka honeysql as base for dsl's on top of it 4 | 5 | [![Build Status](https://travis-ci.org/niquola/ql.svg?branch=master)](https://travis-ci.org/niquola/ql) 6 | 7 | [![Clojars Project](https://img.shields.io/clojars/v/ql.svg)](https://clojars.org/ql) 8 | 9 | 10 | [honeysql](https://github.com/jkk/honeysql) is an awesome idea, but.... 11 | 12 | * composability - it should be easy compose expressions into sql query 13 | * extendibility - to extend - just add one multi-method ql.method/to-sql 14 | * pure functional implementation - sql generation as a tree reduction 15 | * implicit params - manage params style jdbc, postgres, inline 16 | * use namespaced keywords 17 | * validation by clojure.spec 18 | * prefer hash-map over vector (support both, where vector is just sugar) 19 | * dsl's on top of it 20 | 21 | ## Usage 22 | 23 | ```clj 24 | (require '[ql.core :as ql :refer [sql]]) 25 | 26 | (ql/sql 27 | #:ql{:select {:name :u.name} 28 | :from {:u :user} 29 | :where {:by-id [:ql/= :u.id [:ql/param 5]]}}) 30 | 31 | ;;=> 32 | { 33 | :sql = "SELECT u.name AS name FROM u user WHERE /** by-id **/ ( u.id = ? )" 34 | :params = [ 5 ] 35 | } 36 | 37 | ``` 38 | 39 | In the example above `:ql/type :ql/select` is omitted. For root node if no 40 | `:ql/type` provided `:ql/select` is used by default. 41 | 42 | Insert with json and string values example: 43 | 44 | ```clj 45 | (sql 46 | #:ql{:type :ql/insert 47 | :table_name :db_table_name 48 | :value {:column_a {:ql/type :ql/jsonb 49 | :key [:some :values]} 50 | :column_b "value-b"} 51 | :returning :*}) 52 | ;; => 53 | {:sql "INSERT INTO db_table_name ( column_a , column_b ) VALUES ( $JSON${\"key\":[\"some\",\"values\"]}$JSON$ , 'value-b' ) RETURNING *" 54 | :params [] 55 | :opts nil} 56 | ``` 57 | 58 | Pretty printing sql with `{:format :pretty}`: 59 | 60 | ```clj 61 | (:sql (ql/sql 62 | {:ql/select {:a :a 63 | :b :b} 64 | :ql/from {:ql/type :ql/select :ql/select :* :ql/from :user} 65 | :ql/where [:ql/= :user.id 1]} {:format :pretty})) 66 | ;; => SELECT 67 | ;; a AS a , 68 | ;; b AS b 69 | ;; FROM 70 | ;; ( 71 | ;; SELECT 72 | ;; * 73 | ;; FROM 74 | ;; user 75 | ;; ) 76 | ;; WHERE 77 | ;; user.id = 1 78 | 79 | ``` 80 | 81 | Extend select query with `:mssql/options` clause: 82 | 83 | ```clj 84 | (defmethod ql.method/to-sql :mssql/options 85 | [acc expr] 86 | (ql.method/reduce-separated 87 | "," 88 | (fn [acc [k v]] 89 | (-> acc 90 | (ql.method/conj-sql (name k) "=") 91 | (ql.method/to-sql v))) 92 | acc (dissoc expr :ql/type))) 93 | 94 | (ql/sql 95 | {:ql/type :ql/select 96 | :ql/select :* 97 | :ql/from :user 98 | :mssql/options {:a 1}} 99 | (ql.method/add-clause ql/default-opts 100 | :ql/select 101 | :before 102 | :ql/order-by 103 | {:key :mssql/options 104 | :default-type :mssql/options 105 | :token "OPTIONS"})) 106 | ;; => {:sql "SELECT * FROM user OPTIONS a = 1", :params [], :opts ...} 107 | ``` 108 | 109 | ## How it works 110 | 111 | `ql` is a data-driven DSL, which converts tree structure into SQL string with 112 | placeholders and vector of params for following usage with db engine. 113 | 114 | Main building blocks are hash-maps with metainformation provided by qualified 115 | keywords with `ql` namespace. Also, vectors are supported as a syntax sugar. 116 | 117 | Examples: 118 | ```clj 119 | (sql {:ql/type :ql/= 120 | :left "str" 121 | :middle "test" 122 | :right 123}) 123 | ;; => {:sql "'str' = 123", :params [], :opts nil} 124 | (sql [:ql/= "str" 123 "another test"]) 125 | ;; => {:sql "'str' = 123", :params [], :opts nil} 126 | ``` 127 | 128 | As demonstrated in the example above language can contain data of arbitral type, 129 | but this type must be acceptable by `to-sql` multhimethod. 130 | 131 | ```clj 132 | (sql 123) 133 | ;; => {:sql "123", :params [], :opts nil} 134 | (sql :keyword) 135 | ;; => {:sql "keyword", :params [], :opts nil} 136 | (sql {:ql/type :ql/jsonb 137 | :key :value}) 138 | ;; => {:sql "$JSON${\"key\":\"value\"}$JSON$", :params [], :opts nil} 139 | ``` 140 | 141 | `to-sql` accepts two parameters `partial-result` and `value-to-parse`. 142 | `partial-result` is a hash-map with two keys `:sql` and `:params`, which 143 | represent current state of parsing (parts of sql string with placeholders and 144 | vector of parameters respectevly). 145 | 146 | Parsing process starts from `sql` function, which calls `to-sql` with empty 147 | `partial-result` and root node parameters. For traversing tree structure kind of 148 | dfs is used. On each step type of the node is determined based on the following 149 | info: 150 | 151 | - For hash-map `:ql/type` value 152 | - For vector first element 153 | - `type` function for other object 154 | 155 | Node type is used to call proper `to-sql` method. It updates current 156 | `partial-result` and calls `to-sql` for child nodes. Order is determined by 157 | internal implementation of each `to-sql` method. 158 | 159 | After traversal, tokens in `:sql` are joined using " " and sql string is ready 160 | for use. Using `{:format :jdbc}` result will be converted in format suitable for 161 | jdbc. 162 | 163 | ```clj 164 | (sql [:ql/= "str" 123] {:style :honeysql 165 | :format :jdbc}) 166 | ;; => ["? = 123" "str"] 167 | ``` 168 | 169 | More detailed information can be found in [these](./src/ql/core.cljc) [files](./src/ql/method.cljc). 170 | 171 | ## Development 172 | 173 | ``` 174 | source .env 175 | docker-compose up -d 176 | lein repl 177 | ``` 178 | 179 | 180 | ## License 181 | 182 | Copyright © 2018 niquola 183 | 184 | Distributed under the Eclipse Public License either version 1.0 or (at 185 | your option) any later version. 186 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {org.clojure/clojure {:mvn/version "1.10.0-alpha4"}} 3 | 4 | :aliases {:dev {:extra-paths ["test"] 5 | :extra-deps {matcho {:mvn/version "0.1.0-RC6"} 6 | org.clojure/java.jdbc {:mvn/version "0.6.1"} 7 | cheshire {:mvn/version "5.6.3"} 8 | org.postgresql/postgresql {:mvn/version "9.4.1211.jre7"}}}}} 9 | -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to ql 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) 4 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | db: 4 | image: postgres:10.3 5 | container_name: qldb 6 | ports: 7 | - "${PGPORT}:5432" 8 | environment: 9 | POSTGRES_USER: ${PGUSER} 10 | POSTGRES_DB: ${PGDATABASE} 11 | POSTGRES_PASSWORD: ${PGPASSWORD} 12 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject ql "0.0.1-SNAPSHOT" 2 | :description "data dsl for sql generation" 3 | :url "http://example.com/FIXME" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.9.0"]] 7 | :repositories [["clojars" {:url "https://clojars.org/repo" 8 | :sign-releases false}]] 9 | :profiles {:dev {:dependencies [[matcho "0.1.0-RC6"] 10 | [org.clojure/java.jdbc "0.6.1"] 11 | [cheshire "5.6.3"] 12 | [org.postgresql/postgresql "9.4.1211.jre7"]]}}) 13 | -------------------------------------------------------------------------------- /runme: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ProgName=$(basename $0) 4 | 5 | sub_help(){ 6 | echo "Usage: $ProgName [options]\n" 7 | echo "Subcommands:" 8 | echo " repl run repl" 9 | echo " tests run tests" 10 | echo "" 11 | echo "For help with each subcommand run:" 12 | echo "$ProgName -h|--help" 13 | echo "" 14 | } 15 | 16 | sub_repl(){ 17 | 18 | echo "Starting repl..." 19 | 20 | clj \ 21 | -A:dev \ 22 | -Sdeps '{:deps {org.clojure/tools.nrepl {:mvn/version "0.2.13"} refactor-nrepl {:mvn/version "2.4.0-SNAPSHOT"} cider/cider-nrepl {:mvn/version "0.17.0-SNAPSHOT"}}}' \ 23 | -e '(require (quote cider-nrepl.main)) (cider-nrepl.main/init ["refactor-nrepl.middleware/wrap-refactor", "cider.nrepl/cider-middleware"])' 24 | } 25 | 26 | sub_build(){ 27 | clj -Sdeps '{:deps {pack/pack.alpha {:git/url "https://github.com/juxt/pack.alpha.git" :sha "bb2c5a2c78aca9328e023b029c06ba0efdd1e3b7"}}}' \ 28 | -m mach.pack.alpha.capsule deps.edn proto.jar build_dir proto 0.0.1 29 | } 30 | 31 | sub_tests(){ 32 | echo "Running tests..." 33 | 34 | cat << EOF | 35 | -d, --dir DIRNAME Name of the directory containing tests. Defaults to "test". 36 | -n, --namespace SYMBOL Symbol indicating a specific namespace to test. 37 | -v, --var SYMBOL Symbol indicating the fully qualified name of a specific test. 38 | -i, --include KEYWORD Run only tests that have this metadata keyword. 39 | -e, --exclude KEYWORD Exclude tests with this metadata keyword. 40 | -h, --help Display this help message 41 | EOF 42 | 43 | clj -A:dev \ 44 | -Sdeps '{:deps {com.cognitect/test-runner {:git/url "https://github.com/levand/test-runner" :sha "5fb4fc46ad0bf2e0ce45eba5b9117a2e89166479"}}}' \ 45 | -m cognitect.test-runner 46 | 47 | } 48 | 49 | subcommand=$1 50 | case $subcommand in 51 | "" | "-h" | "--help") 52 | sub_help 53 | ;; 54 | *) 55 | shift 56 | sub_${subcommand} $@ 57 | if [ $? = 127 ]; then 58 | echo "Error: '$subcommand' is not a known subcommand." >&2 59 | echo " Run '$ProgName --help' for a list of known subcommands." >&2 60 | exit 1 61 | fi 62 | ;; 63 | esac -------------------------------------------------------------------------------- /src/ql/core.cljc: -------------------------------------------------------------------------------- 1 | (ns ql.core 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.spec.test.alpha :as stest] 4 | [clojure.string :as str] 5 | [ql.select] 6 | [ql.pretty-sql :as pretty-sql] 7 | [ql.insert] 8 | [ql.pg.core] 9 | [ql.method :refer [conj-param conj-sql operator-args to-sql]])) 10 | 11 | (def default-opts (merge ql.select/default-opts)) 12 | 13 | (defmethod to-sql :ql/param 14 | [acc expr] 15 | (let [v (if (vector? expr) (second expr) (:ql/value expr))] 16 | (conj-param acc v))) 17 | 18 | (defmethod to-sql :ql/string 19 | [acc expr] 20 | (conj-sql acc (str "$str$" (:ql/value expr) "$str$"))) 21 | 22 | (defmethod to-sql :ql/= 23 | [acc expr] 24 | (let [[_ a b] (operator-args expr)] 25 | (-> acc 26 | (to-sql a) 27 | (conj-sql "=") 28 | (to-sql b)))) 29 | 30 | (defmethod to-sql :ql/<> 31 | [acc [_ a b]] 32 | (-> acc 33 | (to-sql a) 34 | (conj-sql "<>") 35 | (to-sql b))) 36 | 37 | (defmethod to-sql :ql/* 38 | [acc _] 39 | (conj-sql acc "SELECT" "*")) 40 | 41 | (defmethod to-sql java.lang.Long 42 | [acc expr] 43 | (conj-sql acc (str expr))) 44 | 45 | 46 | (defmethod to-sql java.lang.String 47 | [acc expr] 48 | (if (= :honeysql (get-in acc [:opts :style])) 49 | (conj-param acc expr) 50 | (conj-sql acc (str "'" expr "'")))) 51 | 52 | (defmethod to-sql nil 53 | [acc expr] 54 | (conj-sql acc "NULL")) 55 | 56 | (defmethod to-sql clojure.lang.Keyword 57 | [acc expr] 58 | (conj-sql acc (name expr))) 59 | 60 | 61 | (defmethod to-sql :ql/ident 62 | [acc args] 63 | ;; TODO: do escaping 64 | (conj-sql acc 65 | (str "\"" 66 | (name (if (vector? args) 67 | (second args ) 68 | (:ql/value args))) 69 | "\""))) 70 | 71 | 72 | (defn sql [expr & [opts]] 73 | (let [res (-> 74 | {:sql [] :pretty-sql [] :params [] :opts (merge default-opts opts)} 75 | (to-sql (if (map? expr) 76 | (update expr :ql/type (fn [x] (if x x :ql/select))) 77 | expr)) 78 | (update :sql (fn [x] (str/join " " x))))] 79 | (case (:format opts) 80 | :jdbc (into [(:sql res)] (:params res)) 81 | :pretty (assoc res :sql (pretty-sql/make-pretty-sql (:pretty-sql res))) 82 | res))) 83 | 84 | -------------------------------------------------------------------------------- /src/ql/insert.cljc: -------------------------------------------------------------------------------- 1 | (ns ql.insert 2 | (:require [clojure.string :as str] 3 | [ql.method :refer [to-sql conj-sql conj-param reduce-separated clear-ql-keys]])) 4 | 5 | 6 | (defmethod to-sql :ql/value 7 | [acc expr] 8 | (let [ks (keys (clear-ql-keys expr)) 9 | acc (conj-sql acc "(") 10 | acc (reduce-separated "," (fn [acc k] (conj-sql acc (name k))) acc ks) 11 | acc (conj-sql acc ")" "VALUES" "(") 12 | acc (reduce-separated 13 | "," 14 | (fn [acc k] (to-sql acc (get expr k))) acc ks) 15 | 16 | acc (conj-sql acc ")")] 17 | acc)) 18 | 19 | (defmethod to-sql :ql/insert 20 | [acc expr] 21 | (reduce (fn [acc {prep :token k :key}] 22 | (if-let [v (get expr k)] 23 | (cond-> 24 | acc 25 | prep (conj-sql prep) 26 | true (to-sql (if (and (map? v) (not (:ql/type v))) (assoc v :ql/type k) v))) 27 | acc)) 28 | acc [{:key :ql/table_name 29 | :token "INSERT INTO"} 30 | {:key :ql/values} 31 | {:key :ql/value} 32 | {:key :ql/returning 33 | :token "RETURNING"}])) 34 | 35 | (defmethod to-sql :ql/truncate 36 | [acc expr] 37 | (reduce (fn [acc [prep k]] 38 | (if-let [v (get expr k)] 39 | (cond-> 40 | acc 41 | prep (conj-sql prep) 42 | true (to-sql (if (and (map? v) (not (:ql/type v))) (assoc v :ql/type k) v))) 43 | acc)) 44 | acc [["TRUNCATE" :ql/table_name]])) 45 | 46 | (defmethod to-sql :ql/drop-table 47 | [acc expr] 48 | (reduce (fn [acc [prep k]] 49 | (if-let [v (get expr k)] 50 | (cond-> 51 | acc 52 | prep (conj-sql prep) 53 | true (to-sql (if (and (map? v) (not (:ql/type v))) (assoc v :ql/type k) v))) 54 | acc)) 55 | acc [[(str "DROP TABLE" (when (:ql/if-exists expr) " IF EXISTS")) :ql/table_name]])) 56 | 57 | (defmethod to-sql :ql/column 58 | [acc expr] 59 | (apply conj-sql acc 60 | (->> [(:ql/column-type expr) (when (:ql/primary-key expr) "PRIMARY KEY")] 61 | (filterv identity) 62 | (mapv name)))) 63 | 64 | (defmethod to-sql :ql/columns 65 | [acc expr] 66 | (let [cols (sort-by #(second (:ql/weight %)) (clear-ql-keys expr))] 67 | (-> 68 | (reduce-separated 69 | "," 70 | (fn [acc [k v]] 71 | (-> acc 72 | (conj-sql (name k)) 73 | (to-sql (update v :ql/type (fn [x] (if x x :ql/column)))))) 74 | (conj-sql acc "(") 75 | cols) 76 | (conj-sql ")")))) 77 | 78 | (defmethod to-sql :ql/create-table 79 | [acc expr] 80 | (reduce (fn [acc [prep k]] 81 | (if-let [v (get expr k)] 82 | (cond-> 83 | acc 84 | prep (conj-sql prep) 85 | true (to-sql (if (and (map? v) (not (:ql/type v))) (assoc v :ql/type k) v))) 86 | acc)) 87 | acc [[(str "CREATE TABLE" (when (:ql/if-not-exists expr) " IF NOT EXISTS")) :ql/table_name] 88 | [nil :ql/columns]])) 89 | -------------------------------------------------------------------------------- /src/ql/method.cljc: -------------------------------------------------------------------------------- 1 | (ns ql.method 2 | (:require [clojure.string :as str] 3 | [ql.pretty-sql :as pretty-sql])) 4 | 5 | (defn dispatch-sql [x] 6 | (cond (map? x) (get x :ql/type) 7 | (vector? x) (first x) 8 | :else (type x))) 9 | 10 | (defn simple-type? [x] 11 | (not (or (vector? x) (map? x)))) 12 | 13 | (defmulti to-sql (fn [{sql :sql params :params} x] (dispatch-sql x))) 14 | 15 | (defn cast-to-sql-string [x] 16 | (if (simple-type? x) 17 | (-> (to-sql {} x) 18 | :sql 19 | first) 20 | (str x))) 21 | 22 | (defn conj-sql [acc & sql] 23 | (let [sql (flatten sql) 24 | plain-sql (filter (complement pretty-sql/pretty-operations) sql) 25 | pretty-sql sql] 26 | (-> acc 27 | (update :sql (fn [x] (apply conj x plain-sql))) 28 | (update :pretty-sql (fn [x] (apply conj x pretty-sql)))))) 29 | 30 | (defn conj-param [acc v] 31 | (if (get-in acc [:opts :inline]) 32 | (conj-sql acc (cast-to-sql-string v)) 33 | (-> acc 34 | (conj-sql "?") 35 | (update :params conj v)))) 36 | 37 | (defn reduce-separated [sep f acc coll] 38 | (loop [acc acc 39 | [x & xs] coll] 40 | (if (nil? xs) 41 | (f acc x) 42 | (recur (conj-sql (f acc x) sep) xs)))) 43 | 44 | (defn comma-separated [ks] 45 | (->> ks 46 | (mapv (fn [x] (if (keyword? x) (name x) (str x)))) 47 | (str/join ","))) 48 | 49 | ;; (comma-separated [:a :b 0]) 50 | 51 | (defn operator-args [opts] 52 | (if (map? opts) 53 | [(:ql/type opts) 54 | (or (:left opts) (get opts 0)) 55 | (or (:right opts) (get opts 1))] 56 | opts)) 57 | 58 | (namespace :ql/ups) 59 | 60 | (defn clear-ql-keys [m] 61 | (reduce (fn [m [k v]] 62 | (if (= "ql" (namespace k)) 63 | m (assoc m k v))) {} m)) 64 | 65 | 66 | (defn only-ql-keys [m] 67 | (reduce (fn [m [k v]] 68 | (if (= "ql" (namespace k)) 69 | (assoc m k v) m)) {} m)) 70 | 71 | (defn add-clause 72 | "Add a new clause to reduce order for specific ql-type, 73 | can be used for extending existing to-sql implementation" 74 | [opts ql-type action clause-key new-clause] 75 | (update-in opts [:reduce-order ql-type] 76 | (fn [a] (reduce #(into %1 (if (= clause-key (:key %2)) 77 | (case action 78 | :before [new-clause %2] 79 | :after [%2 new-clause]) 80 | [%2])) 81 | [] 82 | a)))) 83 | 84 | (only-ql-keys {:ql/a 1 :k 2}) 85 | (clear-ql-keys {:ql/a 1 :k 2}) 86 | -------------------------------------------------------------------------------- /src/ql/pg/core.cljc: -------------------------------------------------------------------------------- 1 | (ns ql.pg.core 2 | (:require 3 | [clojure.string :as str] 4 | [ql.method :refer [to-sql conj-sql conj-param reduce-separated]] 5 | [ql.pg.jsonb] 6 | [ql.pg.string])) 7 | 8 | 9 | (defn comma-separated [ks] 10 | (->> ks 11 | (mapv (fn [x] (if (keyword? x) (name x) (str x)))) 12 | (str/join ","))) 13 | 14 | ;; (comma-separated [:a :b 0]) 15 | 16 | (defn operator-args [opts] 17 | (if (map? opts) 18 | [(:ql/type opts) 19 | (or (:left opts) (get opts 0)) 20 | (or (:right opts) (get opts 1))] 21 | opts)) 22 | 23 | -------------------------------------------------------------------------------- /src/ql/pg/jsonb.clj: -------------------------------------------------------------------------------- 1 | (ns ql.pg.jsonb 2 | (:require 3 | [clojure.string :as str] 4 | [cheshire.core :as json] 5 | [ql.method :refer [to-sql conj-sql conj-param reduce-separated operator-args comma-separated]])) 6 | 7 | (defmethod to-sql :ql/jsonb 8 | [acc expr] 9 | (conj-sql 10 | acc (str "$JSON$" (json/generate-string (ql.method/clear-ql-keys expr)) "$JSON$"))) 11 | 12 | ;; -> int Get JSON array element (indexed from zero, negative integers count from the end) '[{"a":"foo"},{"b":"bar"},{"c":"baz"}]'::json->2 {"c":"baz"} 13 | ;; -> text Get JSON object field by key '{"a": {"b":"foo"}}'::json->'a' {"b":"foo"} 14 | (defmethod to-sql :jsonb/-> 15 | [acc args] 16 | (let [[_ col k] (operator-args args)] 17 | (-> acc 18 | (to-sql col) 19 | (conj-sql (str "->'" (name k) "'"))))) 20 | 21 | ;; ->> int Get JSON array element as text '[1,2,3]'::json->>2 3 22 | ;; ->> text Get JSON object field as text '{"a":1,"b":2}'::json->>'b' 2 23 | 24 | (defmethod to-sql :jsonb/->> 25 | [acc args] 26 | (let [[_ col k] (operator-args args)] 27 | (-> acc 28 | (to-sql col) 29 | (conj-sql (str "->>'" (name k) "'"))))) 30 | 31 | ;; #> text[] Get JSON object at specified path '{"a": {"b":{"c": "foo"}}}'::json#>'{a,b}' {"c": "foo"} 32 | 33 | (defmethod to-sql :jsonb/#> 34 | [acc args] 35 | (let [[_ col ks] (operator-args args)] 36 | (-> acc 37 | (to-sql col) 38 | (conj-sql (str "#>'{" (comma-separated ks) "}'"))))) 39 | 40 | ;; #>> text[] Get JSON object at specified path as text '{"a":[1,2,3],"b":[4,5,6]}'::json#>>'{a,2}' 3 41 | (defmethod to-sql :jsonb/#>> 42 | [acc args] 43 | (let [[_ col ks] (operator-args args)] 44 | (-> acc 45 | (to-sql col) 46 | (conj-sql (str "#>>'{" (comma-separated ks) "}'"))))) 47 | 48 | 49 | 50 | ;; @> jsonb Does the left JSON value contain the right JSON path/value entries at the top level? '{"a":1, "b":2}'::jsonb @> '{"b":2}'::jsonb 51 | ;; <@ jsonb Are the left JSON path/value entries contained at the top level within the right JSON value? '{"b":2}'::jsonb <@ '{"a":1, "b":2}'::jsonb 52 | ;; ? text Does the string exist as a top-level key within the JSON value? '{"a":1, "b":2}'::jsonb ? 'b' 53 | ;; ?| text[] Do any of these array strings exist as top-level keys? '{"a":1, "b":2, "c":3}'::jsonb ?| array['b', 'c'] 54 | ;; ?& text[] Do all of these array strings exist as top-level keys? '["a", "b"]'::jsonb ?& array['a', 'b'] 55 | ;; || jsonb Concatenate two jsonb values into a new jsonb value '["a", "b"]'::jsonb || '["c", "d"]'::jsonb 56 | 57 | (defmethod to-sql :jsonb/|| 58 | [acc args] 59 | (let [[_ a b] (operator-args args)] 60 | (-> acc 61 | (to-sql a) 62 | (conj-sql "||") 63 | (to-sql b)))) 64 | 65 | ;; jsonb_agg(expression) any jsonb No aggregates values as a JSON array 66 | (defmethod to-sql :jsonb/agg 67 | [acc args] 68 | (let [expr (cond (map? args) 69 | (:expression args) 70 | (coll? args) 71 | (second args) 72 | :else (assert false (pr-str "Unexpected args for jsonb/agg " )))] 73 | (-> acc 74 | (conj-sql "jsonb_agg(") 75 | (to-sql expr) 76 | (conj-sql ")")))) 77 | 78 | ;; - text Delete key/value pair or string element from left operand. Key/value pairs are matched based on their key value. '{"a": "b"}'::jsonb - 'a' 79 | ;; - integer Delete the array element with specified index (Negative integers count from the end). Throws an error if top level container is not an array. '["a", "b"]'::jsonb - 1 80 | ;; - text[] Delete multiple key/value pairs or string elements from left operand. Key/value pairs are matched based on their key value. '{"a": "b", "c": "d"}'::jsonb - '{a,c}'::text[] 81 | ;; #- text[] Delete the field or element with specified path (for JSON arrays, negative integers count from the end) '["a", {"b":1}]'::jsonb #- '{1,b}' 82 | 83 | 84 | ;; to_json(anyelement) 85 | 86 | ;; to_jsonb(anyelement) 87 | 88 | ;; Returns the value as json or jsonb. Arrays and composites are converted (recursively) to arrays and objects; otherwise, if there is a cast from the type to json, the cast function will be used to perform the conversion; otherwise, a scalar value is produced. For any scalar type other than a number, a Boolean, or a null value, the text representation will be used, in such a fashion that it is a valid json or jsonb value. to_json('Fred said "Hi."'::text) "Fred said \"Hi.\"" 89 | ;; array_to_json(anyarray [, pretty_bool]) Returns the array as a JSON array. A PostgreSQL multidimensional array becomes a JSON array of arrays. Line feeds will be added between dimension-1 elements if pretty_bool is true. array_to_json('{{1,5},{99,100}}'::int[]) [[1,5],[99,100]] 90 | ;; row_to_json(record [, pretty_bool]) Returns the row as a JSON object. Line feeds will be added between level-1 elements if pretty_bool is true. row_to_json(row(1,'foo')) {"f1":1,"f2":"foo"} 91 | ;; json_build_array(VARIADIC "any") 92 | 93 | ;; jsonb_build_array(VARIADIC "any") 94 | 95 | ;; Builds a possibly-heterogeneously-typed JSON array out of a variadic argument list. json_build_array(1,2,'3',4,5) [1, 2, "3", 4, 5] 96 | ;; json_build_object(VARIADIC "any") 97 | 98 | ;; jsonb_build_object(VARIADIC "any") 99 | 100 | 101 | ;; jsonb_object(text[]) 102 | ;; Builds a JSON object out of a text array. The array must have either exactly one dimension with an even number of members, in which case they are taken as alternating key/value pairs, or two dimensions such that each inner array has exactly two elements, which are taken as a key/value pair. 103 | ;; json_object('{a, 1, b, "def", c, 3.5}') 104 | ;; json_object('{{a, 1},{b, "def"},{c, 3.5}}') 105 | 106 | ;; {"a": "1", "b": "def", "c": "3.5"} 107 | ;; json_object(keys text[], values text[]) 108 | 109 | ;; jsonb_object(keys text[], values text[]) This form of json_object takes keys 110 | ;; and values pairwise from two separate arrays. In all other respects it is 111 | ;; identical to the one-argument form. json_object('{a, b}', '{1,2}') 112 | ;; {"a": "1", "b": "2"} 113 | 114 | 115 | (defmethod to-sql :jsonb/build-object 116 | [acc obj] 117 | (let [strip-nulls? (:jsonb/strip-nulls obj) 118 | acc (if strip-nulls? (conj-sql acc "jsonb_strip_nulls(") acc) 119 | acc (-> acc (conj-sql "jsonb_build_object("))] 120 | (-> 121 | (reduce-separated 122 | "," 123 | (fn [acc [k v]] 124 | (-> acc 125 | (to-sql (if (keyword? k) (name k) k)) 126 | (conj-sql ",") 127 | (to-sql v))) 128 | acc (dissoc obj :ql/type :jsonb/strip-nulls)) 129 | (conj-sql (if strip-nulls? "))" ")"))))) 130 | -------------------------------------------------------------------------------- /src/ql/pg/string.cljc: -------------------------------------------------------------------------------- 1 | (ns ql.pg.string 2 | (:require 3 | [clojure.string :as str] 4 | [ql.method :refer [to-sql conj-sql conj-param reduce-separated]])) 5 | 6 | ;; string || string text String concatenation 'Post' || 'greSQL' PostgreSQL 7 | ;; string || non-string or non-string || string text String concatenation with one non-string input 'Value: ' || 42 Value: 42 8 | (defmethod to-sql :pg/str 9 | [acc & parts] 10 | ) 11 | 12 | ;; bit_length(string) int Number of bits in string bit_length('jose') 32 13 | (defmethod to-sql :pg/bit-length 14 | [acc & parts] 15 | ) 16 | 17 | ;; char_length(string) or character_length(string) int Number of characters in string char_length('jose') 4 18 | (defmethod to-sql :pg/char-length 19 | [acc & parts] 20 | ) 21 | 22 | 23 | ;; lower(string) text Convert string to lower case lower('TOM') tom 24 | (defmethod to-sql :pg/lower 25 | [acc & parts] 26 | ) 27 | ;; octet_length(string) int Number of bytes in string octet_length('jose') 4 28 | (defmethod to-sql :pg/octet_length 29 | [acc & parts] 30 | ) 31 | ;; overlay(string placing string from int [for int]) text Replace substring overlay('Txxxxas' placing 'hom' from 2 for 4) Thomas 32 | 33 | (defmethod to-sql :pg/octet_length 34 | [acc {s :string p :placing f :from i :for}] 35 | ) 36 | 37 | ;; position(substring in string) int Location of specified substring position('om' in 'Thomas') 3 38 | (defmethod to-sql :pg/position 39 | [acc {s :substring i :in}] 40 | ) 41 | 42 | ;; substring(string [from int] [for int]) text Extract substring substring('Thomas' from 2 for 3) hom 43 | ;; substring(string from pattern) text Extract substring matching POSIX regular expression. See Section 9.7 for more information on pattern matching. substring('Thomas' from '...$') mas 44 | ;; substring(string from pattern for escape) text Extract substring matching SQL regular expression. See Section 9.7 for more information on pattern matching. substring('Thomas' from '%#"o_a#"_' for '#') oma 45 | (defmethod to-sql :pg/substring 46 | [acc {s :string f :from i :for}] 47 | ) 48 | 49 | ;; trim([leading | trailing | both] [characters] from string) text Remove the longest string containing only characters from characters (a space by default) from the start, end, or both ends (both is the default) of string trim(both 'xyz' from 'yxTomxx') Tom 50 | ;; trim([leading | trailing | both] [from] string [, characters] ) text Non-standard syntax for trim() trim(both from 'yxTomxx', 'xyz') Tom 51 | (defmethod to-sql :pg/trim 52 | [acc {l :leading t :traling b :both f :from c :characters}] 53 | ) 54 | 55 | ;; upper(string) text Convert string to upper case upper('tom') TOM 56 | (defmethod to-sql :pg/upper 57 | [acc s] 58 | 59 | ) 60 | 61 | ;; ascii(string) int ASCII code of the first character of the argument. For UTF8 returns the Unicode code point of the character. For other multibyte encodings, the argument must be an ASCII character. ascii('x') 120 62 | ;; btrim(string text [, characters text]) text Remove the longest string consisting only of characters in characters (a space by default) from the start and end of string btrim('xyxtrimyyx', 'xyz') trim 63 | ;; chr(int) text Character with the given code. For UTF8 the argument is treated as a Unicode code point. For other multibyte encodings the argument must designate an ASCII character. The NULL (0) character is not allowed because text data types cannot store such bytes. chr(65) A 64 | ;; concat(str "any" [, str "any" [, ...] ]) text Concatenate the text representations of all the arguments. NULL arguments are ignored. concat('abcde', 2, NULL, 22) abcde222 65 | ;; concat_ws(sep text, str "any" [, str "any" [, ...] ]) text Concatenate all but the first argument with separators. The first argument is used as the separator string. NULL arguments are ignored. concat_ws(',', 'abcde', 2, NULL, 22) abcde,2,22 66 | ;; convert(string bytea, src_encoding name, dest_encoding name) bytea Convert string to dest_encoding. The original encoding is specified by src_encoding. The string must be valid in this encoding. Conversions can be defined by CREATE CONVERSION. Also there are some predefined conversions. See Table 9.10 for available conversions. convert('text_in_utf8', 'UTF8', 'LATIN1') text_in_utf8 represented in Latin-1 encoding (ISO 8859-1) 67 | ;; convert_from(string bytea, src_encoding name) text Convert string to the database encoding. The original encoding is specified by src_encoding. The string must be valid in this encoding. convert_from('text_in_utf8', 'UTF8') text_in_utf8 represented in the current database encoding 68 | ;; convert_to(string text, dest_encoding name) bytea Convert string to dest_encoding. convert_to('some text', 'UTF8') some text represented in the UTF8 encoding 69 | ;; decode(string text, format text) bytea Decode binary data from textual representation in string. Options for format are same as in encode. decode('MTIzAAE=', 'base64') \x3132330001 70 | ;; encode(data bytea, format text) text Encode binary data into a textual representation. Supported formats are: base64, hex, escape. escape converts zero bytes and high-bit-set bytes to octal sequences (\nnn) and doubles backslashes. encode(E'123\\000\\001', 'base64') MTIzAAE= 71 | ;; format(formatstr text [, formatarg "any" [, ...] ]) text Format arguments according to a format string. This function is similar to the C function sprintf. See Section 9.4.1. format('Hello %s, %1$s', 'World') Hello World, World 72 | ;; initcap(string) text Convert the first letter of each word to upper case and the rest to lower case. Words are sequences of alphanumeric characters separated by non-alphanumeric characters. initcap('hi THOMAS') Hi Thomas 73 | ;; left(str text, n int) text Return first n characters in the string. When n is negative, return all but last |n| characters. left('abcde', 2) ab 74 | ;; length(string) int Number of characters in string length('jose') 4 75 | ;; length(string bytea, encoding name ) int Number of characters in string in the given encoding. The string must be valid in this encoding. length('jose', 'UTF8') 4 76 | ;; lpad(string text, length int [, fill text]) text Fill up the string to length length by prepending the characters fill (a space by default). If the string is already longer than length then it is truncated (on the right). lpad('hi', 5, 'xy') xyxhi 77 | ;; ltrim(string text [, characters text]) text Remove the longest string containing only characters from characters (a space by default) from the start of string ltrim('zzzytest', 'xyz') test 78 | ;; md5(string) text Calculates the MD5 hash of string, returning the result in hexadecimal md5('abc') 900150983cd24fb0 d6963f7d28e17f72 79 | ;; parse_ident(qualified_identifier text [, strictmode boolean DEFAULT true ] ) text[] Split qualified_identifier into an array of identifiers, removing any quoting of individual identifiers. By default, extra characters after the last identifier are considered an error; but if the second parameter is false, then such extra characters are ignored. (This behavior is useful for parsing names for objects like functions.) Note that this function does not truncate over-length identifiers. If you want truncation you can cast the result to name[]. parse_ident('"SomeSchema".someTable') {SomeSchema,sometable} 80 | ;; pg_client_encoding() name Current client encoding name pg_client_encoding() SQL_ASCII 81 | ;; quote_ident(string text) text Return the given string suitably quoted to be used as an identifier in an SQL statement string. Quotes are added only if necessary (i.e., if the string contains non-identifier characters or would be case-folded). Embedded quotes are properly doubled. See also Example 42.1. quote_ident('Foo bar') "Foo bar" 82 | ;; quote_literal(string text) text Return the given string suitably quoted to be used as a string literal in an SQL statement string. Embedded single-quotes and backslashes are properly doubled. Note that quote_literal returns null on null input; if the argument might be null, quote_nullable is often more suitable. See also Example 42.1. quote_literal(E'O\'Reilly') 'O''Reilly' 83 | ;; quote_literal(value anyelement) text Coerce the given value to text and then quote it as a literal. Embedded single-quotes and backslashes are properly doubled. quote_literal(42.5) '42.5' 84 | ;; quote_nullable(string text) text Return the given string suitably quoted to be used as a string literal in an SQL statement string; or, if the argument is null, return NULL. Embedded single-quotes and backslashes are properly doubled. See also Example 42.1. quote_nullable(NULL) NULL 85 | ;; quote_nullable(value anyelement) text Coerce the given value to text and then quote it as a literal; or, if the argument is null, return NULL. Embedded single-quotes and backslashes are properly doubled. quote_nullable(42.5) '42.5' 86 | ;; regexp_match(string text, pattern text [, flags text]) text[] Return captured substring(s) resulting from the first match of a POSIX regular expression to the string. See Section 9.7.3 for more information. regexp_match('foobarbequebaz', '(bar)(beque)') {bar,beque} 87 | ;; regexp_matches(string text, pattern text [, flags text]) setof text[] Return captured substring(s) resulting from matching a POSIX regular expression to the string. See Section 9.7.3 for more information. regexp_matches('foobarbequebaz', 'ba.', 'g') {bar} 88 | ;; {baz} 89 | 90 | ;; (2 rows) 91 | ;; regexp_replace(string text, pattern text, replacement text [, flags text]) text Replace substring(s) matching a POSIX regular expression. See Section 9.7.3 for more information. regexp_replace('Thomas', '.[mN]a.', 'M') ThM 92 | ;; regexp_split_to_array(string text, pattern text [, flags text ]) text[] Split string using a POSIX regular expression as the delimiter. See Section 9.7.3 for more information. regexp_split_to_array('hello world', E'\\s+') {hello,world} 93 | ;; regexp_split_to_table(string text, pattern text [, flags text]) setof text Split string using a POSIX regular expression as the delimiter. See Section 9.7.3 for more information. regexp_split_to_table('hello world', E'\\s+') hello 94 | ;; world 95 | 96 | ;; (2 rows) 97 | ;; repeat(string text, number int) text Repeat string the specified number of times repeat('Pg', 4) PgPgPgPg 98 | ;; replace(string text, from text, to text) text Replace all occurrences in string of substring from with substring to replace('abcdefabcdef', 'cd', 'XX') abXXefabXXef 99 | ;; reverse(str) text Return reversed string. reverse('abcde') edcba 100 | ;; right(str text, n int) text Return last n characters in the string. When n is negative, return all but first |n| characters. right('abcde', 2) de 101 | ;; rpad(string text, length int [, fill text]) text Fill up the string to length length by appending the characters fill (a space by default). If the string is already longer than length then it is truncated. rpad('hi', 5, 'xy') hixyx 102 | ;; rtrim(string text [, characters text]) text Remove the longest string containing only characters from characters (a space by default) from the end of string rtrim('testxxzx', 'xyz') test 103 | ;; split_part(string text, delimiter text, field int) text Split string on delimiter and return the given field (counting from one) split_part('abc~@~def~@~ghi', '~@~', 2) def 104 | ;; strpos(string, substring) int Location of specified substring (same as position(substring in string), but note the reversed argument order) strpos('high', 'ig') 2 105 | ;; substr(string, from [, count]) text Extract substring (same as substring(string from from for count)) substr('alphabet', 3, 2) ph 106 | ;; to_ascii(string text [, encoding text]) text Convert string to ASCII from another encoding (only supports conversion from LATIN1, LATIN2, LATIN9, and WIN1250 encodings) to_ascii('Karel') Karel 107 | ;; to_hex(number int or bigint) text Convert number to its equivalent hexadecimal representation to_hex(2147483647) 7fffffff 108 | ;; translate(string text, from text, to text) text Any character in string that matches a character in the from set is replaced by the corresponding character in the to set. If from is longer than to, occurrences of the extra characters in from are removed. translate('12345', '143', 'ax') a2x5 109 | 110 | ;; format(formatstr text [, formatarg "any" [, ...] ]) 111 | -------------------------------------------------------------------------------- /src/ql/pretty_sql.cljc: -------------------------------------------------------------------------------- 1 | (ns ql.pretty-sql 2 | (:require [clojure.string :as str])) 3 | 4 | 5 | (def ^:private 6 | example-input ["SELECT" ::newline ::ident "a" "alias" "," ::newline "b" "aliasb" ::newline ::deident 7 | "FROM" ::newline ::ident "user" ::newline ::deident]) 8 | 9 | (def 10 | pretty-operations 11 | #{::newline 12 | ::ident 13 | ::deident}) 14 | 15 | (defn- update-result [result state term] 16 | (-> result 17 | (conj (if (:newlined state) 18 | (reduce str "\n" (repeat (:ident-level state) \space)) 19 | \space)) 20 | (conj term))) 21 | 22 | (defn- update-state [state op] 23 | (case op 24 | ::newline (assoc state :newlined true) 25 | ::ident (update state :ident-level + 2) 26 | ::deident (update state :ident-level - 2))) 27 | 28 | (defn- reset-newline [state] 29 | (assoc state :newlined false)) 30 | 31 | (defn- pretty-reducer [acc token] 32 | (if (contains? pretty-operations token) 33 | (update acc :state update-state token) 34 | (-> acc 35 | (update :result update-result (:state acc) token) 36 | (update :state reset-newline)))) 37 | 38 | 39 | (defn make-pretty-sql 40 | "Convert vector of terminals and print control operations into pretty sql string" 41 | [input-code] 42 | (->> (reduce pretty-reducer 43 | {:result [] 44 | :state {:newlined true 45 | :ident-level 0}} 46 | input-code) 47 | :result 48 | rest 49 | str/join)) 50 | -------------------------------------------------------------------------------- /src/ql/select.cljc: -------------------------------------------------------------------------------- 1 | (ns ql.select 2 | (:require [clojure.string :as str] 3 | [ql.pretty-sql :as ps] 4 | [ql.method :refer [to-sql conj-sql conj-param reduce-separated clear-ql-keys]])) 5 | 6 | 7 | 8 | ;; [ WITH [ RECURSIVE ] with_query [, ...] ] 9 | ;; SELECT [ ALL | DISTINCT [ ON ( expression [, ...] ) ] ] 10 | ;; [ * | expression [ [ AS ] output_name ] [, ...] ] 11 | ;; [ FROM from_item [, ...] ] 12 | ;; [ WHERE condition ] 13 | ;; [ GROUP BY grouping_element [, ...] ] 14 | ;; [ HAVING condition [, ...] ] 15 | ;; [ WINDOW window_name AS ( window_definition ) [, ...] ] 16 | ;; [ { UNION | INTERSECT | EXCEPT } [ ALL | DISTINCT ] select ] 17 | ;; [ ORDER BY expression [ ASC | DESC | USING operator ] [ NULLS { FIRST | LAST } ] [, ...] ] 18 | ;; [ LIMIT { count | ALL } ] 19 | ;; [ OFFSET start [ ROW | ROWS ] ] 20 | ;; [ FETCH { FIRST | NEXT } [ count ] { ROW | ROWS } ONLY ] 21 | ;; [ FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE } [ OF table_name [, ...] ] [ NOWAIT | SKIP LOCKED ] [...] ] 22 | 23 | ;; where from_item can be one of: 24 | 25 | ;; [ ONLY ] table_name [ * ] [ [ AS ] alias [ ( column_alias [, ...] ) ] ] 26 | ;; [ TABLESAMPLE sampling_method ( argument [, ...] ) [ REPEATABLE ( seed ) ] ] 27 | ;; [ LATERAL ] ( select ) [ AS ] alias [ ( column_alias [, ...] ) ] 28 | ;; with_query_name [ [ AS ] alias [ ( column_alias [, ...] ) ] ] 29 | ;; [ LATERAL ] function_name ( [ argument [, ...] ] ) 30 | ;; [ WITH ORDINALITY ] [ [ AS ] alias [ ( column_alias [, ...] ) ] ] 31 | ;; [ LATERAL ] function_name ( [ argument [, ...] ] ) [ AS ] alias ( column_definition [, ...] ) 32 | ;; [ LATERAL ] function_name ( [ argument [, ...] ] ) AS ( column_definition [, ...] ) 33 | ;; [ LATERAL ] ROWS FROM( function_name ( [ argument [, ...] ] ) [ AS ( column_definition [, ...] ) ] [, ...] ) 34 | ;; [ WITH ORDINALITY ] [ [ AS ] alias [ ( column_alias [, ...] ) ] ] 35 | ;; from_item [ NATURAL ] join_type from_item [ ON join_condition | USING ( join_column [, ...] ) ] 36 | 37 | ;; and grouping_element can be one of: 38 | 39 | ;; ( ) 40 | ;; expression 41 | ;; ( expression [, ...] ) 42 | ;; ROLLUP ( { expression | ( expression [, ...] ) } [, ...] ) 43 | ;; CUBE ( { expression | ( expression [, ...] ) } [, ...] ) 44 | ;; GROUPING SETS ( grouping_element [, ...] ) 45 | 46 | ;; and with_query is: 47 | 48 | ;; with_query_name [ ( column_name [, ...] ) ] AS ( select | values | insert | update | delete ) 49 | 50 | ;; TABLE [ ONLY ] table_name [ * ] 51 | 52 | (def default-opts {:reduce-order 53 | {:ql/select [{:key :ql/with 54 | :token "WITH" 55 | :default-type :ql/with} 56 | {:key :ql/select 57 | :token "SELECT" 58 | :default-type :ql/projection} 59 | {:key :ql/from 60 | :token "FROM" 61 | :opts {:nested true} 62 | :default-type :ql/from} 63 | {:key :ql/where 64 | :token "WHERE" 65 | :default-type :ql/predicate} 66 | {:key :ql/joins 67 | :default-type :ql/joins} 68 | {:key :ql/group-by 69 | :token "GROUP BY" 70 | :default-type :ql/projection} 71 | {:key :ql/order-by 72 | :token "ORDER BY" 73 | :default-type :ql/list} 74 | {:key :ql/limit 75 | :token "LIMIT" 76 | :default-type :ql/param} 77 | {:key :ql/offset 78 | :token "OFFSET" 79 | :default-type :ql/param}]}}) 80 | 81 | (defmethod to-sql :ql/projection 82 | [acc expr] 83 | (reduce-separated 84 | ["," ::ps/newline] 85 | (fn [acc [k v]] 86 | (let [complex? (or (vector? v) (map? v))] 87 | (cond-> acc 88 | complex? (conj-sql "(") 89 | true (to-sql v) 90 | complex? (conj-sql ")") 91 | true (conj-sql "AS" (cond (keyword? k) (name k) :else k))))) 92 | acc 93 | (dissoc expr :ql/type))) 94 | 95 | (defmethod to-sql :ql/from 96 | [acc expr] 97 | (reduce-separated 98 | "," 99 | (fn [acc [k v]] 100 | (-> acc 101 | (to-sql v) 102 | (conj-sql (name k)))) 103 | acc (dissoc expr :ql/type))) 104 | 105 | (defmethod to-sql :ql/predicate 106 | [acc expr] 107 | (cond 108 | (map? expr) 109 | (reduce-separated 110 | (or (:ql/comp expr) "AND") 111 | (fn [acc [k v]] 112 | (-> acc 113 | (conj-sql "/**" (name k) "**/" "(") 114 | (to-sql v) 115 | (conj-sql ")"))) 116 | acc 117 | (dissoc expr :ql/type :ql/comp)) 118 | 119 | (vector? expr) 120 | (to-sql expr))) 121 | 122 | 123 | 124 | (defmethod to-sql :ql/join 125 | [acc {tp :ql/join-type rel :ql/rel on :ql/on a :ql/alias :as expr}] 126 | (cond-> acc 127 | true (conj-sql "\n") 128 | tp (conj-sql tp) 129 | true (conj-sql "JOIN") 130 | true (to-sql rel) 131 | true (conj-sql (name a) "ON") 132 | true (to-sql (if (map? on) 133 | (update on :ql/type (fn [x] (if x x :ql/predicate))) 134 | on)))) 135 | 136 | (defmethod to-sql :ql/joins 137 | [acc expr] 138 | (reduce 139 | (fn [acc [k v]] 140 | (-> acc 141 | (to-sql (-> v 142 | (assoc :ql/alias k) 143 | (update :ql/type (fn [x] (if x x :ql/join))))))) 144 | acc (dissoc expr :ql/type))) 145 | 146 | 147 | (defmethod to-sql :ql/list 148 | [acc expr] 149 | (let [xs (if (map? expr) 150 | (->> expr 151 | (ql.method/clear-ql-keys) 152 | (sort-by first) 153 | (mapv second)) 154 | (rest expr))] 155 | (reduce-separated "," to-sql acc xs))) 156 | 157 | (defmethod to-sql :ql/with 158 | [acc expr] 159 | (reduce-separated 160 | "," 161 | (fn [acc [k v]] 162 | (-> acc 163 | (conj-sql (name k) "AS") 164 | (assoc-in [:opts :nested] true) 165 | (to-sql (-> v 166 | (update :ql/type (fn [x] (if x x :ql/select))) 167 | (dissoc :ql/weight))) 168 | (conj-sql "\n"))) 169 | acc 170 | (->> 171 | (clear-ql-keys expr) 172 | (sort-by :ql/weight)))) 173 | 174 | (defn parens-if-nested [acc f] 175 | (let [n (get-in acc [:opts :nested])] 176 | (cond-> acc 177 | n (conj-sql "(" ::ps/newline ::ps/ident) 178 | n (assoc-in [:opts :nested] false) 179 | true (f) 180 | n (conj-sql ::ps/deident ")")))) 181 | 182 | (defmethod to-sql :ql/select 183 | [acc expr] 184 | (parens-if-nested 185 | acc 186 | (fn [acc] 187 | (reduce (fn [acc {k :key tk :token tp :default-type opts :opts}] 188 | (if-let [v (get expr k)] 189 | (cond-> acc 190 | opts (update :opts merge opts) 191 | tk (conj-sql tk ::ps/newline ::ps/ident) 192 | true (to-sql (cond 193 | (and (map? v) (not (:ql/type v)) tp) 194 | (assoc v :ql/type tp) 195 | :else v)) 196 | tk (conj-sql ::ps/newline ::ps/deident)) 197 | acc)) 198 | acc (get-in acc [:opts :reduce-order :ql/select]))))) 199 | 200 | (defmethod to-sql :ql/limit 201 | [acc expr] 202 | (if-let [v (:ql/value expr)] 203 | (-> (conj-sql acc "LIMIT") 204 | (to-sql v)) 205 | acc)) 206 | 207 | -------------------------------------------------------------------------------- /src/ql/update.cljc: -------------------------------------------------------------------------------- 1 | (ns ql.update) 2 | 3 | -------------------------------------------------------------------------------- /test/ql/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns ql.core-test 2 | (:require [ql.core :as sut] 3 | [clojure.test :refer :all] 4 | [matcho.core :as matcho] 5 | [ql.core :as ql])) 6 | 7 | (deftest test-ql 8 | 9 | (testing "select" 10 | 11 | (matcho/match 12 | (sut/sql {:ql/type :ql/projection 13 | :alias :column 14 | :constant "string" 15 | :param {:ql/type :ql/param 16 | :ql/value 10}}) 17 | {:sql "column AS alias , 'string' AS constant , ( ? ) AS param" 18 | :params [10]})) 19 | 20 | 21 | (matcho/match 22 | (sut/sql "str" {:style :honeysql}) 23 | {:sql "?" :params ["str"]}) 24 | 25 | (matcho/match 26 | (sut/sql {:ql/type :ql/param :ql/value 10}) 27 | {:sql "?" :params [10]}) 28 | 29 | (matcho/match 30 | (sut/sql {:ql/type :ql/param :ql/value "str"} {:inline true}) 31 | {:sql "'str'" :params []}) 32 | 33 | (matcho/match 34 | (sut/sql {:ql/type :ql/projection 35 | :alias :column}) 36 | {:sql "column AS alias"}) 37 | 38 | (matcho/match 39 | (sut/sql {:ql/type :ql/projection 40 | :alias {:ql/type :ql/string 41 | :ql/value "const"}}) 42 | {:sql "( $str$const$str$ ) AS alias"}) 43 | 44 | (matcho/match 45 | (sut/sql {:ql/type :ql/projection 46 | :alias {:ql/type :ql/param 47 | :ql/value "const"}}) 48 | {:sql "( ? ) AS alias" :params ["const"]}) 49 | 50 | (matcho/match 51 | (sut/sql {:ql/type :ql/projection 52 | :alias {:ql/type :ql/select 53 | :ql/select {:ql/type :ql/projection :x 1}}}) 54 | {:sql "( SELECT 1 AS x ) AS alias" :params []}) 55 | 56 | (matcho/match 57 | (sut/sql {:ql/type :ql/predicate 58 | :ql/comp "AND" 59 | :cond-1 [:ql/= :user.id 1] 60 | :cond-2 [:ql/<> :user.role "admin"]}) 61 | 62 | {:sql "/** cond-1 **/ ( user.id = 1 ) AND /** cond-2 **/ ( user.role <> 'admin' )"}) 63 | 64 | (matcho/match 65 | (sut/sql {:ql/type :ql/predicate 66 | :cond-1 [:ql/= :user.id 1] 67 | :cond-2 [:ql/<> :user.role "admin"]}) 68 | 69 | {:sql "/** cond-1 **/ ( user.id = 1 ) AND /** cond-2 **/ ( user.role <> 'admin' )"}) 70 | 71 | (matcho/match 72 | (sut/sql {:ql/type :ql/predicate 73 | :ql/comp "OR" 74 | :cond-1 [:ql/= :user.id 1] 75 | :cond-2 [:ql/<> :user.role "admin"]}) 76 | 77 | {:sql "/** cond-1 **/ ( user.id = 1 ) OR /** cond-2 **/ ( user.role <> 'admin' )"}) 78 | 79 | (matcho/match 80 | (sut/sql {:ql/type :ql/select 81 | :ql/select {:name :name 82 | :bd :birthDate} 83 | :ql/from {:ql/type :ql/from 84 | :user :user} 85 | :ql/where {:user-ids [:ql/= :user.id 5]} 86 | :ql/limit 10}) 87 | 88 | {:sql "SELECT name AS name , birthDate AS bd FROM user user WHERE /** user-ids **/ ( user.id = 5 ) LIMIT 10" :params []}) 89 | 90 | (matcho/match 91 | (sut/sql {:ql/type :ql/select 92 | :ql/select {:name :name :bd :birthDate} 93 | :ql/from {:user :user} 94 | :ql/where {:user-ids [:ql/= :user.id 5]} 95 | :ql/limit 10}) 96 | 97 | 98 | 99 | {:sql "SELECT name AS name , birthDate AS bd FROM user user WHERE /** user-ids **/ ( user.id = 5 ) LIMIT 10" :params []}) 100 | 101 | (matcho/match 102 | (sut/sql {:ql/select :* 103 | :ql/from {:u :user 104 | :g :group} 105 | :ql/where {:user-ids [:ql/= :u.id :g.user_id] 106 | :group-type [:ql/= :g.name "admin"]}}) 107 | {:sql "SELECT * FROM user u , group g WHERE /** user-ids **/ ( u.id = g.user_id ) AND /** group-type **/ ( g.name = 'admin' )", :params []}) 108 | 109 | (matcho/match 110 | (sut/sql 111 | {:ql/select :* 112 | :ql/from {:post :post} 113 | :ql/joins {:u {:ql/join-type "LEFT" 114 | :ql/rel :user 115 | :ql/on {:by-ids [:ql/= :u.id :post.user_id]}}}}) 116 | {:sql "SELECT * FROM post post \n LEFT JOIN user u ON /** by-ids **/ ( u.id = post.user_id )"}) 117 | 118 | (matcho/match 119 | (sut/sql 120 | {:ql/select :* 121 | :ql/from {:post :post} 122 | :ql/joins {:u {:ql/join-type "LEFT" 123 | :ql/rel :user 124 | :ql/on [:ql/= :u.id :post.user_id]}}}) 125 | {:sql "SELECT * FROM post post \n LEFT JOIN user u ON u.id = post.user_id"}) 126 | 127 | 128 | (matcho/match 129 | (sut/sql 130 | {:ql/type :ql/projection 131 | :resource {:ql/type :jsonb/build-object 132 | :name :user.name 133 | :address [:jsonb/|| 134 | [:jsonb/-> :resource :address] 135 | {:ql/type :jsonb/build-object 136 | :city "NY" 137 | :zip :address.zip}]}}) 138 | 139 | {:sql "( jsonb_build_object( 'name' , user.name , 'address' , resource ->'address' || jsonb_build_object( 'city' , 'NY' , 'zip' , address.zip ) ) ) AS resource"}) 140 | 141 | (matcho/match 142 | (sut/sql 143 | {:ql/type :ql/projection 144 | :resource {:ql/type :jsonb/build-object 145 | :name :user.name 146 | :address [:jsonb/|| 147 | [:jsonb/-> :resource :address] 148 | {:ql/type :jsonb/build-object 149 | :city {:ql/type :ql/param 150 | :ql/value "NY"} 151 | :zip :address.zip}]}}) 152 | 153 | {:sql "( jsonb_build_object( 'name' , user.name , 'address' , resource ->'address' || jsonb_build_object( 'city' , ? , 'zip' , address.zip ) ) ) AS resource" 154 | :params ["NY"]}) 155 | 156 | (matcho/match 157 | (sut/sql {:ql/with {:users {:ql/weight 0 158 | :ql/select {:name :name} 159 | :ql/from {:users :users}} 160 | :roles {:ql/weight 1 161 | :ql/select {:name :name} 162 | :ql/from {:group :group} 163 | :ql/joins {:u {:ql/rel :users 164 | :ql/on {:join-cond [:ql/= :u.id :g.user_id]}}}}} 165 | :ql/select {:u :u.name :g :g.name} 166 | :ql/from {:u :user 167 | :r :roles}}) 168 | 169 | {:sql 170 | "WITH users AS ( SELECT name AS name FROM users users ) \n , roles AS ( SELECT name AS name FROM group group \n JOIN users u ON /** join-cond **/ ( u.id = g.user_id ) ) \n SELECT u.name AS u , g.name AS g FROM user u , roles r", 171 | :params []}) 172 | 173 | 174 | 175 | (testing "group-by" 176 | (matcho/match 177 | (sut/sql {:ql/select {:a :expr :b :other} 178 | :ql/from {:t :t} 179 | :ql/group-by [:ql/list :expr :other]}) 180 | {:sql "SELECT expr AS a , other AS b FROM t t GROUP BY expr , other"})) 181 | 182 | ) 183 | 184 | 185 | 186 | (sut/sql 187 | #:ql{:select {:name :u.name} 188 | :from {:u :user} 189 | :where {:by-id [:ql/= :u.id [:ql/param 5]]}}) 190 | 191 | (sut/sql 192 | {:ql/select {:name :u.name} 193 | :ql/from {:u :user} 194 | :ql/where {:by-id {:ql/type :ql/= 195 | 0 :u.id 196 | 1 {:ql/type :ql/param 197 | :ql/value 5}}}}) 198 | 199 | 200 | 201 | (sut/sql 202 | (merge-with 203 | merge 204 | {:ql/from {:u :user}} 205 | {:ql/from {:g :group}} 206 | {:ql/select {:name :u.name}} 207 | {:ql/select {:group :g.name}} 208 | {:ql/where {:join [:ql/= :g.user_id :u.id]}} 209 | {:ql/where {:by-role [:ql/= :u.role "admin"]}} 210 | {:ql/where {:by-id [:ql/= :u.id [:ql/param 5]]}})) 211 | 212 | 213 | 214 | {:ql/type :ql/fn 215 | :ql/fn "lower" 216 | 0 "param-1" 217 | 1 "param-2"} 218 | 219 | 220 | [:ql/fn "lower" "param-1" "param-2"] 221 | 222 | {:ql/type :ql/cast 223 | :ql/cast :pg/timestamptz 224 | :ql/expression "2011-01-01"} 225 | 226 | [:ql/cast :pg/timestamptz "2011-01-01"] 227 | 228 | (comment 229 | 230 | 231 | ) 232 | -------------------------------------------------------------------------------- /test/ql/insert_test.clj: -------------------------------------------------------------------------------- 1 | (ns ql.insert-test 2 | (:require [ql.core :as ql] 3 | [ql.insert :as sut] 4 | [clojure.test :refer :all] 5 | [matcho.core :as matcho])) 6 | 7 | 8 | (deftest test-dataq 9 | (testing "insert with returning" 10 | (matcho/match 11 | (ql/sql {:ql/type :ql/insert 12 | :ql/table_name :user 13 | :ql/value {:id "1" 14 | :name {:ql/type :ql/jsonb 15 | :name "name"}} 16 | :ql/returning :*}) 17 | {:sql "INSERT INTO user ( id , name ) VALUES ( '1' , $JSON${\"name\":\"name\"}$JSON$ ) RETURNING *"}))) 18 | -------------------------------------------------------------------------------- /test/ql/method_test.clj: -------------------------------------------------------------------------------- 1 | (ns ql.method-test 2 | (:require [ql.method :as sut] 3 | [clojure.test :refer :all])) 4 | 5 | 6 | (deftest test-method-and-helpers 7 | 8 | ) 9 | -------------------------------------------------------------------------------- /test/ql/pg/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns ql.pg.core-test 2 | (:require [ql.pg.core :as sut] 3 | [ql.core :as ql] 4 | [matcho.core :as matcho] 5 | [clojure.test :refer :all])) 6 | 7 | (deftest pg-test 8 | 9 | 10 | (testing "jsonb->" 11 | (matcho/match 12 | (ql/sql [:jsonb/-> :table.column :key]) 13 | {:sql "table.column ->'key'", :params []})) 14 | 15 | (testing "jsonb->>" 16 | (matcho/match 17 | (ql/sql [:jsonb/->> :table.column :key]) 18 | {:sql "table.column ->>'key'", :params []})) 19 | 20 | (testing "jsonb#>" 21 | (matcho/match 22 | (ql/sql [:jsonb/#> :table.column [:a 0 :b]]) 23 | {:sql "table.column #>'{a,0,b}'", :params []})) 24 | 25 | (testing "jsonb#>" 26 | (matcho/match 27 | (ql/sql {:ql/type :jsonb/#> 28 | :left :table.column 29 | :right [:a 0 :b]}) 30 | {:sql "table.column #>'{a,0,b}'", :params []})) 31 | 32 | (testing "jsonb#>>" 33 | (matcho/match 34 | (ql/sql [:jsonb/#>> :table.column [:a 0 :b]]) 35 | {:sql "table.column #>>'{a,0,b}'", :params []})) 36 | 37 | ) 38 | 39 | -------------------------------------------------------------------------------- /test/ql/pg/jsonb_test.clj: -------------------------------------------------------------------------------- 1 | (ns ql.pg.jsonb-test 2 | (:require [ql.pg.jsonb :as sut] 3 | [ql.core :as ql] 4 | [ql.pg.jsonb] 5 | [matcho.core :as matcho] 6 | [clojure.test :refer :all])) 7 | 8 | (deftest pg-jsonb-test 9 | (testing "jsonb_agg" 10 | (matcho/match 11 | (ql/sql [:jsonb/agg :table.*]) 12 | {:sql "jsonb_agg( table.* )", :params []}) 13 | 14 | (matcho/match 15 | (ql/sql {:ql/type :jsonb/agg :expression :table.*}) 16 | {:sql "jsonb_agg( table.* )", :params []}) 17 | ) 18 | ) 19 | 20 | -------------------------------------------------------------------------------- /test/ql/select_test.clj: -------------------------------------------------------------------------------- 1 | (ns ql.select-test 2 | (:require [clojure.test :refer :all] 3 | [matcho.core :as matcho] 4 | [ql.core :as ql] 5 | ql.method)) 6 | 7 | 8 | (defmethod ql.method/to-sql :mssql/options 9 | [acc expr] 10 | (ql.method/reduce-separated 11 | "," 12 | (fn [acc [k v]] 13 | (-> acc 14 | (ql.method/conj-sql (name k) "=") 15 | (ql.method/to-sql v))) 16 | acc (dissoc expr :ql/type))) 17 | 18 | ;; (ql/sql {:ql/type :mssql/options 19 | ;; :a 1}) 20 | 21 | (deftest test-select 22 | 23 | (matcho/match 24 | (ql/sql 25 | {;; node type 26 | :ql/type :ql/select 27 | ;; selection 28 | :ql/select {:alias :u.column} 29 | ;; form {alias tbl | expression} 30 | :ql/from {:u :user} 31 | ;; named conditions 32 | :ql/where {:by-id {:ql/type :ql/= :left :u.id :right 5}} 33 | :ql/order-by [:ql/list :u.name] 34 | ;; :ql/group-by [:ql/group-by :name] 35 | :ql/limit 1 36 | :ql/offset 10}) 37 | {:sql 38 | "SELECT u.column AS alias FROM user u WHERE /** by-id **/ ( u.id = 5 ) ORDER BY u.name LIMIT 1 OFFSET 10", 39 | :params []}) 40 | 41 | (matcho/match 42 | (ql/sql {:ql/with {:_user #:ql {:select :* 43 | :from :user 44 | :where [:ql/= :id [:ql/param 5]]} 45 | :_group {:ql/select :g.* 46 | :ql/from {:u :user} 47 | :ql/joins {:g {:ql/rel :group 48 | :ql/on [:ql/= :g.user_id :u.id]}}}} 49 | :ql/select {:name :u.name :group :g.name} 50 | :ql/from {:u :_user :g :_group} 51 | :ql/where [:ql/= :g.user_id :u.id]}) 52 | {:sql "WITH _user AS ( SELECT * FROM user WHERE id = ? ) \n , _group AS ( SELECT g.* FROM user u \n JOIN group g ON g.user_id = u.id ) \n SELECT u.name AS name , g.name AS group FROM _user u , _group g WHERE g.user_id = u.id", 53 | :params [5]}) 54 | 55 | 56 | (matcho/match 57 | (ql/sql 58 | {:ql/type :ql/select 59 | :ql/select :* 60 | :ql/from :user 61 | :ql/where [:ql/= :u.id 1] 62 | :ql/limit 10 63 | :ql/offset 20}) 64 | {:sql "SELECT * FROM user WHERE u.id = 1 LIMIT 10 OFFSET 20", :params []}) 65 | 66 | (ql/sql 67 | {:ql/select :* 68 | :ql/from {:x {:ql/type :ql/select 69 | :ql/select :* 70 | :ql/from :user 71 | :ql/where [:ql/= 1 1]}} 72 | :ql/where [:ql/= :u.id 1] 73 | :ql/limit 10 74 | :ql/offset 20}) 75 | 76 | (matcho/match 77 | (ql/sql 78 | {:ql/type :ql/select 79 | :ql/select :* 80 | :ql/from :user 81 | :mssql/options {:a 1}} 82 | (ql.method/add-clause ql/default-opts 83 | :ql/select 84 | :before 85 | :ql/order-by 86 | {:key :mssql/options 87 | :default-type :mssql/options 88 | :token "OPTIONS"})) 89 | {:sql "SELECT * FROM user OPTIONS a = 1"})) 90 | 91 | (deftest test-inline-option 92 | (matcho/match 93 | (ql/sql 94 | {:ql/select {:a :a 95 | :b :b} 96 | :ql/from {:ql/type :ql/select :ql/select :* :ql/from :user} 97 | :ql/where [:ql/= :user.id [:ql/param 1]]} {:inline true}) 98 | {:sql "SELECT a AS a , b AS b FROM ( SELECT * FROM user ) WHERE user.id = 1"})) 99 | 100 | (deftest test-pretty-print 101 | (matcho/match 102 | (ql/sql 103 | {:ql/select {:a :a 104 | :b :b} 105 | :ql/from {:ql/type :ql/select :ql/select :* :ql/from :user} 106 | :ql/where [:ql/= :user.id 1]} {:format :pretty}) 107 | {:sql 108 | "SELECT 109 | a AS a , 110 | b AS b 111 | FROM 112 | ( 113 | SELECT 114 | * 115 | FROM 116 | user 117 | ) 118 | WHERE 119 | user.id = 1" 120 | }) 121 | ) 122 | 123 | -------------------------------------------------------------------------------- /test/scratchpad_test.clj: -------------------------------------------------------------------------------- 1 | (ns scratchpad-test 2 | (:require 3 | [ql.core :as ql] 4 | [testdb :as db] 5 | [clojure.test :refer :all] 6 | [matcho.core :as matcho])) 7 | 8 | 9 | (deftest scratchpad 10 | (db/query ["select 1"]) 11 | 12 | (db/query ["select from user"]) 13 | 14 | 15 | (db/execute (ql/sql {:ql/type :ql/drop-table 16 | :ql/if-exists {} 17 | :ql/table_name [:ql/ident :User]} 18 | {:format :jdbc})) 19 | 20 | (db/execute (ql/sql {:ql/type :ql/create-table 21 | :ql/table_name [:ql/ident :User] 22 | :ql/columns {:id {:ql/primary-key true 23 | :ql/weight 0 24 | :ql/column-type :text} 25 | :resource {:ql/column-type :jsonb 26 | :ql/weight 0}}} 27 | {:format :jdbc})) 28 | 29 | (db/execute (ql/sql {:ql/type :ql/truncate 30 | :ql/table_name [:ql/ident :User]} 31 | {:format :jdbc})) 32 | 33 | (db/query (ql/sql {:ql/type :ql/insert 34 | :ql/table_name {:ql/type :ql/ident 35 | :ql/value :User} 36 | :ql/value {:id "user-1" :resource {:ql/type :ql/jsonb :name "Nicola"}} 37 | :ql/returning :*} 38 | 39 | {:format :jdbc})) 40 | 41 | #_(db/query (ql/sql {:ql/type :ql/insert 42 | :ql/table_name {:ql/type :ql/ident 43 | :ql/value :User} 44 | :ql/value {:id "user-1" :resource {:ql/type :ql/jsonb 45 | :email "nicola@health-samurai.io" 46 | :name "Nicola"}} 47 | :ql/on-conflict {:??? :TODO} 48 | :ql/returning :*} 49 | 50 | {:format :jdbc})) 51 | 52 | (matcho/match 53 | (db/query (ql/sql {:ql/type :ql/select 54 | :ql/select {:resource :u.resource 55 | :id :u.id} 56 | :ql/from {:u {:ql/type :ql/ident 57 | :ql/value :User}}} 58 | 59 | {:format :jdbc})) 60 | [{:id #(not (nil? %)) 61 | :resource {:name "Nicola"}}]) 62 | 63 | 64 | ) 65 | 66 | -------------------------------------------------------------------------------- /test/testdb.clj: -------------------------------------------------------------------------------- 1 | (ns testdb 2 | (:require [cheshire.core :as json] 3 | [clojure.java.jdbc :as jdbc] 4 | [clojure.test :refer [deftest]] 5 | [matcho.core :as matcho]) 6 | (:import org.postgresql.util.PGobject)) 7 | 8 | (def cfg 9 | {:connection-uri 10 | (or (System/getenv "DATABASE_URL") 11 | "jdbc:postgresql://localhost:5447/ql?user=postgres&password=verysecret")}) 12 | 13 | (extend-protocol jdbc/IResultSetReadColumn 14 | ;; (result-set-read-column [v _ _] (vec (.getArray v))) 15 | 16 | PGobject 17 | (result-set-read-column [pgobj _metadata _index] 18 | (let [type (.getType pgobj) 19 | value (.getValue pgobj)] 20 | (case type 21 | "json" (json/parse-string value keyword) 22 | "jsonb" (json/parse-string value keyword) 23 | value)))) 24 | 25 | 26 | (defn query [q] 27 | (jdbc/query cfg q)) 28 | 29 | (defn execute [q] 30 | (jdbc/execute! cfg q)) 31 | 32 | (deftest test-query 33 | (matcho/match 34 | (query ["select '[1,3]'::jsonb json_array"]) 35 | [{:json_array [1 3]}])) 36 | 37 | --------------------------------------------------------------------------------