├── .gitignore ├── .travis-scripts ├── install.sh └── run-tests.sh ├── .travis.yml ├── LICENSE ├── README.md ├── _config.yml ├── deps.edn ├── dev ├── resources │ ├── common_unpatched.edn │ ├── config-mysql.edn │ ├── config-postgres.edn │ ├── config-sqlite.edn │ ├── core-mysql.edn │ ├── core-postgres.edn │ └── core-sqlite.edn ├── src │ ├── dev.clj │ ├── dev.cljs │ ├── duct_hierarchy.edn │ ├── user.clj │ └── walkable │ │ └── integration_test │ │ └── helper.clj └── test │ └── walkable │ └── integration_test │ ├── common.cljc │ ├── mysql_test.clj │ ├── postgres_test.clj │ ├── sqlite_test.clj │ └── sqlite_test.cljs ├── doc ├── walkable.png └── walkable.svg ├── package.json ├── project.clj ├── shadow-cljs.edn ├── src └── walkable │ ├── core.cljc │ ├── core_async.cljc │ └── sql_query_builder │ ├── ast.cljc │ ├── emitter.cljc │ ├── expressions.cljc │ ├── floor_plan.cljc │ ├── helper.cljc │ ├── pagination.cljc │ └── pathom_env.cljc └── test └── walkable ├── core_test.cljc └── sql_query_builder ├── ast_test.cljc ├── emitter_test.cljc ├── expressions_test.cljc ├── floor_plan_test.cljc ├── helper_test.cljc └── pagination_test.cljc /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /logs 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | /.dir-locals.el 12 | /profiles.clj 13 | /dev/resources/local.edn 14 | /dev/src/local.clj 15 | /walkable_dev.sqlite 16 | /run.js 17 | /.nrepl-history 18 | node_modules 19 | .shadow-cljs 20 | -------------------------------------------------------------------------------- /.travis-scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ev 3 | 4 | case "$1" in 5 | unit) 6 | lein install 7 | ;; 8 | sqlite) 9 | lein with-profile dev install 10 | npm install 11 | ;; 12 | postgres) 13 | psql -c 'create database walkable_dev;' -U postgres 14 | lein with-profile dev install 15 | ;; 16 | mysql) 17 | mysql -e 'CREATE DATABASE IF NOT EXISTS walkable_dev;' 18 | lein with-profile dev install 19 | ;; 20 | *) 21 | echo $"Usage: $0 {unit|sqlite|mysql|postgres}" 22 | exit 1 23 | esac 24 | -------------------------------------------------------------------------------- /.travis-scripts/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ev 3 | 4 | case "$1" in 5 | unit) 6 | lein test 7 | ;; 8 | sqlite) 9 | lein with-profile dev test :sqlite 10 | ./node_modules/.bin/shadow-cljs compile test 11 | node test.js 12 | ;; 13 | postgres) 14 | lein with-profile dev test :postgres 15 | ;; 16 | mysql) 17 | lein with-profile dev test :mysql 18 | ;; 19 | *) 20 | echo $"Usage: $0 {unit|sqlite|mysql|postgres}" 21 | exit 1 22 | esac 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | arch: arm64 3 | dist: focal 4 | language: clojure 5 | cache: 6 | directories: 7 | - node_modules 8 | - $HOME/.m2 9 | services: 10 | - postgresql 11 | - mysql 12 | addons: 13 | apt: 14 | packages: 15 | - nodejs 16 | - leiningen 17 | jobs: 18 | include: 19 | - name: "unit tests" 20 | env: TEST_SUITE=unit 21 | - name: "sqlite" 22 | env: TEST_SUITE=sqlite 23 | - name: "mysql" 24 | env: TEST_SUITE=mysql 25 | - name: "postgres" 26 | env: TEST_SUITE=postgres 27 | install: ./.travis-scripts/install.sh $TEST_SUITE 28 | script: ./.travis-scripts/run-tests.sh $TEST_SUITE 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), 267 | version(s), and exceptions or additional permissions here}." 268 | 269 | Simply including a copy of this Agreement, including this Exhibit A 270 | is not sufficient to license the Source Code under Secondary Licenses. 271 | 272 | If it is not possible or desirable to put the notice in a particular 273 | file, then You may include the notice in a location (such as a LICENSE 274 | file in a relevant directory) where a recipient would be likely to 275 | look for such a notice. 276 | 277 | You may add additional accurate notices of copyright ownership. 278 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Walkable 2 | 3 | ![Walkable logo](doc/walkable.png) 4 | 5 | Everything you need from an SQL database should be within walking 6 | distance. 7 | 8 | [![Build Status](https://travis-ci.com/walkable-server/walkable.svg?branch=master)](https://travis-ci.com/walkable-server/walkable) 9 | 10 | Please read our [beautiful documentation for the latest version 11 | here.](https://walkable.gitlab.io/) 12 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/spec.alpha {:mvn/version "0.2.187"} 2 | cheshire/cheshire {:mvn/version "5.10.1"} 3 | com.wsscode/pathom {:mvn/version "2.3.0"} 4 | weavejester/dependency {:mvn/version "0.2.1"} 5 | prismatic/plumbing {:mvn/version "0.5.5"} 6 | org.clojure/core.async {:mvn/version "1.2.603"}}} 7 | -------------------------------------------------------------------------------- /dev/resources/common_unpatched.edn: -------------------------------------------------------------------------------- 1 | {;; scenario 1 migrations 2 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-1/create-house-table] 3 | {:up ["CREATE TABLE `house` (`index` VARCHAR(32) PRIMARY KEY, `color` TEXT)" 4 | "INSERT INTO `house` (`index`, `color`) VALUES ('10', 'black'), ('20', 'brown')"] 5 | :down ["DROP TABLE `house`"]} 6 | 7 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-1/create-farmer-table] 8 | {:up ["CREATE TABLE `farmer` (`number` SERIAL PRIMARY KEY, `house_index` VARCHAR(32) REFERENCES `house`(`index`), `name` TEXT)" 9 | "INSERT INTO `farmer` (`number`, `name`, `house_index`) VALUES (1, 'jon', '10'), (2, 'mary', '20'), (3, 'homeless', NULL)"] 10 | :down ["DROP TABLE `farmer`"]} 11 | 12 | ;; scenario 2 migrations 13 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-2/create-kid-table] 14 | {:up ["CREATE TABLE `kid` (`number` SERIAL PRIMARY KEY, `name` TEXT)" 15 | "INSERT INTO `kid` (`number`, `name`) VALUES (1, 'jon'), (2, 'mary')"] 16 | :down ["DROP TABLE `kid`"]} 17 | 18 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-2/create-toy-table] 19 | {:up ["CREATE TABLE `toy` (`index` SERIAL PRIMARY KEY, `owner_number` INTEGER REFERENCES `kid`(`number`), `color` TEXT)" 20 | "INSERT INTO `toy` (`index`, `color`, `owner_number`) VALUES (10, 'yellow', 1), (20, 'green', 2)"] 21 | :down ["DROP TABLE `toy`"]} 22 | 23 | ;; scenario 3 migrations 24 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-3/create-person-table] 25 | {:up ["CREATE TABLE `person` (`number` SERIAL PRIMARY KEY, `name` TEXT, `yob` INTEGER, `secret` TEXT, `hidden` BOOLEAN)" 26 | "INSERT INTO `person` (`number`, `name`, `yob`, `secret`, `hidden`) VALUES (1, 'jon', 1980, 'only jon knows', true), (2, 'mary', 1992, 'surprise', false)" ] 27 | :down ["DROP TABLE `person`"]} 28 | 29 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-3/create-pet-table] 30 | {:up ["CREATE TABLE `pet` (`index` SERIAL PRIMARY KEY, `color` TEXT, `yob` INTEGER, `private` TEXT)" 31 | "INSERT INTO `pet` (`index`, `color`, `yob`, `private`) 32 | VALUES 33 | (10, 'yellow', 2015, 'you bastard'), 34 | (11, 'green', 2012, 'hey'), 35 | (12, 'yellow', 2017, 'secret'), 36 | (13, 'green', 2016, 'nothing'), 37 | (20, 'orange', 2014, 'very important'), 38 | (21, 'green', 2015, 'shhh'), 39 | (22, 'green', 2016, 'idk')"] 40 | :down ["DROP TABLE `pet`"]} 41 | 42 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-3/create-person_pet-table] 43 | {:up ["CREATE TABLE `person_pet` (`id` SERIAL PRIMARY KEY, `person_number` INTEGER REFERENCES `person`(`number`), `pet_index` INTEGER REFERENCES `pet`(`index`), `adoption_year` INTEGER)" 44 | "INSERT INTO `person_pet` (`person_number`, `pet_index`, `adoption_year`) VALUES 45 | (1, 10, 2015), 46 | (1, 11, 2013), 47 | (1, 12, 2018), 48 | (1, 13, 2016), 49 | (2, 20, 2014), 50 | (2, 21, 2016), 51 | (2, 22, 2017)"] 52 | :down ["DROP TABLE `person_pet`"]} 53 | 54 | ;; scenario 4 migrations 55 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-4/create-human-table] 56 | {:up ["CREATE TABLE `human` (`number` SERIAL PRIMARY KEY, `name` TEXT, `yob` INTEGER)" 57 | "INSERT INTO `human` (`number`, `name`, `yob`) VALUES (1, 'jon', 1980), (2, 'mary', 1992), (3, 'peter', 1989), (4, 'sandra', 1970)" ] 58 | :down ["DROP TABLE `human`"]} 59 | 60 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-4/create-follow-table] 61 | {:up ["CREATE TABLE `follow` (`human_1` INTEGER REFERENCES `human`(`number`), `human_2` INTEGER REFERENCES `human`(`number`), `year` INTEGER)" 62 | "INSERT INTO `follow` (`human_1`, `human_2`, `year`) VALUES (1, 2, 2015), (1, 3, 2017), (2, 1, 2014), (2, 3, 2015), (1, 4, 2012)"] 63 | :down ["DROP TABLE `follow`"]} 64 | } 65 | -------------------------------------------------------------------------------- /dev/resources/config-mysql.edn: -------------------------------------------------------------------------------- 1 | {:duct.profile/base 2 | {:duct.core/project-ns walkable-dev 3 | 4 | :duct.migrator/ragtime 5 | {:strategy :rebase 6 | :migrations [;; common scenarios (starting from 1) 7 | #ig/ref :walkable-demo.migration.scenario-1/create-house-table 8 | #ig/ref :walkable-demo.migration.scenario-1/create-farmer-table 9 | #ig/ref :walkable-demo.migration.scenario-2/create-kid-table 10 | #ig/ref :walkable-demo.migration.scenario-2/create-toy-table 11 | #ig/ref :walkable-demo.migration.scenario-3/create-person-table 12 | #ig/ref :walkable-demo.migration.scenario-3/create-pet-table 13 | #ig/ref :walkable-demo.migration.scenario-3/create-person_pet-table 14 | #ig/ref :walkable-demo.migration.scenario-4/create-human-table 15 | #ig/ref :walkable-demo.migration.scenario-4/create-follow-table 16 | ;; mysql-specific scenarios (starting from 100) 17 | ]} 18 | } 19 | 20 | :duct.profile/common #duct/include "core-mysql" 21 | :duct.profile/local #duct/include "local" 22 | :duct.profile/prod {} 23 | 24 | :duct.module/logging {} 25 | 26 | :duct.module/sql {:database-url "jdbc:mysql://localhost:3306/walkable_dev?user=root"} 27 | } 28 | -------------------------------------------------------------------------------- /dev/resources/config-postgres.edn: -------------------------------------------------------------------------------- 1 | {:duct.profile/base 2 | {:duct.core/project-ns walkable-dev 3 | 4 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-100/create-land-animal-table] 5 | {:up ["CREATE SCHEMA \"land\"" 6 | "CREATE TABLE \"land\".\"animal\" (\"id\" SERIAL PRIMARY KEY, \"name\" TEXT)" 7 | "INSERT INTO \"land\".\"animal\" (\"id\", \"name\") VALUES (1, 'elephant'), (2, 'rhino')"] 8 | :down ["DROP TABLE \"land\".\"animal\"" 9 | "DROP SCHEMA \"land\""]} 10 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-101/create-ocean-animal-table] 11 | {:up ["CREATE SCHEMA \"ocean\"" 12 | "CREATE TABLE \"ocean\".\"animal\" (\"id\" SERIAL PRIMARY KEY, \"name\" TEXT)" 13 | "INSERT INTO \"ocean\".\"animal\" (\"id\", \"name\") VALUES (10, 'whale'), (20, 'shark')"] 14 | :down ["DROP TABLE \"ocean\".\"animal\"" 15 | "DROP SCHEMA \"ocean\""]} 16 | 17 | :duct.migrator/ragtime 18 | {:strategy :rebase 19 | :migrations [;; common scenarios (starting from 1) 20 | #ig/ref :walkable-demo.migration.scenario-1/create-house-table 21 | #ig/ref :walkable-demo.migration.scenario-1/create-farmer-table 22 | #ig/ref :walkable-demo.migration.scenario-2/create-kid-table 23 | #ig/ref :walkable-demo.migration.scenario-2/create-toy-table 24 | #ig/ref :walkable-demo.migration.scenario-3/create-person-table 25 | #ig/ref :walkable-demo.migration.scenario-3/create-pet-table 26 | #ig/ref :walkable-demo.migration.scenario-3/create-person_pet-table 27 | #ig/ref :walkable-demo.migration.scenario-4/create-human-table 28 | #ig/ref :walkable-demo.migration.scenario-4/create-follow-table 29 | ;; postgres-specific scenarios (starting from 100) 30 | 31 | #ig/ref :walkable-demo.migration.scenario-100/create-land-animal-table 32 | 33 | #ig/ref :walkable-demo.migration.scenario-101/create-ocean-animal-table 34 | ]} 35 | } 36 | 37 | :duct.profile/common #duct/include "core-postgres" 38 | :duct.profile/local #duct/include "local" 39 | :duct.profile/prod {} 40 | 41 | :duct.module/logging {} 42 | ;; ... more module keys 43 | :duct.module/sql {:database-url "jdbc:postgresql://localhost:5432/walkable_dev?user=postgres"} 44 | } 45 | -------------------------------------------------------------------------------- /dev/resources/config-sqlite.edn: -------------------------------------------------------------------------------- 1 | {:duct.profile/base 2 | {:duct.core/project-ns walkable-dev 3 | 4 | :duct.migrator/ragtime 5 | {:strategy :rebase 6 | :migrations [;; common scenarios (starting from 1) 7 | #ig/ref :walkable-demo.migration.scenario-1/create-house-table 8 | #ig/ref :walkable-demo.migration.scenario-1/create-farmer-table 9 | #ig/ref :walkable-demo.migration.scenario-2/create-kid-table 10 | #ig/ref :walkable-demo.migration.scenario-2/create-toy-table 11 | #ig/ref :walkable-demo.migration.scenario-3/create-person-table 12 | #ig/ref :walkable-demo.migration.scenario-3/create-pet-table 13 | #ig/ref :walkable-demo.migration.scenario-3/create-person_pet-table 14 | #ig/ref :walkable-demo.migration.scenario-4/create-human-table 15 | #ig/ref :walkable-demo.migration.scenario-4/create-follow-table 16 | ;; sqlite-specific scenarios (starting from 100) 17 | ]} 18 | } 19 | 20 | :duct.profile/common #duct/include "core-sqlite" 21 | :duct.profile/local #duct/include "local" 22 | :duct.profile/prod {} 23 | 24 | :duct.module/logging {} 25 | 26 | :duct.module/sql {:database-url "jdbc:sqlite:walkable_dev.sqlite"} 27 | } 28 | -------------------------------------------------------------------------------- /dev/resources/core-mysql.edn: -------------------------------------------------------------------------------- 1 | {;; scenario 1 migrations 2 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-1/create-house-table] 3 | {:up ["CREATE TABLE `house` (`index` VARCHAR(32) PRIMARY KEY, `color` TEXT)" 4 | "INSERT INTO `house` (`index`, `color`) VALUES ('10', 'black'), ('20', 'brown')"] 5 | :down ["DROP TABLE `house`"]} 6 | 7 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-1/create-farmer-table] 8 | {:up ["CREATE TABLE `farmer` (`number` SERIAL PRIMARY KEY, `house_index` VARCHAR(32) REFERENCES `house`(`index`), `name` TEXT)" 9 | "INSERT INTO `farmer` (`number`, `name`, `house_index`) VALUES (1, 'jon', '10'), (2, 'mary', '20'), (3, 'homeless', NULL)"] 10 | :down ["DROP TABLE `farmer`"]} 11 | 12 | ;; scenario 2 migrations 13 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-2/create-kid-table] 14 | {:up ["CREATE TABLE `kid` (`number` SERIAL PRIMARY KEY, `name` TEXT)" 15 | "INSERT INTO `kid` (`number`, `name`) VALUES (1, 'jon'), (2, 'mary')"] 16 | :down ["DROP TABLE `kid`"]} 17 | 18 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-2/create-toy-table] 19 | {:up ["CREATE TABLE `toy` (`index` SERIAL PRIMARY KEY, `owner_number` INTEGER REFERENCES `kid`(`number`), `color` TEXT)" 20 | "INSERT INTO `toy` (`index`, `color`, `owner_number`) VALUES (10, 'yellow', 1), (20, 'green', 2)"] 21 | :down ["DROP TABLE `toy`"]} 22 | 23 | ;; scenario 3 migrations 24 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-3/create-person-table] 25 | {:up ["CREATE TABLE `person` (`number` SERIAL PRIMARY KEY, `name` TEXT, `yob` INTEGER, `secret` TEXT, `hidden` BOOLEAN)" 26 | "INSERT INTO `person` (`number`, `name`, `yob`, `secret`, `hidden`) VALUES (1, 'jon', 1980, 'only jon knows', true), (2, 'mary', 1992, 'surprise', false)" ] 27 | :down ["DROP TABLE `person`"]} 28 | 29 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-3/create-pet-table] 30 | {:up ["CREATE TABLE `pet` (`index` SERIAL PRIMARY KEY, `color` TEXT, `yob` INTEGER, `private` TEXT)" 31 | "INSERT INTO `pet` (`index`, `color`, `yob`, `private`) 32 | VALUES 33 | (10, 'yellow', 2015, 'you bastard'), 34 | (11, 'green', 2012, 'hey'), 35 | (12, 'yellow', 2017, 'secret'), 36 | (13, 'green', 2016, 'nothing'), 37 | (20, 'orange', 2014, 'very important'), 38 | (21, 'green', 2015, 'shhh'), 39 | (22, 'green', 2016, 'idk')"] 40 | :down ["DROP TABLE `pet`"]} 41 | 42 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-3/create-person_pet-table] 43 | {:up ["CREATE TABLE `person_pet` (`id` SERIAL PRIMARY KEY, `person_number` INTEGER REFERENCES `person`(`number`), `pet_index` INTEGER REFERENCES `pet`(`index`), `adoption_year` INTEGER)" 44 | "INSERT INTO `person_pet` (`person_number`, `pet_index`, `adoption_year`) VALUES 45 | (1, 10, 2015), 46 | (1, 11, 2013), 47 | (1, 12, 2018), 48 | (1, 13, 2016), 49 | (2, 20, 2014), 50 | (2, 21, 2016), 51 | (2, 22, 2017)"] 52 | :down ["DROP TABLE `person_pet`"]} 53 | 54 | ;; scenario 4 migrations 55 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-4/create-human-table] 56 | {:up ["CREATE TABLE `human` (`number` SERIAL PRIMARY KEY, `name` TEXT, `yob` INTEGER)" 57 | "INSERT INTO `human` (`number`, `name`, `yob`) VALUES (1, 'jon', 1980), (2, 'mary', 1992), (3, 'peter', 1989), (4, 'sandra', 1970)" ] 58 | :down ["DROP TABLE `human`"]} 59 | 60 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-4/create-follow-table] 61 | {:up ["CREATE TABLE `follow` (`human_1` INTEGER REFERENCES `human`(`number`), `human_2` INTEGER REFERENCES `human`(`number`), `year` INTEGER)" 62 | "INSERT INTO `follow` (`human_1`, `human_2`, `year`) VALUES (1, 2, 2015), (1, 3, 2017), (2, 1, 2014), (2, 3, 2015), (1, 4, 2012)"] 63 | :down ["DROP TABLE `follow`"]} 64 | } 65 | -------------------------------------------------------------------------------- /dev/resources/core-postgres.edn: -------------------------------------------------------------------------------- 1 | {;; scenario 1 migrations 2 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-1/create-house-table] 3 | {:up ["CREATE TABLE \"house\" (\"index\" VARCHAR(32) PRIMARY KEY, \"color\" TEXT)" 4 | "INSERT INTO \"house\" (\"index\", \"color\") VALUES ('10', 'black'), ('20', 'brown')"] 5 | :down ["DROP TABLE \"house\""]} 6 | 7 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-1/create-farmer-table] 8 | {:up ["CREATE TABLE \"farmer\" (\"number\" SERIAL PRIMARY KEY, \"house_index\" VARCHAR(32) REFERENCES \"house\"(\"index\"), \"name\" TEXT)" 9 | "INSERT INTO \"farmer\" (\"number\", \"name\", \"house_index\") VALUES (1, 'jon', '10'), (2, 'mary', '20'), (3, 'homeless', NULL)"] 10 | :down ["DROP TABLE \"farmer\""]} 11 | 12 | ;; scenario 2 migrations 13 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-2/create-kid-table] 14 | {:up ["CREATE TABLE \"kid\" (\"number\" SERIAL PRIMARY KEY, \"name\" TEXT)" 15 | "INSERT INTO \"kid\" (\"number\", \"name\") VALUES (1, 'jon'), (2, 'mary')"] 16 | :down ["DROP TABLE \"kid\""]} 17 | 18 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-2/create-toy-table] 19 | {:up ["CREATE TABLE \"toy\" (\"index\" SERIAL PRIMARY KEY, \"owner_number\" INTEGER REFERENCES \"kid\"(\"number\"), \"color\" TEXT)" 20 | "INSERT INTO \"toy\" (\"index\", \"color\", \"owner_number\") VALUES (10, 'yellow', 1), (20, 'green', 2)"] 21 | :down ["DROP TABLE \"toy\""]} 22 | 23 | ;; scenario 3 migrations 24 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-3/create-person-table] 25 | {:up ["CREATE TABLE \"person\" (\"number\" SERIAL PRIMARY KEY, \"name\" TEXT, \"yob\" INTEGER, \"secret\" TEXT, \"hidden\" BOOLEAN)" 26 | "INSERT INTO \"person\" (\"number\", \"name\", \"yob\", \"secret\", \"hidden\") VALUES (1, 'jon', 1980, 'only jon knows', true), (2, 'mary', 1992, 'surprise', false)" ] 27 | :down ["DROP TABLE \"person\""]} 28 | 29 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-3/create-pet-table] 30 | {:up ["CREATE TABLE \"pet\" (\"index\" SERIAL PRIMARY KEY, \"color\" TEXT, \"yob\" INTEGER, \"private\" TEXT)" 31 | "INSERT INTO \"pet\" (\"index\", \"color\", \"yob\", \"private\") 32 | VALUES 33 | (10, 'yellow', 2015, 'you bastard'), 34 | (11, 'green', 2012, 'hey'), 35 | (12, 'yellow', 2017, 'secret'), 36 | (13, 'green', 2016, 'nothing'), 37 | (20, 'orange', 2014, 'very important'), 38 | (21, 'green', 2015, 'shhh'), 39 | (22, 'green', 2016, 'idk')"] 40 | :down ["DROP TABLE \"pet\""]} 41 | 42 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-3/create-person_pet-table] 43 | {:up ["CREATE TABLE \"person_pet\" (\"id\" SERIAL PRIMARY KEY, \"person_number\" INTEGER REFERENCES \"person\"(\"number\"), \"pet_index\" INTEGER REFERENCES \"pet\"(\"index\"), \"adoption_year\" INTEGER)" 44 | "INSERT INTO \"person_pet\" (\"person_number\", \"pet_index\", \"adoption_year\") VALUES 45 | (1, 10, 2015), 46 | (1, 11, 2013), 47 | (1, 12, 2018), 48 | (1, 13, 2016), 49 | (2, 20, 2014), 50 | (2, 21, 2016), 51 | (2, 22, 2017)"] 52 | :down ["DROP TABLE \"person_pet\""]} 53 | 54 | ;; scenario 4 migrations 55 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-4/create-human-table] 56 | {:up ["CREATE TABLE \"human\" (\"number\" SERIAL PRIMARY KEY, \"name\" TEXT, \"yob\" INTEGER)" 57 | "INSERT INTO \"human\" (\"number\", \"name\", \"yob\") VALUES (1, 'jon', 1980), (2, 'mary', 1992), (3, 'peter', 1989), (4, 'sandra', 1970)" ] 58 | :down ["DROP TABLE \"human\""]} 59 | 60 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-4/create-follow-table] 61 | {:up ["CREATE TABLE \"follow\" (\"human_1\" INTEGER REFERENCES \"human\"(\"number\"), \"human_2\" INTEGER REFERENCES \"human\"(\"number\"), \"year\" INTEGER)" 62 | "INSERT INTO \"follow\" (\"human_1\", \"human_2\", \"year\") VALUES (1, 2, 2015), (1, 3, 2017), (2, 1, 2014), (2, 3, 2015), (1, 4, 2012)"] 63 | :down ["DROP TABLE \"follow\""]} 64 | } 65 | -------------------------------------------------------------------------------- /dev/resources/core-sqlite.edn: -------------------------------------------------------------------------------- 1 | {;; scenario 1 migrations 2 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-1/create-house-table] 3 | {:up ["CREATE TABLE `house` (`index` VARCHAR(32) PRIMARY KEY, `color` TEXT)" 4 | "INSERT INTO `house` (`index`, `color`) VALUES ('10', 'black'), ('20', 'brown')"] 5 | :down ["DROP TABLE `house`"]} 6 | 7 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-1/create-farmer-table] 8 | {:up ["CREATE TABLE `farmer` (`number` SERIAL PRIMARY KEY, `house_index` VARCHAR(32) REFERENCES `house`(`index`), `name` TEXT)" 9 | "INSERT INTO `farmer` (`number`, `name`, `house_index`) VALUES (1, 'jon', '10'), (2, 'mary', '20'), (3, 'homeless', NULL)"] 10 | :down ["DROP TABLE `farmer`"]} 11 | 12 | ;; scenario 2 migrations 13 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-2/create-kid-table] 14 | {:up ["CREATE TABLE `kid` (`number` SERIAL PRIMARY KEY, `name` TEXT)" 15 | "INSERT INTO `kid` (`number`, `name`) VALUES (1, 'jon'), (2, 'mary')"] 16 | :down ["DROP TABLE `kid`"]} 17 | 18 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-2/create-toy-table] 19 | {:up ["CREATE TABLE `toy` (`index` SERIAL PRIMARY KEY, `owner_number` INTEGER REFERENCES `kid`(`number`), `color` TEXT)" 20 | "INSERT INTO `toy` (`index`, `color`, `owner_number`) VALUES (10, 'yellow', 1), (20, 'green', 2)"] 21 | :down ["DROP TABLE `toy`"]} 22 | 23 | ;; scenario 3 migrations 24 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-3/create-person-table] 25 | {:up ["CREATE TABLE `person` (`number` SERIAL PRIMARY KEY, `name` TEXT, `yob` INTEGER, `secret` TEXT, `hidden` BOOLEAN)" 26 | "INSERT INTO `person` (`number`, `name`, `yob`, `secret`, `hidden`) VALUES (1, 'jon', 1980, 'only jon knows', 1), (2, 'mary', 1992, 'surprise', 0)" ] 27 | :down ["DROP TABLE `person`"]} 28 | 29 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-3/create-pet-table] 30 | {:up ["CREATE TABLE `pet` (`index` SERIAL PRIMARY KEY, `color` TEXT, `yob` INTEGER, `private` TEXT)" 31 | "INSERT INTO `pet` (`index`, `color`, `yob`, `private`) 32 | VALUES 33 | (10, 'yellow', 2015, 'you bastard'), 34 | (11, 'green', 2012, 'hey'), 35 | (12, 'yellow', 2017, 'secret'), 36 | (13, 'green', 2016, 'nothing'), 37 | (20, 'orange', 2014, 'very important'), 38 | (21, 'green', 2015, 'shhh'), 39 | (22, 'green', 2016, 'idk')"] 40 | :down ["DROP TABLE `pet`"]} 41 | 42 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-3/create-person_pet-table] 43 | {:up ["CREATE TABLE `person_pet` (`id` SERIAL PRIMARY KEY, `person_number` INTEGER REFERENCES `person`(`number`), `pet_index` INTEGER REFERENCES `pet`(`index`), `adoption_year` INTEGER)" 44 | "INSERT INTO `person_pet` (`person_number`, `pet_index`, `adoption_year`) VALUES 45 | (1, 10, 2015), 46 | (1, 11, 2013), 47 | (1, 12, 2018), 48 | (1, 13, 2016), 49 | (2, 20, 2014), 50 | (2, 21, 2016), 51 | (2, 22, 2017)"] 52 | :down ["DROP TABLE `person_pet`"]} 53 | 54 | ;; scenario 4 migrations 55 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-4/create-human-table] 56 | {:up ["CREATE TABLE `human` (`number` SERIAL PRIMARY KEY, `name` TEXT, `yob` INTEGER)" 57 | "INSERT INTO `human` (`number`, `name`, `yob`) VALUES (1, 'jon', 1980), (2, 'mary', 1992), (3, 'peter', 1989), (4, 'sandra', 1970)" ] 58 | :down ["DROP TABLE `human`"]} 59 | 60 | [:duct.migrator.ragtime/sql :walkable-demo.migration.scenario-4/create-follow-table] 61 | {:up ["CREATE TABLE `follow` (`human_1` INTEGER REFERENCES `human`(`number`), `human_2` INTEGER REFERENCES `human`(`number`), `year` INTEGER)" 62 | "INSERT INTO `follow` (`human_1`, `human_2`, `year`) VALUES (1, 2, 2015), (1, 3, 2017), (2, 1, 2014), (2, 3, 2015), (1, 4, 2012)"] 63 | :down ["DROP TABLE `follow`"]} 64 | } 65 | -------------------------------------------------------------------------------- /dev/src/dev.clj: -------------------------------------------------------------------------------- 1 | (ns dev 2 | (:refer-clojure :exclude [test]) 3 | (:require [clojure.repl :refer :all] 4 | [fipp.edn :refer [pprint]] 5 | [clojure.tools.namespace.repl :refer [set-refresh-dirs refresh]] 6 | [clojure.java.io :as io] 7 | [duct.core :as duct] 8 | [eftest.runner :as eftest] 9 | [integrant.core :as ig] 10 | [com.wsscode.pathom.core :as p] 11 | [com.wsscode.pathom.profile :as pp] 12 | [com.wsscode.pathom.connect :as pc] 13 | [com.wsscode.pathom.connect.planner :as pcp] 14 | [clojure.java.jdbc :as jdbc] 15 | [clojure.core.async :as async :refer [go-loop >! (duct/read-config (config-by-db db)) 37 | (duct/prep-config profiles))) 38 | 39 | (integrant.repl/set-prep! #(prepare-system :sqlite)) 40 | 41 | (defn test [] 42 | (eftest/run-tests (eftest/find-tests "test"))) 43 | 44 | (when (io/resource "local.clj") 45 | (load "local")) 46 | 47 | (defn db [] 48 | (-> system (ig/find-derived-1 :duct.database/sql) val :spec)) 49 | 50 | (defn q [sql] 51 | (jdbc/query (db) sql)) 52 | 53 | (defn e [sql] 54 | (jdbc/execute! (db) sql)) 55 | 56 | (comment 57 | ;; make changes to database right from your editor 58 | (e "CREATE TABLE `foo` (`id` INTEGER)") 59 | (e "INSERT INTO `foo` (`id`) VALUES (1)") 60 | (q "SELECT * from foo") 61 | (e "DROP TABLE `foo`")) 62 | 63 | 64 | ;; End of Duct framework helpers >>> 65 | 66 | (defn run-print-query 67 | "jdbc/query wrapped by println" 68 | [& xs] 69 | (let [[q & args] (rest xs)] 70 | (println q) 71 | (println args)) 72 | (apply jdbc/query xs)) 73 | 74 | (defn async-run-print-query 75 | [db q] 76 | (let [c (promise-chan)] 77 | (let [r (run-print-query db q)] 78 | ;; (println "back from sql: " r) 79 | (put! c r)) 80 | c)) 81 | 82 | ;; Simple join examples 83 | 84 | ;; I named the primary columns "index" and "number" instead of "id" to 85 | ;; ensure arbitrary columns will work. 86 | 87 | (defn now [] 88 | (.format (java.text.SimpleDateFormat. "HH:mm") (java.util.Date.))) 89 | 90 | (comment 91 | (->> (floor-plan/compile-floor-plan* common/person-pet-registry) 92 | :attributes 93 | (filter #(= :people/count (:key %))) 94 | first) 95 | 96 | (let [reg (floor-plan/conditionally-update 97 | farmer-house-registry 98 | #(= :farmers/farmers (:key %)) 99 | #(merge % {:default-order-by [:farmer/name :desc] 100 | :validate-order-by #{:farmer/name :farmer/number}})) 101 | f (->> (floor-plan/compile-floor-plan* reg) 102 | :attributes 103 | (filter #(= :farmers/farmers (:key %))) 104 | first 105 | :compiled-pagination-fallbacks 106 | :limit-fallback)] 107 | (f 2))) 108 | 109 | (def w* (walkable-parser :sqlite common/person-pet-registry)) 110 | 111 | (defn w 112 | [q] 113 | (w* {:db (db)} q)) 114 | 115 | (comment 116 | (w `[{(:pets/by-color {:order-by [:pet/color :desc]}) 117 | [:pet/color]}]) 118 | 119 | (w `[(:people/count {:filter [:and {:person/pet [:or [:= :pet/color "white"] 120 | [:= :pet/color "yellow"]]} 121 | [:< :person/number 10]]})])) 122 | -------------------------------------------------------------------------------- /dev/src/dev.cljs: -------------------------------------------------------------------------------- 1 | (ns dev 2 | (:require [com.wsscode.pathom.core :as p] 3 | [com.wsscode.pathom.connect :as pc] 4 | [com.wsscode.pathom.connect.planner :as pcp] 5 | [walkable.core-async :as walkable] 6 | [walkable.sql-query-builder.emitter :as emitter] 7 | [walkable.sql-query-builder.floor-plan :as floor-plan] 8 | [walkable.integration-test.common :as common] 9 | ["sqlite3" :as sqlite3] 10 | [cljs.core.async :as async :refer [put! >! clj r :keywordize-keys true)] 23 | (println "\nsql query: " q) 24 | (println "sql params: " params) 25 | (println "sql results:" x) 26 | (put! c x)))) 27 | c)) 28 | 29 | (defn walkable-parser 30 | [db-type registry] 31 | (p/async-parser 32 | {::p/env {::p/reader [p/map-reader 33 | pc/reader3 34 | pc/open-ident-reader 35 | p/env-placeholder-reader] 36 | ::p/process-error 37 | (fn [_ err] 38 | (js/console.error err) 39 | (p/error-str err))} 40 | ::p/plugins [(pc/connect-plugin {::pc/register []}) 41 | (walkable/connect-plugin {:db-type db-type 42 | :registry registry 43 | :query-env #(async-run-print-query (:db %1) %2)}) 44 | p/elide-special-outputs-plugin 45 | p/error-handler-plugin 46 | p/trace-plugin]})) 47 | 48 | (def w* (walkable-parser :sqlite common/person-pet-registry)) 49 | 50 | (defn w 51 | [q] 52 | (w* {:db db} q)) 53 | 54 | (comment 55 | (w `[{(:pets/by-color {:order-by [:pet/color :desc]}) 56 | [:pet/color]}]) 57 | 58 | (w `[(:people/count {:filter [:and {:person/pet [:or [:= :pet/color "white"] 59 | [:= :pet/color "yellow"]]} 60 | [:< :person/number 10]]})])) 61 | 62 | (defn main [] 63 | (println "walkable runs in nodejs!!!")) 64 | -------------------------------------------------------------------------------- /dev/src/duct_hierarchy.edn: -------------------------------------------------------------------------------- 1 | {:duct.profile/common [:duct.profile/base]} 2 | -------------------------------------------------------------------------------- /dev/src/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require [clojure.java.io :as io] 3 | [clojure.string])) 4 | 5 | (defn patch-common-edn-files 6 | "Produce multiple versions of common.edn for each database type. 7 | You only need to run this if you have modified the file 8 | `common_unpatched.edn`" 9 | [] 10 | (let [config-unpatched (slurp (io/resource "common_unpatched.edn"))] 11 | ;; the sql code in common_unpatched.edn is for mysql 12 | ;; therefore no patch is needed for it 13 | (spit "dev/resources/core-mysql.edn" config-unpatched) 14 | ;; replace all mysql's backticks with quotation marks 15 | ;; and you get postgres version 16 | (spit "dev/resources/core-postgres.edn" 17 | (-> config-unpatched (clojure.string/replace #"`" "\\\\\\\""))) 18 | ;; sqlite can work with backticks, but it doesn't have 19 | ;; boolean type, so all `true`s must be replaced with `1` 20 | ;; and all `false`s with 0. 21 | (spit "dev/resources/core-sqlite.edn" 22 | (-> config-unpatched 23 | (clojure.string/replace #"true" "1") 24 | (clojure.string/replace #"false" "0"))))) 25 | 26 | (comment 27 | (patch-common-edn-files) 28 | ) 29 | (defn dev 30 | "Load and switch to the 'dev' namespace." 31 | [] 32 | (require 'dev) 33 | (in-ns 'dev) 34 | :loaded) 35 | -------------------------------------------------------------------------------- /dev/src/walkable/integration_test/helper.clj: -------------------------------------------------------------------------------- 1 | (ns walkable.integration-test.helper 2 | (:require [clojure.java.jdbc :as jdbc] 3 | [com.wsscode.pathom.core :as p] 4 | [com.wsscode.pathom.connect :as pc] 5 | [walkable.core :as walkable] 6 | [clojure.test :refer [testing is]])) 7 | 8 | (defn walkable-parser 9 | [db-type registry] 10 | (p/parser 11 | {::p/env {::p/reader [p/map-reader 12 | pc/reader3 13 | pc/open-ident-reader 14 | p/env-placeholder-reader]} 15 | ::p/plugins [(pc/connect-plugin {::pc/register []}) 16 | (walkable/connect-plugin {:db-type db-type 17 | :registry registry 18 | :query-env #(jdbc/query (:db %1) %2)}) 19 | p/elide-special-outputs-plugin 20 | p/error-handler-plugin 21 | p/trace-plugin]})) 22 | 23 | (defn run-scenario-tests 24 | [db db-type scenarios] 25 | (into [] 26 | (for [[scenario {:keys [:registry :test-suite]}] scenarios 27 | {:keys [:message :env :query :expected]} test-suite] 28 | (testing (str "In scenario " scenario " for " db-type ", testing " message) 29 | (is (= expected 30 | (let [parser (walkable-parser db-type registry)] 31 | (parser (assoc env :db db) 32 | query)))))))) 33 | -------------------------------------------------------------------------------- /dev/test/walkable/integration_test/common.cljc: -------------------------------------------------------------------------------- 1 | (ns walkable.integration-test.common 2 | (:require [com.wsscode.pathom.core :as p] 3 | [walkable.sql-query-builder.floor-plan :as floor-plan] 4 | [plumbing.core :refer [fnk sum]])) 5 | 6 | (def farmer-house-registry 7 | [{:key :farmers/farmers 8 | :type :root 9 | :table "farmer" 10 | :output [:farmer/number :farmer/name :farmer/house]} 11 | {:key :farmer/number 12 | :type :true-column 13 | :primary-key true 14 | :output [:farmer/name :farmer/house-plus :farmer/house-count :farmer/house]} 15 | {:key :farmer/house 16 | :type :join 17 | :join-path [:farmer/house-index :house/index] 18 | :output [:house/color] 19 | :cardinality :one} 20 | {:key :house/owner 21 | :type :join 22 | :join-path [:house/index :farmer/house-index] 23 | :output [:farmer/number] 24 | :cardinality :one}]) 25 | 26 | (def kid-toy-registry 27 | [{:key :kids/kids 28 | :type :root 29 | :table "kid" 30 | :output [:kid/name]} 31 | {:key :toy/owner 32 | :type :join 33 | :join-path [:toy/owner-number :kid/number] 34 | :output [:kid/name :kid/number] 35 | :cardinality :one} 36 | {:key :kid/toy 37 | :type :join 38 | :join-path [:kid/number :toy/owner-number] 39 | :output [:toy/color :toy/index] 40 | :cardinality :one} 41 | {:key :kid/number 42 | :type :true-column 43 | :primary-key true 44 | :output [:kid/name]}]) 45 | 46 | (def human-follow-registry 47 | [{:key :humans/humans 48 | :type :root 49 | :table "human" 50 | :output [:human/number :human/name :human/yob :human/age]} 51 | {:key :human/follow 52 | :type :join 53 | :join-path [:human/number :follow/human-1 :follow/human-2 :human/number] 54 | :output [:human/number :human/name :human/yob :human/age]}]) 55 | 56 | (def person-pet-registry 57 | [{:key :people/people 58 | :type :root 59 | :table "person" 60 | :output [:person/number] 61 | :filter 62 | [:or [:= :person/hidden true] 63 | [:= :person/hidden false]]} 64 | {:key :person/age 65 | :type :pseudo-column 66 | :formula [:- 2018 :person/yob]} 67 | {:key :person/number 68 | :type :true-column 69 | :primary-key true 70 | :output [:person/name :person/yob :person/age] 71 | :filter 72 | [:or [:= :person/hidden true] 73 | [:= :person/hidden false]]} 74 | {:key :people/count 75 | :type :root 76 | :aggregate true 77 | :table "person" 78 | :formula [:count-*] 79 | :filter 80 | [:or [:= :person/hidden true] 81 | [:= :person/hidden false]]} 82 | {:key :person/pet 83 | :type :join 84 | :join-path 85 | [:person/number :person-pet/person-number 86 | :person-pet/pet-index :pet/index] 87 | :output [:pet/index 88 | :person-pet/adoption-year 89 | :pet/name 90 | :pet/yob 91 | :pet/color]} 92 | {:key :pet/owner 93 | :type :join 94 | :join-path 95 | [:pet/index :person-pet/pet-index :person-pet/person-number :person/number] 96 | :output [:person/number]} 97 | {:key :person/pet-count 98 | :type :join 99 | :join-path 100 | [:person/number :person-pet/person-number 101 | :person-pet/pet-index :pet/index] 102 | :aggregate true 103 | :formula [:count-*]} 104 | {:key :pets/by-color 105 | :type :root 106 | :table "pet" 107 | :output [:pet/color :color/pet-count] 108 | :group-by [:pet/color] 109 | :having [:< 1 :color/pet-count]} 110 | {:key :color/pet-count 111 | :type :pseudo-column 112 | :formula [:count :pet/index]}]) 113 | 114 | (def common-scenarios 115 | {:farmer-house 116 | {:registry farmer-house-registry 117 | :test-suite 118 | [{:message "filters should work" 119 | :query 120 | `[{(:farmers/farmers {:filter {:farmer/house {:house/owner [:= :farmer/name "mary"]}}}) 121 | [:farmer/number :farmer/name 122 | {:farmer/house [:house/index :house/color]}]}] 123 | :expected 124 | {:farmers/farmers [#:farmer{:number 2, :name "mary", :house #:house {:index "20", :color "brown"}}]}} 125 | {:message "no pagination" 126 | :query 127 | `[{:farmers/farmers 128 | [:farmer/number :farmer/name 129 | {:farmer/house [:house/index :house/color]}]}] 130 | :expected 131 | #:farmers {:farmers [#:farmer{:number 1, :name "jon", :house #:house {:index "10", :color "black"}} 132 | #:farmer{:number 2, :name "mary", :house #:house {:index "20", :color "brown"}} 133 | #:farmer{:number 3, :name "homeless", :house #:house {}}]}}]} 134 | :farmer-house-paginated 135 | {:registry (floor-plan/conditionally-update 136 | farmer-house-registry 137 | #(= :farmers/farmers (:key %)) 138 | #(merge % {:default-order-by [:farmer/name :desc] 139 | :validate-order-by #{:farmer/name :farmer/number}})) 140 | :test-suite 141 | [{:message "pagination fallbacks" 142 | :query 143 | `[{:farmers/farmers 144 | [:farmer/number :farmer/name 145 | {:farmer/house [:house/index :house/color]}]}] 146 | :expected 147 | #:farmers{:farmers [#:farmer{:number 2, :name "mary", :house #:house {:index "20", :color "brown"}} 148 | #:farmer{:number 1, :name "jon", :house #:house {:index "10", :color "black"}} 149 | #:farmer{:number 3, :name "homeless", :house #:house {}}]}} 150 | {:message "supplied pagination" 151 | :query 152 | `[{(:farmers/farmers {:limit 1}) 153 | [:farmer/number :farmer/name 154 | {:farmer/house [:house/index :house/color]}]}] 155 | :expected 156 | #:farmers{:farmers [#:farmer{:number 2, :name "mary", :house #:house {:index "20", :color "brown"}}]}} 157 | {:message "without order-by column in query" 158 | :query 159 | `[{(:farmers/farmers {:limit 1}) 160 | [:farmer/number 161 | {:farmer/house [:house/index :house/color]}]}] 162 | :expected 163 | #:farmers{:farmers [#:farmer{:number 2, :house #:house {:index "20", :color "brown"}}]}}]} 164 | 165 | :kid-toy 166 | {:registry kid-toy-registry 167 | :test-suite 168 | [{:message "idents should work" 169 | :query 170 | '[{[:kid/number 1] [:kid/number :kid/name 171 | {:kid/toy [:toy/index :toy/color]}]}] 172 | :expected 173 | {[:kid/number 1] #:kid {:number 1, :name "jon", :toy #:toy {:index 10, :color "yellow"}}}}]} 174 | 175 | :human-follow 176 | {:registry human-follow-registry 177 | :test-suite 178 | [{:message "self-join should work" 179 | :query 180 | `[{(:humans/humans {:order-by :human/number}) 181 | [:human/number :human/name 182 | {(:human/follow {:order-by :human/number}) 183 | [:human/number 184 | :human/name 185 | :human/yob]}]}] 186 | :expected 187 | {:humans/humans [#:human{:number 1, :name "jon", 188 | :follow [#:human{:number 2, :name "mary", :yob 1992} 189 | #:human{:number 3, :name "peter", :yob 1989} 190 | #:human{:number 4, :name "sandra", :yob 1970}]} 191 | #:human{:number 2, :name "mary", 192 | :follow [#:human{:number 1, :name "jon", :yob 1980} 193 | #:human{:number 3, :name "peter", :yob 1989}]} 194 | #:human{:number 3, :name "peter", :follow []} 195 | #:human{:number 4, :name "sandra", :follow []}]}}]} 196 | 197 | :human-follow-variable-getters 198 | {:registry (concat human-follow-registry 199 | [{:key :human/age 200 | :type :pseudo-column 201 | :formula [:- 'current-year :human/yob]} 202 | {:key :human/stats 203 | :type :pseudo-column 204 | :formula [:str 'stats-header 205 | ", m: " 'm 206 | ", v: " 'v]} 207 | {:key 'current-year 208 | :type :variable 209 | :compute (fn [env] (:current-year env))} 210 | ;; TODO: convention for variable-graph's :key 211 | {:key [:graph :hello] 212 | :type :variable-graph 213 | :graph 214 | {:xs (fnk [env] (get-in env [:xs])) 215 | :n (fnk [xs] (count xs)) 216 | :m (fnk [xs n] (/ (sum identity xs) n)) 217 | :m2 (fnk [xs n] (/ (sum #(* % %) xs) n)) 218 | :v (fnk [m m2] (str (- m2 (* m m)))) 219 | :stats-header (fnk [xs] (str "stats for xs =" (pr-str xs)))}}]) 220 | :test-suite 221 | [{:message "variable-getters should work" 222 | :env {:current-year 2019} 223 | :query 224 | `[{(:humans/humans {:order-by :human/number}) 225 | [:human/number :human/name :human/age 226 | {(:human/follow {:order-by :human/number}) 227 | [:human/number 228 | :human/name 229 | :human/age]}]}] 230 | :expected 231 | {:humans/humans [#:human{:number 1, :name "jon", :age 39, 232 | :follow [#:human{:number 2, :name "mary", :age 27} 233 | #:human{:number 3, :name "peter", :age 30} 234 | #:human{:number 4, :name "sandra", :age 49}]} 235 | #:human{:number 2, :name "mary", :age 27, 236 | :follow [#:human{:number 1, :name "jon", :age 39} 237 | #:human{:number 3, :name "peter", :age 30}]} 238 | #:human{:number 3, :name "peter", :age 30, :follow []} 239 | #:human{:number 4, :name "sandra", :age 49, :follow []}]}} 240 | #_{:message "variable-getter-graphs should work" 241 | ;; choose this sequence so stats values are integers 242 | ;; therefore the output string is the same in all sql dbs 243 | :env {:xs [2 4 6 8]} 244 | :query 245 | `[{(:humans/humans {:order-by :human/number}) 246 | [:human/number :human/name :human/stats]}] 247 | :expected 248 | #:humans{:humans [#:human{:number 1, :name "jon", 249 | :stats "stats for xs =[2 4 6 8], m: 5, v: 5"} 250 | #:human{:number 2, :name "mary", 251 | :stats "stats for xs =[2 4 6 8], m: 5, v: 5"} 252 | #:human{:number 3, :name "peter", 253 | :stats "stats for xs =[2 4 6 8], m: 5, v: 5"} 254 | #:human{:number 4, :name "sandra", 255 | :stats "stats for xs =[2 4 6 8], m: 5, v: 5"}]}}]} 256 | :person-pet 257 | {:registry person-pet-registry 258 | :test-suite 259 | [{:message "join-table should work" 260 | :query 261 | `[{[:person/number 1] 262 | [:person/number 263 | :person/name 264 | :person/yob 265 | {:person/pet [:pet/index 266 | :pet/yob 267 | :pet/color 268 | :person-pet/adoption-year 269 | {:pet/owner [:person/name]}]}]}] 270 | :expected 271 | {[:person/number 1] 272 | #:person {:number 1, 273 | :name "jon", 274 | :yob 1980, 275 | :pet 276 | [{:pet/index 10, 277 | :pet/yob 2015, 278 | :pet/color "yellow", 279 | :person-pet/adoption-year 2015, 280 | :pet/owner [#:person{:name "jon"}]} 281 | {:pet/index 11, 282 | :pet/yob 2012, 283 | :pet/color "green", 284 | :person-pet/adoption-year 2013, 285 | :pet/owner [#:person{:name "jon"}]} 286 | {:pet/index 12, 287 | :pet/yob 2017, 288 | :pet/color "yellow", 289 | :person-pet/adoption-year 2018, 290 | :pet/owner [#:person{:name "jon"}]} 291 | {:pet/index 13, 292 | :pet/yob 2016, 293 | :pet/color "green", 294 | :person-pet/adoption-year 2016, 295 | :pet/owner [#:person{:name "jon"}]}]}}} 296 | 297 | {:message "aggregate should work" 298 | :query 299 | `[(:people/count {:filter [:and {:person/pet [:or [:= :pet/color "white"] 300 | [:= :pet/color "yellow"]]} 301 | [:< :person/number 10]]})] 302 | :expected 303 | #:people {:count 1}} 304 | 305 | {:message "filters should work" 306 | :query 307 | `[{(:people/people {:filter [:and {:person/pet [:or [:= :pet/color "white"] 308 | [:= :pet/color "yellow"]]} 309 | [:< :person/number 10]] 310 | :order-by [:person/name]}) 311 | [:person/number :person/name 312 | :person/pet-count 313 | {:person/pet [:pet/index 314 | :pet/yob 315 | ;; columns from join table work, too 316 | :person-pet/adoption-year 317 | :pet/color]}]}] 318 | :expected 319 | #:people{:people 320 | [#:person{:number 1, 321 | :name "jon", 322 | :pet-count 4, 323 | :pet 324 | [{:pet/index 10, 325 | :pet/yob 2015, 326 | :person-pet/adoption-year 2015, 327 | :pet/color "yellow"} 328 | {:pet/index 11, 329 | :pet/yob 2012, 330 | :person-pet/adoption-year 2013, 331 | :pet/color "green"} 332 | {:pet/index 12, 333 | :pet/yob 2017, 334 | :person-pet/adoption-year 2018, 335 | :pet/color "yellow"} 336 | {:pet/index 13, 337 | :pet/yob 2016, 338 | :person-pet/adoption-year 2016, 339 | :pet/color "green"}]}]}} 340 | 341 | {:message "placeholders should work" 342 | :env {::p/placeholder-prefixes #{"ph" "placeholder"}} 343 | :query 344 | `[{:people/people 345 | [{:placeholder/info [:person/yob :person/name]} 346 | {:person/pet [:pet/index 347 | :pet/yob 348 | :pet/color]} 349 | {:ph/deep [{:ph/nested [{:placeholder/play [{:person/pet [:pet/index 350 | :pet/yob 351 | :pet/color]}]}]}]}]}] 352 | :expected 353 | #:people{:people 354 | [{:placeholder/info #:person {:yob 1980, :name "jon"}, 355 | :person/pet 356 | [#:pet{:index 10, :yob 2015, :color "yellow"} 357 | #:pet{:index 11, :yob 2012, :color "green"} 358 | #:pet{:index 12, :yob 2017, :color "yellow"} 359 | #:pet{:index 13, :yob 2016, :color "green"}], 360 | :ph/deep 361 | #:ph {:nested 362 | #:placeholder {:play 363 | #:person {:pet 364 | [#:pet{:index 10, 365 | :yob 2015, 366 | :color "yellow"} 367 | #:pet{:index 11, 368 | :yob 2012, 369 | :color "green"} 370 | #:pet{:index 12, 371 | :yob 2017, 372 | :color "yellow"} 373 | #:pet{:index 13, 374 | :yob 2016, 375 | :color "green"}]}}}} 376 | {:placeholder/info #:person {:yob 1992, :name "mary"}, 377 | :person/pet 378 | [#:pet{:index 20, :yob 2014, :color "orange"} 379 | #:pet{:index 21, :yob 2015, :color "green"} 380 | #:pet{:index 22, :yob 2016, :color "green"}], 381 | :ph/deep 382 | #:ph {:nested 383 | #:placeholder {:play 384 | #:person {:pet 385 | [#:pet{:index 20, 386 | :yob 2014, 387 | :color "orange"} 388 | #:pet{:index 21, 389 | :yob 2015, 390 | :color "green"} 391 | #:pet{:index 22, 392 | :yob 2016, 393 | :color "green"}]}}}}]}} 394 | 395 | {:message "grouping should work" 396 | :query 397 | `[{(:pets/by-color {:order-by [:pet/color :desc]}) 398 | [:pet/color]}] 399 | :expected 400 | #:pets {:by-color 401 | [{:pet/color "yellow"} 402 | {:pet/color "green"}]}} 403 | 404 | {:message "grouping with count should work" 405 | :query 406 | `[{(:pets/by-color {:order-by [:pet/color :asc]}) 407 | [:color/pet-count :pet/color]}] 408 | :expected 409 | #:pets{:by-color 410 | [{:color/pet-count 4, :pet/color "green"} 411 | {:color/pet-count 2, :pet/color "yellow"}]}} 412 | 413 | {:message "pseudo-columns should work" 414 | :query 415 | `[{:people/people [:person/number 416 | :person/name 417 | :person/yob 418 | :person/age]}] 419 | :expected 420 | #:people {:people [#:person{:number 1, :name "jon", :yob 1980, :age 38} 421 | #:person{:number 2, :name "mary", :yob 1992, :age 26}]}}]}}) 422 | -------------------------------------------------------------------------------- /dev/test/walkable/integration_test/mysql_test.clj: -------------------------------------------------------------------------------- 1 | (ns walkable.integration-test.mysql-test 2 | {:integration true :mysql true} 3 | (:require [walkable.integration-test.helper :refer [run-scenario-tests]] 4 | [walkable.integration-test.common :refer [common-scenarios]] 5 | [clojure.java.io :as io] 6 | [integrant.core :as ig] 7 | [duct.core :as duct] 8 | [clojure.test :as t :refer [deftest use-fixtures]])) 9 | 10 | (def ^:dynamic *db* :not-initialized) 11 | 12 | (defn setup [f] 13 | (duct/load-hierarchy) 14 | (let [system (-> (duct/read-config (io/resource "config-mysql.edn")) 15 | (duct/prep-config) 16 | (ig/init))] 17 | (binding [*db* (-> system (ig/find-derived-1 :duct.database/sql) val :spec)] 18 | (f) 19 | (ig/halt! system)))) 20 | 21 | (use-fixtures :once setup) 22 | 23 | (deftest common-scenarios-test 24 | (run-scenario-tests *db* :mysql common-scenarios)) 25 | 26 | (deftest mysql-specific-scenarios-test 27 | (run-scenario-tests *db* :mysql {})) 28 | -------------------------------------------------------------------------------- /dev/test/walkable/integration_test/postgres_test.clj: -------------------------------------------------------------------------------- 1 | (ns walkable.integration-test.postgres-test 2 | {:integration true :postgres true} 3 | (:require [walkable.integration-test.helper :refer [run-scenario-tests]] 4 | [walkable.integration-test.common :refer [common-scenarios]] 5 | [clojure.java.io :as io] 6 | [integrant.core :as ig] 7 | [duct.core :as duct] 8 | [clojure.test :as t :refer [deftest use-fixtures]])) 9 | 10 | (def ^:dynamic *db* :not-initialized) 11 | 12 | (defn setup [f] 13 | (duct/load-hierarchy) 14 | (let [system (-> (duct/read-config (io/resource "config-postgres.edn")) 15 | (duct/prep-config) 16 | (ig/init))] 17 | (binding [*db* (-> system (ig/find-derived-1 :duct.database/sql) val :spec)] 18 | (f) 19 | (ig/halt! system)))) 20 | 21 | (use-fixtures :once setup) 22 | 23 | (deftest common-scenarios-test 24 | (run-scenario-tests *db* :postgres common-scenarios)) 25 | 26 | (def planet-inhabitant-registry 27 | [{:key :land.animal/animals 28 | :type :root 29 | :table "land.animal" 30 | :output [:land.animal/id :land.animal/name]} 31 | {:key :ocean.animal/animals 32 | :type :root 33 | :table "ocean.animal" 34 | :output [:ocean.animal/id :ocean.animal/name]} 35 | {:key :land.animal/id 36 | :type :true-column 37 | :primary-key true 38 | :output [:land.animal/name]} 39 | {:key :ocean.animal/id 40 | :type :true-column 41 | :primary-key true 42 | :output [:ocean.animal/name]}]) 43 | 44 | (def postgres-scenarios 45 | {:planet-species 46 | {:registry planet-inhabitant-registry 47 | :test-suite 48 | [{:message "postgres schema should work" 49 | :query 50 | `[{:ocean.animal/animals 51 | [:ocean.animal/id :ocean.animal/name]} 52 | {[:land.animal/id 1] [:land.animal/id :land.animal/name]}] 53 | :expected 54 | {:ocean.animal/animals [#:ocean.animal{:id 10, :name "whale"} 55 | #:ocean.animal{:id 20, :name "shark"}] 56 | [:land.animal/id 1] #:land.animal{:id 1, :name "elephant"}}}]}}) 57 | 58 | (deftest postgres-specific-scenarios-test 59 | (run-scenario-tests *db* :postgres postgres-scenarios)) 60 | -------------------------------------------------------------------------------- /dev/test/walkable/integration_test/sqlite_test.clj: -------------------------------------------------------------------------------- 1 | (ns walkable.integration-test.sqlite-test 2 | {:integration true :sqlite true} 3 | (:require [walkable.integration-test.helper :refer [run-scenario-tests]] 4 | [walkable.integration-test.common :refer [common-scenarios]] 5 | [clojure.java.io :as io] 6 | [integrant.core :as ig] 7 | [duct.core :as duct] 8 | [clojure.test :refer [deftest use-fixtures]])) 9 | 10 | (def ^:dynamic *db* :not-initialized) 11 | 12 | (defn setup [f] 13 | (duct/load-hierarchy) 14 | (let [system (-> (duct/read-config (io/resource "config-sqlite.edn")) 15 | (duct/prep-config) 16 | (ig/init))] 17 | (binding [*db* (-> system (ig/find-derived-1 :duct.database/sql) val :spec)] 18 | (f) 19 | (ig/halt! system)))) 20 | 21 | (use-fixtures :once setup) 22 | 23 | (deftest common-scenarios-test 24 | (run-scenario-tests *db* :sqlite common-scenarios)) 25 | 26 | (deftest sqlite-specific-scenarios-test 27 | (run-scenario-tests *db* :sqlite {})) 28 | -------------------------------------------------------------------------------- /dev/test/walkable/integration_test/sqlite_test.cljs: -------------------------------------------------------------------------------- 1 | (ns walkable.integration-test.sqlite-test 2 | (:require-macros [cljs.core.async.macros :refer [go]]) 3 | (:require [walkable.integration-test.common :refer [common-scenarios]] 4 | [cljs.core.async :as async :refer [put! clj r :keywordize-keys true)] 17 | (put! c x)))) 18 | c)) 19 | 20 | (defn walkable-parser 21 | [db-type registry] 22 | (p/async-parser 23 | {::p/env {::p/reader [p/map-reader 24 | pc/reader3 25 | pc/open-ident-reader 26 | p/env-placeholder-reader]} 27 | ::p/plugins [(pc/connect-plugin {::pc/register []}) 28 | (walkable/connect-plugin {:db-type db-type 29 | :registry registry 30 | :query-env #(async-run-query (:sqlite-db %1) %2)}) 31 | p/elide-special-outputs-plugin 32 | p/error-handler-plugin 33 | p/trace-plugin]})) 34 | 35 | (def db (sqlite3/Database. "walkable_dev.sqlite")) 36 | 37 | (defn run-scenario-tests* 38 | [db db-type scenarios] 39 | (for [[scenario {:keys [:registry :test-suite]}] scenarios 40 | {:keys [:message :env :query :expected]} test-suite] 41 | {:msg (str "In scenario " scenario " for " db-type ", testing " message) 42 | :expected expected 43 | :result 44 | (let [parser (walkable-parser db-type registry)] 45 | (parser (assoc env :sqlite-db db) 46 | query))})) 47 | 48 | (defn run-scenario-tests 49 | [db db-type scenarios] 50 | (t/async done 51 | (go 52 | (doseq [{:keys [:msg :expected :result]} 53 | (run-scenario-tests* db db-type scenarios)] 54 | (testing msg 55 | (is (= expected ( 2 | 13 | Walkable logo 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | Walkable logo 24 | 26 | 27 | 28 | Hoang Minh Thang 29 | 30 | 31 | 2018-03-20 32 | 33 | 35 | 37 | 39 | 41 | 43 | 45 | 47 | 48 | 49 | 50 | 52 | 56 | 60 | 64 | 68 | 69 | 73 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "shadow-cljs": "^2.11.18", 4 | "sqlite3": "^4.0.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject walkable "1.3.0-alpha0" 2 | :description "A Clojure(script) SQL library for building APIs" 3 | :url "https://walkable.gitlab.io" 4 | :license {:name "Eclipse Public License - v 1.0" 5 | :url "http://www.eclipse.org/legal/epl-v10.html" 6 | :distribution :repo 7 | :comments "same as Clojure"} 8 | :min-lein-version "2.0.0" 9 | :dependencies [[org.clojure/clojure "1.10.1" :scope "provided"] 10 | [cheshire "5.10.1"] 11 | [org.clojure/clojurescript "1.10.764" :scope "provided"] 12 | [org.clojure/spec.alpha "0.2.187"] 13 | [com.wsscode/pathom "2.3.0"] 14 | [weavejester/dependency "0.2.1"] 15 | [prismatic/plumbing "0.5.5"] 16 | [org.clojure/core.async "1.2.603" :scope "provided"]] 17 | :resource-paths ["resources"] 18 | :test-selectors {:default (complement :integration) 19 | :integration :integration 20 | :sqlite :sqlite 21 | :postgres :postgres 22 | :mysql :mysql} 23 | :profiles 24 | {:dev [:project/dev] 25 | :repl {:prep-tasks ^:replace ["javac" "compile"] 26 | :repl-options {:init-ns user 27 | :timeout 120000}} 28 | :project/dev {:plugins [[duct/lein-duct "0.12.1"]] 29 | :source-paths ["dev/src"] 30 | :test-paths ["dev/test"] 31 | :resource-paths ["dev/resources"] 32 | :dependencies [[duct/core "0.8.0"] 33 | [duct/module.logging "0.5.0"] 34 | [duct/module.sql "0.6.0"] 35 | [org.clojure/test.check "1.0.0"] 36 | [cheshire "5.10.0"] 37 | [integrant/repl "0.3.1"] 38 | [eftest "0.5.9"] 39 | [kerodon "0.9.1"] 40 | ;; sql flavors 41 | [org.xerial/sqlite-jdbc "3.31.1"] 42 | [org.postgresql/postgresql "42.2.12"] 43 | [mysql/mysql-connector-java "8.0.20"]]}}) 44 | -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:dependencies 2 | [[org.clojure/clojurescript "1.10.764"] 3 | [com.wsscode/pathom "2.3.0" :exclusions [org.clojure/clojurescript]] 4 | [prismatic/plumbing "0.5.5"] 5 | [weavejester/dependency "0.2.1"] 6 | [org.clojure/test.check "1.0.0"] 7 | [org.clojure/spec.alpha "0.2.187"]] 8 | 9 | :source-paths 10 | ["src" "dev/src" "test" "dev/test"] 11 | 12 | :builds 13 | {:app {:target :node-script 14 | :output-to "run.js" 15 | :main dev/main 16 | :devtools {;; :before-load-async demo.script/stop 17 | :after-load dev/main}} 18 | :test {:target :node-test 19 | :output-to "test.js"}}} 20 | -------------------------------------------------------------------------------- /src/walkable/core.cljc: -------------------------------------------------------------------------------- 1 | (ns walkable.core 2 | (:require [clojure.zip :as z] 3 | [walkable.sql-query-builder.expressions :as expressions] 4 | [walkable.sql-query-builder.ast :as ast] 5 | [walkable.sql-query-builder.floor-plan :as floor-plan] 6 | [com.wsscode.pathom.core :as p] 7 | [com.wsscode.pathom.connect :as pc] 8 | [com.wsscode.pathom.connect.planner :as pcp])) 9 | 10 | (defn ast-map-loc [f ast] 11 | (loop [loc (ast/ast-zipper ast)] 12 | (if (z/end? loc) 13 | (z/root loc) 14 | (recur 15 | (z/next 16 | (let [node (z/node loc)] 17 | (if (= :root (:type node)) 18 | loc 19 | (f loc)))))))) 20 | 21 | (defn ->build-and-run-query 22 | [query-env] 23 | (fn [env entities prepared-query] 24 | (let [q (-> (prepared-query env entities) 25 | (expressions/build-parameterized-sql-query))] 26 | (if q 27 | (query-env env q) 28 | [])))) 29 | 30 | ;; top-down process 31 | (defn fetch-data 32 | [build-and-run-query env ast] 33 | (->> ast 34 | (ast-map-loc (fn [loc] 35 | (let [{::ast/keys [prepared-query]} (z/node loc)] 36 | (if prepared-query 37 | (let [parent (last (z/path loc)) 38 | ;; TODO: partition entities and concat back 39 | entities (build-and-run-query env (:entities parent) prepared-query)] 40 | (z/edit loc #(-> % (dissoc ::ast/prepared-query) (assoc :entities entities)))) 41 | loc)))))) 42 | 43 | ;; bottom-up process, start from lowest levels (ones without children) 44 | ;; and go up using z/up and prepared-merge-sub-entities 45 | 46 | (defn move-to-nth-child 47 | [loc n] 48 | (nth (iterate z/right (z/down loc)) n)) 49 | 50 | (defn merge-data-in-bottom-branches* 51 | [wrap-merge ast] 52 | (loop [loc (ast/ast-zipper ast)] 53 | (if (z/end? loc) 54 | (z/root loc) 55 | (recur 56 | (z/next 57 | (let [{:keys [children] :as node} (z/node loc) 58 | {::ast/keys [prepared-merge-sub-entities]} node] 59 | (if (or (not prepared-merge-sub-entities) ;; node can be nil 60 | (not-empty children)) 61 | loc 62 | (let [parent (z/up loc) 63 | ;; save current position 64 | position-to-parent (count (z/lefts loc)) 65 | 66 | merged-entities 67 | ((wrap-merge prepared-merge-sub-entities) 68 | (:entities (z/node parent)) 69 | (:entities node))] 70 | (-> (z/edit parent assoc :entities merged-entities) 71 | ;; come back to previous position 72 | (move-to-nth-child position-to-parent)))))))))) 73 | 74 | (defn merge-data-in-bottom-branches 75 | [wrap-merge ast] 76 | (->> (merge-data-in-bottom-branches* wrap-merge ast) 77 | (ast/filterz #(not-empty (:children %))))) 78 | 79 | (defn merge-data 80 | [wrap-merge ast] 81 | (loop [{:keys [children] :as root} ast] 82 | (if (empty? children) 83 | (:entities root) 84 | (recur (merge-data-in-bottom-branches wrap-merge root))))) 85 | 86 | (defn ast-resolver* 87 | [{:keys [build-and-run-query floor-plan env wrap-merge ast]}] 88 | (->> (ast/prepare-ast floor-plan ast) 89 | (fetch-data build-and-run-query env) 90 | (merge-data wrap-merge))) 91 | 92 | (defn prepared-ast-resolver* 93 | [{:keys [build-and-run-query env wrap-merge prepared-ast]}] 94 | (->> prepared-ast 95 | (fetch-data build-and-run-query env) 96 | (merge-data wrap-merge))) 97 | 98 | (defn query-resolver* 99 | [{:keys [floor-plan env resolver query]}] 100 | (resolver floor-plan env (p/query->ast query))) 101 | 102 | (defn ast-resolver 103 | [floor-plan query-env env ast] 104 | (ast-resolver* {:floor-plan floor-plan 105 | :build-and-run-query (->build-and-run-query query-env) 106 | :env env 107 | :wrap-merge identity 108 | :ast ast})) 109 | 110 | (defn prepared-ast-resolver 111 | [query-env env prepared-ast] 112 | (prepared-ast-resolver* {:env env 113 | :build-and-run-query (->build-and-run-query query-env) 114 | :wrap-merge identity 115 | :prepared-ast prepared-ast})) 116 | 117 | (defn query-resolver 118 | [floor-plan query-env env query] 119 | (query-resolver* {:floor-plan floor-plan 120 | :build-and-run-query (->build-and-run-query query-env) 121 | :env env 122 | :resolver ast-resolver 123 | :query query})) 124 | 125 | (defn ident-keyword [env] 126 | (-> env ::pcp/node ::pcp/input ffirst)) 127 | 128 | (defn ident 129 | [env] 130 | (when-let [k (ident-keyword env)] 131 | [k (get (p/entity env) k)])) 132 | 133 | (defn wrap-with-ident 134 | [ast ident] 135 | (if ident 136 | (let [main (assoc ast :type :join :key ident :dispatch-key (first ident))] 137 | {:type :root 138 | :children [main]}) 139 | ast)) 140 | 141 | (comment 142 | (p/ast->query (wrap-with-ident (p/query->ast [:x/a :x/b {:x/c [:c/d]}]) [:x/i 1])) 143 | [{[:x/i 1] [:x/a :x/b #:x{:c [:c/d]}]}]) 144 | 145 | (defn dynamic-resolver 146 | [floor-plan query-env env] 147 | (let [i (ident env) 148 | ast (-> env ::pcp/node ::pcp/foreign-ast 149 | (wrap-with-ident i)) 150 | result (ast-resolver floor-plan query-env env ast)] 151 | (if i 152 | (get result i) 153 | result))) 154 | 155 | (defn compute-indexes [resolver-sym ios] 156 | (reduce (fn [acc x] (pc/add acc resolver-sym x)) 157 | {} 158 | ios)) 159 | 160 | (defn internalize-indexes 161 | [indexes {::pc/keys [sym] :as dynamic-resolver}] 162 | (-> indexes 163 | (update ::pc/index-resolvers 164 | (fn [resolvers] 165 | (into {} 166 | (map (fn [[r v]] [r (assoc v ::pc/dynamic-sym sym)])) 167 | resolvers))) 168 | (assoc-in [::pc/index-resolvers sym] 169 | dynamic-resolver))) 170 | 171 | (defn connect-plugin 172 | [{:keys [:resolver-sym :registry :resolver :autocomplete-ignore :db-type :query-env] 173 | :or {resolver dynamic-resolver 174 | ;; query-env (->query-env :db) 175 | resolver-sym `walkable-resolver}}] 176 | (let [{:keys [:inputs-outputs] compiled-floor-plan :floor-plan} 177 | (floor-plan/compile-floor-plan (if db-type 178 | (floor-plan/with-db-type db-type registry) 179 | registry))] 180 | {::p/wrap-parser2 181 | (fn [parser {::p/keys [plugins]}] 182 | (let [resolve-fn (fn [env _] 183 | (resolver compiled-floor-plan query-env env)) 184 | all-indexes (-> (compute-indexes resolver-sym inputs-outputs) 185 | (internalize-indexes 186 | {::pc/sym (gensym resolver-sym) 187 | ::pc/cache? false 188 | ::pc/dynamic-resolver? true 189 | ::pc/resolve resolve-fn}) 190 | (merge {::pc/autocomplete-ignore (or autocomplete-ignore #{})})) 191 | idx-atoms (keep ::pc/indexes plugins)] 192 | (doseq [idx* idx-atoms] 193 | (swap! idx* pc/merge-indexes all-indexes)) 194 | (fn [env tx] (parser env tx))))})) 195 | -------------------------------------------------------------------------------- /src/walkable/core_async.cljc: -------------------------------------------------------------------------------- 1 | (ns walkable.core-async 2 | (:require [walkable.core :as core] 3 | [walkable.sql-query-builder.expressions :as expressions] 4 | [com.wsscode.pathom.connect.planner :as pcp] 5 | [clojure.core.async :as async :refer [go !]])) 6 | 7 | (defn wrap-merge 8 | [f] 9 | (fn wrapped-merge [entities sub-entites] 10 | (let [ch (async/promise-chan)] 11 | (go (let [e (when entities (! ch (f e se)))) 14 | ch))) 15 | 16 | (comment 17 | (go (println (build-and-run-query 20 | [query-env] 21 | (fn [env entities prepared-query] 22 | (let [ch (async/promise-chan)] 23 | (go 24 | (let [q (->> (when entities (! ch result))) 29 | ch))) 30 | 31 | (defn ast-resolver 32 | [floor-plan query-env env ast] 33 | (core/ast-resolver* {:floor-plan floor-plan 34 | :build-and-run-query (->build-and-run-query query-env) 35 | :env env 36 | :wrap-merge wrap-merge 37 | :ast ast})) 38 | 39 | (defn prepared-ast-resolver 40 | [query-env env prepared-ast] 41 | (core/prepared-ast-resolver* {:env env 42 | :build-and-run-query (->build-and-run-query query-env) 43 | :wrap-merge wrap-merge 44 | :prepared-ast prepared-ast})) 45 | 46 | (defn query-resolver 47 | [floor-plan query-env env query] 48 | (core/query-resolver* {:floor-plan floor-plan 49 | :build-and-run-query (->build-and-run-query query-env) 50 | :env env 51 | :resolver ast-resolver 52 | :query query})) 53 | 54 | (defn dynamic-resolver 55 | [floor-plan query-env env] 56 | (let [i (core/ident env) 57 | ast (-> env ::pcp/node ::pcp/foreign-ast 58 | (core/wrap-with-ident i)) 59 | result (ast-resolver floor-plan query-env env ast)] 60 | (if i 61 | (let [ch (async/promise-chan)] 62 | (go (>! ch (get (graph-index 107 | [floor-plan] 108 | (-> floor-plan :variable->graph-index)) 109 | 110 | (defn compiled-variable-getter 111 | [floor-plan] 112 | (-> floor-plan :compiled-variable-getter)) 113 | 114 | (defn compiled-variable-getter-graph 115 | [floor-plan] 116 | (-> floor-plan :compiled-variable-getter-graph)) 117 | 118 | (defn process-supplied-filter 119 | [{:keys [compiled-formulas operators join-filter-subqueries]} 120 | ast] 121 | (let [supplied-condition (get-in ast [:params :filter])] 122 | (when supplied-condition 123 | (->> supplied-condition 124 | (expressions/compile-to-string 125 | {:operators operators 126 | :join-filter-subqueries join-filter-subqueries}) 127 | (expressions/substitute-atomic-variables 128 | {:variable-values compiled-formulas}))))) 129 | 130 | (defn all-filters 131 | [floor-plan {:keys [dispatch-key] :as ast}] 132 | (let [f (get-in floor-plan [:all-filters dispatch-key])] 133 | (f (process-supplied-filter floor-plan ast)))) 134 | 135 | (defn query-dispatch 136 | [{:keys [aggregator? cte?]} _main-args] 137 | (mapv boolean [aggregator? cte?])) 138 | 139 | (defn process-children* 140 | "Infers which columns to include in SQL query from child keys in ast" 141 | [floor-plan ast] 142 | (let [all-children (:children ast) 143 | 144 | {:keys [columns joins]} 145 | (group-by #(keyword-type floor-plan %) all-children) 146 | 147 | child-column-keys 148 | (into #{} (map :dispatch-key) columns) 149 | 150 | child-source-columns 151 | (into #{} (map #(source-column floor-plan %)) joins)] 152 | {:columns-to-query (set/union child-column-keys child-source-columns)})) 153 | 154 | (defn process-children 155 | "Infers which columns to include in SQL query from child keys in env ast" 156 | [floor-plan ast] 157 | (if (aggregator? floor-plan ast) 158 | {:columns-to-query #{(:dispatch-key ast)}} 159 | (process-children* floor-plan ast))) 160 | 161 | (defn process-selection [floor-plan columns-to-query] 162 | (let [{:keys [:compiled-selection]} floor-plan 163 | compiled-normal-selection (mapv compiled-selection columns-to-query)] 164 | (expressions/concat-with-comma compiled-normal-selection))) 165 | 166 | (defn conj-some [coll x] 167 | (if x 168 | (conj coll x) 169 | coll)) 170 | 171 | (defn individual-query-template-aggregator 172 | [{:keys [floor-plan ast]}] 173 | (let [selection (compiled-join-aggregator-selection floor-plan ast) 174 | conditions (all-filters floor-plan ast) 175 | sql-query {:raw-string 176 | (emitter/->query-string 177 | {:target-table (target-table floor-plan ast) 178 | :join-statement (join-statement floor-plan ast) 179 | :selection (:raw-string selection) 180 | :conditions (:raw-string conditions)}) 181 | :params (expressions/combine-params selection conditions)}] 182 | sql-query)) 183 | 184 | (defn individual-query-template 185 | [{:keys [floor-plan ast pagination]}] 186 | (let [ident? (vector? (:key ast)) 187 | 188 | {:keys [:columns-to-query]} (process-children floor-plan ast) 189 | target-column (target-column floor-plan ast) 190 | 191 | {:keys [:offset :limit :order-by :order-by-columns]} 192 | (when-not ident? pagination) 193 | 194 | columns-to-query 195 | (-> (clojure.set/union columns-to-query order-by-columns) 196 | (conj-some target-column)) 197 | 198 | selection 199 | (process-selection floor-plan columns-to-query) 200 | 201 | conditions (all-filters floor-plan ast) 202 | 203 | having (compiled-having floor-plan ast) 204 | 205 | sql-query {:raw-string 206 | (emitter/->query-string 207 | {:target-table (target-table floor-plan ast) 208 | :join-statement (join-statement floor-plan ast) 209 | :selection (:raw-string selection) 210 | :conditions (:raw-string conditions) 211 | ;; TODO: 212 | ;; use :raw-string/:params here in case there are variables in group-by columns 213 | :group-by (compiled-group-by floor-plan ast) 214 | :having (:raw-string having) 215 | :offset offset 216 | :limit limit 217 | :order-by order-by}) 218 | :params (expressions/combine-params selection conditions having)}] 219 | sql-query)) 220 | 221 | (defn combine-with-cte [{:keys [:shared-query :batched-individuals]}] 222 | (expressions/concatenate #(apply str %) 223 | [shared-query batched-individuals])) 224 | 225 | #_(defn combine-without-cte [{:keys [batched-individuals]}] 226 | batched-individuals) 227 | 228 | (defn source-column-variable-values 229 | [v] 230 | {:variable-values {`floor-plan/source-column-value 231 | (expressions/compile-to-string {} v)}}) 232 | 233 | (defn compute-graphs [floor-plan env variables] 234 | (let [variable->graph-index (variable->graph-index floor-plan) 235 | graph-index->graph (compiled-variable-getter-graph floor-plan)] 236 | (into {} 237 | (comp (map variable->graph-index) 238 | (remove nil?) 239 | (distinct) 240 | (map #(do [% (graph-index->graph %)])) 241 | (map (fn [[index graph]] [index (graph env)]))) 242 | variables))) 243 | 244 | (defn compute-variables 245 | [floor-plan env {:keys [computed-graphs variables]}] 246 | (let [getters (select-keys (compiled-variable-getter floor-plan) variables)] 247 | (into {} 248 | (map (fn [[k f]] 249 | (let [v (f env computed-graphs)] 250 | ;; wrap in single-raw-string to feed 251 | ;; `expressions/substitute-atomic-variables` 252 | [k (expressions/single-raw-string v)]))) 253 | getters))) 254 | 255 | (defn process-variables 256 | [floor-plan env {:keys [variables]}] 257 | (compute-variables floor-plan 258 | env 259 | {:computed-graphs {} #_(compute-graphs floor-plan env variables) 260 | :variables variables})) 261 | 262 | (defn process-query 263 | [floor-plan env query] 264 | (let [values (process-variables floor-plan env 265 | {:variables (expressions/find-variables query)})] 266 | (expressions/substitute-atomic-variables 267 | {:variable-values values} 268 | query))) 269 | 270 | (defn eliminate-unknown-variables [query] 271 | (let [remaining-variables (zipmap (expressions/find-variables query) 272 | (repeat expressions/conformed-nil))] 273 | (expressions/substitute-atomic-variables 274 | {:variable-values remaining-variables} 275 | query))) 276 | 277 | (defn prepare-ident-query 278 | [floor-plan ast] 279 | (let [params {:floor-plan floor-plan 280 | :ast ast 281 | :pagination (process-pagination floor-plan ast)} 282 | 283 | individual-query (individual-query-template params) 284 | 285 | batched-individuals 286 | (fn [_env _entities] 287 | (expressions/substitute-atomic-variables 288 | {:variable-values {`floor-plan/ident-value 289 | (expressions/compile-to-string {} (second (:key ast)))}} 290 | 291 | individual-query))] 292 | (fn final-query [env entities] 293 | (let [q (batched-individuals env entities)] 294 | (when (not-empty (:raw-string q)) 295 | (->> q 296 | (process-query floor-plan env) 297 | eliminate-unknown-variables)))))) 298 | 299 | (defn prepare-root-query 300 | [floor-plan ast] 301 | (let [params {:floor-plan floor-plan 302 | :ast ast 303 | :pagination (process-pagination floor-plan ast)} 304 | 305 | template (individual-query-template params) 306 | 307 | batched-individuals (fn [_env _entities] template)] 308 | (fn final-query [env entities] 309 | (let [q (batched-individuals env entities)] 310 | (when (not-empty (:raw-string q)) 311 | (->> q 312 | (process-query floor-plan env) 313 | eliminate-unknown-variables)))))) 314 | 315 | (defn prepare-join-query 316 | [floor-plan ast] 317 | (let [params {:floor-plan floor-plan 318 | :ast ast 319 | :pagination (process-pagination floor-plan ast)} 320 | template (if (aggregator? floor-plan ast) 321 | (individual-query-template-aggregator params) 322 | (individual-query-template params)) 323 | 324 | multiplier (query-multiplier floor-plan ast) 325 | batched-individuals (multiplier template)] 326 | (fn final-query [env entities] 327 | (let [q (batched-individuals env entities)] 328 | (when (not-empty (:raw-string q)) 329 | (->> q 330 | (process-query floor-plan env) 331 | eliminate-unknown-variables)))))) 332 | 333 | (defn prepare-query 334 | [floor-plan ast] 335 | (let [ident? (let [i (:key ast)] 336 | (and (vector? i) 337 | (contains? (:ident-keywords floor-plan) (first i)))) 338 | kt (keyword-type floor-plan ast)] 339 | (when (or ident? (#{:roots :joins} kt)) 340 | (cond 341 | ident? 342 | (prepare-ident-query floor-plan ast) 343 | 344 | (= :roots kt) 345 | (prepare-root-query floor-plan ast) 346 | 347 | :else 348 | (prepare-join-query floor-plan ast))))) 349 | 350 | (defn prepare-merge-sub-entities 351 | [floor-plan ast] 352 | (let [m (merge-sub-entities floor-plan ast)] 353 | (m (result-key ast)))) 354 | 355 | (defn ast-zipper 356 | "Make a zipper to navigate an ast tree." 357 | [ast] 358 | (->> ast 359 | (z/zipper 360 | (fn branch? [x] (and (map? x) 361 | (#{:root :join} (:type x)))) 362 | (fn children [x] (:children x)) 363 | (fn make-node [x xs] (assoc x :children (vec xs)))))) 364 | 365 | (defn mapz [f ast] 366 | (loop [loc (ast-zipper ast)] 367 | (if (z/end? loc) 368 | (z/root loc) 369 | (recur 370 | (z/next 371 | (let [node (z/node loc)] 372 | (if (= :root (:type node)) 373 | loc 374 | (z/edit loc f)))))))) 375 | 376 | (defn filterz [f ast] 377 | (loop [loc (ast-zipper ast)] 378 | (if (z/end? loc) 379 | (z/root loc) 380 | (recur 381 | (z/next 382 | (let [node (z/node loc)] 383 | (if (f node) 384 | loc 385 | (z/next (z/remove loc))))))))) 386 | 387 | (defn prepare-ast 388 | [floor-plan ast] 389 | (->> ast 390 | (mapz (fn [ast-item] (if-let [pq (prepare-query floor-plan ast-item)] 391 | (-> ast-item 392 | (dissoc :query) 393 | (assoc ::prepared-query pq 394 | ::prepared-merge-sub-entities (prepare-merge-sub-entities floor-plan ast-item))) 395 | ast-item))) 396 | (filterz #(or (= :root (:type %)) (::prepared-query %))))) 397 | -------------------------------------------------------------------------------- /src/walkable/sql_query_builder/emitter.cljc: -------------------------------------------------------------------------------- 1 | (ns walkable.sql-query-builder.emitter 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.string :as string] 4 | [walkable.sql-query-builder.expressions :as expressions] 5 | [walkable.sql-query-builder.pagination :as pagination])) 6 | 7 | (def backticks 8 | (repeat 2 "`")) 9 | 10 | (def quotation-marks 11 | (repeat 2 "\"")) 12 | 13 | (def apostrophes 14 | (repeat 2 "'")) 15 | 16 | (defn dash-to-underscore [s] 17 | (string/replace s #"-" "_")) 18 | 19 | (defn with-quote-marks [this s] 20 | (let [[quote-open quote-close] (:quote-marks this)] 21 | (str quote-open s quote-close))) 22 | 23 | (defn transform-table-name [this table-name] 24 | ((:transform-table-name this) table-name)) 25 | 26 | (defn transform-column-name [this column-name] 27 | ((:transform-column-name this) column-name)) 28 | 29 | (defn table-name* [this t] 30 | (->> (clojure.string/split t #"\.") 31 | (map #(with-quote-marks this 32 | (or (get (:rename-tables this) %) 33 | (transform-table-name this %)))) 34 | (clojure.string/join "."))) 35 | 36 | (defn true-keyword [this k] 37 | ((:rename-keywords this) k k)) 38 | 39 | (defn table-name [this k] 40 | (let [t (namespace (true-keyword this k))] 41 | (table-name* this t))) 42 | 43 | (defn column-name [this k] 44 | (let [k (true-keyword this k)] 45 | (str (table-name this k) "." 46 | (let [c (name k)] 47 | (with-quote-marks this 48 | (or (get (:rename-columns this) c) 49 | (transform-column-name this c))))))) 50 | 51 | (defn clojuric-name [this k] 52 | (with-quote-marks this (subs (str k) 1))) 53 | 54 | (defn wrap-select [this s] 55 | (let [[wrap-open wrap-close] (:wrap-select-strings this)] 56 | (str wrap-open s wrap-close))) 57 | 58 | (def conform-integer #(s/conform integer? %)) 59 | 60 | (def default-emitter 61 | {:quote-marks quotation-marks 62 | 63 | :transform-table-name dash-to-underscore 64 | :transform-column-name dash-to-underscore 65 | :rename-tables {} 66 | :rename-columns {} 67 | :rename-keywords {} 68 | 69 | :wrap-select-strings ["(" ")"] 70 | 71 | :conform-offset conform-integer 72 | :wrap-validate-offset identity 73 | :stringify-offset #(str " OFFSET " %) 74 | 75 | :conform-limit conform-integer 76 | :wrap-validate-limit identity 77 | :stringify-limit #(str " LIMIT " %) 78 | 79 | :conform-order-by (pagination/->conform-order-by #{:asc :desc :nils-first :nils-last}) 80 | :stringify-order-by (pagination/->stringify-order-by 81 | {:asc " ASC" 82 | :desc " DESC" 83 | :nils-first " NULLS FIRST" 84 | :nils-last " NULLS LAST"})}) 85 | 86 | (def sqlite-emitter 87 | (merge default-emitter 88 | {:wrap-select-strings ["SELECT * FROM (" ")"] 89 | 90 | :conform-order-by (pagination/->conform-order-by #{:asc :desc}) 91 | :stringify-order-by (pagination/->stringify-order-by 92 | {:asc " ASC" 93 | :desc " DESC"})})) 94 | 95 | (def postgres-emitter 96 | default-emitter) 97 | 98 | (def mysql-emitter 99 | (merge default-emitter 100 | {:quote-marks backticks})) 101 | 102 | (defn oracle-conform-limit 103 | [limit] 104 | (->> (if (sequential? limit) limit [limit]) 105 | (s/conform (s/cat :limit integer? 106 | :percent (s/? #(= :percent %)) 107 | :with-ties (s/? #(= :with-ties %)))))) 108 | 109 | (defn oracle-stringify-limit 110 | [{:keys [limit percent with-ties]}] 111 | (str " FETCH FIRST " limit 112 | (when percent " PERCENT") 113 | " ROWS" 114 | (if with-ties " WITH TIES" " ONLY"))) 115 | 116 | (def oracle-emitter 117 | (merge default-emitter 118 | {:conform-limit oracle-conform-limit 119 | :stringify-limit oracle-stringify-limit 120 | 121 | :stringify-offset #(str " OFFSET " % " ROWS")})) 122 | 123 | (def predefined-emitters 124 | {:mysql mysql-emitter 125 | :postgres postgres-emitter 126 | :sqlite sqlite-emitter 127 | :oracle oracle-emitter}) 128 | 129 | (defn build-emitter 130 | [{:keys [:base] :or {base :postgres} :as attr}] 131 | (let [custom-props (select-keys attr (keys default-emitter))] 132 | (merge (get predefined-emitters base) 133 | custom-props))) 134 | 135 | (s/def ::target-table string?) 136 | 137 | (s/def ::query-string-input 138 | (s/keys :req-un [::selection ::target-table] 139 | :opt-un [::join-statement ::conditions 140 | ::offset ::limit ::order-by])) 141 | 142 | (defn ->query-string 143 | "Builds the final query string ready for SQL server." 144 | [{:keys [selection target-table join-statement conditions 145 | group-by having 146 | offset limit order-by] 147 | :as input}] 148 | {:pre [(s/valid? ::query-string-input input)] 149 | :post [string?]} 150 | (str "SELECT " selection 151 | " FROM " target-table 152 | join-statement 153 | (when conditions (str " WHERE " conditions)) 154 | group-by 155 | having 156 | order-by 157 | offset 158 | limit)) 159 | 160 | (defn emitter->batch-query [emitter] 161 | (fn [parameterized-queries] 162 | (-> (if (= 1 (count parameterized-queries)) 163 | (first parameterized-queries) 164 | (expressions/concatenate 165 | (fn [q] (->> q 166 | (mapv #(wrap-select emitter %)) 167 | (clojure.string/join "\nUNION ALL\n"))) 168 | parameterized-queries))))) 169 | -------------------------------------------------------------------------------- /src/walkable/sql_query_builder/expressions.cljc: -------------------------------------------------------------------------------- 1 | (ns walkable.sql-query-builder.expressions 2 | (:require #?(:clj [cheshire.core :refer [generate-string]]) 3 | [clojure.spec.alpha :as s] 4 | [clojure.string :as string])) 5 | 6 | #?(:cljs 7 | (defn generate-string 8 | "Equivalent of cheshire.core/generate-string for Clojurescript" 9 | [ds] 10 | (.stringify js/JSON (clj->js ds)))) 11 | 12 | (defrecord AtomicVariable [name]) 13 | 14 | (defn av [n] 15 | (AtomicVariable. n)) 16 | 17 | (defn atomic-variable? [x] 18 | (instance? AtomicVariable x)) 19 | 20 | (declare inline-params) 21 | 22 | (defn namespaced-keyword? 23 | [x] 24 | (and (keyword? x) (namespace x))) 25 | 26 | (defn unnamespaced-keyword? 27 | [x] 28 | (and (keyword? x) (not (namespace x)))) 29 | 30 | (s/def ::namespaced-keyword namespaced-keyword?) 31 | 32 | (s/def ::unnamespaced-keyword unnamespaced-keyword?) 33 | 34 | (defprotocol EmittableAtom 35 | (emit [this])) 36 | 37 | (defn emittable-atom? [x] 38 | (satisfies? EmittableAtom x)) 39 | 40 | (defn verbatim-raw-string [s] 41 | {:raw-string s 42 | :params []}) 43 | 44 | (defn single-raw-string [x] 45 | {:raw-string "?" 46 | :params [x]}) 47 | 48 | (def conformed-nil 49 | (verbatim-raw-string "NULL")) 50 | 51 | (def conformed-true 52 | (verbatim-raw-string "TRUE")) 53 | 54 | (def conformed-false 55 | (verbatim-raw-string "FALSE")) 56 | 57 | (extend-protocol EmittableAtom 58 | #?(:clj Boolean :cljs boolean) 59 | (emit [boolean-val] 60 | (if boolean-val conformed-true conformed-false)) 61 | #?(:clj Number :cljs number) 62 | (emit [number] 63 | (verbatim-raw-string (str number))) 64 | #?(:clj String :cljs string) 65 | (emit [string] 66 | (single-raw-string string)) 67 | #?(:clj java.util.UUID :cljs UUID) 68 | (emit [uuid] 69 | (single-raw-string uuid)) 70 | nil 71 | (emit [a-nil] conformed-nil)) 72 | 73 | (s/def ::expression 74 | (s/or 75 | :atomic-variable atomic-variable? 76 | :symbol symbol? 77 | 78 | :emittable-atom emittable-atom? 79 | 80 | :column ::namespaced-keyword 81 | 82 | :expression 83 | (s/and vector? 84 | (s/cat :operator ::unnamespaced-keyword 85 | :params (s/* (constantly true)))) 86 | :join-filters 87 | (s/coll-of 88 | (s/or :join-filter 89 | (s/cat :join-key ::namespaced-keyword 90 | :expression ::expression)) 91 | :min-count 1 92 | :kind map? 93 | :into []))) 94 | 95 | ;; the rule for parentheses in :raw-string 96 | ;; outer raw string should provide them 97 | ;; inner ones shouldn't 98 | 99 | (defmulti process-expression 100 | (fn dispatcher [_env [kw _expression]] kw)) 101 | 102 | (defmethod process-expression :emittable-atom 103 | [_env [_kw val]] 104 | (emit val)) 105 | 106 | (defn infix-notation 107 | "Common implementation for +, -, *, /" 108 | [operator-string params] 109 | {:raw-string (string/join operator-string 110 | (repeat (count params) "(?)")) 111 | :params params}) 112 | 113 | (defn multiple-compararison 114 | "Common implementation of process-operator for comparison operators: =, <, >, <=, >=" 115 | [comparator-string params] 116 | (assert (< 1 (count params)) 117 | (str "There must be at least two arguments to " comparator-string)) 118 | (let [params (partition 2 1 params)] 119 | {:raw-string (string/join " AND " 120 | (repeat (count params) (str "(?)" comparator-string "(?)"))) 121 | :params (flatten params)})) 122 | 123 | (def common-cast-type->string 124 | {:integer "integer" 125 | :text "text" 126 | :date "date" 127 | :datetime "datetime"}) 128 | 129 | (defn compile-cast-type [cast-type->string] 130 | (fn [env [_operator [expression type-params]]] 131 | (let [expression (s/conform ::expression expression) 132 | type-str (cast-type->string type-params)] 133 | (assert (not (s/invalid? expression)) 134 | (str "First argument to `cast` must be a valid expression.")) 135 | (assert type-str 136 | (str "Invalid type to `cast`. You may want to implement `cast-type` for the given type.")) 137 | (inline-params env 138 | {:raw-string (str "CAST (? AS " type-str ")") 139 | :params [(process-expression env expression)]})))) 140 | 141 | (defn default-sql-name 142 | [key] 143 | (let [symbol-name (name key)] 144 | (string/replace symbol-name #"-" "_"))) 145 | 146 | (defn infix-operator-fn 147 | [{:keys [arity sql-name] operator :key}] 148 | (case arity 149 | 2 150 | (fn [_env [_operator params]] 151 | (assert (= 2 (count params)) 152 | (str "There must exactly two arguments to " operator)) 153 | {:raw-string (str "(?)" sql-name "(?)") 154 | :params params}) 155 | (fn [_env [_operator params]] 156 | (let [n (count params)] 157 | {:raw-string (string/join sql-name 158 | (repeat n "(?)")) 159 | :params params})))) 160 | 161 | (defn postfix-operator-fn 162 | [{:keys [arity sql-name] :or {arity 1} operator :key}] 163 | (assert (= 1 arity) 164 | (str "Postfix operators always have arity 1. Please check operator " operator "'s definition.")) 165 | (fn [_env [_operator params]] 166 | (assert (= 1 (count params)) 167 | (str "There must be exactly one argument to " operator)) 168 | {:raw-string (str "(?)" sql-name) 169 | :params params})) 170 | 171 | (defn no-params-operator-fn 172 | [{:keys [arity sql-name] :or {arity 0} operator :key}] 173 | (assert (= 0 arity) 174 | (str "Postfix operators always have arity 0. Please check operator " operator "'s definition.")) 175 | (fn [_env [_operator params]] 176 | (assert (= 0 (count params)) 177 | (str "There must be no argument to " operator)) 178 | {:raw-string sql-name 179 | :params params})) 180 | 181 | (defn prefix-operator-fn 182 | [{:keys [arity sql-name] operator :key}] 183 | (case arity 184 | 0 185 | (fn [_env [_operator params]] 186 | (assert (zero? (count params)) 187 | (str "There must be no argument to " operator)) 188 | {:raw-string (str sql-name "()") 189 | :params []}) 190 | 1 191 | (fn [_env [_operator params]] 192 | (assert (= 1 (count params)) 193 | (str "There must exactly one argument to " operator)) 194 | {:raw-string (str sql-name "(?)") 195 | :params params}) 196 | ;; else 197 | (fn [_env [_operator params]] 198 | (let [n (count params)] 199 | {:raw-string (str (str sql-name "(") 200 | (string/join ", " 201 | (repeat n \?)) 202 | ")") 203 | :params params})))) 204 | 205 | (defn plain-operator-fn 206 | [{:keys [:key :sql-name :params-position] :as opts}] 207 | (let [opts (if sql-name 208 | opts 209 | (assoc opts :sql-name (default-sql-name key)))] 210 | (case params-position 211 | :infix 212 | (infix-operator-fn opts) 213 | :postfix 214 | (postfix-operator-fn opts) 215 | :no-params 216 | (no-params-operator-fn opts) 217 | ;; default 218 | (prefix-operator-fn opts)))) 219 | 220 | (defn plain-operator [opts] 221 | {:key (:key opts) 222 | :type :operator 223 | :compile-args true 224 | :compile-fn (plain-operator-fn opts)}) 225 | 226 | (def common-operators 227 | [{:key :and 228 | :type :operator 229 | :compile-args true 230 | :compile-fn 231 | (fn [_env [_operator params]] 232 | (if (empty? params) 233 | (single-raw-string true) 234 | {:raw-string (string/join " AND " 235 | (repeat (count params) "(?)")) 236 | :params params}))} 237 | 238 | {:key :or 239 | :type :operator 240 | :compile-args true 241 | :compile-fn 242 | (fn 243 | [_env [_operator params]] 244 | (if (empty? params) 245 | (single-raw-string false) 246 | {:raw-string (string/join " OR " 247 | (repeat (count params) "(?)")) 248 | :params params}))} 249 | 250 | {:key := 251 | :type :operator 252 | :compile-args true 253 | :compile-fn 254 | (fn [_env [_operator params]] 255 | (multiple-compararison "=" params))} 256 | 257 | {:key :> 258 | :type :operator 259 | :compile-args true 260 | :compile-fn 261 | (fn [_env [_operator params]] 262 | (multiple-compararison ">" params))} 263 | 264 | {:key :>= 265 | :type :operator 266 | :compile-args true 267 | :compile-fn 268 | (fn [_env [_operator params]] 269 | (multiple-compararison ">=" params))} 270 | 271 | {:key :< 272 | :type :operator 273 | :compile-args true 274 | :compile-fn 275 | (fn [_env [_operator params]] 276 | (multiple-compararison "<" params))} 277 | 278 | {:key :<= 279 | :type :operator 280 | :compile-args true 281 | :compile-fn 282 | (fn [_env [_operator params]] 283 | (multiple-compararison "<=" params))} 284 | 285 | {:key :+ 286 | :type :operator 287 | :compile-args true 288 | :compile-fn 289 | (fn [_env [_operator params]] 290 | (if (empty? params) 291 | {:raw-string "0" 292 | :params []} 293 | (infix-notation "+" params)))} 294 | 295 | {:key :* 296 | :type :operator 297 | :compile-args true 298 | :compile-fn 299 | (fn [_env [_operator params]] 300 | (if (empty? params) 301 | {:raw-string "1" 302 | :params []} 303 | (infix-notation "*" params)))} 304 | 305 | {:key :- 306 | :type :operator 307 | :compile-args true 308 | :compile-fn 309 | (fn [_env [_operator params]] 310 | (assert (not-empty params) 311 | "There must be at least one parameter to `-`") 312 | (if (= 1 (count params)) 313 | {:raw-string "0-(?)" 314 | :params params} 315 | (infix-notation "-" params)))} 316 | 317 | {:key :/ 318 | :type :operator 319 | :compile-args true 320 | :compile-fn 321 | (fn [_env [_operator params]] 322 | (assert (not-empty params) 323 | "There must be at least one parameter to `/`") 324 | (if (= 1 (count params)) 325 | {:raw-string "1/(?)" 326 | :params params} 327 | (infix-notation "/" params)))} 328 | 329 | {:key :in 330 | :type :operator 331 | :compile-args true 332 | :compile-fn 333 | (fn [_env [_operator params]] 334 | ;; decrease by 1 to exclude the first param 335 | ;; which should go before `IN` 336 | (let [n (dec (count params))] 337 | (assert (pos? n) "There must be at least two parameters to `:in`") 338 | {:raw-string (str "(?) IN (" 339 | (string/join ", " 340 | (repeat n \?)) 341 | ")") 342 | :params params}))} 343 | {:key :tuple 344 | :type :operator 345 | :compile-args true 346 | :compile-fn 347 | (fn [_env [_operator params]] 348 | (let [n (count params)] 349 | (assert (pos? n) "There must be at least one parameter to `:tuple`") 350 | {:raw-string (str "(" 351 | (string/join ", " 352 | (repeat (count params) \?)) 353 | ")") 354 | :params params}))} 355 | 356 | {:key :case 357 | :type :operator 358 | :compile-args true 359 | :compile-fn 360 | (fn 361 | [_env [_kw expressions]] 362 | (let [n (count expressions)] 363 | (assert (> n 2) 364 | "`case` must have at least three arguments") 365 | (let [when+else-count (dec n) 366 | else? (odd? when+else-count) 367 | when-count (if else? (dec when+else-count) when+else-count)] 368 | {:raw-string (str "CASE (?)" 369 | (apply str (repeat (/ when-count 2) " WHEN (?) THEN (?)")) 370 | (when else? " ELSE (?)") 371 | " END") 372 | :params expressions})))} 373 | {:key :cond 374 | :type :operator 375 | :compile-args true 376 | :compile-fn 377 | (fn [_env [_kw expressions]] 378 | (let [n (count expressions)] 379 | (assert (even? n) 380 | "`cond` requires an even number of arguments") 381 | (assert (not= n 0) 382 | "`cond` must have at least two arguments") 383 | {:raw-string (str "CASE" 384 | (apply str (repeat (/ n 2) " WHEN (?) THEN (?)")) 385 | " END") 386 | :params expressions}))} 387 | {:key :if 388 | :type :operator 389 | :compile-args true 390 | :compile-fn 391 | (fn [_env [_kw expressions]] 392 | (let [n (count expressions)] 393 | (assert (#{2 3} n) 394 | "`if` must have either two or three arguments") 395 | (let [else? (= 3 n)] 396 | {:raw-string (str "CASE WHEN (?) THEN (?)" 397 | (when else? " ELSE (?)") 398 | " END") 399 | :params expressions})))} 400 | {:key :when 401 | :type :operator 402 | :compile-args true 403 | :compile-fn 404 | (fn [_env [_kw expressions]] 405 | (let [n (count expressions)] 406 | (assert (= 2 n) 407 | "`when` must have exactly two arguments") 408 | {:raw-string "CASE WHEN (?) THEN (?) END" 409 | :params expressions}))} 410 | (plain-operator {:key :count-* 411 | :sql-name "COUNT(*)" 412 | :params-position :no-params}) 413 | (plain-operator {:key :sum 414 | :arity 1}) 415 | (plain-operator {:key :count 416 | :arity 1}) 417 | (plain-operator {:key :not 418 | :arity 1}) 419 | (plain-operator {:key :avg 420 | :arity 1}) 421 | (plain-operator {:key :bit-not 422 | :arity 1 423 | :sql-name "~"}) 424 | (plain-operator {:key :now 425 | :arity 0}) 426 | (plain-operator {:key :format}) 427 | (plain-operator {:key :min}) 428 | (plain-operator {:key :max}) 429 | (plain-operator {:key :str 430 | :sql-name "CONCAT"}) 431 | (plain-operator {:key :nil? 432 | :params-position :postfix 433 | :sql-name " is null"}) 434 | (plain-operator {:key :like 435 | :params-position :infix}) 436 | (plain-operator {:key :bit-and 437 | :params-position :infix 438 | :sql-name "&"}) 439 | (plain-operator {:key :bit-or 440 | :params-position :infix 441 | :sql-name "|"}) 442 | (plain-operator {:key :bit-shift-left 443 | :arity 2 444 | :params-position :infix 445 | :sql-name "<<"}) 446 | (plain-operator {:key :bit-shift-right 447 | :arity 2 448 | :params-position :infix 449 | :sql-name ">>"})]) 450 | 451 | (def postgres-operator-set 452 | (concat (mapv #(plain-operator {:key %}) 453 | [:array-append 454 | :array-cat 455 | :array-fill 456 | :array-length 457 | :array-lower 458 | :array-position 459 | :array-positions 460 | :array-prepend 461 | :array-remove 462 | :array-replace 463 | :array-to-string 464 | :array-upper 465 | :cardinality 466 | :string-to-array 467 | :unnest]) 468 | ;; Use long names instead of "?", "?|" 469 | ;; Source: https://stackoverflow.com/questions/30629076/how-to-escape-the-question-mark-operator-to-query-postgresql-jsonb-type-in-r 470 | (mapv #(plain-operator {:key %}) 471 | [:to-char :to-date :to-number :to-timestamp 472 | :iso-timestamp :json-agg :jsonb-contains 473 | :jsonb-exists :jsonb-exists-any :jsonb-exists-all :jsonb-delete-path]) 474 | (mapv #(plain-operator {:key % :arity 1}) 475 | [:array-ndims :array-dims]) 476 | 477 | ;; Source: 478 | ;; https://www.postgresql.org/docs/current/static/functions-json.html 479 | (for [[k sql-name] 480 | {:get "->" 481 | :get-as-text "->>" 482 | :get-in "#>" 483 | :get-in-as-text "#>>" 484 | :contains "@>" 485 | :overlap "&&"}] 486 | (plain-operator {:key k 487 | :arity 2 488 | :params-position :infix 489 | :sql-name sql-name})) 490 | [(plain-operator {:key :concat 491 | :params-position :infix 492 | :sql-name "||"}) 493 | 494 | {:key :array 495 | :type :operator 496 | :compile-args true 497 | :compile-fn 498 | (fn [_env [_operator params]] 499 | {:raw-string (str "ARRAY[" 500 | (string/join ", " 501 | (repeat (count params) "?")) 502 | "]") 503 | :params params})} 504 | 505 | {:key :json-text 506 | :type :operator 507 | :compile-args false 508 | :compile-fn 509 | (fn [_env [_operator [json]]] 510 | (let [json-string (generate-string json)] 511 | {:raw-string "?" 512 | :params [json-string]}))} 513 | 514 | {:key :json 515 | :type :operator 516 | :compile-args false 517 | :compile-fn 518 | (fn [_env [_operator [json]]] 519 | (let [json-string (generate-string json)] 520 | {:raw-string "?::json" 521 | :params [json-string]}))} 522 | 523 | {:key :jsonb 524 | :type :operator 525 | :compile-args false 526 | :compile-fn 527 | (fn [_env [_operator [json]]] 528 | (let [json-string (generate-string json)] 529 | {:raw-string "?::jsonb" 530 | :params [json-string]}))} 531 | 532 | {:key :cast 533 | :type :operator 534 | :compile-args false 535 | :compile-fn (compile-cast-type 536 | (merge common-cast-type->string 537 | {:json "json" 538 | :jsonb "jsonb"}))}])) 539 | 540 | ;; TODO: more cast types here 541 | (def mysql-operator-set 542 | [{:key :cast 543 | :type :operator 544 | :compile-args false 545 | :compile-fn (compile-cast-type 546 | common-cast-type->string)}]) 547 | 548 | (def sqlite-operator-set 549 | [{:key :cast 550 | :type :operator 551 | :compile-args false 552 | :compile-fn (compile-cast-type 553 | common-cast-type->string)}]) 554 | 555 | (def predefined-operator-sets 556 | ;; TODO: different :cast operators 557 | {:postgres 558 | (into common-operators postgres-operator-set) 559 | :mysql 560 | (into common-operators mysql-operator-set) 561 | :sqlite 562 | (into common-operators sqlite-operator-set)}) 563 | 564 | (defn build-operator-set 565 | [{:keys [:base :except] :or {base :postgres}}] 566 | (remove #((set except) (:key %)) (get predefined-operator-sets base))) 567 | 568 | (defmethod process-expression :expression 569 | [{:keys [:operators] :as env} 570 | [_kw {:keys [:operator :params] :or {operator :and}}]] 571 | (if-let [operator-config (get operators operator)] 572 | (let [{:keys [:compile-args :compile-fn]} operator-config] 573 | (if compile-args 574 | (let [conformed-params (mapv #(s/conform ::expression %) params)] 575 | (if-let [failed (some s/invalid? conformed-params)] 576 | (throw (ex-info (str "Invalid expression: " (pr-str failed)) 577 | {:type :invalid-expression 578 | :expression params})) 579 | (let [compiled-params (mapv #(process-expression env %) conformed-params)] 580 | (inline-params env 581 | (compile-fn env [operator compiled-params]))))) 582 | (compile-fn env [operator params]))) 583 | (throw (ex-info (str "Unknow operator: " operator) 584 | {:type :unknow-operator 585 | :name operator})))) 586 | 587 | (defmethod process-expression :join-filter 588 | [env [_kw {:keys [join-key expression]}]] 589 | (let [subquery (-> env :join-filter-subqueries join-key)] 590 | (assert subquery 591 | (str "No join filter found for join key " join-key)) 592 | (inline-params env 593 | {:raw-string subquery 594 | :params [(process-expression env expression)]}))) 595 | 596 | (defmethod process-expression :join-filters 597 | [env [_kw join-filters]] 598 | (inline-params env 599 | {:raw-string (str "(" 600 | (string/join ") AND (" 601 | (repeat (count join-filters) \?)) 602 | ")") 603 | :params (mapv #(process-expression env %) join-filters)})) 604 | 605 | (defmethod process-expression :atomic-variable 606 | [_env [_kw atomic-variable]] 607 | (single-raw-string atomic-variable)) 608 | 609 | (defmethod process-expression :column 610 | [_env [_kw column-keyword]] 611 | (single-raw-string (AtomicVariable. column-keyword))) 612 | 613 | (defmethod process-expression :symbol 614 | [_env [_kw sym]] 615 | (single-raw-string (AtomicVariable. sym))) 616 | 617 | (defn substitute-atomic-variables 618 | [{:keys [variable-values] :as env} {:keys [raw-string params]}] 619 | (inline-params env 620 | {:raw-string raw-string 621 | :params (->> params 622 | (mapv (fn [param] 623 | (or (and (atomic-variable? param) 624 | (get variable-values (:name param))) 625 | (single-raw-string param)))))})) 626 | 627 | (defn inline-params 628 | [_env {:keys [raw-string params]}] 629 | {:params (into [] (flatten (map :params params))) 630 | :raw-string (->> (conj (mapv :raw-string params) nil) 631 | (interleave (if (= "?" raw-string) 632 | ["" ""] 633 | (string/split raw-string #"\?"))) 634 | (apply str))}) 635 | 636 | (defn concatenate 637 | [joiner compiled-expressions] 638 | {:params (vec (apply concat (map :params compiled-expressions))) 639 | :raw-string (joiner (map :raw-string compiled-expressions))}) 640 | 641 | (defn concat-with-and* [xs] 642 | (string/join " AND " 643 | (mapv (fn [x] (str "(" x ")")) xs))) 644 | 645 | (defn concat-with-and [xs] 646 | (when (not-empty xs) 647 | (concatenate concat-with-and* xs))) 648 | 649 | (defn concat-with-comma* [xs] 650 | (when (not-empty xs) 651 | (string/join ", " xs))) 652 | 653 | (def select-all {:raw-string "*" :params []}) 654 | 655 | (defn concat-with-comma [xs] 656 | (when (not-empty xs) 657 | (concatenate concat-with-comma* xs))) 658 | 659 | (defn combine-params 660 | [& compiled-exprs] 661 | (into [] (comp (map :params) cat) 662 | compiled-exprs)) 663 | 664 | (defn compile-to-string 665 | [env clauses] 666 | (let [form (s/conform ::expression clauses)] 667 | (assert (not (s/invalid? form)) 668 | (str "Invalid expression: " clauses)) 669 | (process-expression env form))) 670 | 671 | (defn find-variables [{:keys [params]}] 672 | (into #{} (comp (filter atomic-variable?) 673 | (map :name)) 674 | params)) 675 | 676 | (defn selection [compiled-formula clojuric-name] 677 | (inline-params {} 678 | {:raw-string "(?) AS ?" 679 | :params [compiled-formula (verbatim-raw-string clojuric-name)]})) 680 | 681 | (defn build-parameterized-sql-query 682 | [{:keys [raw-string params]}] 683 | (vec (cons raw-string params))) 684 | -------------------------------------------------------------------------------- /src/walkable/sql_query_builder/floor_plan.cljc: -------------------------------------------------------------------------------- 1 | (ns walkable.sql-query-builder.floor-plan 2 | (:require [clojure.set :as set] 3 | [clojure.spec.alpha :as s] 4 | [clojure.string :as string] 5 | [com.wsscode.pathom.connect :as pc] 6 | [com.wsscode.pathom.core :as p] 7 | [walkable.sql-query-builder.emitter :as emitter] 8 | [walkable.sql-query-builder.expressions :as expressions] 9 | [walkable.sql-query-builder.helper :as helper] 10 | [walkable.sql-query-builder.pagination :as pagination])) 11 | 12 | (s/def ::without-join-table 13 | (s/coll-of ::expressions/namespaced-keyword 14 | :count 2)) 15 | 16 | (s/def ::with-join-table 17 | (s/coll-of ::expressions/namespaced-keyword 18 | :count 4)) 19 | 20 | (s/def ::join-path 21 | (s/or 22 | :without-join-table ::without-join-table 23 | :with-join-table ::with-join-table)) 24 | 25 | (def prop->func 26 | {:table-name emitter/table-name 27 | :column-name emitter/column-name 28 | :clojuric-name emitter/clojuric-name}) 29 | 30 | (defn true-column? [attr] 31 | (= :true-column (:type attr))) 32 | 33 | (defn known-true-columns [attributes] 34 | (->> attributes 35 | (filterv true-column?) 36 | (helper/build-index :key))) 37 | 38 | (defn keyword-prop 39 | [{:keys [:emitter :attributes]} k prop] 40 | (or (get-in (known-true-columns attributes) [k prop]) 41 | (let [f (prop->func prop)] 42 | (f emitter k)))) 43 | 44 | (defn join-statement* 45 | [registry {:keys [:join-path]}] 46 | (let [[tag] (s/conform ::join-path join-path)] 47 | (when (= :with-join-table tag) 48 | (let [[_source _join-source join-target target] join-path] 49 | (str 50 | " JOIN " (keyword-prop registry target :table-name) 51 | " ON " (keyword-prop registry join-target :column-name) 52 | ;; TODO: what about other operators than `=`? 53 | " = " (keyword-prop registry target :column-name)))))) 54 | 55 | (defn conditionally-update* [x pred f] 56 | (if (pred x) 57 | (f x) 58 | x)) 59 | 60 | (defn conditionally-update 61 | [xs pred f] 62 | (mapv #(conditionally-update* % pred f) xs)) 63 | 64 | (defn derive-missing-key [m k f] 65 | (if (contains? m k) 66 | m 67 | (assoc m k (f)))) 68 | 69 | (defn join-statement 70 | "Generate JOIN statement if not provided. Only for \"two-hop\" join paths." 71 | [registry item] 72 | (derive-missing-key item :join-statement #(join-statement* registry item))) 73 | 74 | (defn join-source-column 75 | [_registry {:keys [:join-path] :as item}] 76 | (derive-missing-key item :source-column #(first join-path))) 77 | 78 | (defn join-target-column 79 | [_registry {:keys [:join-path] :as item}] 80 | (derive-missing-key item :target-column #(second join-path))) 81 | 82 | (defn join-target-table 83 | [registry {:keys [:target-column] :as item}] 84 | (derive-missing-key item :target-table #(keyword-prop registry target-column :table-name))) 85 | 86 | (defn join-filter-subquery* 87 | [registry {:keys [:join-statement :target-table :source-column :target-column]}] 88 | (str (keyword-prop registry source-column :column-name) 89 | " IN (" 90 | (emitter/->query-string 91 | {:selection (keyword-prop registry target-column :column-name) 92 | :target-table target-table 93 | :join-statement join-statement}) 94 | " WHERE ?)")) 95 | 96 | (defn join-filter-subquery 97 | [registry {:keys [:aggregate] :as item}] 98 | (if aggregate 99 | item 100 | (derive-missing-key item :join-filter-subquery #(join-filter-subquery* registry item)))) 101 | 102 | (defn compile-join 103 | [registry attribute] 104 | (->> attribute 105 | (join-statement registry) 106 | (join-source-column registry) 107 | (join-target-column registry) 108 | (join-target-table registry) 109 | (join-filter-subquery registry))) 110 | 111 | (defn compile-joins 112 | [registry] 113 | (update registry :attributes 114 | conditionally-update 115 | (fn [attr] (= :join (:type attr))) 116 | (fn [attr] (compile-join registry attr)))) 117 | 118 | (defn replace-join-with-source-column-in-outputs 119 | [{:keys [:attributes] :as registry}] 120 | (let [join->source-column 121 | (->> attributes 122 | (filter #(= :join (:type %))) 123 | (helper/build-index-of :source-column)) 124 | 125 | plain-join-keys 126 | (->> attributes 127 | (filter #(and (= :join (:type %)) (not (:aggregate %)))) 128 | (map :key) 129 | set) 130 | 131 | join-keys 132 | (set (keys join->source-column))] 133 | (update registry :attributes 134 | conditionally-update 135 | :output 136 | (fn [{:keys [:output] :as attr}] 137 | (let [joins-in-output 138 | (filter join-keys output) 139 | 140 | source-column-is-ident? 141 | (if (:primary-key attr) 142 | #{(:key attr)} 143 | (constantly false)) 144 | 145 | source-columns 146 | (->> (mapv join->source-column joins-in-output) 147 | (remove source-column-is-ident?) 148 | set) 149 | 150 | new-output 151 | (set/union source-columns 152 | (set (remove plain-join-keys output)))] 153 | (assoc attr :output (vec new-output))))))) 154 | 155 | (defn check-duplicate-keys [attrs] 156 | ;; TODO: implement with loop/recur to 157 | ;; tell which key is duplicated 158 | (when-not (apply distinct? (map :key attrs)) 159 | (throw (ex-info "Duplicate keys" {})))) 160 | 161 | (defn compile-formula 162 | [registry item] 163 | ;; TODO: catch exception when joins are used inside formulas 164 | (derive-missing-key item :compiled-formula 165 | #(expressions/compile-to-string registry (:formula item)))) 166 | 167 | (defn has-formula? 168 | [attr] 169 | (or (:aggregate attr) 170 | (= :pseudo-column (:type attr)) 171 | (:primary-key attr))) 172 | 173 | (defn compile-formulas 174 | [registry] 175 | (update registry :attributes 176 | conditionally-update 177 | has-formula? 178 | #(compile-formula registry %))) 179 | 180 | (defn keyset [coll] 181 | (->> coll 182 | (map :key) 183 | (into #{}))) 184 | 185 | (defn find-aggregators 186 | [attributes] 187 | (->> attributes 188 | (filter :aggregate) 189 | (keyset))) 190 | 191 | (defn find-pseudo-columns 192 | [attributes] 193 | (->> attributes 194 | (filter #(= :pseudo-column (:type %))) 195 | (keyset))) 196 | 197 | (defn collect-dependencies 198 | [attributes] 199 | ;; TODO: use clojure.core transducers 200 | (reduce (fn [acc attr] 201 | (let [k (:key attr) 202 | dependencies (->> attr :compiled-formula :params 203 | (filter expressions/atomic-variable?) 204 | (map :name) 205 | distinct 206 | (map #(vector k %)))] 207 | (concat acc dependencies))) 208 | () 209 | (filter :compiled-formula attributes))) 210 | 211 | (defn check-dependency-on-aggregators 212 | [aggregators dependencies] 213 | (when-let [[dependent dependency] 214 | (some (fn [[dependent dependency]] 215 | (and (contains? aggregators dependency) 216 | ;; returned by some: 217 | [dependent dependency])) 218 | dependencies)] 219 | (throw (ex-info (str "Please check `" dependent "`'s formula which contains an aggregator (`" 220 | dependency "`)") 221 | {:formula dependent 222 | :dependency dependency})))) 223 | 224 | (defn collect-independent-pseudo-columns 225 | [pseudo-columns dependencies] 226 | (let [deps 227 | (filter (fn [[x y]] 228 | (and (contains? pseudo-columns x) 229 | (contains? pseudo-columns y))) 230 | dependencies) 231 | Xs (into #{} (map first deps)) 232 | Ys (into #{} (map second deps))] 233 | (set/difference Ys Xs))) 234 | 235 | (defn get-prop [attrs k prop] 236 | (let [x (some #(and (= k (:key %)) %) attrs)] 237 | (get x prop))) 238 | 239 | (defn direct-dependents [dependencies independents] 240 | (->> dependencies 241 | (filter (fn [[_ y]] (contains? independents y))) 242 | (map first) 243 | (into #{}))) 244 | 245 | (defn get-compiled-formula [attrs ks] 246 | (->> (for [k ks] 247 | [k (get-prop attrs k :compiled-formula)]) 248 | (into {}))) 249 | 250 | (defn expand-nested-pseudo-columns* 251 | [attrs] 252 | (loop [attrs attrs] 253 | (let [dependencies (collect-dependencies attrs) 254 | pseudo-columns (find-pseudo-columns attrs) 255 | independents (collect-independent-pseudo-columns pseudo-columns dependencies)] 256 | (if (empty? independents) 257 | attrs 258 | (let [realized-formulas (get-compiled-formula attrs independents)] 259 | (recur (conditionally-update attrs 260 | (fn [attr] (contains? (direct-dependents dependencies independents) 261 | (:key attr))) 262 | (fn [attr] 263 | (update attr :compiled-formula 264 | (fn [cf] 265 | (expressions/substitute-atomic-variables 266 | {:variable-values realized-formulas} 267 | cf))))))))))) 268 | 269 | (defn expand-nested-pseudo-columns 270 | [registry] 271 | (update registry :attributes expand-nested-pseudo-columns*)) 272 | 273 | (defn expand-pseudo-columns-in-aggregators* 274 | [attrs] 275 | (let [compiled-pseudo-columns (get-compiled-formula attrs (find-pseudo-columns attrs))] 276 | (conditionally-update attrs 277 | #(:aggregate %) 278 | #(update % :compiled-formula 279 | (fn [cf] 280 | (expressions/substitute-atomic-variables 281 | {:variable-values compiled-pseudo-columns} 282 | cf)))))) 283 | 284 | (defn expand-pseudo-columns-in-aggregators 285 | [registry] 286 | (update registry :attributes expand-pseudo-columns-in-aggregators*)) 287 | 288 | (defn prefix-having [compiled-having] 289 | (expressions/inline-params {} 290 | {:raw-string " HAVING (?)" 291 | :params [compiled-having]})) 292 | 293 | (defn join-filter-subqueries 294 | [{:keys [:attributes] :as registry}] 295 | (let [jfs (->> attributes 296 | (filter #(and (= :join (:type %)) (not (:aggregate %)))) 297 | (helper/build-index-of :join-filter-subquery))] 298 | (assoc registry :join-filter-subqueries jfs))) 299 | 300 | (defn compile-filters 301 | ;; TODO: just extracted join-filter-subqueries above, not checked calling of compile-filters yet 302 | [{:keys [:attributes] :as registry}] 303 | (let [compiled-pseudo-columns (get-compiled-formula attributes (find-pseudo-columns attributes))] 304 | (assoc registry :attributes 305 | (-> attributes 306 | (conditionally-update 307 | #(and (:filter %) (not (:compiled-filter %))) 308 | (fn [{condition :filter :as attr}] 309 | (->> condition 310 | (expressions/compile-to-string registry) 311 | (expressions/substitute-atomic-variables 312 | {:variable-values compiled-pseudo-columns}) 313 | (assoc attr :compiled-filter)))) 314 | (conditionally-update 315 | #(and (:ident-filter %) (not (:compiled-ident-filter %))) 316 | (fn [{condition :ident-filter :as attr}] 317 | (->> condition 318 | (expressions/compile-to-string registry) 319 | (expressions/substitute-atomic-variables 320 | {:variable-values compiled-pseudo-columns}) 321 | (assoc attr :compiled-ident-filter)))) 322 | (conditionally-update 323 | #(and (:join-filter %) (not (:compiled-join-filter %))) 324 | (fn [{condition :join-filter :as attr}] 325 | (->> condition 326 | (expressions/compile-to-string registry) 327 | (expressions/substitute-atomic-variables 328 | {:variable-values compiled-pseudo-columns}) 329 | (assoc attr :compiled-join-filter)))) 330 | (conditionally-update 331 | #(and (:having %) (not (:compiled-having %))) 332 | (fn [{condition :having :as attr}] 333 | (->> condition 334 | (expressions/compile-to-string registry) 335 | (expressions/substitute-atomic-variables 336 | {:variable-values compiled-pseudo-columns}) 337 | (prefix-having) 338 | (assoc attr :compiled-having)))))))) 339 | 340 | (defn compile-all-filters 341 | [{:keys [:attributes] :as registry}] 342 | (assoc registry :attributes 343 | (-> attributes 344 | (conditionally-update 345 | :primary-key 346 | (fn [{:keys [:compiled-filter :compiled-ident-filter] :as attr}] 347 | (assoc attr :all-filters 348 | (let [without-supplied-filter 349 | (expressions/concat-with-and 350 | (into [] (remove nil?) [compiled-filter compiled-ident-filter]))] 351 | (fn [_supplied-filter] 352 | without-supplied-filter))))) 353 | (conditionally-update 354 | #(#{:join :root} (:type %)) 355 | (fn [{:keys [:compiled-filter :compiled-join-filter] :as attr}] 356 | (let [without-supplied-filter 357 | (expressions/concat-with-and 358 | (into [] (remove nil?) [compiled-filter compiled-join-filter])) 359 | with-supplied-filter 360 | (expressions/concat-with-and 361 | (into [] (remove nil?) [compiled-filter 362 | compiled-join-filter 363 | (expressions/compile-to-string {} (expressions/av `supplied-filter))]))] 364 | (assoc attr :all-filters 365 | (fn [supplied-filter] 366 | (if supplied-filter 367 | (expressions/substitute-atomic-variables 368 | {:variable-values {`supplied-filter supplied-filter}} 369 | with-supplied-filter) 370 | without-supplied-filter))))))))) 371 | 372 | (defn compile-variable 373 | [{k :key :keys [cached? compute] :as attr}] 374 | (let [f (if cached? 375 | (fn [env _computed-graphs] 376 | (p/cached env [:walkable/variable-getter k] 377 | (compute env))) 378 | (fn [env _computed-graphs] 379 | (compute env)))] 380 | (assoc attr :compiled-variable-getter f))) 381 | 382 | (defn compile-variables* 383 | [attrs] 384 | (conditionally-update attrs 385 | #(= :variable (:type %)) 386 | compile-variable)) 387 | 388 | (defn compile-variables 389 | [registry] 390 | (update registry :attributes compile-variables*)) 391 | 392 | (defn compile-pagination-fallback 393 | [{:keys [emitter clojuric-names]} attr] 394 | (assoc attr :compiled-pagination-fallbacks 395 | {:offset-fallback (pagination/offset-fallback emitter 396 | {:default (:default-offset attr) 397 | :validate (:validate-offset attr) 398 | :throw? (= :error (:on-invalid-offset attr))}) 399 | :limit-fallback (pagination/limit-fallback emitter 400 | {:default (:default-limit attr) 401 | :validate (:validate-limit attr) 402 | :throw? (= :error (:on-invalid-limit attr))}) 403 | :order-by-fallback (pagination/order-by-fallback emitter clojuric-names 404 | {:default (:default-order-by attr) 405 | :validate (:validate-order-by attr) 406 | :throw? (= :error (:on-invalid-order-by attr))})})) 407 | 408 | (defn compile-pagination-fallbacks 409 | [registry] 410 | (update registry :attributes 411 | conditionally-update 412 | #(or (and (#{:root :join} (:type %)) (not (:aggregate %))) 413 | (= pagination/default-fallbacks (:key %))) 414 | #(compile-pagination-fallback registry %))) 415 | 416 | (defn ident-filter 417 | [_registry item] 418 | (derive-missing-key item :ident-filter (constantly [:= (:key item) (expressions/av `ident-value)]))) 419 | 420 | (defn derive-ident-cardinality 421 | [registry] 422 | (update registry :attributes 423 | conditionally-update 424 | :primary-key 425 | (fn [attr] (assoc attr :cardinality :one)))) 426 | 427 | (defn derive-ident-filters 428 | [registry] 429 | (update registry :attributes 430 | conditionally-update 431 | :primary-key 432 | (fn [attr] (ident-filter registry attr)))) 433 | 434 | (defn join-filter 435 | ;; Note: :target-column must exist => derive it first 436 | [_registry {:keys [:target-column] :as item}] 437 | ;; TODO: (if (:use-cte item)) 438 | (derive-missing-key item :join-filter (constantly [:= target-column (expressions/av `source-column-value)]))) 439 | 440 | (defn derive-join-filters 441 | [registry] 442 | (update registry :attributes 443 | conditionally-update 444 | (fn [attr] (= :join (:type attr))) 445 | (fn [attr] (join-filter registry attr)))) 446 | 447 | (defn collect-compiled-formulas 448 | [{:keys [:attributes] :as registry}] 449 | (assoc registry :compiled-formulas 450 | (helper/build-index-of :compiled-formula (filter #(#{:true-column :pseudo-column} (:type %)) attributes)))) 451 | 452 | (defn compile-group-by* 453 | [compiled-formulas group-by-keys] 454 | (->> group-by-keys 455 | (map compiled-formulas) 456 | (map :raw-string) 457 | (string/join ", ") 458 | (str " GROUP BY "))) 459 | 460 | (defn compile-group-by 461 | [{:keys [:compiled-formulas] :as registry}] 462 | (update registry :attributes 463 | conditionally-update 464 | (fn [attr] (and (= :root (:type attr)) (:group-by attr))) 465 | (fn [attr] (derive-missing-key attr :compiled-group-by 466 | #(compile-group-by* compiled-formulas (:group-by attr)))))) 467 | 468 | (defn return-one [entities] 469 | (if (not-empty entities) 470 | (first entities) 471 | {})) 472 | 473 | (defn return-many [entities] 474 | (if (not-empty entities) 475 | entities 476 | [])) 477 | 478 | (defn compile-return-function 479 | [{:keys [:cardinality :aggregate] k :key :as attr}] 480 | (let [f (if aggregate 481 | #(get (first %) k) 482 | (if (= :one cardinality) 483 | return-one 484 | return-many))] 485 | (assoc attr :return f))) 486 | 487 | (defn compile-return-functions 488 | [registry] 489 | (update registry :attributes 490 | conditionally-update 491 | #(or (#{:root :join} (:type %)) (:primary-key %)) 492 | compile-return-function)) 493 | 494 | (defn add-except [m ks] 495 | (update m :except (comp set #(clojure.set/union % ks)))) 496 | 497 | (comment 498 | (add-except {} [:a :b]) 499 | => 500 | {:except #{:b :a}} 501 | 502 | (add-except {:except [:c :d]} [:a :b]) 503 | => 504 | {:except #{:c :b :d :a}}) 505 | 506 | (defn operator-set [flat-registry new-operator-keys] 507 | (some #(and (= `operator-set (:key %)) 508 | (expressions/build-operator-set (add-except % new-operator-keys))) flat-registry)) 509 | 510 | (defn group-registry 511 | [flat-registry] 512 | (let [emitter (or (some #(and (= `emitter (:key %)) (emitter/build-emitter %)) flat-registry) 513 | emitter/default-emitter) 514 | new-operators (filter #(= :operator (:type %)) flat-registry) 515 | new-operator-keys (into #{} (map :key) new-operators)] 516 | {:emitter emitter 517 | :batch-query (emitter/emitter->batch-query emitter) 518 | :operators (helper/build-index :key (concat (or (operator-set flat-registry new-operator-keys) 519 | expressions/common-operators) 520 | new-operators)) 521 | :attributes flat-registry})) 522 | 523 | (defn collect-outputs [attrs] 524 | (into #{} (mapcat :output) attrs)) 525 | 526 | (defn collect-join-paths [attrs] 527 | (into #{} (mapcat :join-path) attrs)) 528 | 529 | (defn collect-under-key [k] 530 | (comp (map k) (remove nil?) (mapcat :params) (filter expressions/atomic-variable?) (map :name))) 531 | 532 | (defn collect-atomic-variables* 533 | [attrs] 534 | (set/union 535 | (into #{} (collect-under-key :compiled-formula) 536 | attrs) 537 | (into #{} (collect-under-key :compiled-filter) 538 | attrs) 539 | (into #{} (collect-under-key :compiled-ident-filter) 540 | attrs) 541 | (into #{} (collect-under-key :compiled-join-filter) 542 | attrs) 543 | (into #{} (collect-under-key :compiled-having) 544 | attrs))) 545 | 546 | (defn group-atomic-variables [coll] 547 | (set/rename-keys (group-by symbol? coll) {true :found-variables false :found-columns})) 548 | 549 | (defn collect-atomic-variables 550 | [attrs] 551 | (group-atomic-variables (collect-atomic-variables* attrs))) 552 | 553 | (defn variables-and-true-columns 554 | [{:keys [:attributes] :as registry}] 555 | (let [{:keys [:found-variables :found-columns]} (collect-atomic-variables attributes) 556 | non-true-columns (into #{} (comp (filter #(#{:root :join :pseudo-column} (:type %))) (map :key)) attributes) 557 | all-columns (set (set/union found-columns 558 | (collect-outputs attributes) 559 | (collect-join-paths attributes))) 560 | true-columns (set/difference all-columns non-true-columns)] 561 | (merge registry 562 | {:true-columns true-columns 563 | :found-variables found-variables}))) 564 | 565 | (defn fill-true-column-attributes 566 | [{:keys [:attributes :true-columns] :as registry}] 567 | (let [exists (set (keys (known-true-columns attributes))) 568 | new (set/difference true-columns exists)] 569 | (update registry :attributes into (mapv (fn [k] {:key k :type :true-column}) new)))) 570 | 571 | (defn selectable? [attr] 572 | (or (#{:true-column :pseudo-column} (:type attr)) 573 | (:aggregate attr))) 574 | 575 | (defn compile-clojuric-names 576 | [{:keys [:emitter] :as registry}] 577 | (update registry :attributes 578 | conditionally-update 579 | selectable? 580 | #(assoc % :clojuric-name (emitter/clojuric-name emitter (:key %))))) 581 | 582 | (defn collect-clojuric-names 583 | [{:keys [:attributes] :as registry}] 584 | (assoc registry :clojuric-names 585 | (helper/build-index-of :clojuric-name (filter :clojuric-name attributes)))) 586 | 587 | (defn compile-true-columns 588 | [{:keys [:emitter] :as registry}] 589 | (update registry :attributes 590 | conditionally-update 591 | #(#{:true-column} (:type %)) 592 | ;; TODO: take into account existing prop :table, etc 593 | #(let [inline-form (emitter/column-name emitter (:key %))] 594 | (assoc % :compiled-formula {:raw-string inline-form :params []})))) 595 | 596 | (defn inline-into 597 | [k inline-forms] 598 | (fn [attr] 599 | (update attr k 600 | #(expressions/substitute-atomic-variables 601 | {:variable-values inline-forms} %)))) 602 | 603 | (defn inline-true-columns 604 | [{:keys [:attributes] :as registry}] 605 | (let [inline-forms (->> attributes 606 | (filterv true-column?) 607 | (helper/build-index-of :compiled-formula))] 608 | (assoc registry :attributes 609 | (-> attributes 610 | (conditionally-update 611 | #(:compiled-filter %) 612 | (inline-into :compiled-filter inline-forms)) 613 | (conditionally-update 614 | #(:compiled-ident-filter %) 615 | (inline-into :compiled-ident-filter inline-forms)) 616 | (conditionally-update 617 | #(:compiled-join-filter %) 618 | (inline-into :compiled-join-filter inline-forms)) 619 | (conditionally-update 620 | #(:compiled-having %) 621 | (inline-into :compiled-having inline-forms)) 622 | (conditionally-update 623 | #(and (:compiled-formula %) (not (= :true-column (:type %)))) 624 | (inline-into :compiled-formula inline-forms)))))) 625 | 626 | (defn compile-selection 627 | [registry] 628 | (update registry :attributes 629 | conditionally-update 630 | #(or (#{:true-column :pseudo-column} (:type %)) (:aggregate %)) 631 | (fn [{:keys [:compiled-formula :clojuric-name] :as attr}] 632 | (let [selection (expressions/selection compiled-formula clojuric-name)] 633 | (assoc attr :compiled-selection selection))))) 634 | 635 | (defn compile-join-aggregator-selection 636 | [{:keys [:emitter] :as registry}] 637 | (update registry :attributes 638 | conditionally-update 639 | #(and (= :join (:type %)) (:aggregate %)) 640 | (fn [{:keys [:compiled-formula :clojuric-name] :as attr}] 641 | (let [{:keys [:target-column]} attr 642 | 643 | aggregator-selection 644 | (expressions/selection compiled-formula clojuric-name) 645 | 646 | source-column-selection 647 | (expressions/selection 648 | {:raw-string "?" 649 | :params [(expressions/av `source-column-value)]} 650 | (emitter/clojuric-name emitter target-column))] 651 | (assoc attr :compiled-join-aggregator-selection 652 | (expressions/concatenate #(clojure.string/join ", " %) 653 | [source-column-selection aggregator-selection])))))) 654 | 655 | (defn compile-join-selection 656 | [{:keys [:emitter] :as registry}] 657 | (update registry :attributes 658 | conditionally-update 659 | #(= :join (:type %)) 660 | (fn [{:keys [:target-column] :as attr}] 661 | (assoc attr :selection 662 | (expressions/selection 663 | {:raw-string "?" 664 | :params [(expressions/av `source-column-value)]} 665 | (emitter/clojuric-name emitter target-column)))))) 666 | 667 | (defn compile-traverse-scheme 668 | [{attr-type :type :as attr}] 669 | (cond 670 | (= :root attr-type) 671 | (assoc attr :traverse-scheme :roots) 672 | 673 | (= :join attr-type) 674 | (assoc attr :traverse-scheme :joins) 675 | 676 | (#{:true-column :pseudo-column} attr-type) 677 | (assoc attr :traverse-scheme :columns) 678 | 679 | :else 680 | attr)) 681 | 682 | (defn compile-traverse-schemes [registry] 683 | (update registry :attributes 684 | #(mapv compile-traverse-scheme %))) 685 | 686 | (defn compile-join-sub-entities 687 | [registry] 688 | (update registry :attributes 689 | conditionally-update 690 | #(= :join (:type %)) 691 | (fn [{:keys [:return :target-column :source-column] :as attr}] 692 | (assoc attr :merge-sub-entities 693 | (fn ->merge-sub-entities [result-key] 694 | (fn merge-sub-entities [entities sub-entities] 695 | (if (empty? sub-entities) 696 | entities 697 | (let [groups (group-by target-column sub-entities)] 698 | (mapv (fn [entity] (let [source-column-value (get entity source-column)] 699 | (assoc entity result-key (return (get groups source-column-value))))) 700 | entities))))))))) 701 | 702 | (defn compile-root-sub-entities 703 | [registry] 704 | (update registry :attributes 705 | conditionally-update 706 | #(or (= :root (:type %)) (:primary-key %)) 707 | (fn [{:keys [:return] :as attr}] 708 | (assoc attr :merge-sub-entities 709 | (fn ->merge-sub-entities [result-key] 710 | (fn merge-sub-entities [entities sub-entities] 711 | (if (empty? sub-entities) 712 | entities 713 | (assoc entities result-key (return sub-entities))))))))) 714 | 715 | (defn source-column-variable-values 716 | [v] 717 | {:variable-values {`source-column-value 718 | (expressions/compile-to-string {} v)}}) 719 | 720 | (defn compile-query-multiplier 721 | [{:keys [batch-query] :as registry}] 722 | (update registry :attributes 723 | conditionally-update 724 | #(= :join (:type %)) 725 | (fn [{:keys [:source-column] :as attr}] 726 | (assoc attr :query-multiplier 727 | (fn ->query-multiplier [individual-query-template] 728 | (let [xform (comp (map #(get % source-column)) 729 | (remove nil?) 730 | (map #(-> (expressions/substitute-atomic-variables 731 | (source-column-variable-values %) 732 | individual-query-template) 733 | ;; attach source-column-value as meta data 734 | (with-meta {:source-column-value %}))))] 735 | (fn query-multiplier* [_env entities] 736 | (->> entities 737 | ;; TODO: substitue-atomic-variables per entity 738 | (into [] (comp xform)) 739 | batch-query)))))))) 740 | 741 | (defn ident-table* 742 | [registry item] 743 | (derive-missing-key item :table #(keyword-prop registry (:key item) :table-name))) 744 | 745 | (defn derive-ident-table 746 | [registry] 747 | (update registry :attributes 748 | conditionally-update 749 | :primary-key 750 | (fn [attr] (ident-table* registry attr)))) 751 | 752 | (defn inputs-outputs 753 | [{:keys [:attributes]}] 754 | (let [plain-roots 755 | (->> attributes 756 | (filter #(and (= :root (:type %)) (not (:aggregate %)))) 757 | (mapv (fn [{k :key :keys [:output]}] 758 | {::pc/input #{} 759 | ::pc/output [{k output}]}))) 760 | 761 | root-aggregators 762 | (->> attributes 763 | (filter #(and (= :root (:type %)) (:aggregate %))) 764 | (mapv (fn [{k :key}] 765 | {::pc/input #{} 766 | ::pc/output [k]}))) 767 | 768 | plain-joins 769 | (->> attributes 770 | (filter #(and (= :join (:type %)) (not (:aggregate %)))) 771 | (mapv (fn [{k :key :keys [:output :source-column]}] 772 | {::pc/input #{source-column} 773 | ::pc/output [{k output}]}))) 774 | 775 | join-aggregators 776 | (->> attributes 777 | (filter #(and (= :join (:type %)) (:aggregate %))) 778 | (mapv (fn [{k :key :keys [:source-column]}] 779 | {::pc/input #{source-column} 780 | ::pc/output [k]}))) 781 | 782 | idents (->> attributes 783 | (filter :primary-key) 784 | (mapv (fn [{k :key :keys [:output]}] 785 | {::pc/input #{k} 786 | ::pc/output output})))] 787 | (concat plain-roots root-aggregators plain-joins join-aggregators idents))) 788 | 789 | (defn floor-plan [{:keys [:attributes]}] 790 | ;; build a compact version suitable for expressions/compile-to-string 791 | (merge 792 | {:target-table (merge (helper/build-index-of :target-table (filter #(= :join (:type %)) attributes)) 793 | (helper/build-index-of :table (filter #(or (= :root (:type %)) (:primary-key %)) attributes)))} 794 | {:target-column (helper/build-index-of :target-column (filter #(= :join (:type %)) attributes))} 795 | {:source-column (helper/build-index-of :source-column (filter #(= :join (:type %)) attributes))} 796 | {:merge-sub-entities (helper/build-index-of :merge-sub-entities (filter #(or (#{:root :join} (:type %)) (:primary-key %)) attributes))} 797 | {:query-multiplier (helper/build-index-of :query-multiplier (filter :query-multiplier attributes))} 798 | {:join-statement (helper/build-index-of :join-statement (filter :join-statement attributes))} 799 | {:compiled-join-selection (helper/build-index-of :compiled-join-selection (filter :compiled-join-selection attributes))} 800 | {:compiled-join-aggregator-selection (helper/build-index-of :compiled-join-aggregator-selection (filter :compiled-join-aggregator-selection attributes))} 801 | {:keyword-type (helper/build-index-of :traverse-scheme (filter #(#{:root :join :true-column :pseudo-column} (:type %)) attributes))} 802 | {:compiled-pagination-fallbacks (helper/build-index-of :compiled-pagination-fallbacks (filter :compiled-pagination-fallbacks attributes))} 803 | {:return (helper/build-index-of :return (filter #(or (#{:root :join} (:type %)) (:primary-key %)) attributes))} 804 | {:aggregator-keywords (into #{} (comp (filter #(:aggregate %)) (map :key)) attributes)} 805 | {:compiled-variable-getter (helper/build-index-of :compiled-variable-getter (filter :compiled-variable-getter attributes))} 806 | {:all-filters (helper/build-index-of :all-filters (filter :all-filters attributes))} 807 | {:compiled-having (helper/build-index-of :compiled-having (filter :compiled-having attributes))} 808 | {:compiled-group-by (helper/build-index-of :compiled-group-by (filter :compiled-group-by attributes))} 809 | {:ident-keywords (into #{} (comp (filter #(:primary-key %)) (map :key)) attributes)} 810 | {:compiled-selection (helper/build-index-of :compiled-selection (filter :compiled-selection attributes))})) 811 | 812 | (defn compact [registry] 813 | {:floor-plan (merge (select-keys registry [:emitter :operators :join-filter-subqueries :batch-query :compiled-formulas :clojuric-names]) 814 | (floor-plan registry)) 815 | :inputs-outputs (inputs-outputs registry)}) 816 | 817 | (defn with-db-type 818 | [db-type registry] 819 | (concat registry 820 | [{:key `emitter 821 | :base db-type} 822 | {:key `operator-set 823 | :base db-type}])) 824 | 825 | (defn compile-floor-plan* 826 | [flat-attributes] 827 | (let [registry (group-registry flat-attributes)] 828 | (->> registry 829 | (compile-formulas) 830 | (expand-nested-pseudo-columns) 831 | (expand-pseudo-columns-in-aggregators) 832 | (compile-joins) 833 | (replace-join-with-source-column-in-outputs) 834 | (derive-ident-table) 835 | (join-filter-subqueries) 836 | (derive-ident-filters) 837 | (derive-join-filters) 838 | (derive-ident-cardinality) 839 | (compile-filters) 840 | (compile-variables) 841 | (compile-return-functions) 842 | (variables-and-true-columns) 843 | (fill-true-column-attributes) 844 | (compile-true-columns) 845 | (compile-clojuric-names) 846 | (collect-clojuric-names) 847 | (inline-true-columns) 848 | (compile-pagination-fallbacks) 849 | (compile-selection) 850 | (compile-join-selection) 851 | (compile-join-aggregator-selection) 852 | (compile-traverse-schemes) 853 | (compile-join-sub-entities) 854 | (compile-root-sub-entities) 855 | (compile-query-multiplier) 856 | (compile-all-filters) 857 | (collect-compiled-formulas) 858 | (compile-group-by)))) 859 | 860 | (defn compile-floor-plan 861 | [flat-attributes] 862 | (->> flat-attributes 863 | (compile-floor-plan*) 864 | (compact))) 865 | -------------------------------------------------------------------------------- /src/walkable/sql_query_builder/helper.cljc: -------------------------------------------------------------------------------- 1 | (ns walkable.sql-query-builder.helper 2 | (:require [weavejester.dependency :as dep])) 3 | 4 | (defn check-circular-dependency! 5 | [graph] 6 | (try 7 | (reduce (fn [acc [x y]] (dep/depend acc y x)) 8 | (dep/graph) 9 | graph) 10 | (catch #?(:clj Exception :cljs js/Error) e 11 | (let [{:keys [node dependency] :as data} (ex-data e)] 12 | (throw (ex-info (str "Circular dependency between " node " and " dependency) 13 | data)))))) 14 | 15 | (defn build-index [k coll] 16 | (into {} (for [o coll] [(get o k) o]))) 17 | 18 | (defn build-index-of [k coll] 19 | (into {} (for [x coll 20 | :let [i (:key x) 21 | j (get x k)]] 22 | [i j]))) 23 | -------------------------------------------------------------------------------- /src/walkable/sql_query_builder/pagination.cljc: -------------------------------------------------------------------------------- 1 | (ns walkable.sql-query-builder.pagination 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.string :as string] 4 | [walkable.sql-query-builder.expressions :as expressions] 5 | [clojure.set :as set])) 6 | 7 | (defn column+order-params-spec 8 | [allowed-keys] 9 | (s/+ 10 | (s/cat 11 | :column ::expressions/namespaced-keyword 12 | :params (s/* allowed-keys)))) 13 | 14 | (defn ->conform-order-by 15 | [allowed-keys] 16 | (fn conform-order-by [order-by] 17 | (let [order-by (if (sequential? order-by) order-by [order-by])] 18 | (s/conform (column+order-params-spec allowed-keys) order-by)))) 19 | 20 | (defn ->stringify-order-by 21 | [order-params->string] 22 | (fn stringify-order-by [clojuric-names conformed-order-by] 23 | (when conformed-order-by 24 | (->> conformed-order-by 25 | (map (fn [{:keys [column params]}] 26 | (str 27 | (get clojuric-names column) 28 | (->> params 29 | (map order-params->string) 30 | (apply str))))) 31 | (clojure.string/join ", ") 32 | (str " ORDER BY "))))) 33 | 34 | (defn columns-and-string 35 | [conformed stringify] 36 | {:columns (into #{} (map :column) conformed) 37 | :string (stringify conformed)}) 38 | 39 | (defn wrap-validate-order-by [f] 40 | (comp boolean 41 | (if (ifn? f) 42 | (fn wrapped-validate-order-by [conformed-order-by] 43 | (when-not (s/invalid? conformed-order-by) 44 | (every? f (map :column conformed-order-by)))) 45 | identity))) 46 | 47 | (defn order-by-fallback* 48 | [{:keys [conform stringify]} 49 | {:keys [default validate throw?]}] 50 | (let [default (when default 51 | (let [conformed (conform default)] 52 | (assert (not (s/invalid? conformed)) 53 | "Malformed default value") 54 | (columns-and-string conformed stringify))) 55 | validate (wrap-validate-order-by validate)] 56 | (if throw? 57 | (fn aggressive-fallback [supplied] 58 | (let [conformed (conform supplied)] 59 | (if (s/invalid? conformed) 60 | (throw (ex-info "Malformed!" {})) 61 | (if (validate conformed) 62 | (columns-and-string conformed stringify) 63 | (throw (ex-info "Invalid!" {})))))) 64 | (fn silent-fallback [supplied] 65 | (let [conformed (conform supplied)] 66 | (if (and (not (s/invalid? conformed)) (validate conformed)) 67 | (columns-and-string conformed stringify) 68 | default)))))) 69 | 70 | (defn order-by-fallback 71 | [{:keys [conform-order-by stringify-order-by] :as emitter} 72 | clojuric-names order-by-config] 73 | (order-by-fallback* 74 | {:conform conform-order-by 75 | :stringify #(stringify-order-by clojuric-names %)} 76 | order-by-config)) 77 | 78 | (defn number-fallback 79 | [{:keys [stringify conform wrap-validate]} 80 | {:keys [default validate throw?]}] 81 | (let [default 82 | (when default 83 | (let [conformed (conform default)] 84 | (assert (not (s/invalid? conformed)) 85 | "Malformed default value") 86 | (stringify conformed))) 87 | validate (wrap-validate (or validate (constantly true)))] 88 | (if throw? 89 | (fn aggressive-fallback [supplied] 90 | (let [conformed (conform supplied)] 91 | (if (s/invalid? conformed) 92 | (throw (ex-info "Malformed!" {})) 93 | (if (validate conformed) 94 | (stringify conformed) 95 | (throw (ex-info "Invalid!" {})))))) 96 | (fn silent-fallback [supplied] 97 | (let [conformed (conform supplied) 98 | valid? (and (not (s/invalid? conformed)) 99 | (validate conformed))] 100 | (if valid? 101 | (stringify conformed) 102 | default)))))) 103 | 104 | (defn offset-fallback 105 | [emitter offset-config] 106 | (number-fallback {:stringify (:stringify-offset emitter) 107 | :conform (:conform-offset emitter) 108 | :wrap-validate (:wrap-validate-offset emitter)} 109 | offset-config)) 110 | 111 | (defn limit-fallback 112 | [emitter limit-config] 113 | (number-fallback {:stringify (:stringify-limit emitter) 114 | :conform (:conform-limit emitter) 115 | :wrap-validate (:wrap-validate-limit emitter)} 116 | limit-config)) 117 | 118 | (def default-fallbacks 119 | "Key for default of defaults." 120 | `default-fallbacks) 121 | 122 | (defn merge-pagination 123 | [default-fallbacks 124 | {:keys [offset-fallback limit-fallback order-by-fallback]} 125 | {:keys [offset limit order-by]}] 126 | (let [offset-fallback (or offset-fallback (get default-fallbacks :offset-fallback)) 127 | limit-fallback (or limit-fallback (get default-fallbacks :limit-fallback)) 128 | order-by-fallback (or order-by-fallback (get default-fallbacks :order-by-fallback)) 129 | 130 | {:keys [string columns]} (when order-by-fallback (order-by-fallback order-by))] 131 | {:offset (when offset-fallback (offset-fallback offset)) 132 | :limit (when limit-fallback (limit-fallback limit)) 133 | :order-by string 134 | :order-by-columns columns})) 135 | -------------------------------------------------------------------------------- /src/walkable/sql_query_builder/pathom_env.cljc: -------------------------------------------------------------------------------- 1 | (ns walkable.sql-query-builder.pathom-env 2 | (:refer-clojure :exclude [key]) 3 | (:require [com.wsscode.pathom.core :as p] 4 | [com.wsscode.pathom.connect :as pc])) 5 | 6 | (defn planner-node [env] 7 | (-> env 8 | :com.wsscode.pathom.connect.planner/node)) 9 | 10 | (defn planner-input [env] 11 | (-> env 12 | :com.wsscode.pathom.connect.planner/node 13 | :com.wsscode.pathom.connect.planner/input)) 14 | 15 | (defn planner-requires [env] 16 | (-> env 17 | :com.wsscode.pathom.connect.planner/node 18 | :com.wsscode.pathom.connect.planner/requires)) 19 | 20 | (defn planner-foreign-ast [env] 21 | (-> env 22 | :com.wsscode.pathom.connect.planner/node 23 | :com.wsscode.pathom.connect.planner/foreign-ast)) 24 | 25 | (defn config 26 | [env] 27 | (get-in env [::pc/resolver-data :walkable.core/config])) 28 | 29 | (defn floor-plan 30 | [env] 31 | (get-in env [::pc/resolver-data :walkable.core/config :walkable.core/floor-plan])) 32 | -------------------------------------------------------------------------------- /test/walkable/core_test.cljc: -------------------------------------------------------------------------------- 1 | (ns walkable.core-test 2 | (:require [walkable.core :as sut] 3 | [walkable.sql-query-builder.floor-plan :as floor-plan] 4 | [clojure.test :as t :refer [deftest is]] 5 | [com.wsscode.pathom.core :as p] 6 | [com.wsscode.pathom.connect :as pc])) 7 | 8 | #_(deftest process-children-test 9 | (is (= (sut/process-children 10 | {:ast (p/query->ast [:pet/age 11 | :pet/will-be-ignored 12 | :pet/owner 13 | :abc/unknown 14 | {:> [:pet/age]}]) 15 | ::p/placeholder-prefixes #{">"} 16 | 17 | ::pc/resolver-data 18 | {::sut/config 19 | {::sut/floor-plan {::floor-plan/column-keywords 20 | #{:pet/yob} 21 | ::floor-plan/required-columns 22 | {:pet/age #{:pet/yob}} 23 | ::floor-plan/source-columns 24 | {:pet/owner :person/number}}}}}) 25 | {:join-children #{{:type :prop, 26 | :dispatch-key :pet/owner, 27 | :key :pet/owner}}, 28 | :columns-to-query #{:pet/yob :person/number}}))) 29 | 30 | #_(deftest combine-params-test 31 | (is (= (sut/combine-params {:params [1]} {:params [2 3]} {:params [4 5 6]}) 32 | [1 2 3 4 5 6])) 33 | (is (= (sut/combine-params {:params [1]} {:params [2 3]} nil) 34 | [1 2 3])) 35 | (is (= (sut/combine-params {:params [1]} {:params []} {:params [4 5 6]}) 36 | [1 4 5 6])) 37 | (is (= (sut/combine-params {:params [1]} nil {:params [4 5 6]}) 38 | [1 4 5 6])) 39 | (is (= (sut/combine-params {:params [1]} nil nil) 40 | [1])) 41 | (is (= (sut/combine-params nil nil nil) 42 | []))) 43 | -------------------------------------------------------------------------------- /test/walkable/sql_query_builder/ast_test.cljc: -------------------------------------------------------------------------------- 1 | (ns walkable.sql-query-builder.ast-test 2 | (:require [walkable.sql-query-builder.ast :as sut] 3 | [com.wsscode.pathom.core :as p] 4 | [clojure.test :as t :refer [deftest testing is]])) 5 | 6 | (deftest find-all-children-test 7 | ) 8 | -------------------------------------------------------------------------------- /test/walkable/sql_query_builder/emitter_test.cljc: -------------------------------------------------------------------------------- 1 | (ns walkable.sql-query-builder.emitter-test 2 | (:require [walkable.sql-query-builder.emitter :as sut] 3 | [clojure.spec.alpha :as s] 4 | [clojure.test :as t :refer [deftest testing is]])) 5 | 6 | (deftest table-name-test 7 | (is (= (sut/table-name sut/postgres-emitter :prefix.foo/bar) 8 | "\"prefix\".\"foo\"")) 9 | (is (= (sut/table-name sut/postgres-emitter :foo/bar) 10 | "\"foo\"")) 11 | (is (= (sut/table-name sut/sqlite-emitter :foo/bar) 12 | "\"foo\"")) 13 | (is (= (sut/table-name sut/mysql-emitter :foo/bar) 14 | "`foo`"))) 15 | 16 | (deftest column-name-test 17 | (is (= (sut/column-name sut/postgres-emitter :prefix.foo/bar) 18 | "\"prefix\".\"foo\".\"bar\"")) 19 | (is (= (sut/column-name sut/postgres-emitter :foo/bar) 20 | "\"foo\".\"bar\"")) 21 | (is (= (sut/column-name sut/sqlite-emitter :foo/bar) 22 | "\"foo\".\"bar\"")) 23 | (is (= (sut/column-name sut/mysql-emitter :foo/bar) 24 | "`foo`.`bar`")) 25 | (is (= (sut/column-name (assoc sut/default-emitter 26 | :transform-column-name identity) 27 | :prefix-foo/bar-bar) 28 | "\"prefix_foo\".\"bar-bar\""))) 29 | 30 | (deftest clojuric-name-test 31 | (is (= (sut/clojuric-name sut/mysql-emitter :foo/bar) 32 | "`foo/bar`")) 33 | (is (= (sut/clojuric-name sut/postgres-emitter :prefix.foo/bar) 34 | "\"prefix.foo/bar\""))) 35 | 36 | (deftest emitter->batch-query-test 37 | (is (= ((sut/emitter->batch-query sut/default-emitter) 38 | [{:raw-string "x" :params ["a" "b"]} 39 | {:raw-string "y" :params ["c" "d"]}]) 40 | {:params ["a" "b" "c" "d"], 41 | :raw-string "(x)\nUNION ALL\n(y)"})) 42 | (is (= ((sut/emitter->batch-query sut/sqlite-emitter) 43 | [{:raw-string "x" :params ["a" "b"]} 44 | {:raw-string "y" :params ["c" "d"]}]) 45 | {:params ["a" "b" "c" "d"], 46 | :raw-string "SELECT * FROM (x)\nUNION ALL\nSELECT * FROM (y)"}))) 47 | -------------------------------------------------------------------------------- /test/walkable/sql_query_builder/expressions_test.cljc: -------------------------------------------------------------------------------- 1 | (ns walkable.sql-query-builder.expressions-test 2 | (:require [walkable.sql-query-builder.expressions :as sut] 3 | [walkable.sql-query-builder.helper :as helper] 4 | [clojure.spec.alpha :as s] 5 | [clojure.string :as string] 6 | [clojure.test :as t :refer [deftest testing is]])) 7 | 8 | (deftest inline-params-tests 9 | (= (sut/inline-params {} 10 | {:raw-string "? + 1" 11 | :params [{:raw-string "2018 - `human`.`yob` + ?" 12 | :params [(sut/av :a/b)]}]}) 13 | {:raw-string "2018 - `human`.`yob` + ? + 1" 14 | :params [(sut/av :a/b)]}) 15 | (is (= (sut/inline-params {} 16 | {:raw-string "?" 17 | :params [{:raw-string "2018 - `human`.`yob`" 18 | :params []}]}) 19 | {:raw-string "2018 - `human`.`yob`" 20 | :params []})) 21 | (is (= (sut/inline-params {} 22 | {:raw-string " ?" 23 | :params [{:raw-string "2018 - `human`.`yob`" 24 | :params []}]}) 25 | {:raw-string " 2018 - `human`.`yob`", 26 | :params []})) 27 | (is (= (sut/inline-params {} 28 | {:raw-string "? " 29 | :params [{:raw-string "2018 - `human`.`yob`" 30 | :params []}]}) 31 | {:raw-string "2018 - `human`.`yob` ", 32 | :params []}))) 33 | 34 | (deftest concatenate-params-tests 35 | (is (= (sut/concatenate #(apply str %) 36 | [{:raw-string "? as a" 37 | :params [(sut/av :a/b)]} 38 | {:raw-string "? as b" 39 | :params [(sut/av :c/d)]}]) 40 | {:params [(sut/av :a/b) (sut/av :c/d)], 41 | :raw-string "? as a? as b"})) 42 | (is (= (sut/concatenate #(string/join ", " %) 43 | [{:raw-string "? as a" 44 | :params [(sut/av :a/b)]} 45 | {:raw-string "? as b" 46 | :params [(sut/av :c/d)]}]) 47 | {:params [(sut/av :a/b) (sut/av :c/d)], 48 | :raw-string "? as a, ? as b"}))) 49 | 50 | (deftest compile-to-string-tests 51 | (is (= (sut/compile-to-string {:operators (helper/build-index :key sut/common-operators) 52 | :join-filter-subqueries 53 | {:x/a "x.a_id IN (SELECT a.id FROM a WHERE ?)" 54 | :x/b "x.id IN (SELECT x_b.x_id FROM x_b JOIN b ON b.id = x_b.b_id WHERE ?)"}} 55 | [:or {:x/a [:= :a/foo "meh"]} 56 | {:x/b [:= :b/bar "mere"]}]) 57 | {:params [(sut/av :a/foo) "meh" (sut/av :b/bar) "mere"] 58 | :raw-string "((x.a_id IN (SELECT a.id FROM a WHERE (?)=(?)))) OR ((x.id IN (SELECT x_b.x_id FROM x_b JOIN b ON b.id = x_b.b_id WHERE (?)=(?))))"})) 59 | (is (= (->> [:cast :a/c :text] 60 | (sut/compile-to-string {:operators (helper/build-index :key (concat sut/common-operators 61 | sut/sqlite-operator-set))})) 62 | {:params [(sut/av :a/c)] 63 | :raw-string "CAST (? AS text)"})) 64 | (is (= (->> [:cast "{\"foo\": 1}" :json] 65 | (sut/compile-to-string {:operators (helper/build-index :key (concat sut/common-operators 66 | sut/postgres-operator-set))})) 67 | {:params ["{\"foo\": 1}"], 68 | :raw-string "CAST (? AS json)"})) 69 | (is (= (->> [:json {:foo "bar"}] 70 | (sut/compile-to-string {:operators (helper/build-index :key (concat sut/common-operators 71 | sut/postgres-operator-set))})) 72 | {:raw-string "?::json", 73 | :params ["{\"foo\":\"bar\"}"]})) 74 | (is (= (->> [:json-text {:foo "bar"}] 75 | (sut/compile-to-string {:operators (helper/build-index :key (concat sut/common-operators 76 | sut/postgres-operator-set))})) 77 | {:raw-string "?", 78 | :params ["{\"foo\":\"bar\"}"]}))) 79 | 80 | (deftest substitute-atomic-variables-test 81 | (is (= (let [registry {:operators (helper/build-index :key sut/common-operators)}] 82 | (->> [:= :x/a "abc" [:+ 24 [:+ :x/b 2]]] 83 | (sut/compile-to-string registry) 84 | (sut/substitute-atomic-variables {:variable-values {:x/a {:raw-string "?" :params ["def"]}}}) 85 | (sut/substitute-atomic-variables {:variable-values {:x/b (sut/compile-to-string registry [:+ 2018 "713"])}}))) 86 | {:params ["def" "abc" "abc" "713"], :raw-string "(?)=(?) AND (?)=((24)+(((2018)+(?))+(2)))"}))) 87 | -------------------------------------------------------------------------------- /test/walkable/sql_query_builder/floor_plan_test.cljc: -------------------------------------------------------------------------------- 1 | (ns walkable.sql-query-builder.floor-plan-test 2 | (:require [walkable.sql-query-builder.floor-plan :as sut] 3 | [walkable.sql-query-builder.emitter :as emitter] 4 | [clojure.test :as t :refer [deftest testing is]])) 5 | 6 | (deftest keyword-prop-tests 7 | (is (= (sut/keyword-prop {:emitter emitter/default-emitter :attributes []} 8 | :a/b :table-name) 9 | "\"a\"")) 10 | (is (= (sut/keyword-prop {:emitter emitter/default-emitter :attributes [{:key :a/b 11 | :type :true-column 12 | :table-name "z"}]} 13 | :a/b :table-name) 14 | "z"))) 15 | 16 | (deftest join-statement*-tests 17 | (is (= (sut/join-statement* 18 | {:emitter emitter/default-emitter} 19 | {:key :person/friend 20 | :type :join 21 | :join-path [:person/id :friendship/first-person :friendship/second-person :person/id]}) 22 | " JOIN \"person\" ON \"friendship\".\"second_person\" = \"person\".\"id\"")) 23 | (is (nil? (sut/join-statement* 24 | {:emitter emitter/default-emitter} 25 | {:key :person/house 26 | :type :join 27 | :join-path [:person/id :house/owner-id]})))) 28 | -------------------------------------------------------------------------------- /test/walkable/sql_query_builder/helper_test.cljc: -------------------------------------------------------------------------------- 1 | (ns walkable.sql-query-builder.helper-test 2 | (:require [walkable.sql-query-builder.helper :as sut] 3 | [clojure.test :as t :refer [deftest testing is]])) 4 | 5 | (deftest build-index-tests 6 | (is (= (sut/build-index :key [{:key :foo :a 1} 7 | {:key :bar :b 2}]) 8 | {:foo {:key :foo, :a 1} 9 | :bar {:key :bar, :b 2}}))) 10 | 11 | (deftest build-index-of-tests 12 | (is (= (sut/build-index-of :a [{:key :foo :a 1} 13 | {:key :bar :a 2}]) 14 | {:foo 1, :bar 2}))) 15 | -------------------------------------------------------------------------------- /test/walkable/sql_query_builder/pagination_test.cljc: -------------------------------------------------------------------------------- 1 | (ns walkable.sql-query-builder.pagination-test 2 | (:require [walkable.sql-query-builder.pagination :as sut] 3 | [walkable.sql-query-builder.emitter :as emitter] 4 | [clojure.spec.alpha :as s] 5 | [clojure.test :as t :refer [deftest testing is are]])) 6 | 7 | (deftest ->stringify-order-by-tests 8 | (is (= ((sut/->stringify-order-by 9 | {:asc " ASC" 10 | :desc " DESC" 11 | :nils-first " NULLS FIRST" 12 | :nils-last " NULLS LAST"}) 13 | {:person/name "`p`.`n`" :person/age "`p`.`a`"} 14 | [{:column :person/name} {:column :person/age, :params [:desc :nils-last]}]) 15 | " ORDER BY `p`.`n`, `p`.`a` DESC NULLS LAST")) 16 | 17 | (is (nil? ((sut/->stringify-order-by 18 | {:asc " ASC" 19 | :desc " DESC" 20 | :nils-first " NULLS FIRST" 21 | :nils-last " NULLS LAST"}) 22 | {:person/name "`p`.`n`" :person/age "`p`.`a`"} 23 | nil)))) 24 | 25 | (deftest ->conform-order-by-test 26 | (are [order-by conformed] 27 | (= ((sut/->conform-order-by #{:asc :desc :nils-first :nils-last}) 28 | order-by) 29 | conformed) 30 | 31 | :x/a 32 | [{:column :x/a}] 33 | 34 | [:x/a :asc :x/b :desc :nils-first :x/c] 35 | [{:column :x/a, :params [:asc]} 36 | {:column :x/b, :params [:desc :nils-first]} 37 | {:column :x/c}] 38 | 39 | [:x/a :asc :x/b :desc :nils-first 'invalid-type] 40 | ::s/invalid 41 | 42 | [:x/a :asc :x/b :desc :nils-first] 43 | [{:column :x/a, :params [:asc]} 44 | {:column :x/b, :params [:desc :nils-first]}] 45 | 46 | :invalid-type 47 | ::s/invalid)) 48 | 49 | (deftest wrap-validate-order-by-test 50 | (let [simple-validate (sut/wrap-validate-order-by #{:x/a :x/b}) 51 | default-validate (sut/wrap-validate-order-by nil)] 52 | (are [validate conformed-order-by valid?] 53 | (= (validate conformed-order-by) valid?) 54 | 55 | simple-validate 56 | [{:column :x/a, :params [:asc]} {:column :x/b, :params [:desc :nils-first]}] 57 | true 58 | 59 | simple-validate 60 | [{:column :x/a, :params [:asc]} 61 | {:column :x/invalid-key, :params [:desc :nils-first]}] 62 | false 63 | 64 | default-validate 65 | [{:column :x/a, :params [:asc]} 66 | {:column :x/b, :params [:desc :nils-first]}] 67 | true 68 | 69 | default-validate 70 | [{:column :x/a, :params [:asc]} 71 | {:column :x/any-key, :params [:desc :nils-first]}] 72 | true))) 73 | 74 | (deftest offset-fallback-with-default-emitter-test 75 | (is (= (mapv (sut/offset-fallback emitter/default-emitter 76 | {:default 99 :validate #(<= 2 % 4)}) 77 | (range 8)) 78 | (mapv #(str " OFFSET " %) [99 99 2 3 4 99 99 99]))) 79 | (is (= (map (sut/offset-fallback emitter/default-emitter 80 | {:default 99 :validate #(<= 2 % 4)}) 81 | [:invalid 'types]) 82 | (mapv #(str " OFFSET " %) [99 99])))) 83 | 84 | (deftest limit-fallback-with-default-emitter-test 85 | (is (= (mapv (sut/limit-fallback emitter/default-emitter 86 | {:default 99 :validate #(<= 2 % 4)}) 87 | (range 8)) 88 | (mapv #(str " LIMIT " %) [99 99 2 3 4 99 99 99]))) 89 | (is (= (map (sut/limit-fallback emitter/default-emitter 90 | {:default 99 :validate #(<= 2 % 4)}) 91 | [:invalid 'types]) 92 | (mapv #(str " LIMIT " %) [99 99]))) 93 | (is (thrown-with-msg? #?(:clj Exception :cljs js/Error) 94 | #"Malformed" 95 | ((sut/limit-fallback emitter/default-emitter 96 | {:default 99 :validate #(<= 2 % 4) 97 | :throw? true}) 98 | :abc))) 99 | (is (thrown-with-msg? #?(:clj Exception :cljs js/Error) 100 | #"Invalid" 101 | ((sut/limit-fallback emitter/default-emitter 102 | {:default 99 :validate #(<= 2 % 4) 103 | :throw? true}) 104 | 1)))) 105 | 106 | (defn oracle-validate-limit 107 | [{:keys [limit percent with-ties]}] 108 | (if percent 109 | (< 0 limit 5) 110 | (<= 2 limit 4))) 111 | 112 | (deftest limit-fallback-with-oracle-emitter-test 113 | (is (= (mapv (sut/limit-fallback emitter/oracle-emitter 114 | {:default 99 :validate oracle-validate-limit}) 115 | (range 8)) 116 | (mapv #(str " FETCH FIRST " % " ROWS ONLY") [99 99 2 3 4 99 99 99]))) 117 | (is (= (mapv (sut/limit-fallback emitter/oracle-emitter 118 | {:default [99 :percent] :validate oracle-validate-limit}) 119 | (mapv #(do [% :percent]) (range 8))) 120 | [" FETCH FIRST 99 PERCENT ROWS ONLY" 121 | " FETCH FIRST 1 PERCENT ROWS ONLY" 122 | " FETCH FIRST 2 PERCENT ROWS ONLY" 123 | " FETCH FIRST 3 PERCENT ROWS ONLY" 124 | " FETCH FIRST 4 PERCENT ROWS ONLY" 125 | " FETCH FIRST 99 PERCENT ROWS ONLY" 126 | " FETCH FIRST 99 PERCENT ROWS ONLY" 127 | " FETCH FIRST 99 PERCENT ROWS ONLY"])) 128 | (is (= (mapv (sut/limit-fallback emitter/oracle-emitter 129 | {:default [99 :percent] :validate oracle-validate-limit}) 130 | (mapv #(do [% :percent :with-ties]) (range 8))) 131 | [" FETCH FIRST 99 PERCENT ROWS ONLY" 132 | " FETCH FIRST 1 PERCENT ROWS WITH TIES" 133 | " FETCH FIRST 2 PERCENT ROWS WITH TIES" 134 | " FETCH FIRST 3 PERCENT ROWS WITH TIES" 135 | " FETCH FIRST 4 PERCENT ROWS WITH TIES" 136 | " FETCH FIRST 99 PERCENT ROWS ONLY" 137 | " FETCH FIRST 99 PERCENT ROWS ONLY" 138 | " FETCH FIRST 99 PERCENT ROWS ONLY"])) 139 | (is (= (mapv (sut/limit-fallback emitter/oracle-emitter 140 | {:default [99 :percent]}) 141 | (mapv #(do [% :percent :with-ties]) (range 8))) 142 | [" FETCH FIRST 0 PERCENT ROWS WITH TIES" 143 | " FETCH FIRST 1 PERCENT ROWS WITH TIES" 144 | " FETCH FIRST 2 PERCENT ROWS WITH TIES" 145 | " FETCH FIRST 3 PERCENT ROWS WITH TIES" 146 | " FETCH FIRST 4 PERCENT ROWS WITH TIES" 147 | " FETCH FIRST 5 PERCENT ROWS WITH TIES" 148 | " FETCH FIRST 6 PERCENT ROWS WITH TIES" 149 | " FETCH FIRST 7 PERCENT ROWS WITH TIES"])) 150 | (is (= (mapv (sut/limit-fallback emitter/oracle-emitter 151 | {:default [99 :percent]}) 152 | (mapv #(do [% :percent :with-ties-typo]) (range 8))) 153 | (repeat 8 " FETCH FIRST 99 PERCENT ROWS ONLY"))) 154 | (is (= (map (sut/limit-fallback emitter/oracle-emitter 155 | {:default 99 :validate #(<= 2 % 4)}) 156 | [:invalid 'types]) 157 | (mapv #(str " FETCH FIRST " % " ROWS ONLY") [99 99]))) 158 | (is (thrown-with-msg? #?(:clj Exception :cljs js/Error) 159 | #"Malformed" 160 | ((sut/limit-fallback emitter/oracle-emitter 161 | {:default 99 :validate oracle-validate-limit 162 | :throw? true}) 163 | :abc))) 164 | (is (thrown-with-msg? #?(:clj Exception :cljs js/Error) 165 | #"Invalid" 166 | ((sut/limit-fallback emitter/oracle-emitter 167 | {:default 99 :validate oracle-validate-limit 168 | :throw? true}) 169 | 1)))) 170 | 171 | (deftest order-by-fallback-test 172 | (is (= (mapv (sut/order-by-fallback 173 | emitter/default-emitter 174 | {:x/a "x.a" :x/b "x.b"} 175 | {:default [:x/a :asc :x/b] 176 | :validate #{:x/a :x/b}}) 177 | [[:x/a :desc :x/b :desc :nils-first] 178 | [:x/a :desc :x/invalid-key :desc :nils-first] 179 | nil]) 180 | [{:columns #{:x/a :x/b}, 181 | :string " ORDER BY x.a DESC, x.b DESC NULLS FIRST"} 182 | {:columns #{:x/a :x/b}, 183 | :string " ORDER BY x.a ASC, x.b"} 184 | {:columns #{:x/a :x/b}, 185 | :string " ORDER BY x.a ASC, x.b"}]))) 186 | 187 | ;; FIXME 188 | #_ 189 | (deftest merge-pagination-test 190 | (let [all-fallbacks (sut/compile-fallbacks 191 | emitter/default-emitter 192 | {:x/a "`x/a`" :x/b "`x/b`" :x/random-key "`x/random-key`"} 193 | {:people/people 194 | {:offset {:default 5 195 | :validate #(<= 2 % 4)} 196 | :limit {:default 10 197 | :validate #(<= 12 % 14)} 198 | :order-by {:default [:x/a] 199 | :validate #{:x/a :x/b}}}}) 200 | current-fallbacks (:people/people all-fallbacks) 201 | default-fallbacks (get all-fallbacks `sut/default-fallbacks)] 202 | (is (= (sut/merge-pagination 203 | default-fallbacks 204 | nil 205 | {:offset 4 206 | :limit 8 207 | :order-by [{:column :x/b}]}) 208 | {:offset " OFFSET 4", 209 | :limit " LIMIT 8", 210 | :order-by nil, 211 | :order-by-columns nil})) 212 | (is (= (sut/merge-pagination 213 | default-fallbacks 214 | current-fallbacks 215 | {:offset 4 216 | :limit 8 217 | :conformed-order-by [:x/invalid-key]}) 218 | {:offset " OFFSET 4", 219 | :limit " LIMIT 10", 220 | :order-by " ORDER BY `x/a`", 221 | :order-by-columns #{:x/a}})) 222 | (is (= (sut/merge-pagination 223 | default-fallbacks 224 | current-fallbacks 225 | {:offset 4 226 | :limit :invalid-type 227 | :order-by [:x/a :x/b]}) 228 | {:offset " OFFSET 4", 229 | :limit " LIMIT 10", 230 | :order-by " ORDER BY `x/a`, `x/b`", 231 | :order-by-columns #{:x/a :x/b}})))) 232 | --------------------------------------------------------------------------------