├── .editorconfig ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── haskell.yml ├── .gitignore ├── .stylish-haskell.yaml ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── Setup.hs ├── cabal.project ├── changelog.md ├── docs └── blog_post_2012_08_06 ├── esqueleto.cabal ├── esqueleto.png ├── examples ├── .gitignore ├── Blog.hs ├── LICENSE ├── Main.hs ├── README.md ├── Setup.hs ├── esqueleto-examples.cabal └── package.yaml ├── screwdriver.yaml ├── src └── Database │ ├── Esqueleto.hs │ └── Esqueleto │ ├── Experimental.hs │ ├── Experimental │ ├── From.hs │ ├── From │ │ ├── CommonTableExpression.hs │ │ ├── Join.hs │ │ └── SqlSetOperation.hs │ ├── ToAlias.hs │ ├── ToAliasReference.hs │ └── ToMaybe.hs │ ├── Internal │ ├── ExprParser.hs │ ├── Internal.hs │ └── PersistentImport.hs │ ├── Legacy.hs │ ├── MySQL.hs │ ├── PostgreSQL.hs │ ├── PostgreSQL │ ├── JSON.hs │ └── JSON │ │ └── Instances.hs │ ├── Record.hs │ └── SQLite.hs ├── stack-8.10.yaml ├── stack-8.2.yaml ├── stack-8.4.yaml ├── stack-8.6.yaml ├── stack-8.8.yaml ├── stack-8.8.yaml.lock ├── stack-9.0.yaml ├── stack-nightly.yaml ├── stack-nightly.yaml.lock ├── stack.yaml ├── style-guide.md └── test ├── Common ├── Record.hs ├── Test.hs └── Test │ ├── CTE.hs │ ├── Import.hs │ ├── Models.hs │ └── Select.hs ├── MySQL └── Test.hs ├── PostgreSQL ├── MigrateJSON.hs └── Test.hs ├── SQLite └── Test.hs ├── Spec.hs ├── docker-compose.yml ├── expected-compile-failures ├── .gitignore ├── README.md ├── Setup.hs ├── package.yaml ├── src │ └── Lib.hs ├── stack-8.2.yaml ├── stack-8.4.yaml ├── stack-8.6.yaml ├── stack.yaml ├── stack.yaml.lock ├── test.sh ├── update-read-role │ └── Main.hs └── write-read-role │ └── Main.hs └── new-join-compiler-errors ├── README.md ├── bad-errors └── Main.hs ├── new-join-compiler-errors.cabal ├── package.yaml ├── src └── Lib.hs └── stack.yaml /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [Makefile] 5 | indent_style = tabs 6 | indent_size = 8 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{hs,md,php}] 13 | indent_style = space 14 | indent_size = 4 15 | tab_width = 4 16 | end_of_line = lf 17 | charset = utf-8 18 | trim_trailing_whitespace = true 19 | insert_final_newline = true 20 | max_line_length = 80 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Before submitting your PR, check that you've: 2 | 3 | - [ ] Bumped the version number. 4 | - [ ] Documented new APIs with [Haddock markup](https://www.haskell.org/haddock/doc/html/index.html). 5 | - [ ] Added [`@since` declarations](http://haskell-haddock.readthedocs.io/en/latest/markup.html#since) to the Haddock. 6 | - [ ] Ran `stylish-haskell` and otherwise adhered to the [style guide](https://github.com/bitemyapp/esqueleto/blob/master/style-guide.yaml). 7 | 8 | After submitting your PR: 9 | 10 | - [ ] Update the Changelog.md file with a link to your PR. 11 | - [ ] Check that CI passes (or if it fails, for reasons unrelated to your change, like CI timeouts). 12 | 13 | 20 | -------------------------------------------------------------------------------- /.github/workflows/haskell.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | types: 8 | - opened 9 | - synchronize 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | services: 14 | # mysql-service Label used to access the service container 15 | mysql-service: 16 | # Docker Hub image (also with version) 17 | image: mysql:8.0 18 | env: 19 | ## Accessing to Github secrets, where you can store your configuration 20 | MYSQL_USER: travis 21 | MYSQL_PASSWORD: esqutest 22 | MYSQL_ROOT_PASSWORD: esqutest 23 | MYSQL_DATABASE: esqutest 24 | ## map the "external" 33306 port with the "internal" 3306 25 | ports: 26 | - 33306:3306 27 | # Set health checks to wait until mysql database has started (it takes some seconds to start) 28 | options: >- 29 | --health-cmd="mysqladmin ping" 30 | --health-interval=10s 31 | --health-timeout=5s 32 | --health-retries=3 33 | strategy: 34 | matrix: 35 | cabal: ["3.10.2.1"] 36 | ghc: 37 | - "8.6" 38 | - "8.8" 39 | - "8.10" 40 | - "9.0" 41 | - "9.2" 42 | - "9.4" 43 | - "9.6" 44 | - "9.8" 45 | - "9.10" 46 | # - "9.12" 47 | env: 48 | CONFIG: "--enable-tests --enable-benchmarks " 49 | steps: 50 | - uses: actions/checkout@v4 51 | - uses: haskell-actions/setup@v2 52 | id: setup-haskell-cabal 53 | with: 54 | ghc-version: ${{ matrix.ghc }} 55 | cabal-version: ${{ matrix.cabal }} 56 | - uses: harmon758/postgresql-action@v1 57 | with: 58 | postgresql version: '12' # See https://hub.docker.com/_/postgres for available versions 59 | postgresql user: esqutest 60 | postgresql password: esqutest 61 | postgresql db: esqutest 62 | - name: Create MySQL 63 | run: mysql -utravis -pesqutest -h127.0.0.1 --port=33306 esqutest -e "SELECT 1;" 64 | # - name: Shutdown Ubuntu MySQL (SUDO) 65 | # run: sudo service mysql stop 66 | # - uses: mirromutth/mysql-action@v1.1 67 | # with: 68 | # mysql version: '8.0' # Optional, default value is "latest". The version of the MySQL 69 | # mysql database: 'esqutest' # Optional, default value is "test". The specified database which will be create 70 | # mysql user: 'travis' # Required if "mysql root password" is empty, default is empty. The superuser for the specified database. Can use secrets, too 71 | # mysql password: 'esqutest' # Required if "mysql user" exists. The password for the "mysql user" 72 | - run: sudo apt-get update && sudo apt-get install -y libpcre3-dev 73 | - run: cabal v2-update 74 | - run: cabal v2-freeze $CONFIG 75 | - uses: actions/cache@v4 76 | with: 77 | path: | 78 | ${{ steps.setup-haskell-cabal.outputs.cabal-store }} 79 | dist-newstyle 80 | key: ${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }} 81 | restore-keys: | 82 | ${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }} 83 | ${{ runner.os }}-${{ matrix.ghc }}- 84 | - run: cabal v2-build --disable-optimization -j $CONFIG 85 | - run: cabal v2-test --disable-optimization -j $CONFIG --test-options "--fail-on-focus" 86 | - if: ${{ matrix.ghc != '8.6' }} 87 | run: cabal v2-haddock -j $CONFIG 88 | - run: cabal v2-sdist 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .stack-work 2 | stack.yaml.lock 3 | *.yaml.lock 4 | /dist* 5 | *~ 6 | .cabal-sandbox/ 7 | cabal.sandbox.config 8 | .hspec-failures 9 | *.sqlite* 10 | cabal.project.freeze 11 | -------------------------------------------------------------------------------- /.stylish-haskell.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - imports: 3 | align: none 4 | list_align: with_module_name 5 | pad_module_names: false 6 | long_list_align: new_line_multiline 7 | empty_list_align: inherit 8 | list_padding: 7 # length "import " 9 | separate_lists: false 10 | space_surround: false 11 | - language_pragmas: 12 | style: vertical 13 | align: false 14 | remove_redundant: true 15 | - simple_align: 16 | cases: false 17 | top_level_patterns: false 18 | records: false 19 | - trailing_whitespace: {} 20 | indent: 4 21 | columns: 80 22 | newline: native 23 | language_extensions: 24 | - BlockArguments 25 | - DataKinds 26 | - DeriveGeneric 27 | - DerivingStrategies 28 | - DerivingVia 29 | - ExplicitForAll 30 | - FlexibleContexts 31 | - MultiParamTypeClasses 32 | - NamedFieldPuns 33 | - OverloadedStrings 34 | - QuantifiedConstraints 35 | - RecordWildCards 36 | - ScopedTypeVariables 37 | - TemplateHaskell 38 | - TypeApplications 39 | - ViewPatterns 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | 3 | services: 4 | - mysql 5 | 6 | addons: 7 | postgresql: "10" 8 | apt: 9 | packages: 10 | - libgmp-dev 11 | - postgresql-10 12 | - postgresql-client-10 13 | - postgresql-server-dev-all 14 | 15 | env: 16 | global: 17 | - PGPORT=5432 18 | matrix: 19 | - GHCVER=8.2 20 | - GHCVER=8.4 21 | - GHCVER=8.6 22 | - GHCVER=8.8 23 | - GHCVER=nightly 24 | 25 | jobs: 26 | fast_finish: true 27 | allow_failures: 28 | - env: GHCVER=nightly 29 | 30 | install: 31 | - export STACK_YAML=stack-$GHCVER.yaml 32 | - mkdir -p ~/.local/bin 33 | - export PATH=$HOME/.local/bin:$PATH 34 | - travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack' 35 | - stack --version 36 | - psql -c "CREATE USER esqutest WITH PASSWORD 'esqutest';" -U postgres 37 | - createdb -O esqutest esqutest 38 | - mysql -e 'CREATE DATABASE esqutest;' 39 | - stack setup 40 | 41 | script: 42 | - stack build --fast $(if [[ $GHCVER == "nightly" ]]; then echo "--resolver nightly"; fi) 43 | - stack test --fast $(if [[ $GHCVER == "nightly" ]]; then echo "--resolver nightly"; fi) 44 | - cd test/expected-compile-failures/ 45 | - bash test.sh 46 | 47 | cache: 48 | directories: 49 | - $HOME/.stack 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Felipe Lessa 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Felipe Lessa nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | STACK := stack --jobs $(shell nproc) 2 | 3 | match ?= 4 | 5 | STACK_TEST_ARGS := $(if $(match),--test-arguments "--match $(match)",) 6 | 7 | .PHONY: build 8 | build: 9 | $(STACK) build 10 | 11 | .PHONY: build-tests 12 | build-tests: 13 | $(STACK) build --test --no-run-tests 14 | 15 | .PHONY: ghci 16 | ghci: 17 | $(STACK) ghci 18 | 19 | .PHONY: test 20 | test: 21 | $(STACK) test $(STACK_TEST_ARGS) 22 | 23 | # Intended for use in local dev 24 | .PHONY: test-postgresql 25 | test-postgresql: reset-pgsql 26 | $(STACK) test esqueleto:postgresql $(STACK_TEST_ARGS) 27 | 28 | .PHONY: test-mysql 29 | test-mysql: 30 | $(STACK) test esqueleto:mysql $(STACK_TEST_ARGS) 31 | 32 | .PHONY: test-ghci 33 | test-ghci: 34 | $(STACK) ghci esqueleto:test:sqlite $(STACK_TEST_ARGS) 35 | 36 | .PHONY: test-ghcid 37 | test-ghcid: 38 | ghcid -c "$(STACK) ghci --ghci-options -fobject-code esqueleto --test" \ 39 | --warnings \ 40 | --restart "stack.yaml" \ 41 | --restart "esqueleto.cabal" \ 42 | --test main 43 | 44 | .PHONY: test-ghcid-build 45 | test-ghcid-build: 46 | ghcid -c "$(STACK) ghci --ghci-options -fobject-code esqueleto --test" \ 47 | --warnings \ 48 | --restart "stack.yaml" \ 49 | --restart "esqueleto.cabal" 50 | 51 | .PHONY: haddock doc 52 | haddock: doc 53 | doc: 54 | $(STACK) haddock 55 | 56 | .PHONY: clean 57 | clean: 58 | $(STACK) clean 59 | 60 | .PHONY: init-pgsql 61 | 62 | init-pgsql: 63 | sudo -u postgres -- createuser -s esqutest 64 | 65 | .PHONY: reset-pgsql 66 | reset-pgsql: 67 | -sudo -u postgres dropdb esqutest 68 | -sudo -u postgres dropuser esqutest 69 | echo "CREATE USER esqutest WITH PASSWORD 'esqutest';" | sudo -u postgres psql 70 | sudo -u postgres createdb -O esqutest esqutest 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Esqueleto [![CI](https://github.com/bitemyapp/esqueleto/actions/workflows/haskell.yml/badge.svg?branch=master)](https://github.com/bitemyapp/esqueleto/actions/workflows/haskell.yml) 2 | ========== 3 | 4 | ![Skeleton](./esqueleto.png) 5 | Image courtesy [Chrissy Long](https://www.flickr.com/photos/chrissylong/313800029/) 6 | 7 | # Esqueleto, a SQL DSL for Haskell 8 | 9 | Esqueleto is a bare bones, type-safe EDSL for SQL queries that works with unmodified persistent SQL backends. The name of this library means "skeleton" in Portuguese and contains all three SQL letters in the correct order =). It was inspired by Scala's Squeryl but created from scratch. Its language closely resembles SQL. Currently, SELECTs, UPDATEs, INSERTs and DELETEs are supported. 10 | 11 | In particular, esqueleto is the recommended library for type-safe JOINs on persistent SQL backends. (The alternative is using raw SQL, but that's error prone and does not offer any composability.). For more information read [esqueleto](http://hackage.haskell.org/package/esqueleto). 12 | 13 | ## Setup 14 | 15 | If you're already using `persistent`, then you're ready to use `esqueleto`, no further setup is needed. If you're just starting a new project and would like to use `esqueleto`, take a look at `persistent`'s [book](http://www.yesodweb.com/book/persistent) first to learn how to define your schema. 16 | 17 | If you need to use `persistent`'s default support for queries as well, either import it qualified: 18 | 19 | ```haskell 20 | -- For a module that mostly uses esqueleto. 21 | import Database.Esqueleto 22 | import qualified Database.Persistent as P 23 | ``` 24 | 25 | or import `esqueleto` itself qualified: 26 | 27 | ```haskell 28 | -- For a module that uses esqueleto just on some queries. 29 | import Database.Persistent 30 | import qualified Database.Esqueleto as E 31 | ``` 32 | 33 | Other than identifier name clashes, `esqueleto` does not conflict with `persistent` in any way. 34 | 35 | 36 | ## Goals 37 | 38 | The main goals of `esqueleto` are: 39 | 40 | - Be easily translatable to SQL. (You should be able to know exactly how the SQL query will end up.) 41 | - Support the most widely used SQL features. 42 | - Be as type-safe as possible. 43 | 44 | It is _not_ a goal to be able to write portable SQL. We do not try to hide the differences between DBMSs from you 45 | 46 | 47 | ## Introduction 48 | 49 | For the following examples, we'll use this example schema: 50 | 51 | ```haskell 52 | share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist| 53 | Person 54 | name String 55 | age Int Maybe 56 | deriving Eq Show 57 | BlogPost 58 | title String 59 | authorId PersonId 60 | deriving Eq Show 61 | Follow 62 | follower PersonId 63 | followed PersonId 64 | deriving Eq Show 65 | |] 66 | ``` 67 | 68 | ## Select 69 | 70 | Most of `esqueleto` was created with `SELECT` statements in mind, not only because they're the most common but also because they're the most complex kind of statement. The most simple kind of `SELECT` would be: 71 | 72 | ```haskell 73 | putPersons :: SqlPersist m () 74 | putPersons = do 75 | people <- select $ 76 | from $ \person -> do 77 | return person 78 | liftIO $ mapM_ (putStrLn . personName . entityVal) people 79 | ``` 80 | 81 | which generates this SQL: 82 | 83 | ```sql 84 | SELECT * 85 | FROM Person 86 | ``` 87 | 88 | `esqueleto` knows that we want an `Entity Person` just because of the `personName` that is printed. 89 | 90 | ## Where 91 | 92 | Filtering by `PersonName`: 93 | 94 | ```haskell 95 | select $ 96 | from $ \p -> do 97 | where_ (p ^. PersonName ==. val "John") 98 | return p 99 | ``` 100 | 101 | which generates this SQL: 102 | 103 | ```sql 104 | SELECT * 105 | FROM Person 106 | WHERE Person.name = "John" 107 | ``` 108 | 109 | The `(^.)` operator is used to project a field from an entity. The field name is the same one generated by `persistent`s Template Haskell functions. We use `val` to lift a constant Haskell value into the SQL query. 110 | 111 | Another example: 112 | 113 | In `esqueleto`, we may write the same query above as: 114 | 115 | ```haskell 116 | select $ 117 | from $ \p -> do 118 | where_ (p ^. PersonAge >=. just (val 18)) 119 | return p 120 | ``` 121 | 122 | which generates this SQL: 123 | 124 | ```sql 125 | SELECT * 126 | FROM Person 127 | WHERE Person.age >= 18 128 | ``` 129 | 130 | Since `age` is an optional `Person` field, we use `just` to lift `val 18 :: SqlExpr (Value Int)` into `just (val 18) ::SqlExpr (Value (Maybe Int))`. 131 | 132 | ### Alternative Field Projections 133 | 134 | The `(^.)` operator works on an `EntityField` value, which are generated by 135 | `persistent` as the table name + the field name. This can get a little bit 136 | verbose. As of `persistent-2.11`, you can use `OverloadedLabels` to make this a 137 | bit more concise: 138 | 139 | ```haskell 140 | {-# LANGUAGE OverloadedLabels #-} 141 | 142 | select $ do 143 | p <- from $ table @Person 144 | pure 145 | ( p ^. PersonName 146 | , p ^. #name 147 | ) 148 | ``` 149 | 150 | The `OverloadedLabels` support uses the `fieldName` as given by the Persistent 151 | entity definition syntax - no type name prefix necessary. Additionally, these 152 | field accesses are *polymorphic* - the following query filters any table that 153 | has a `name` column: 154 | 155 | ```haskell 156 | rowsByName 157 | :: forall rec. 158 | ( PersistEntity rec 159 | , PersistEntityBackend rec ~ SqlBackend 160 | , SymbolToField "name" rec Text 161 | ) 162 | => SqlExpr (Value Text) 163 | -> SqlQuery (SqlExpr (Entity rec)) 164 | rowsByName name = do 165 | rec <- from $ table @rec 166 | where_ $ rec ^. #name ==. name 167 | pure rec 168 | ``` 169 | 170 | GHC 9.2 introduces the `OverloadedRecordDot` language extension, and `esqueleto` 171 | supports this on `SqlExpr (Entity rec)` and `SqlExpr (Maybe (Entity rec))`. It 172 | looks like this: 173 | 174 | ```haskell 175 | select $ do 176 | (person, blogPost) <- 177 | from $ 178 | table @Person 179 | `leftJoin` table @BlogPost 180 | `on` do 181 | \(person :& blogPost) -> 182 | just person.id ==. blogPost.authorId 183 | pure (person.name, blogPost.title) 184 | ``` 185 | 186 | ## Experimental/New Joins 187 | 188 | There's a new way to write `JOIN`s in esqueleto! It has less potential for 189 | runtime errors and is much more powerful than the old syntax. To opt in to the 190 | new syntax, import: 191 | 192 | ```haskell 193 | import Database.Esqueleto.Experimental 194 | ``` 195 | 196 | This will conflict with the definition of `from` and `on` in the 197 | `Database.Esqueleto` module, so you'll want to remove that import. 198 | 199 | This style will become the new "default" in esqueleto-4.0.0.0, so it's a good 200 | idea to port your code to using it soon. 201 | 202 | The module documentation in `Database.Esqueleto.Experimental` has many examples, 203 | and they won't be repeated here. Here's a quick sample: 204 | 205 | ```haskell 206 | select $ do 207 | (a :& b) <- 208 | from $ 209 | Table @BlogPost 210 | `InnerJoin` 211 | Table @Person 212 | `on` do \(bp :& a) -> 213 | bp ^. BlogPostAuthorId ==. a ^. PersonId 214 | pure (a, b) 215 | ``` 216 | 217 | Advantages: 218 | 219 | - `ON` clause is attached directly to the relevant join, so you never need to 220 | worry about how they're ordered, nor will you ever run into bugs where the 221 | `on` clause is on the wrong `JOIN` 222 | - The `ON` clause lambda will exclusively have all the available tables in it. This forbids 223 | runtime errors where an `ON` clause refers to a table that isn't in scope yet. 224 | - You can join on a table twice, and the aliases work out fine with the `ON` 225 | clause. 226 | - You can use `UNION`, `EXCEPT`, `INTERSECTION` etc with this new syntax! 227 | - You can reuse subqueries more easily. 228 | 229 | ## Legacy Joins 230 | 231 | Implicit joins are represented by tuples. 232 | 233 | For example, to get the list of all blog posts and their authors, we could write: 234 | 235 | ```haskell 236 | select $ 237 | from $ \(b, p) -> do 238 | where_ (b ^. BlogPostAuthorId ==. p ^. PersonId) 239 | orderBy [asc (b ^. BlogPostTitle)] 240 | return (b, p) 241 | ``` 242 | 243 | which generates this SQL: 244 | 245 | ```sql 246 | SELECT BlogPost.*, Person.* 247 | FROM BlogPost, Person 248 | WHERE BlogPost.authorId = Person.id 249 | ORDER BY BlogPost.title ASC 250 | ``` 251 | 252 | 253 | However, you may want your results to include people who don't have any blog posts as well using a `LEFT OUTER JOIN`: 254 | 255 | ```haskell 256 | select $ 257 | from $ \(p `LeftOuterJoin` mb) -> do 258 | on (just (p ^. PersonId) ==. mb ?. BlogPostAuthorId) 259 | orderBy [asc (p ^. PersonName), asc (mb ?. BlogPostTitle)] 260 | return (p, mb) 261 | ``` 262 | 263 | which generates this SQL: 264 | 265 | ```sql 266 | SELECT Person.*, BlogPost.* 267 | FROM Person LEFT OUTER JOIN BlogPost 268 | ON Person.id = BlogPost.authorId 269 | ORDER BY Person.name ASC, BlogPost.title ASC 270 | ``` 271 | 272 | ## Left Outer Join 273 | 274 | On a `LEFT OUTER JOIN` the entity on the right hand side may not exist (i.e. there may be a `Person` without any `BlogPost`s), so while `p :: SqlExpr (Entity Person)`, we have `mb :: SqlExpr (Maybe (Entity BlogPost))`. The whole expression above has type `SqlPersist m [(Entity Person, Maybe (Entity BlogPost))]`. Instead of using `(^.)`, we used `(?.)` to project a field from a `Maybe (Entity a)`. 275 | 276 | We are by no means limited to joins of two tables, nor by joins of different tables. For example, we may want a list of the `Follow` entity: 277 | 278 | ```haskell 279 | select $ 280 | from $ \(p1 `InnerJoin` f `InnerJoin` p2) -> do 281 | on (p2 ^. PersonId ==. f ^. FollowFollowed) 282 | on (p1 ^. PersonId ==. f ^. FollowFollower) 283 | return (p1, f, p2) 284 | ``` 285 | 286 | which generates this SQL: 287 | 288 | ```sql 289 | SELECT P1.*, Follow.*, P2.* 290 | FROM Person AS P1 291 | INNER JOIN Follow ON P1.id = Follow.follower 292 | INNER JOIN Person AS P2 ON P2.id = Follow.followed 293 | ``` 294 | 295 | ## Update and Delete 296 | 297 | ```haskell 298 | do update $ \p -> do 299 | set p [ PersonName =. val "João" ] 300 | where_ (p ^. PersonName ==. val "Joao") 301 | delete $ 302 | from $ \p -> do 303 | where_ (p ^. PersonAge <. just (val 14)) 304 | ``` 305 | 306 | The results of queries can also be used for insertions. In `SQL`, we might write the following, inserting a new blog post for every user: 307 | 308 | ```haskell 309 | insertSelect $ from $ \p-> 310 | return $ BlogPost <# "Group Blog Post" <&> (p ^. PersonId) 311 | ``` 312 | 313 | which generates this SQL: 314 | 315 | ```sql 316 | INSERT INTO BlogPost 317 | SELECT ('Group Blog Post', id) 318 | FROM Person 319 | ``` 320 | 321 | Individual insertions can be performed through Persistent's `insert` function, reexported for convenience. 322 | 323 | ### Re-exports 324 | 325 | We re-export many symbols from `persistent` for convenience: 326 | - "Store functions" from "Database.Persist". 327 | - Everything from "Database.Persist.Class" except for `PersistQuery` and `delete` (use `deleteKey` instead). 328 | - Everything from "Database.Persist.Types" except for `Update`, `SelectOpt`, `BackendSpecificFilter` and `Filter`. 329 | - Everything from "Database.Persist.Sql" except for `deleteWhereCount` and `updateWhereCount`. 330 | 331 | ### RDBMS Specific 332 | 333 | There are many differences between SQL syntax and functions supported by different RDBMSs. Since version 2.2.8, `esqueleto` includes modules containing functions that are specific to a given RDBMS. 334 | 335 | - PostgreSQL: `Database.Esqueleto.PostgreSQL` 336 | - MySQL: `Database.Esqueleto.MySQL` 337 | - SQLite: `Database.Esqueleto.SQLite` 338 | 339 | In order to use these functions, you need to explicitly import their corresponding modules. 340 | 341 | ### Unsafe functions, operators and values 342 | 343 | Esqueleto doesn't support every possible function, and it can't - many functions aren't available on every RDBMS platform, and sometimes the same functionality is hidden behind different names. To overcome this problem, Esqueleto exports a number of unsafe functions to call any function, operator or value. These functions can be found in Database.Esqueleto.Internal.Sql module. 344 | 345 | Warning: the functions discussed in this section must always be used with an explicit type signature,and the user must be careful to provide a type signature that corresponds correctly with the underlying code. The functions have extremely general types, and if you allow type inference to figure everything out for you, it may not correspond with the underlying SQL types that you want. This interface is effectively the FFI to SQL database, so take care! 346 | 347 | The most common use of these functions is for calling RDBMS specific or custom functions, 348 | for that end we use `unsafeSqlFunction`. For example, if we wish to consult the postgres 349 | `now` function we could so as follow: 350 | 351 | ```haskell 352 | postgresTime :: (MonadIO m, MonadLogger m) => SqlWriteT m UTCTime 353 | postgresTime = 354 | result <- select (pure now) 355 | case result of 356 | [x] -> pure x 357 | _ -> error "now() is guaranteed to return a single result" 358 | where 359 | now :: SqlExpr (Value UTCTime) 360 | now = unsafeSqlFunction "now" () 361 | ``` 362 | 363 | which generates this SQL: 364 | 365 | ```sql 366 | SELECT now() 367 | ``` 368 | 369 | With the `now` function we could now use the current time of the postgres RDBMS on any query. 370 | Do notice that `now` does not use any arguments, so we use `()` that is an instance of 371 | `UnsafeSqlFunctionArgument` to represent no arguments, an empty list cast to a correct value 372 | will yield the same result as `()`. 373 | 374 | We can also use `unsafeSqlFunction` for more complex functions with customs values using 375 | `unsafeSqlValue` which turns any string into a sql value of whatever type we want, disclaimer: 376 | if you use it badly you will cause a runtime error. For example, say we want to try postgres' 377 | `date_part` function and get the day of a timestamp, we could use: 378 | 379 | ```haskell 380 | postgresTimestampDay :: (MonadIO m, MonadLogger m) => SqlWriteT m Int 381 | postgresTimestampDay = 382 | result <- select (return $ dayPart date) 383 | case result of 384 | [x] -> pure x 385 | _ -> error "dayPart is guaranteed to return a single result" 386 | where 387 | dayPart :: SqlExpr (Value UTCTime) -> SqlExpr (Value Int) 388 | dayPart s = unsafeSqlFunction "date_part" (unsafeSqlValue "\'day\'" :: SqlExpr (Value String) ,s) 389 | date :: SqlExpr (Value UTCTime) 390 | date = unsafeSqlValue "TIMESTAMP \'2001-02-16 20:38:40\'" 391 | ``` 392 | 393 | which generates this SQL: 394 | 395 | ```sql 396 | SELECT date_part('day', TIMESTAMP '2001-02-16 20:38:40') 397 | ``` 398 | 399 | Using `unsafeSqlValue` we were required to also define the type of the value. 400 | 401 | Another useful unsafe function is `unsafeSqlCastAs`, which allows us to cast any type 402 | to another within a query. For example, say we want to use our previews `dayPart` function 403 | on the current system time, we could: 404 | 405 | ```haskell 406 | postgresTimestampDay :: (MonadIO m, MonadLogger m) => SqlWriteT m Int 407 | postgresTimestampDay = do 408 | currentTime <- liftIO getCurrentTime 409 | result <- select (return $ dayPart (toTIMESTAMP $ val currentTime)) 410 | case result of 411 | [x] -> pure x 412 | _ -> error "dayPart is guaranteed to return a single result" 413 | where 414 | dayPart :: SqlExpr (Value UTCTime) -> SqlExpr (Value Int) 415 | dayPart s = unsafeSqlFunction "date_part" (unsafeSqlValue "\'day\'" :: SqlExpr (Value String) ,s) 416 | toTIMESTAMP :: SqlExpr (Value UTCTime) -> SqlExpr (Value UTCTime) 417 | toTIMESTAMP = unsafeSqlCastAs "TIMESTAMP" 418 | ``` 419 | 420 | which generates this SQL: 421 | 422 | ```sql 423 | SELECT date_part('day', CAST('2019-10-28 23:19:39.400898344Z' AS TIMESTAMP)) 424 | ``` 425 | 426 | ### SQL injection 427 | 428 | Esqueleto uses parameterization to prevent sql injections on values and arguments 429 | on all queries, for example, if we have: 430 | 431 | ```haskell 432 | myEvilQuery :: (MonadIO m, MonadLogger m) => SqlWriteT m () 433 | myEvilQuery = 434 | select (return $ val ("hi\'; DROP TABLE foo; select \'bye\'" :: String)) >>= liftIO . print 435 | ``` 436 | 437 | which generates this SQL(when using postgres): 438 | 439 | ```sql 440 | SELECT 'hi''; DROP TABLE foo; select ''bye''' 441 | ``` 442 | 443 | And the printed value is `hi\'; DROP TABLE foo; select \'bye\'` and no table is dropped. This is good 444 | and makes the use of strings values safe. Unfortunately this is not the case when using unsafe functions. 445 | Let's see an example of defining a new evil `now` function: 446 | 447 | ```haskell 448 | myEvilQuery :: (MonadIO m, MonadLogger m) => SqlWriteT m () 449 | myEvilQuery = 450 | select (return nowWithInjection) >>= liftIO . print 451 | where 452 | nowWithInjection :: SqlExpr (Value UTCTime) 453 | nowWithInjection = unsafeSqlFunction "0; DROP TABLE bar; select now" ([] :: [SqlExpr (Value Int)]) 454 | ``` 455 | 456 | which generates this SQL: 457 | 458 | ```sql 459 | SELECT 0; DROP TABLE bar; select now() 460 | ``` 461 | 462 | If we were to run the above code we would see the postgres time printed but the table `bar` 463 | will be erased with no indication whatsoever. Another example of this behavior is seen when using 464 | `unsafeSqlValue`: 465 | 466 | ```haskell 467 | myEvilQuery :: (MonadIO m, MonadLogger m) => SqlWriteT m () 468 | myEvilQuery = 469 | select (return $ dayPart dateWithInjection) >>= liftIO . print 470 | where 471 | dayPart :: SqlExpr (Value UTCTime) -> SqlExpr (Value Int) 472 | dayPart s = unsafeSqlFunction "date_part" (unsafeSqlValue "\'day\'" :: SqlExpr (Value String) ,s) 473 | dateWithInjection :: SqlExpr (Value UTCTime) 474 | dateWithInjection = unsafeSqlValue "TIMESTAMP \'2001-02-16 20:38:40\');DROP TABLE bar; select (16" 475 | ``` 476 | 477 | which generates this SQL: 478 | 479 | ```sql 480 | SELECT date_part('day', TIMESTAMP '2001-02-16 20:38:40');DROP TABLE bar; select (16) 481 | ``` 482 | 483 | This will print 16 and also erase the `bar` table. The main take away of this examples is to 484 | never use any user or third party input inside an unsafe function without first parsing it or 485 | heavily sanitizing the input. 486 | 487 | ### Tests 488 | 489 | To run the tests, do `stack test`. This tests all the backends, so you'll need 490 | to have MySQL and Postgresql installed. 491 | 492 | #### Postgres 493 | 494 | Using apt-get, you should be able to do: 495 | 496 | ``` 497 | sudo apt-get install postgresql postgresql-contrib 498 | sudo apt-get install libpq-dev 499 | ``` 500 | 501 | Using homebrew on OSx 502 | 503 | ``` 504 | brew install postgresql 505 | brew install libpq 506 | ``` 507 | 508 | Detailed instructions on the Postgres wiki [here](https://wiki.postgresql.org/wiki/Detailed_installation_guides) 509 | 510 | The connection details are located near the bottom of the [test/PostgreSQL/Test.hs](test/PostgreSQL/Test.hs) file: 511 | 512 | ``` 513 | withConn = 514 | R.runResourceT . withPostgresqlConn "host=localhost port=5432 user=esqutest password=esqutest dbname=esqutest" 515 | ``` 516 | 517 | You can change these if you like but to just get them working set up as follows on linux: 518 | 519 | ``` 520 | $ sudo -u postgres createuser esqutest 521 | $ sudo -u postgres createdb esqutest 522 | $ sudo -u postgres psql 523 | postgres=# \password esqutest 524 | ``` 525 | 526 | And on osx 527 | 528 | ``` 529 | $ createuser esqutest 530 | $ createdb esqutest 531 | $ psql postgres 532 | postgres=# \password esqutest 533 | ``` 534 | 535 | #### MySQL 536 | 537 | To test MySQL, you'll need to have a MySQL server installation. 538 | Then, you'll need to create a database `esqutest` and a `'travis'@'localhost'` 539 | user which can access it: 540 | 541 | ``` 542 | mysql> CREATE DATABASE esqutest; 543 | mysql> CREATE USER 'travis'@'localhost'; 544 | mysql> ALTER USER 'travis'@'localhost' IDENTIFIED BY 'esqutest'; 545 | mysql> GRANT ALL ON esqutest.* TO 'travis'@'localhost'; 546 | ``` 547 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /cabal.project: -------------------------------------------------------------------------------- 1 | -- Generated by stackage-to-hackage 2 | 3 | packages: 4 | ./ 5 | , examples/ 6 | -------------------------------------------------------------------------------- /docs/blog_post_2012_08_06: -------------------------------------------------------------------------------- 1 | Announcing esqueleto, a type-safe ESDL for SQL queries 2 | 3 | I'm very pleased to announce a preview release of esqueleto, a bare bones, type-safe EDSL for SQL queries. 4 | 5 | 6 | 7 | On the first part of this blog post I'll talk about persistent and HaskellDB. You may jump right into where I talk about esqueleto if you want, though. 8 | 9 |

Background

10 | 11 | Yesod is very modular, and each of its components may be used separately. You may also take libraries from other web frameworks. However, it's more convenient to use Yesod with its standard set of libraries. 12 | 13 | One of those libraries is persistent. It fills the role of communicating with the database: serialization and deserialization of data types, insertions, updates, queries, schema migration, and so on. It currently has both SQL and NoSQL backends, such as persistent-mysql, persistent-postgresql, persistent-sqlite and persistent-mongoDB. 14 | 15 |

Persistent's weakness

16 | 17 | Even though persistent's use of Template Haskell has been criticized by some, it's almost consensual that its largest drawback lies on its query API. For example, if you want to see all of John's posts, and you know that John's key on the database is [cci_haskell]johnId[/cci_haskell], then you may write: 18 | 19 | [cc_haskell] 20 | posts <- selectList [BlogPostAuthorId ==. johnId] [] 21 | [/cc_haskell] 22 | 23 | Even though the meaning of those lists is somewhat cryptic, it's not a bad line of code. However, what if you didn't know John's key, just its e-mail (assuming that there is an uniqueness constraint on e-mails)? Unfortunately, with persistent you'll need either two queries: 24 | 25 | [cc_haskell] 26 | Entity johnId john <- getBy (UniqueEmail john'sEmail) 27 | posts <- selectList [BlogPostAuthorId ==. johnId] [] 28 | [/cc_haskell] 29 | 30 | Or you could do it in one query using the ad hoc Database.Persist.Query.Join.Sql module: 31 | 32 | [cc_haskell] 33 | runJoin $ SelectOneMany 34 | { somFilterOne = [ PersonEmail ==. john'sEmail ] 35 | , somOrderOne = [] 36 | , somFilterMany = [] 37 | , somOrderMany = [] 38 | , somFilterKeys = (BlogPostAuthorId <-.) 39 | , somGetKey = blogPostAuthorId 40 | , somIncludeNoMatch = False 41 | } 42 | [/cc_haskell] 43 | 44 | Compare to the equivalent SQL query: 45 | 46 | [cc_sql] 47 | SELECT BlogPost.* 48 | FROM Person INNER JOIN BlogPost 49 | ON Person.id = BlogPost.authorId 50 | WHERE Person.email = ? 51 | [/cc_sql] 52 | 53 | But it gets even worse: you have support only for simple queries (using [cci_haskell]selectList[/cci_haskell]) and for simple one-to-many joins (using the ad hoc [cci_haskell]SelectOneMany[/cci_haskell]). If you need anything else then you're on your own. For instance, there's no support for doing many-to-many joins. 54 | 55 | For these reasons I've created the [cci_haskell]rawSql[/cci_haskell] function 8 month ago. It was nice to be able to write raw SQL queries and still be able to use persistent to deserialize the results, but we still get all the drawbacks of using raw SQL queries: 56 | 57 | 62 | 63 | 64 |

HaskellDB: a solution?

65 | 66 | HaskellDB is a type-safe EDSL that allows you to write SQL queries using relational algebra. It's as old as Parsec, having been introduced in 1999! 67 | 68 | Recently some people have been showing interest in using it with Yesod. Last month Mats Rauhala wrote the following summary about his opinion at the time on Yesod's mailing list: 69 | 70 |
71 | 1. Direct sql 72 | - No, or very little type safety 73 | - No, or very little compile time checks 74 | - Raw queries are ugly 75 | + Full power of SQL 76 | 77 | 2. ORM 78 | + Type safety (ala persistent) 79 | - No control over queries 80 | - No proper join support(?, persistent) 81 | + Abstract 82 | +- High-level 83 | 84 | 3. Relational algebra 85 | + Type safety (ala HaskellDB) 86 | + Great control over queries 87 | + Good control over abstractions 88 | + It's algebra, therefore fits Haskell (tongue in cheek) 89 | - Forgotten (HaskellDB) 90 |
91 | 92 | Although it's rather painful to setup HaskellDB's tables, I'll won't count that as a big drawback since some Template Haskell could certainly solve this shortcoming. Its lack of migration capabilities is not a huge problem, too, since you may use persistent just for the migrations (although I'm not sure if anyone has ever put something like this into production). 93 | 94 | HaskellDB's biggest drawback is being a relational algebra library. "What?", I hear you say. I like relational algebra as much as the next functional programmer, but it comes with two drawbacks, one of them being a major one: 95 | 96 |
    97 |
  1. Being something different than what we're used to means that it takes some time to learn how to use it and get productive. This is a minor drawback, but nevertheless it is a drawback.
  2. 98 |
  3. However, the biggest drawback is that it's very hard to map into efficient SQL. Back in 2008, Geoff Wilson wrote a blog post about HaskellDB's performance. A simple [cci_sql]INNER JOIN[/cci_sql] was taking between 40x and 160x more time to execute when using HaskellDB and comparing against a handwritten SQL query.
  4. 99 |
100 | 101 | While I'm sure that some work has been done on HaskellDB's optimizer since that blog post, Chris Done found out last November that it still isn't very good: 102 | 103 |
104 | Don’t expect good performance from HaskellDB if you’re using MySQL. 105 |
106 | 107 | Even if I started using PostgreSQL, I wouldn't want to rely on its optimizer when doing, say, a join between five tables on HaskellDB. (If you didn't already know, I'm persistent-mysql's author.) 108 | 109 | Please don't get me wrong, HaskellDB is amazing! But it won't work for my production systems. 110 | 111 | 112 | 113 |

Esqueleto rises

114 | 115 | Last Sunday my co-worker was bitten by a nasty bug due to a raw SQL query. He changed an entity's field so that it would be optional. After fixing all type errors, he found out that some parts of the application were not working, but no error messages were to be found anywhere. Turns out an implicit join in a raw SQL query started dropping rows from the result since the value was [cci_sql]NULL[/cci_sql]. After we found the bug, he proceeded to show me Squeryl. My initial thought after seeing the examples was: how could I write this in Haskell? 116 | 117 | Thus esqueleto was born. It's a bare bones, type-safe EDSL for SQL-queries. Like HaskellDB, it has composable, type-checked queries. Unlike HaskellDB, it's not relational algebra, it's SQL. I was inspired by Squeryl but created esqueleto from scratch. 118 | 119 | It sits on top of persistent and requires no further setup: if you're already using persistent then you already have everything it takes to use esqueleto. Although I haven't tested, yet, it should work on any SQL backend. 120 | 121 | Let's remember the first query we did on the beginning of this post: 122 | 123 | [cc_haskell] 124 | selectList [BlogPostAuthorId ==. johnId] [] 125 | [/cc_haskell] 126 | 127 | You could write the same query in plain SQL as: 128 | 129 | [cc_sql] 130 | SELECT BlogPost.* 131 | FROM BlogPost 132 | WHERE BlogPost.authorId = ? 133 | [/cc_sql] 134 | 135 | With esqueleto, you may say: 136 | 137 | [cc_haskell] 138 | select $ 139 | from $ \b -> do 140 | where_ (b ^. BlogPostAuthorId ==. val johnId) 141 | return b 142 | [/cc_haskell] 143 | 144 | Arguably more verbose than persistent's [cci_haskell]selectList[/cci_haskell], but extremely close to the handwriten SQL: just take the [cci_haskell]return[/cci_haskell] and move it to the top! 145 | 146 | Better still, the SQL that esqueleto generates is: 147 | 148 | [cc_sql] 149 | SELECT BlogPost.id, BlogPost.title 150 | FROM BlogPost 151 | WHERE BlogPost.authorId = ? 152 | [/cc_sql] 153 | 154 | I'm not kidding! The only difference from the handwritten query is the explicit list of columns, which is needed for correctness (the order your database returns the columns may not be what persistent expects). 155 | 156 | Remember the one-to-many join? Here's the esqueleto version: 157 | 158 | [cc_haskell] 159 | select $ 160 | from $ \(p `InnerJoin` b) -> do 161 | on (p ^. PersonId ==. b ^. BlogPostAuthorId) 162 | where_ (p ^. PersonEmail ==. val john'sEmail) 163 | return b 164 | [/cc_haskell] 165 | 166 | The SQL esqueleto generates for this query is: 167 | 168 | [cc_sql] 169 | SELECT BlogPost.id, BlogPost.title 170 | FROM Person INNER JOIN BlogPost 171 | ON Person.id = BlogPost.authorId 172 | WHERE Person.email = ? 173 | [/cc_sql] 174 | 175 | Let's take Geoff Wilson's post as an example again. Since this is a preview release of esqueleto, it does not have support for [cci_sql]IN[/cci_sql] yet, so I'll rewrite his code slightly. The HaskellDB code he wrote was: 176 | 177 | [cc_haskell] 178 | query db $ do 179 | s <- table S.stock 180 | e <- table E.end_of_day 181 | restrict (s!S.stock_id .==. e!E.stock_id .&&. 182 | s!S.ticker .==. constant ticker .&&. 183 | e!E.trade_date .==. constant stockDate) 184 | r <- project (closing_price << e!E.closing_price # 185 | trade_date << e!E.trade_date) 186 | return r 187 | [/cc_haskell] 188 | 189 | The SQL that HaskellDB generated on 2008 was (adapted): 190 | 191 | [cc_sql] 192 | SELECT closing_price2 as closing_price, 193 | trade_date2 as trade_date 194 | FROM (SELECT stock_id as stock_id2, 195 | trade_date as trade_date2, 196 | closing_price as closing_price2 197 | FROM end_of_day as T1) as T1, 198 | (SELECT stock_id as stock_id1, 199 | ticker as ticker1 200 | FROM stock as T1) as T2 201 | WHERE stock_id1 = stock_id2 202 | AND ticker1 = 'FXJ' 203 | AND trade_date2 = '2008-02-14 00:00:00'; 204 | [/cc_sql] 205 | 206 | His handwritten SQL was (adapted as well): 207 | 208 | [cc_sql] 209 | SELECT e.closing_price as closing_price, 210 | e.trade_date as trade_date 211 | FROM 212 | stock s, end_of_day e 213 | WHERE 214 | s.stock_id = e.stock_id 215 | AND s.ticker = 'FXJ' 216 | AND e.trade_date = '2008-02-14 00:00:00'; 217 | [/cc_sql] 218 | 219 | The same query could be written using persistent and esqueleto as: 220 | 221 | [cc_haskell] 222 | select $ 223 | from $ \(s, e) -> do 224 | where_ (s ^. StockId ==. e ^. EndOfDayStockId &&. 225 | s ^. StockTicker ==. val ticker &&. 226 | s ^. EndOfDayTradeDate ==. val stockDate) 227 | return (e ^. EndOfDayClosingPrice, e ^. EndOfDayTradeDate) 228 | [/cc_haskell] 229 | 230 | The generated SQL would be: 231 | 232 | [cc_sql] 233 | SELECT end_of_day.closing_price, end_of_day.trade_date 234 | FROM stock, end_of_day 235 | WHERE stock.stock_id = end_of_day.stock_id AND (stock.ticker = ? AND end_of_day.trade_date = ?) 236 | [/cc_sql] 237 | 238 | 239 |

Conclusion

240 | 241 | The full power of raw SQL. Type-checked queries, no type signatures required. Complete control over the resulting SQL. The robustness and performance of persistent. And with only 800 source lines of code (+ 400 SLOC for the test suite). What's not to like about esqueleto? =D 242 | 243 | This is just a preview release. I'm eager to hear what you have to say about it. Send pull requests, open issues, comment about it on reddit or send e-mails to Yesod's mailing list. Or, even better, give it a spin a let me know how it went! Its Haddock documentation should get you started. 244 | 245 | Thanks for reading this rather long blog post! =) 246 | -------------------------------------------------------------------------------- /esqueleto.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 1.12 2 | 3 | name: esqueleto 4 | version: 3.6.0.0 5 | synopsis: Type-safe EDSL for SQL queries on persistent backends. 6 | description: @esqueleto@ is a bare bones, type-safe EDSL for SQL queries that works with unmodified @persistent@ SQL backends. Its language closely resembles SQL, so you don't have to learn new concepts, just new syntax, and it's fairly easy to predict the generated SQL and optimize it for your backend. Most kinds of errors committed when writing SQL are caught as compile-time errors---although it is possible to write type-checked @esqueleto@ queries that fail at runtime. 7 | . 8 | @persistent@ is a library for type-safe data serialization. It has many kinds of backends, such as SQL backends (@persistent-mysql@, @persistent-postgresql@, @persistent-sqlite@) and NoSQL backends (@persistent-mongoDB@). While @persistent@ is a nice library for storing and retrieving records, including with filters, it does not try to support some of the features that are specific to SQL backends. In particular, @esqueleto@ is the recommended library for type-safe @JOIN@s on @persistent@ SQL backends. (The alternative is using raw SQL, but that's error prone and does not offer any composability.) 9 | . 10 | Currently, @SELECT@s, @UPDATE@s, @INSERT@s and @DELETE@s are supported. Not all SQL features are available, but most of them can be easily added (especially functions), so please open an issue or send a pull request if you need anything that is not covered by @esqueleto@ on . 11 | . 12 | The name of this library means \"skeleton\" in Portuguese and contains all three SQL letters in the correct order =). It was inspired by Scala's Squeryl but created from scratch. 13 | category: Database 14 | homepage: https://github.com/bitemyapp/esqueleto 15 | author: Felipe Lessa 16 | maintainer: cma@bitemyapp.com 17 | copyright: (c) 2012-2016 Felipe Almeida Lessa 18 | license: BSD3 19 | license-file: LICENSE 20 | build-type: Simple 21 | extra-source-files: 22 | README.md 23 | changelog.md 24 | 25 | source-repository head 26 | type: git 27 | location: git://github.com/bitemyapp/esqueleto.git 28 | 29 | library 30 | exposed-modules: 31 | Database.Esqueleto 32 | Database.Esqueleto.Legacy 33 | Database.Esqueleto.Experimental 34 | Database.Esqueleto.Internal.Internal 35 | Database.Esqueleto.Internal.ExprParser 36 | Database.Esqueleto.MySQL 37 | Database.Esqueleto.PostgreSQL 38 | Database.Esqueleto.PostgreSQL.JSON 39 | Database.Esqueleto.Record 40 | Database.Esqueleto.SQLite 41 | Database.Esqueleto.Experimental.From 42 | Database.Esqueleto.Experimental.From.CommonTableExpression 43 | Database.Esqueleto.Experimental.From.Join 44 | Database.Esqueleto.Experimental.From.SqlSetOperation 45 | Database.Esqueleto.Experimental.ToAlias 46 | Database.Esqueleto.Experimental.ToAliasReference 47 | Database.Esqueleto.Experimental.ToMaybe 48 | other-modules: 49 | Database.Esqueleto.PostgreSQL.JSON.Instances 50 | Database.Esqueleto.Internal.PersistentImport 51 | Paths_esqueleto 52 | hs-source-dirs: 53 | src/ 54 | build-depends: 55 | base >=4.12 && <5.0 56 | , aeson >=1.0 57 | , attoparsec >= 0.13 && < 0.15 58 | , blaze-html 59 | , bytestring 60 | , conduit >=1.3 61 | , containers 62 | , monad-logger 63 | , persistent >=2.13 && <3 64 | , resourcet >=1.2 65 | , tagged >=0.2 66 | , template-haskell 67 | , text >=0.11 && <2.2 68 | , time >=1.5.0.1 && <=1.13 69 | , transformers >=0.2 70 | , unliftio 71 | , unordered-containers >=0.2 72 | ghc-options: 73 | -Wall 74 | -Wno-redundant-constraints 75 | -Wincomplete-uni-patterns 76 | -Wincomplete-record-updates 77 | -Wpartial-fields 78 | -Wmissing-home-modules 79 | -Widentities 80 | -Wcpp-undef 81 | -Wcpp-undef 82 | default-language: Haskell2010 83 | 84 | test-suite specs 85 | type: exitcode-stdio-1.0 86 | main-is: Spec.hs 87 | other-modules: 88 | Common.Test 89 | Common.Test.CTE 90 | Common.Test.Models 91 | Common.Test.Import 92 | Common.Test.Select 93 | Common.Record 94 | PostgreSQL.MigrateJSON 95 | SQLite.Test 96 | PostgreSQL.Test 97 | MySQL.Test 98 | default-extensions: 99 | RankNTypes 100 | hs-source-dirs: 101 | test 102 | ghc-options: -Wall -threaded 103 | build-depends: 104 | base >=4.8 && <5.0 105 | , aeson 106 | , attoparsec 107 | , blaze-html 108 | , bytestring 109 | , conduit 110 | , containers 111 | , esqueleto 112 | , exceptions 113 | , hspec 114 | , hspec-core 115 | , monad-logger 116 | , mtl 117 | , mysql 118 | , mysql-simple 119 | , persistent 120 | , persistent-mysql 121 | , persistent-postgresql 122 | , persistent-sqlite 123 | , postgresql-simple 124 | , QuickCheck 125 | , resourcet 126 | , tagged 127 | , template-haskell 128 | , text 129 | , time 130 | , transformers 131 | , unliftio 132 | , unordered-containers 133 | default-language: Haskell2010 134 | -------------------------------------------------------------------------------- /esqueleto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitemyapp/esqueleto/7821cbed8952aac76f5bbb9a9ab20815c1831baf/esqueleto.png -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitemyapp/esqueleto/7821cbed8952aac76f5bbb9a9ab20815c1831baf/examples/.gitignore -------------------------------------------------------------------------------- /examples/Blog.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleInstances #-} 2 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 3 | {-# LANGUAGE MultiParamTypeClasses #-} 4 | {-# LANGUAGE StandaloneDeriving #-} 5 | {-# LANGUAGE TypeFamilies #-} 6 | {-# LANGUAGE UndecidableInstances #-} 7 | 8 | module Blog 9 | ( runBlogT 10 | ) where 11 | 12 | import Control.Monad.Base (MonadBase (..)) 13 | import Control.Monad.IO.Unlift (MonadUnliftIO(..), wrappedWithRunInIO) 14 | import Control.Monad.Logger (MonadLoggerIO, MonadLogger, NoLoggingT (..)) 15 | import Control.Monad.Reader 16 | import Control.Monad.Trans.Control (ComposeSt, MonadBaseControl (..), 17 | MonadTransControl (..), 18 | defaultLiftBaseWith, 19 | defaultRestoreM) 20 | import Database.Persist.Postgresql (ConnectionString) 21 | 22 | newtype BlogT m a = BlogT { unBlogT :: NoLoggingT (ReaderT ConnectionString m) a } 23 | deriving ( Functor 24 | , Applicative 25 | , Monad 26 | , MonadLogger 27 | , MonadReader ConnectionString 28 | , MonadIO 29 | , MonadLoggerIO 30 | ) 31 | 32 | instance MonadUnliftIO m => MonadUnliftIO (BlogT m) where 33 | withRunInIO = wrappedWithRunInIO BlogT unBlogT 34 | 35 | instance MonadTrans BlogT where 36 | lift = BlogT . lift . lift 37 | 38 | 39 | deriving instance (MonadBase b m) => MonadBase b (BlogT m) 40 | 41 | 42 | instance MonadBaseControl b m => MonadBaseControl b (BlogT m) where 43 | type StM (BlogT m) a = ComposeSt BlogT m a 44 | liftBaseWith = defaultLiftBaseWith 45 | restoreM = defaultRestoreM 46 | 47 | 48 | instance MonadTransControl BlogT where 49 | type StT BlogT a = StT NoLoggingT (StT (ReaderT ConnectionString) a) 50 | liftWith f = BlogT $ liftWith $ \run -> 51 | liftWith $ \run' -> 52 | f (run' . run . unBlogT) 53 | restoreT = BlogT . restoreT . restoreT 54 | 55 | 56 | runBlogT :: ConnectionString -> BlogT m a -> m a 57 | runBlogT backend (BlogT m) = 58 | runReaderT (runNoLoggingT m) backend 59 | -------------------------------------------------------------------------------- /examples/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Felipe Lessa 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Felipe Lessa nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /examples/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE DerivingStrategies #-} 3 | {-# LANGUAGE FlexibleContexts #-} 4 | {-# LANGUAGE FlexibleInstances #-} 5 | {-# LANGUAGE GADTs #-} 6 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 7 | {-# LANGUAGE MultiParamTypeClasses #-} 8 | {-# LANGUAGE OverloadedStrings #-} 9 | {-# LANGUAGE QuasiQuotes #-} 10 | {-# LANGUAGE ScopedTypeVariables #-} 11 | {-# LANGUAGE StandaloneDeriving #-} 12 | {-# LANGUAGE TemplateHaskell #-} 13 | {-# LANGUAGE TypeApplications #-} 14 | {-# LANGUAGE TypeFamilies #-} 15 | {-# LANGUAGE UndecidableInstances #-} 16 | {-# OPTIONS_GHC -fno-warn-unused-top-binds #-} 17 | 18 | module Main 19 | ( main 20 | ) where 21 | 22 | import Blog 23 | import Control.Monad (void) 24 | import Control.Monad.IO.Class (MonadIO, liftIO) 25 | import Control.Monad.IO.Unlift (MonadUnliftIO) 26 | import Control.Monad.Logger (MonadLogger, MonadLoggerIO) 27 | import Control.Monad.Reader (MonadReader(..), runReaderT) 28 | import Control.Monad.Trans.Control (MonadBaseControl) 29 | import Database.Esqueleto.Experimental 30 | import Database.Persist.Postgresql (ConnectionString, withPostgresqlConn) 31 | import qualified Database.Persist.Sql as Persistent 32 | import Database.Persist.TH 33 | ( mkMigrate 34 | , mkPersist 35 | , persistLowerCase 36 | , share 37 | , sqlSettings 38 | ) 39 | 40 | share [ mkPersist sqlSettings 41 | , mkMigrate "migrateAll"] [persistLowerCase| 42 | Person 43 | name String 44 | age Int Maybe 45 | deriving Eq Show 46 | BlogPost 47 | title String 48 | authorId PersonId OnDeleteCascade 49 | deriving Eq Show 50 | Follow 51 | follower PersonId OnDeleteCascade 52 | followed PersonId OnDeleteCascade 53 | deriving Eq Show 54 | |] 55 | 56 | putPersons :: (MonadIO m, MonadLogger m) 57 | => SqlPersistT m () 58 | putPersons = do 59 | -- | Select all values from the `person` table 60 | people <- select $ from $ table @Person 61 | 62 | -- | entityVal extracts the Person value, which we then extract 63 | -- | the person name from the record and print it 64 | liftIO $ mapM_ (putStrLn . ("Name: " ++) . personName . entityVal) people 65 | 66 | 67 | getJohns :: (MonadIO m, MonadLogger m) 68 | => SqlReadT m [Entity Person] 69 | getJohns = 70 | -- | Select all persons where their name is equal to "John" 71 | select $ do 72 | people <- from $ table @Person 73 | where_ (people ^. PersonName ==. val "John") 74 | return people 75 | 76 | 77 | getAdults :: (MonadIO m, MonadLogger m) 78 | => SqlReadT m [Entity Person] 79 | getAdults = 80 | -- | Select any Person where their age is >= 18 and NOT NULL 81 | select $ do 82 | people <- from $ table @Person 83 | where_ (people ^. PersonAge >=. just (val 18)) 84 | return people 85 | 86 | 87 | getBlogPostsByAuthors :: (MonadIO m, MonadLogger m) 88 | => SqlReadT m [(Entity BlogPost, Entity Person)] 89 | getBlogPostsByAuthors = 90 | -- | Select all persons and their blogposts, ordering by title 91 | select $ do 92 | (people :& blogPosts) <- 93 | from $ table @Person 94 | `innerJoin` table @BlogPost 95 | `on` (\(people :& blogPosts) -> 96 | people ^. PersonId ==. blogPosts ^. BlogPostAuthorId) 97 | orderBy [asc (blogPosts ^. BlogPostTitle)] 98 | pure (blogPosts, people) 99 | 100 | 101 | getAuthorMaybePosts :: (MonadIO m, MonadLogger m) 102 | => SqlReadT m [(Entity Person, Maybe (Entity BlogPost))] 103 | getAuthorMaybePosts = 104 | -- | Select all persons doing a left outer join on blogposts 105 | -- | Since a person may not have any blogposts the BlogPost Entity is wrapped 106 | -- | in a Maybe 107 | select $ do 108 | p :& mb <- from $ 109 | table @Person 110 | `leftJoin` 111 | table @BlogPost 112 | `on` (do 113 | \(p :& mb) -> just (p ^. PersonId) ==. mb ?. BlogPostAuthorId) 114 | orderBy [asc (p ^. PersonName), asc (mb ?. BlogPostTitle)] 115 | return (p, mb) 116 | 117 | 118 | followers :: (MonadIO m, MonadLogger m) 119 | => SqlReadT m [(Entity Person, Entity Follow, Entity Person)] 120 | followers = 121 | -- | Select mutual follow relationships 122 | -- | Note carefully that the order of the ON clauses is reversed! 123 | -- | You're required to write your ons in reverse order because that helps composability 124 | -- | (see the documentation of on for more details). 125 | select $ do 126 | p1 :& f :& p2 <- from $ 127 | table @Person 128 | `innerJoin` 129 | table @Follow 130 | `on` (\(p1 :& f) -> p1 ^. PersonId ==. f ^. FollowFollowed) 131 | `innerJoin` 132 | table @Person 133 | `on` (\(_ :& f :& p2) -> f ^. FollowFollower ==. p2 ^. PersonId) 134 | pure (p1, f, p2) 135 | 136 | -- from $ \(p1 `InnerJoin` f `InnerJoin` p2) -> do 137 | -- on (p2 ^. PersonId ==. f ^. FollowFollowed) 138 | -- on (p1 ^. PersonId ==. f ^. FollowFollower) 139 | -- return (p1, f, p2) 140 | 141 | 142 | updateJoao :: (MonadIO m, MonadLogger m) 143 | => SqlWriteT m () 144 | updateJoao = 145 | -- Update the name of any Joao in our person table to João 146 | update $ \p -> do 147 | set p [ PersonName =. val "João" ] 148 | where_ (p ^. PersonName ==. val "Joao") 149 | 150 | 151 | deleteYoungsters :: (MonadIO m, MonadLogger m) 152 | => SqlPersistT m () 153 | deleteYoungsters = do 154 | -- | Delete any persons under the age of 14 155 | delete $ do 156 | p <- from $ table @Person 157 | where_ (p ^. PersonAge <. just (val 14)) 158 | 159 | 160 | insertBlogPosts :: (MonadIO m, MonadLogger m) 161 | => SqlWriteT m () 162 | insertBlogPosts = 163 | -- | Insert a new blogpost for every person 164 | insertSelect $ do 165 | p <- from $ table @Person 166 | return $ BlogPost <# (val "Group Blog Post") <&> (p ^. PersonId) 167 | 168 | 169 | runDB :: (MonadReader ConnectionString m, 170 | MonadIO m, 171 | MonadBaseControl IO m, 172 | MonadUnliftIO m, 173 | MonadLoggerIO m, 174 | MonadLogger m) 175 | => SqlPersistT m a -> m a 176 | runDB query = do 177 | -- | Helper for running a query 178 | conn <- ask 179 | withPostgresqlConn conn $ \backend -> runReaderT query backend 180 | 181 | 182 | setupDb :: (MonadIO m, MonadLogger m) 183 | => SqlPersistT m () 184 | setupDb = do 185 | -- | Run migrations and create the test database entries 186 | runMigration migrateAll 187 | createDb 188 | where 189 | createDb :: (MonadIO m, MonadLogger m) 190 | => SqlPersistT m () 191 | createDb = do 192 | john <- insert $ Person "John" (Just 24) 193 | sean <- insert $ Person "Seán" (Just 70) 194 | joao <- insert $ Person "Joao" (Just 13) 195 | void $ insertMany [ BlogPost "How to play a bodhrán" sean 196 | , BlogPost "Haskell for the working class hero" john 197 | ] 198 | void $ insert $ Follow john sean 199 | void $ insert $ Follow sean john 200 | void $ insert $ Follow joao sean 201 | 202 | 203 | cleanDb :: (MonadIO m, MonadLogger m) 204 | => SqlPersistT m () 205 | cleanDb = do 206 | -- | Drop the tables so we can re-run the script again if needed 207 | dropTable "follow" 208 | dropTable "blog_post" 209 | dropTable "person" 210 | where 211 | dropTable tableName = rawExecute ("DROP TABLE " <> tableName) [] 212 | 213 | main :: IO () 214 | main = do 215 | -- Connection string for the postrgesql database 216 | runBlogT connection . runDB $ do 217 | setupDb 218 | putPersons 219 | 220 | johns <- getJohns 221 | mapM_ say johns 222 | 223 | adults <- getAdults 224 | mapM_ say adults 225 | 226 | authorBlogPosts <- getBlogPostsByAuthors 227 | mapM_ say authorBlogPosts 228 | 229 | authoMaybePosts <- getAuthorMaybePosts 230 | mapM_ say authoMaybePosts 231 | 232 | mutualFollowers <- followers 233 | mapM_ say mutualFollowers 234 | 235 | updateJoao 236 | deleteYoungsters 237 | insertBlogPosts 238 | cleanDb 239 | where 240 | say :: (MonadIO m, Show a) => a -> m () 241 | say = liftIO . print 242 | connection = "host=localhost port=5432 user=postgres dbname=esqueleto_blog_example password=***" 243 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Esqueleto Examples 2 | 3 | These examples can be build via `stack build`. 4 | -------------------------------------------------------------------------------- /examples/Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /examples/esqueleto-examples.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 1.12 2 | 3 | -- This file has been generated from package.yaml by hpack version 0.35.0. 4 | -- 5 | -- see: https://github.com/sol/hpack 6 | -- 7 | -- hash: ec7b9640e401d9b5f6939c8ac50f7d322b4b00354179825fd41ef4ea92401aaa 8 | 9 | name: esqueleto-examples 10 | version: 0.0.0.0 11 | category: Database 12 | homepage: https://github.com/bitemyapp/esqueleto#readme 13 | bug-reports: https://github.com/bitemyapp/esqueleto/issues 14 | author: Fintan Halpenny 15 | maintainer: cma@bitemyapp.com 16 | copyright: 2019, Chris Allen 17 | license: BSD3 18 | license-file: LICENSE 19 | build-type: Simple 20 | extra-source-files: 21 | README.md 22 | 23 | source-repository head 24 | type: git 25 | location: https://github.com/bitemyapp/esqueleto 26 | 27 | flag werror 28 | description: Treat warnings as errors 29 | manual: True 30 | default: False 31 | 32 | executable blog-example 33 | main-is: Main.hs 34 | other-modules: 35 | Blog 36 | ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N 37 | build-depends: 38 | base 39 | , esqueleto 40 | , monad-control 41 | , monad-logger 42 | , mtl 43 | , persistent >=2.12 44 | , persistent-postgresql 45 | , transformers-base 46 | , unliftio-core 47 | default-language: Haskell2010 48 | if flag(werror) 49 | ghc-options: -Werror 50 | -------------------------------------------------------------------------------- /examples/package.yaml: -------------------------------------------------------------------------------- 1 | name: esqueleto-examples 2 | version: '0.0.0.0' 3 | category: Database 4 | author: Fintan Halpenny 5 | maintainer: cma@bitemyapp.com 6 | copyright: 2019, Chris Allen 7 | license: BSD3 8 | github: bitemyapp/esqueleto 9 | 10 | extra-source-files: 11 | - README.md 12 | 13 | dependencies: 14 | - base 15 | - esqueleto 16 | - persistent >= 2.12 17 | - persistent-postgresql 18 | - mtl 19 | - monad-logger 20 | - monad-control 21 | - transformers-base 22 | - unliftio-core 23 | 24 | ghc-options: 25 | - '-Wall' 26 | - '-threaded' 27 | - '-rtsopts' 28 | - '-with-rtsopts=-N' 29 | 30 | when: 31 | - condition: flag(werror) 32 | ghc-options: '-Werror' 33 | 34 | executables: 35 | blog-example: 36 | other-modules: 37 | - Blog 38 | main: Main.hs 39 | 40 | flags: 41 | werror: 42 | description: "Treat warnings as errors" 43 | manual: true 44 | default: false 45 | -------------------------------------------------------------------------------- /screwdriver.yaml: -------------------------------------------------------------------------------- 1 | workflow: 2 | - esqueleto 3 | 4 | shared: 5 | image: fpco/stack-build 6 | 7 | jobs: 8 | main: 9 | steps: 10 | - stack-setup: stack setup 11 | 12 | esqueleto: 13 | steps: 14 | - build: stack build 15 | - test: stack test 16 | -------------------------------------------------------------------------------- /src/Database/Esqueleto.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE FlexibleInstances #-} 3 | {-# LANGUAGE GADTs #-} 4 | {-# LANGUAGE RankNTypes #-} 5 | 6 | -- | The @esqueleto@ EDSL (embedded domain specific language). 7 | -- This module replaces @Database.Persist@, so instead of 8 | -- importing that module you should just import this one: 9 | -- 10 | -- @ 11 | -- -- For a module using just esqueleto. 12 | -- import Database.Esqueleto 13 | -- @ 14 | -- 15 | -- If you need to use @persistent@'s default support for queries 16 | -- as well, either import it qualified: 17 | -- 18 | -- @ 19 | -- -- For a module that mostly uses esqueleto. 20 | -- import Database.Esqueleto 21 | -- import qualified Database.Persist as P 22 | -- @ 23 | -- 24 | -- or import @esqueleto@ itself qualified: 25 | -- 26 | -- @ 27 | -- -- For a module that uses esqueleto just on some queries. 28 | -- import Database.Persist 29 | -- import qualified Database.Esqueleto as E 30 | -- @ 31 | -- 32 | -- Other than identifier name clashes, @esqueleto@ does not 33 | -- conflict with @persistent@ in any way. 34 | -- 35 | -- Note that the facilities for @JOIN@ have been significantly improved in the 36 | -- "Database.Esqueleto.Experimental" module. The definition of 'from' and 'on' 37 | -- in this module will be replaced with those at the 4.0.0.0 version, so you are 38 | -- encouraged to migrate to the new method. 39 | -- 40 | -- This module has an attached WARNING message indicating that the Experimental 41 | -- syntax will become the default. If you want to continue using the old syntax, 42 | -- please refer to "Database.Esqueleto.Legacy" as a drop-in replacement. 43 | module Database.Esqueleto {-# WARNING "This module will switch over to the Experimental syntax in an upcoming major version release. Please migrate to the Database.Esqueleto.Legacy module to continue using the old syntax, or translate to the new and improved syntax in Database.Esqueleto.Experimental." #-} 44 | ( -- * Setup 45 | -- $setup 46 | 47 | -- * Introduction 48 | -- $introduction 49 | 50 | -- * Getting started 51 | -- $gettingstarted 52 | 53 | -- * @esqueleto@'s Language 54 | where_, on, groupBy, orderBy, asc, desc, limit, offset 55 | , distinct, distinctOn, don, distinctOnOrderBy, having, locking 56 | , (^.), (?.) 57 | , val, isNothing, just, just', nothing, joinV, joinV', withNonNull 58 | , countRows, count, countDistinct 59 | , not_, (==.), (>=.), (>.), (<=.), (<.), (!=.), (&&.), (||.) 60 | , between, (+.), (-.), (/.), (*.) 61 | , round_, ceiling_, floor_ 62 | , min_, max_, sum_, avg_, castNum, castNumM 63 | , coalesce, coalesceDefault 64 | , lower_, upper_, trim_, ltrim_, rtrim_, length_, left_, right_ 65 | , like, ilike, (%), concat_, (++.), castString 66 | , subList_select, valList, justList 67 | , in_, notIn, exists, notExists 68 | , set, (=.), (+=.), (-=.), (*=.), (/=.) 69 | , case_, toBaseId, fromBaseId, toBaseIdMaybe, fromBaseIdMaybe 70 | , subSelect 71 | , subSelectMaybe 72 | , subSelectCount 73 | , subSelectForeign 74 | , subSelectList 75 | , subSelectUnsafe 76 | , ToBaseId(..) 77 | , when_ 78 | , then_ 79 | , else_ 80 | , from 81 | , Value(..) 82 | , ValueList(..) 83 | , OrderBy 84 | , DistinctOn 85 | , LockingKind(..) 86 | , forUpdate 87 | , forUpdateSkipLocked 88 | , LockableEntity(..) 89 | , SqlString 90 | -- ** Joins 91 | , InnerJoin(..) 92 | , CrossJoin(..) 93 | , LeftOuterJoin(..) 94 | , RightOuterJoin(..) 95 | , FullOuterJoin(..) 96 | , JoinKind(..) 97 | , OnClauseWithoutMatchingJoinException(..) 98 | -- * SQL backend 99 | , SqlQuery 100 | , SqlExpr 101 | , SqlEntity 102 | , select 103 | , selectOne 104 | , selectSource 105 | , delete 106 | , deleteCount 107 | , update 108 | , updateCount 109 | , insertSelect 110 | , insertSelectCount 111 | , (<#) 112 | , (<&>) 113 | -- ** Rendering Queries 114 | , renderQueryToText 115 | , renderQuerySelect 116 | , renderQueryUpdate 117 | , renderQueryDelete 118 | , renderQueryInsertInto 119 | -- * Internal.Language 120 | , From 121 | -- * RDBMS-specific modules 122 | -- $rdbmsSpecificModules 123 | 124 | -- * Helpers 125 | , valkey 126 | , valJ 127 | , associateJoin 128 | 129 | -- * Re-exports 130 | -- $reexports 131 | , deleteKey 132 | , module Database.Esqueleto.Internal.PersistentImport 133 | ) where 134 | 135 | import Database.Esqueleto.Legacy 136 | import Database.Esqueleto.Internal.PersistentImport 137 | 138 | 139 | -- $setup 140 | -- 141 | -- If you're already using @persistent@, then you're ready to use 142 | -- @esqueleto@, no further setup is needed. If you're just 143 | -- starting a new project and would like to use @esqueleto@, take 144 | -- a look at @persistent@'s book first 145 | -- () to learn how to 146 | -- define your schema. 147 | 148 | 149 | ---------------------------------------------------------------------- 150 | 151 | 152 | -- $introduction 153 | -- 154 | -- The main goals of @esqueleto@ are to: 155 | -- 156 | -- * Be easily translatable to SQL. When you take a look at a 157 | -- @esqueleto@ query, you should be able to know exactly how 158 | -- the SQL query will end up. (As opposed to being a 159 | -- relational algebra EDSL such as HaskellDB, which is 160 | -- non-trivial to translate into SQL.) 161 | -- 162 | -- * Support the most widely used SQL features. We'd like you to be 163 | -- able to use @esqueleto@ for all of your queries, no 164 | -- exceptions. Send a pull request or open an issue on our 165 | -- project page () if 166 | -- there's anything missing that you'd like to see. 167 | -- 168 | -- * Be as type-safe as possible. We strive to provide as many 169 | -- type checks as possible. If you get bitten by some invalid 170 | -- code that type-checks, please open an issue on our project 171 | -- page so we can take a look. 172 | -- 173 | -- However, it is /not/ a goal to be able to write portable SQL. 174 | -- We do not try to hide the differences between DBMSs from you, 175 | -- and @esqueleto@ code that works for one database may not work 176 | -- on another. This is a compromise we have to make in order to 177 | -- give you as much control over the raw SQL as possible without 178 | -- losing too much convenience. This also means that you may 179 | -- type-check a query that doesn't work on your DBMS. 180 | 181 | 182 | ---------------------------------------------------------------------- 183 | 184 | 185 | -- $gettingstarted 186 | -- 187 | -- We like clean, easy-to-read EDSLs. However, in order to 188 | -- achieve this goal we've used a lot of type hackery, leading to 189 | -- some hard-to-read type signatures. On this section, we'll try 190 | -- to build some intuition about the syntax. 191 | -- 192 | -- For the following examples, we'll use this example schema: 193 | -- 194 | -- @ 195 | -- share [mkPersist sqlSettings, mkMigrate \"migrateAll\"] [persist| 196 | -- Person 197 | -- name String 198 | -- age Int Maybe 199 | -- deriving Eq Show 200 | -- BlogPost 201 | -- title String 202 | -- authorId PersonId 203 | -- deriving Eq Show 204 | -- Follow 205 | -- follower PersonId 206 | -- followed PersonId 207 | -- deriving Eq Show 208 | -- |] 209 | -- @ 210 | -- 211 | -- Most of @esqueleto@ was created with @SELECT@ statements in 212 | -- mind, not only because they're the most common but also 213 | -- because they're the most complex kind of statement. The most 214 | -- simple kind of @SELECT@ would be: 215 | -- 216 | -- @ 217 | -- SELECT * 218 | -- FROM Person 219 | -- @ 220 | -- 221 | -- In @esqueleto@, we may write the same query above as: 222 | -- 223 | -- @ 224 | -- do people <- 'select' $ 225 | -- 'from' $ \\person -> do 226 | -- return person 227 | -- liftIO $ mapM_ (putStrLn . personName . entityVal) people 228 | -- @ 229 | -- 230 | -- The expression above has type @SqlPersist m ()@, while 231 | -- @people@ has type @[Entity Person]@. The query above will be 232 | -- translated into exactly the same query we wrote manually, but 233 | -- instead of @SELECT *@ it will list all entity fields (using 234 | -- @*@ is not robust). Note that @esqueleto@ knows that we want 235 | -- an @Entity Person@ just because of the @personName@ that we're 236 | -- printing later. 237 | -- 238 | -- However, most of the time we need to filter our queries using 239 | -- @WHERE@. For example: 240 | -- 241 | -- @ 242 | -- SELECT * 243 | -- FROM Person 244 | -- WHERE Person.name = \"John\" 245 | -- @ 246 | -- 247 | -- In @esqueleto@, we may write the same query above as: 248 | -- 249 | -- @ 250 | -- 'select' $ 251 | -- 'from' $ \\p -> do 252 | -- 'where_' (p '^.' PersonName '==.' 'val' \"John\") 253 | -- return p 254 | -- @ 255 | -- 256 | -- Although @esqueleto@'s code is a bit more noisy, it's has 257 | -- almost the same structure (save from the @return@). The 258 | -- @('^.')@ operator is used to project a field from an entity. 259 | -- The field name is the same one generated by @persistent@'s 260 | -- Template Haskell functions. We use 'val' to lift a constant 261 | -- Haskell value into the SQL query. 262 | -- 263 | -- Another example would be: 264 | -- 265 | -- @ 266 | -- SELECT * 267 | -- FROM Person 268 | -- WHERE Person.age >= 18 269 | -- @ 270 | -- 271 | -- In @esqueleto@, we may write the same query above as: 272 | -- 273 | -- @ 274 | -- 'select' $ 275 | -- 'from' $ \\p -> do 276 | -- 'where_' (p '^.' PersonAge '>=.' 'just' ('val' 18)) 277 | -- return p 278 | -- @ 279 | -- 280 | -- Since @age@ is an optional @Person@ field, we use 'just' to lift 281 | -- @'val' 18 :: SqlExpr (Value Int)@ into @just ('val' 18) :: 282 | -- SqlExpr (Value (Maybe Int))@. 283 | -- 284 | -- Implicit joins are represented by tuples. For example, to get 285 | -- the list of all blog posts and their authors, we could write: 286 | -- 287 | -- @ 288 | -- SELECT BlogPost.*, Person.* 289 | -- FROM BlogPost, Person 290 | -- WHERE BlogPost.authorId = Person.id 291 | -- ORDER BY BlogPost.title ASC 292 | -- @ 293 | -- 294 | -- In @esqueleto@, we may write the same query above as: 295 | -- 296 | -- @ 297 | -- 'select' $ 298 | -- 'from' $ \\(b, p) -> do 299 | -- 'where_' (b '^.' BlogPostAuthorId '==.' p '^.' PersonId) 300 | -- 'orderBy' ['asc' (b '^.' BlogPostTitle)] 301 | -- return (b, p) 302 | -- @ 303 | -- 304 | -- However, you may want your results to include people who don't 305 | -- have any blog posts as well using a @LEFT OUTER JOIN@: 306 | -- 307 | -- @ 308 | -- SELECT Person.*, BlogPost.* 309 | -- FROM Person LEFT OUTER JOIN BlogPost 310 | -- ON Person.id = BlogPost.authorId 311 | -- ORDER BY Person.name ASC, BlogPost.title ASC 312 | -- @ 313 | -- 314 | -- In @esqueleto@, we may write the same query above as: 315 | -- 316 | -- @ 317 | -- 'select' $ 318 | -- 'from' $ \\(p `'LeftOuterJoin`` mb) -> do 319 | -- 'on' ('just' (p '^.' PersonId) '==.' mb '?.' BlogPostAuthorId) 320 | -- 'orderBy' ['asc' (p '^.' PersonName), 'asc' (mb '?.' BlogPostTitle)] 321 | -- return (p, mb) 322 | -- @ 323 | -- 324 | -- On a @LEFT OUTER JOIN@ the entity on the right hand side may 325 | -- not exist (i.e. there may be a @Person@ without any 326 | -- @BlogPost@s), so while @p :: SqlExpr (Entity Person)@, we have 327 | -- @mb :: SqlExpr (Maybe (Entity BlogPost))@. The whole 328 | -- expression above has type @SqlPersist m [(Entity Person, Maybe 329 | -- (Entity BlogPost))]@. Instead of using @(^.)@, we used 330 | -- @('?.')@ to project a field from a @Maybe (Entity a)@. 331 | -- 332 | -- We are by no means limited to joins of two tables, nor by 333 | -- joins of different tables. For example, we may want a list 334 | -- of the @Follow@ entity: 335 | -- 336 | -- @ 337 | -- SELECT P1.*, Follow.*, P2.* 338 | -- FROM Person AS P1 339 | -- INNER JOIN Follow ON P1.id = Follow.follower 340 | -- INNER JOIN Person AS P2 ON P2.id = Follow.followed 341 | -- @ 342 | -- 343 | -- In @esqueleto@, we may write the same query above as: 344 | -- 345 | -- @ 346 | -- 'select' $ 347 | -- 'from' $ \\(p1 `'InnerJoin`` f `'InnerJoin`` p2) -> do 348 | -- 'on' (p1 '^.' PersonId '==.' f '^.' FollowFollower) 349 | -- 'on' (p2 '^.' PersonId '==.' f '^.' FollowFollowed) 350 | -- return (p1, f, p2) 351 | -- @ 352 | -- 353 | -- We also currently support @UPDATE@ and @DELETE@ statements. 354 | -- For example: 355 | -- 356 | -- @ 357 | -- do 'update' $ \\p -> do 358 | -- 'set' p [ PersonName '=.' 'val' \"João\" ] 359 | -- 'where_' (p '^.' PersonName '==.' 'val' \"Joao\") 360 | -- 'delete' $ 361 | -- 'from' $ \\p -> do 362 | -- 'where_' (p '^.' PersonAge '<.' 'just' ('val' 14)) 363 | -- @ 364 | -- 365 | -- The results of queries can also be used for insertions. 366 | -- In @SQL@, we might write the following, inserting a new blog 367 | -- post for every user: 368 | -- 369 | -- @ 370 | -- INSERT INTO BlogPost 371 | -- SELECT ('Group Blog Post', id) 372 | -- FROM Person 373 | -- @ 374 | -- 375 | -- In @esqueleto@, we may write the same query above as: 376 | -- 377 | -- @ 378 | -- 'insertSelect' $ 'from' $ \\p-> 379 | -- return $ BlogPost '<#' \"Group Blog Post\" '<&>' (p '^.' PersonId) 380 | -- @ 381 | -- 382 | -- Individual insertions can be performed through Persistent's 383 | -- 'insert' function, reexported for convenience. 384 | 385 | 386 | ---------------------------------------------------------------------- 387 | 388 | 389 | -- $reexports 390 | -- 391 | -- We re-export many symbols from @persistent@ for convenince: 392 | -- 393 | -- * \"Store functions\" from "Database.Persist". 394 | -- 395 | -- * Everything from "Database.Persist.Class" except for 396 | -- @PersistQuery@ and @delete@ (use 'deleteKey' instead). 397 | -- 398 | -- * Everything from "Database.Persist.Types" except for 399 | -- @Update@, @SelectOpt@, @BackendSpecificFilter@ and @Filter@. 400 | -- 401 | -- * Everything from "Database.Persist.Sql" except for 402 | -- @deleteWhereCount@ and @updateWhereCount@. 403 | 404 | 405 | ---------------------------------------------------------------------- 406 | 407 | 408 | -- $rdbmsSpecificModules 409 | -- 410 | -- There are many differences between SQL syntax and functions 411 | -- supported by different RDBMSs. Since version 2.2.8, 412 | -- @esqueleto@ includes modules containing functions that are 413 | -- specific to a given RDBMS. 414 | -- 415 | -- * PostgreSQL: "Database.Esqueleto.PostgreSQL". 416 | -- 417 | -- In order to use these functions, you need to explicitly import 418 | -- their corresponding modules, they're not re-exported here. 419 | -------------------------------------------------------------------------------- /src/Database/Esqueleto/Experimental.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE PatternSynonyms #-} 2 | 3 | -- | This module contains a new way (introduced in 3.3.3.0) of using @FROM@ in 4 | -- Haskell. The old method was a bit finicky and could permit runtime errors, 5 | -- and this new way is both significantly safer and much more powerful. 6 | -- 7 | -- This syntax will become the default syntax exported from the library in 8 | -- version @3.6.0.0@. To use the old syntax, see "Database.Esqueleto.Legacy". 9 | module Database.Esqueleto.Experimental 10 | ( -- * Setup 11 | -- $setup 12 | 13 | -- * Introduction 14 | -- $introduction 15 | 16 | -- * A New Syntax 17 | -- $new-syntax 18 | 19 | -- * Documentation 20 | 21 | -- ** Basic Queries 22 | from 23 | , table 24 | , Table(..) 25 | , selectQuery 26 | 27 | -- ** Joins 28 | , (:&)(..) 29 | , on 30 | , innerJoin 31 | , innerJoinLateral 32 | , leftJoin 33 | , leftJoinLateral 34 | , rightJoin 35 | , fullOuterJoin 36 | , crossJoin 37 | , crossJoinLateral 38 | 39 | -- ** Set Operations 40 | -- $sql-set-operations 41 | , union_ 42 | , unionAll_ 43 | , except_ 44 | , intersect_ 45 | 46 | -- ** Common Table Expressions 47 | , with 48 | , withRecursive 49 | 50 | -- ** Internals 51 | , From(..) 52 | , ToMaybe(..) 53 | , ToAlias(..) 54 | , ToAliasReference(..) 55 | , ToSqlSetOperation(..) 56 | , SqlSelect 57 | , Nullable 58 | 59 | -- * The Normal Stuff 60 | , where_ 61 | , groupBy 62 | , groupBy_ 63 | , orderBy 64 | , asc 65 | , desc 66 | , limit 67 | , offset 68 | 69 | , distinct 70 | , distinctOn 71 | , don 72 | , distinctOnOrderBy 73 | , having 74 | , locking 75 | , forUpdate 76 | , forUpdateSkipLocked 77 | 78 | , (^.) 79 | , (?.) 80 | 81 | , val 82 | , isNothing 83 | , isNothing_ 84 | , just 85 | , nothing 86 | , joinV 87 | , withNonNull 88 | 89 | , countRows 90 | , count 91 | , countDistinct 92 | 93 | , not_ 94 | , (==.) 95 | , (>=.) 96 | , (>.) 97 | , (<=.) 98 | , (<.) 99 | , (!=.) 100 | , (&&.) 101 | , (||.) 102 | 103 | , between 104 | , (+.) 105 | , (-.) 106 | , (/.) 107 | , (*.) 108 | 109 | , round_ 110 | , ceiling_ 111 | , floor_ 112 | 113 | , min_ 114 | , max_ 115 | , sum_ 116 | , avg_ 117 | , castNum 118 | , castNumM 119 | 120 | , coalesce 121 | , coalesceDefault 122 | 123 | , lower_ 124 | , upper_ 125 | , trim_ 126 | , ltrim_ 127 | , rtrim_ 128 | , length_ 129 | , left_ 130 | , right_ 131 | 132 | , like 133 | , ilike 134 | , (%) 135 | , concat_ 136 | , (++.) 137 | , castString 138 | 139 | , subList_select 140 | , valList 141 | , justList 142 | 143 | , in_ 144 | , notIn 145 | , exists 146 | , notExists 147 | 148 | , set 149 | , (=.) 150 | , (+=.) 151 | , (-=.) 152 | , (*=.) 153 | , (/=.) 154 | 155 | , case_ 156 | , toBaseId 157 | , toBaseIdMaybe 158 | , fromBaseId 159 | , fromBaseIdMaybe 160 | , subSelect 161 | , subSelectMaybe 162 | , subSelectCount 163 | , subSelectForeign 164 | , subSelectList 165 | , subSelectUnsafe 166 | , ToBaseId(..) 167 | , when_ 168 | , then_ 169 | , else_ 170 | , Value(..) 171 | , ValueList(..) 172 | , OrderBy 173 | , DistinctOn 174 | , LockingKind(..) 175 | , LockableEntity(..) 176 | , SqlString 177 | 178 | -- ** Joins 179 | , InnerJoin(..) 180 | , CrossJoin(..) 181 | , LeftOuterJoin(..) 182 | , RightOuterJoin(..) 183 | , FullOuterJoin(..) 184 | , JoinKind(..) 185 | , OnClauseWithoutMatchingJoinException(..) 186 | -- *** Join Helpers 187 | , getTable 188 | , getTableMaybe 189 | , GetFirstTable(..) 190 | 191 | -- ** SQL backend 192 | , SqlQuery 193 | , SqlExpr 194 | , SqlEntity 195 | , select 196 | , selectOne 197 | , selectSource 198 | , delete 199 | , deleteCount 200 | , update 201 | , updateCount 202 | , insertSelect 203 | , insertSelectCount 204 | , (<#) 205 | , (<&>) 206 | 207 | -- ** Rendering Queries 208 | , renderQueryToText 209 | , renderQuerySelect 210 | , renderQueryUpdate 211 | , renderQueryDelete 212 | , renderQueryInsertInto 213 | 214 | -- ** Helpers 215 | , valkey 216 | , valJ 217 | , associateJoin 218 | 219 | -- ** Re-exports 220 | -- $reexports 221 | , deleteKey 222 | , module Database.Esqueleto.Internal.PersistentImport 223 | ) where 224 | 225 | import Database.Esqueleto.Internal.Internal hiding (From, from, on) 226 | import Database.Esqueleto.Internal.PersistentImport 227 | 228 | import Database.Esqueleto.Experimental.From 229 | import Database.Esqueleto.Experimental.From.CommonTableExpression 230 | import Database.Esqueleto.Experimental.From.Join 231 | import Database.Esqueleto.Experimental.From.SqlSetOperation 232 | import Database.Esqueleto.Experimental.ToAlias 233 | import Database.Esqueleto.Experimental.ToAliasReference 234 | import Database.Esqueleto.Experimental.ToMaybe 235 | 236 | -- $setup 237 | -- 238 | -- If you're already using "Database.Esqueleto", then you can get 239 | -- started using this module just by changing your imports slightly, 240 | -- as well as enabling the [TypeApplications](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#extension-TypeApplications) extension. 241 | -- 242 | -- @ 243 | -- {-\# LANGUAGE TypeApplications \#-} 244 | -- 245 | -- ... 246 | -- 247 | -- import Database.Esqueleto.Experimental 248 | -- @ 249 | -- 250 | -- Note: Prior to @esqueleto-3.3.4.0@, the @Database.Esqueleto.Experimental@ 251 | -- module did not reexport @Data.Esqueleto@. 252 | 253 | ---------------------------------------------------------------------- 254 | 255 | -- $introduction 256 | -- 257 | -- This module is fully backwards-compatible extension to the @esqueleto@ 258 | -- EDSL that expands subquery functionality and enables 259 | -- [SQL set operations](https://en.wikipedia.org/wiki/Set_operations_(SQL\)) 260 | -- to be written directly in Haskell. Specifically, this enables: 261 | -- 262 | -- * Subqueries in 'JOIN' statements 263 | -- * 'UNION' 264 | -- * 'UNION' 'ALL' 265 | -- * 'INTERSECT' 266 | -- * 'EXCEPT' 267 | -- 268 | -- As a consequence of this, several classes of runtime errors are now 269 | -- caught at compile time. This includes missing 'on' clauses and improper 270 | -- handling of @Maybe@ values in outer joins. 271 | -- 272 | -- This module can be used in conjunction with the main "Database.Esqueleto" 273 | -- module, but doing so requires qualified imports to avoid ambiguous 274 | -- definitions of 'on' and 'from', which are defined in both modules. 275 | -- 276 | -- Below we will give an overview of how to use this module and the 277 | -- features it enables. 278 | 279 | ---------------------------------------------------------------------- 280 | 281 | -- $new-syntax 282 | -- 283 | -- This module introduces a new syntax that serves to enable the aforementioned 284 | -- features. This new syntax also changes how joins written in the @esqueleto@ 285 | -- EDSL to more closely resemble the underlying SQL. 286 | -- 287 | -- For our examples, we'll use a schema similar to the one in the Getting Started 288 | -- section of "Database.Esqueleto": 289 | -- 290 | -- @ 291 | -- share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist| 292 | -- Person 293 | -- name String 294 | -- age Int Maybe 295 | -- deriving Eq Show 296 | -- BlogPost 297 | -- title String 298 | -- authorId PersonId 299 | -- deriving Eq Show 300 | -- Follow 301 | -- follower PersonId 302 | -- followed PersonId 303 | -- deriving Eq Show 304 | -- |] 305 | -- @ 306 | -- 307 | -- === Example 1: Simple select 308 | -- 309 | -- Let's select all people who are named \"John\". 310 | -- 311 | -- ==== "Database.Esqueleto": 312 | -- 313 | -- @ 314 | -- select $ 315 | -- from $ \\people -> do 316 | -- where_ (people ^. PersonName ==. val \"John\") 317 | -- pure people 318 | -- @ 319 | -- 320 | -- ==== "Database.Esqueleto.Experimental": 321 | -- 322 | -- @ 323 | -- select $ do 324 | -- people <- from $ table \@Person 325 | -- where_ (people ^. PersonName ==. val \"John\") 326 | -- pure people 327 | -- @ 328 | -- 329 | -- 330 | -- === Example 2: Select with join 331 | -- 332 | -- Let's select all people and their blog posts who are over 333 | -- the age of 18. 334 | -- 335 | -- ==== "Database.Esqueleto": 336 | -- 337 | -- @ 338 | -- select $ 339 | -- from $ \\(people \`LeftOuterJoin\` blogPosts) -> do 340 | -- on (just (people ^. PersonId) ==. blogPosts ?. BlogPostAuthorId) 341 | -- where_ (people ^. PersonAge >. just (val 18)) 342 | -- pure (people, blogPosts) 343 | -- @ 344 | -- 345 | -- ==== "Database.Esqueleto.Experimental": 346 | -- 347 | -- Here we use the ':&' operator to pattern match against the joined tables. 348 | -- 349 | -- @ 350 | -- select $ do 351 | -- (people :& blogPosts) <- 352 | -- from $ table \@Person 353 | -- \`leftJoin\` table \@BlogPost 354 | -- \`on\` (\\(people :& blogPosts) -> 355 | -- just (people ^. PersonId) ==. blogPosts ?. BlogPostAuthorId) 356 | -- where_ (people ^. PersonAge >. just (val 18)) 357 | -- pure (people, blogPosts) 358 | -- @ 359 | -- 360 | -- === Example 3: Select with multi-table join 361 | -- 362 | -- Let's select all people who follow a person named \"John\", including 363 | -- the name of each follower. 364 | -- 365 | -- ==== "Database.Esqueleto": 366 | -- 367 | -- @ 368 | -- select $ 369 | -- from $ \\( 370 | -- people1 371 | -- \`InnerJoin\` followers 372 | -- \`InnerJoin\` people2 373 | -- ) -> do 374 | -- on (people1 ^. PersonId ==. followers ^. FollowFollowed) 375 | -- on (followers ^. FollowFollower ==. people2 ^. PersonId) 376 | -- where_ (people1 ^. PersonName ==. val \"John\") 377 | -- pure (followers, people2) 378 | -- @ 379 | -- 380 | -- ==== "Database.Esqueleto.Experimental": 381 | -- 382 | -- In this version, with each successive 'on' clause, only the tables 383 | -- we have already joined into are in scope, so we must pattern match 384 | -- accordingly. In this case, in the second 'innerJoin', we do not use 385 | -- the first `Person` reference, so we use @_@ as a placeholder to 386 | -- ignore it. This prevents a possible runtime error where a table 387 | -- is referenced before it appears in the sequence of 'JOIN's. 388 | -- 389 | -- @ 390 | -- select $ do 391 | -- (people1 :& followers :& people2) <- 392 | -- from $ table \@Person 393 | -- \`innerJoin` table \@Follow 394 | -- \`on\` (\\(people1 :& followers) -> 395 | -- people1 ^. PersonId ==. followers ^. FollowFollowed) 396 | -- \`innerJoin` table \@Person 397 | -- \`on\` (\\(_ :& followers :& people2) -> 398 | -- followers ^. FollowFollower ==. people2 ^. PersonId) 399 | -- where_ (people1 ^. PersonName ==. val \"John\") 400 | -- pure (followers, people2) 401 | -- @ 402 | -- 403 | -- === Example 4: Counting results of a subquery 404 | -- 405 | -- Let's count the number of people who have posted at least 10 posts 406 | -- 407 | -- ==== "Database.Esqueleto": 408 | -- 409 | -- @ 410 | -- select $ pure $ subSelectCount $ 411 | -- from $ \\( 412 | -- people 413 | -- \`InnerJoin\` blogPosts 414 | -- ) -> do 415 | -- on (people ^. PersonId ==. blogPosts ^. BlogPostAuthorId) 416 | -- groupBy (people ^. PersonId) 417 | -- having ((count $ blogPosts ^. BlogPostId) >. val 10) 418 | -- pure people 419 | -- @ 420 | -- 421 | -- ==== "Database.Esqueleto.Experimental": 422 | -- 423 | -- @ 424 | -- select $ do 425 | -- peopleWithPosts <- 426 | -- from $ do 427 | -- (people :& blogPosts) <- 428 | -- from $ table \@Person 429 | -- \`innerJoin\` table \@BlogPost 430 | -- \`on\` (\\(p :& bP) -> 431 | -- p ^. PersonId ==. bP ^. BlogPostAuthorId) 432 | -- groupBy (people ^. PersonId) 433 | -- having ((count $ blogPosts ^. BlogPostId) >. val 10) 434 | -- pure people 435 | -- pure $ count (peopleWithPosts ^. PersonId) 436 | -- @ 437 | -- 438 | -- We now have the ability to refactor this 439 | -- 440 | -- === Example 5: Sorting the results of a UNION with limits 441 | -- 442 | -- Out of all of the posts created by a person and the people they follow, 443 | -- generate a list of the first 25 posts, sorted alphabetically. 444 | -- 445 | -- ==== "Database.Esqueleto": 446 | -- 447 | -- Since 'UNION' is not supported, this requires using `Database.Esqueleto.rawSql`. (Not shown) 448 | -- 449 | -- ==== "Database.Esqueleto.Experimental": 450 | -- 451 | -- Since this module supports all set operations (see `SqlSetOperation`), we can use 452 | -- `Union` to write this query. 453 | -- 454 | -- @ 455 | -- select $ do 456 | -- (authors, blogPosts) <- from $ 457 | -- (do 458 | -- (author :& blogPost) <- 459 | -- from $ table \@Person 460 | -- \`innerJoin\` table \@BlogPost 461 | -- \`on\` (\\(a :& bP) -> 462 | -- a ^. PersonId ==. bP ^. BlogPostAuthorId) 463 | -- where_ (author ^. PersonId ==. val currentPersonId) 464 | -- pure (author, blogPost) 465 | -- ) 466 | -- \`union_\` 467 | -- (do 468 | -- (follow :& blogPost :& author) <- 469 | -- from $ table \@Follow 470 | -- \`innerJoin\` table \@BlogPost 471 | -- \`on\` (\\(f :& bP) -> 472 | -- f ^. FollowFollowed ==. bP ^. BlogPostAuthorId) 473 | -- \`innerJoin\` table \@Person 474 | -- \`on\` (\\(_ :& bP :& a) -> 475 | -- bP ^. BlogPostAuthorId ==. a ^. PersonId) 476 | -- where_ (follow ^. FollowFollower ==. val currentPersonId) 477 | -- pure (author, blogPost) 478 | -- ) 479 | -- orderBy [ asc (blogPosts ^. BlogPostTitle) ] 480 | -- limit 25 481 | -- pure (authors, blogPosts) 482 | -- @ 483 | -- 484 | -- === Example 6: LATERAL JOIN 485 | -- 486 | -- As of version @3.4.0.0@, lateral subquery joins are supported. 487 | -- 488 | -- 489 | -- @ 490 | -- select $ do 491 | -- (salesPerson :& maxSaleAmount :& maxSaleCustomerName) <- 492 | -- from $ table \@SalesPerson 493 | -- \`crossJoinLateral\` (\\salesPerson -> do 494 | -- sales <- from $ table \@Sale 495 | -- where_ $ sales ^. SaleSalesPersonId ==. salesPerson ^. SalesPersonId 496 | -- pure $ max_ (sales ^. SaleAmount) 497 | -- ) 498 | -- \`crossJoinLateral\` (\\(salesPerson :& maxSaleAmount) -> do 499 | -- sales <- from $ table \@Sale 500 | -- where_ $ sales ^. SaleSalesPersonId ==. salesPerson ^. SalesPersonId 501 | -- &&. sales ^. SaleAmount ==. maxSaleAmount 502 | -- pure $ sales ^. SaleCustomerName) 503 | -- ) 504 | -- pure (salesPerson ^. SalesPersonName, maxSaleAmount, maxSaleCustomerName) 505 | -- @ 506 | -- 507 | -- This is the equivalent to the following SQL (example taken from the 508 | -- [MySQL Lateral Derived Table](https://dev.mysql.com/doc/refman/8.0/en/lateral-derived-tables.html) 509 | -- documentation): 510 | -- 511 | -- @ 512 | -- SELECT 513 | -- salesperson.name, 514 | -- max_sale.amount, 515 | -- max_sale_customer.customer_name 516 | -- FROM 517 | -- salesperson, 518 | -- -- calculate maximum size, cache it in transient derived table max_sale 519 | -- LATERAL 520 | -- (SELECT MAX(amount) AS amount 521 | -- FROM all_sales 522 | -- WHERE all_sales.salesperson_id = salesperson.id) 523 | -- AS max_sale, 524 | -- LATERAL 525 | -- (SELECT customer_name 526 | -- FROM all_sales 527 | -- WHERE all_sales.salesperson_id = salesperson.id 528 | -- AND all_sales.amount = 529 | -- -- the cached maximum size 530 | -- max_sale.amount) 531 | -- AS max_sale_customer; 532 | -- @ 533 | 534 | -- $sql-set-operations 535 | -- 536 | -- Data type that represents SQL set operations. This includes 537 | -- 'UNION', 'UNION' 'ALL', 'EXCEPT', and 'INTERSECT'. These types form 538 | -- a binary tree, with @SqlQuery@ values on the leaves. 539 | -- 540 | -- Each function corresponding to the aforementioned set operations 541 | -- can be used as an infix in a @from@ to help with readability 542 | -- and lead to code that closely resembles the underlying SQL. For example, 543 | -- 544 | -- @ 545 | -- select $ from $ 546 | -- (do 547 | -- a <- from $ table @A 548 | -- pure $ a ^. ASomeCol 549 | -- ) 550 | -- \`union_\` 551 | -- (do 552 | -- b <- from $ table @B 553 | -- pure $ b ^. BSomeCol 554 | -- ) 555 | -- @ 556 | -- 557 | -- is translated into 558 | -- 559 | -- @ 560 | -- SELECT * FROM ( 561 | -- (SELECT a.some_col FROM a) 562 | -- UNION 563 | -- (SELECT b.some_col FROM b) 564 | -- ) 565 | -- @ 566 | -- 567 | -------------------------------------------------------------------------------- /src/Database/Esqueleto/Experimental/From.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | {-# LANGUAGE DataKinds #-} 3 | {-# LANGUAGE DerivingStrategies #-} 4 | {-# LANGUAGE FlexibleContexts #-} 5 | {-# LANGUAGE FlexibleInstances #-} 6 | {-# LANGUAGE FunctionalDependencies #-} 7 | {-# LANGUAGE GADTs #-} 8 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 9 | {-# LANGUAGE MultiParamTypeClasses #-} 10 | {-# LANGUAGE OverloadedStrings #-} 11 | {-# LANGUAGE PatternSynonyms #-} 12 | {-# LANGUAGE ScopedTypeVariables #-} 13 | {-# LANGUAGE TypeApplications #-} 14 | {-# LANGUAGE TypeFamilies #-} 15 | {-# LANGUAGE TypeOperators #-} 16 | {-# LANGUAGE UndecidableInstances #-} 17 | 18 | module Database.Esqueleto.Experimental.From 19 | where 20 | 21 | import qualified Control.Monad.Trans.Writer as W 22 | import Data.Coerce (coerce) 23 | import Data.Proxy 24 | import qualified Data.Text.Lazy.Builder as TLB 25 | import Database.Esqueleto.Experimental.ToAlias 26 | import Database.Esqueleto.Experimental.ToAliasReference 27 | import Database.Esqueleto.Internal.Internal hiding (From(..), from, on) 28 | import Database.Esqueleto.Internal.PersistentImport 29 | 30 | -- | 'FROM' clause, used to bring entities into scope. 31 | -- 32 | -- Internally, this function uses the `From` datatype. 33 | -- Unlike the old `Database.Esqueleto.from`, this does not 34 | -- take a function as a parameter, but rather a value that 35 | -- represents a 'JOIN' tree constructed out of instances of `From`. 36 | -- This implementation eliminates certain 37 | -- types of runtime errors by preventing the construction of 38 | -- invalid SQL (e.g. illegal nested-@from@). 39 | from :: ToFrom a a' => a -> SqlQuery a' 40 | from f = do 41 | (a, clause) <- unFrom (toFrom f) 42 | Q $ W.tell mempty{sdFromClause=[FromRaw $ clause]} 43 | pure a 44 | 45 | type RawFn = NeedParens -> IdentInfo -> (TLB.Builder, [PersistValue]) 46 | 47 | -- | Data type defining the "From" language. This should not 48 | -- constructed directly in application code. 49 | -- 50 | -- A @From@ is a SqlQuery which returns a reference to the result of calling from 51 | -- and a function that produces a portion of a FROM clause. This gets passed to 52 | -- the FromRaw FromClause constructor directly when converting 53 | -- from a @From@ to a @SqlQuery@ using @from@ 54 | -- 55 | -- @since 3.5.0.0 56 | newtype From a = From 57 | { unFrom :: SqlQuery (a, RawFn)} 58 | 59 | 60 | -- | A helper class primarily designed to allow using @SqlQuery@ directly in 61 | -- a From expression. This is also useful for embedding a @SqlSetOperation@, 62 | -- as well as supporting backwards compatibility for the 63 | -- data constructor join tree used prior to /3.5.0.0/ 64 | -- 65 | -- @since 3.5.0.0 66 | class ToFrom a r | a -> r where 67 | toFrom :: a -> From r 68 | instance ToFrom (From a) a where 69 | toFrom = id 70 | 71 | {-# DEPRECATED Table "@since 3.5.0.0 - use 'table' instead" #-} 72 | data Table a = Table 73 | 74 | instance PersistEntity ent => ToFrom (Table ent) (SqlExpr (Entity ent)) where 75 | toFrom _ = table 76 | 77 | -- | Bring a PersistEntity into scope from a table 78 | -- 79 | -- @ 80 | -- select $ from $ table \@People 81 | -- @ 82 | -- 83 | -- @since 3.5.0.0 84 | table :: forall ent. PersistEntity ent => From (SqlExpr (Entity ent)) 85 | table = From $ do 86 | let ed = entityDef (Proxy @ent) 87 | ident <- newIdentFor (coerce $ getEntityDBName ed) 88 | let entity = unsafeSqlEntity ident 89 | pure $ ( entity, const $ base ident ed ) 90 | where 91 | base ident@(I identText) def info = 92 | let db = coerce $ getEntityDBName def 93 | in ( (fromDBName info (coerce db)) <> 94 | if db == identText 95 | then mempty 96 | else " AS " <> useIdent info ident 97 | , mempty 98 | ) 99 | 100 | 101 | instance (SqlSelect a r, ToAlias a, ToAliasReference a) => ToFrom (SqlQuery a) a where 102 | toFrom = selectQuery 103 | 104 | -- | Select from a subquery, often used in conjuction with joins but can be 105 | -- used without any joins. Because @SqlQuery@ has a @ToFrom@ instance you probably 106 | -- dont need to use this function directly. 107 | -- 108 | -- @ 109 | -- select $ 110 | -- p <- from $ 111 | -- selectQuery do 112 | -- p <- from $ table \@Person 113 | -- limit 5 114 | -- orderBy [ asc p ^. PersonAge ] 115 | -- ... 116 | -- @ 117 | -- 118 | -- @since 3.5.0.0 119 | selectQuery :: (SqlSelect a r, ToAlias a, ToAliasReference a) => SqlQuery a -> From a 120 | selectQuery subquery = From $ do 121 | -- We want to update the IdentState without writing the query to side data 122 | (ret, sideData) <- Q $ W.censor (\_ -> mempty) $ W.listen $ unQ subquery 123 | aliasedValue <- toAlias ret 124 | -- Make a fake query with the aliased results, this allows us to ensure that the query is only run once 125 | let aliasedQuery = Q $ W.WriterT $ pure (aliasedValue, sideData) 126 | -- Add the FromQuery that renders the subquery to our side data 127 | subqueryAlias <- newIdentFor (DBName "q") 128 | -- Pass the aliased results of the subquery to the outer query 129 | -- create aliased references from the outer query results (e.g value from subquery will be `subquery`.`value`), 130 | -- this is probably overkill as the aliases should already be unique but seems to be good practice. 131 | ref <- toAliasReference subqueryAlias aliasedValue 132 | 133 | pure (ref, \_ info -> 134 | let (queryText,queryVals) = toRawSql SELECT info aliasedQuery 135 | in 136 | ( (parens queryText) <> " AS " <> useIdent info subqueryAlias 137 | , queryVals 138 | ) 139 | ) 140 | -------------------------------------------------------------------------------- /src/Database/Esqueleto/Experimental/From/CommonTableExpression.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleInstances #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE TypeFamilies #-} 4 | 5 | module Database.Esqueleto.Experimental.From.CommonTableExpression 6 | where 7 | 8 | import qualified Control.Monad.Trans.Writer as W 9 | import qualified Data.Text.Lazy.Builder as TLB 10 | import Database.Esqueleto.Experimental.From 11 | import Database.Esqueleto.Experimental.From.SqlSetOperation 12 | import Database.Esqueleto.Experimental.ToAlias 13 | import Database.Esqueleto.Experimental.ToAliasReference 14 | import Database.Esqueleto.Internal.Internal hiding (From(..), from, on) 15 | 16 | -- | @WITH@ clause used to introduce a [Common Table Expression (CTE)](https://en.wikipedia.org/wiki/Hierarchical_and_recursive_queries_in_SQL#Common_table_expression). 17 | -- CTEs are supported in most modern SQL engines and can be useful 18 | -- in performance tuning. In Esqueleto, CTEs should be used as a 19 | -- subquery memoization tactic. When writing plain SQL, CTEs 20 | -- are sometimes used to organize the SQL code, in Esqueleto, this 21 | -- is better achieved through function that return 'SqlQuery' values. 22 | -- 23 | -- @ 24 | -- select $ do 25 | -- cte <- with subQuery 26 | -- cteResult <- from cte 27 | -- where_ $ cteResult ... 28 | -- pure cteResult 29 | -- @ 30 | -- 31 | -- __WARNING__: In some SQL engines using a CTE can diminish performance. 32 | -- In these engines the CTE is treated as an optimization fence. You should 33 | -- always verify that using a CTE will in fact improve your performance 34 | -- over a regular subquery. 35 | -- 36 | -- Notably, in PostgreSQL prior to version 12, CTEs are always fully 37 | -- calculated, which can potentially significantly pessimize queries. As of 38 | -- PostgreSQL 12, non-recursive and side-effect-free queries may be inlined and 39 | -- optimized accordingly if not declared @MATERIALIZED@ to get the previous 40 | -- behaviour. See [the PostgreSQL CTE documentation](https://www.postgresql.org/docs/current/queries-with.html#id-1.5.6.12.7), 41 | -- section Materialization, for more information. To use a @MATERIALIZED@ query 42 | -- in Esquelto, see functions 'withMaterialized' and 'withRecursiveMaterialized'. 43 | -- 44 | -- /Since: 3.4.0.0/ 45 | with :: ( ToAlias a 46 | , ToAliasReference a 47 | , SqlSelect a r 48 | ) => SqlQuery a -> SqlQuery (From a) 49 | with query = do 50 | (ret, sideData) <- Q $ W.censor (\_ -> mempty) $ W.listen $ unQ query 51 | aliasedValue <- toAlias ret 52 | let aliasedQuery = Q $ W.WriterT $ pure (aliasedValue, sideData) 53 | ident <- newIdentFor (DBName "cte") 54 | let clause = CommonTableExpressionClause NormalCommonTableExpression (\_ _ -> "") ident (\info -> toRawSql SELECT info aliasedQuery) 55 | Q $ W.tell mempty{sdCteClause = [clause]} 56 | ref <- toAliasReference ident aliasedValue 57 | pure $ From $ do 58 | newIdent <- newIdentFor (DBName "cte") 59 | localRef <- toAliasReference newIdent ref 60 | let makeLH info = useIdent info ident <> " AS " <> useIdent info newIdent 61 | pure (localRef, (\_ info -> (makeLH info, mempty))) 62 | 63 | -- | @WITH@ @RECURSIVE@ allows one to make a recursive subquery, which can 64 | -- reference itself. Like @WITH@, this is supported in most modern SQL engines. 65 | -- Useful for hierarchical, self-referential data, like a tree of data. 66 | -- 67 | -- @ 68 | -- select $ do 69 | -- cte <- withRecursive 70 | -- (do 71 | -- person <- from $ table \@Person 72 | -- where_ $ person ^. PersonId ==. val personId 73 | -- pure person 74 | -- ) 75 | -- unionAll_ 76 | -- (\\self -> do 77 | -- (p :& f :& p2 :& pSelf) <- from self 78 | -- \`innerJoin\` $ table \@Follow 79 | -- \`on\` (\\(p :& f) -> 80 | -- p ^. PersonId ==. f ^. FollowFollower) 81 | -- \`innerJoin\` $ table \@Person 82 | -- \`on\` (\\(p :& f :& p2) -> 83 | -- f ^. FollowFollowed ==. p2 ^. PersonId) 84 | -- \`leftJoin\` self 85 | -- \`on\` (\\(_ :& _ :& p2 :& pSelf) -> 86 | -- just (p2 ^. PersonId) ==. pSelf ?. PersonId) 87 | -- where_ $ isNothing (pSelf ?. PersonId) 88 | -- groupBy (p2 ^. PersonId) 89 | -- pure p2 90 | -- ) 91 | -- from cte 92 | -- @ 93 | -- 94 | -- /Since: 3.4.0.0/ 95 | withRecursive :: ( ToAlias a 96 | , ToAliasReference a 97 | , SqlSelect a r 98 | ) 99 | => SqlQuery a 100 | -> UnionKind 101 | -> (From a -> SqlQuery a) 102 | -> SqlQuery (From a) 103 | withRecursive baseCase unionKind recursiveCase = do 104 | (ret, sideData) <- Q $ W.censor (\_ -> mempty) $ W.listen $ unQ baseCase 105 | aliasedValue <- toAlias ret 106 | let aliasedQuery = Q $ W.WriterT $ pure (aliasedValue, sideData) 107 | ident <- newIdentFor (DBName "cte") 108 | ref <- toAliasReference ident aliasedValue 109 | let refFrom = From (pure (ref, (\_ info -> (useIdent info ident, mempty)))) 110 | let recursiveQuery = recursiveCase refFrom 111 | let noModifier _ _ = "" 112 | let clause = CommonTableExpressionClause RecursiveCommonTableExpression noModifier ident 113 | (\info -> (toRawSql SELECT info aliasedQuery) 114 | <> ("\n" <> (unUnionKind unionKind) <> "\n", mempty) 115 | <> (toRawSql SELECT info recursiveQuery) 116 | ) 117 | Q $ W.tell mempty{sdCteClause = [clause]} 118 | pure refFrom 119 | 120 | newtype UnionKind = UnionKind { unUnionKind :: TLB.Builder } 121 | instance Union_ UnionKind where 122 | union_ = UnionKind "UNION" 123 | instance UnionAll_ UnionKind where 124 | unionAll_ = UnionKind "UNION ALL" 125 | -------------------------------------------------------------------------------- /src/Database/Esqueleto/Experimental/From/SqlSetOperation.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE FlexibleInstances #-} 3 | {-# LANGUAGE FunctionalDependencies #-} 4 | {-# LANGUAGE MultiParamTypeClasses #-} 5 | {-# LANGUAGE OverloadedStrings #-} 6 | {-# LANGUAGE PatternSynonyms #-} 7 | {-# LANGUAGE ScopedTypeVariables #-} 8 | {-# LANGUAGE TypeApplications #-} 9 | {-# LANGUAGE TypeFamilies #-} 10 | {-# LANGUAGE UndecidableInstances #-} 11 | 12 | module Database.Esqueleto.Experimental.From.SqlSetOperation 13 | where 14 | 15 | import Control.Arrow (first) 16 | import Control.Monad.Trans.Class (lift) 17 | import qualified Control.Monad.Trans.State as S 18 | import qualified Control.Monad.Trans.Writer as W 19 | import qualified Data.Text.Lazy.Builder as TLB 20 | import Database.Esqueleto.Experimental.From 21 | import Database.Esqueleto.Experimental.ToAlias 22 | import Database.Esqueleto.Experimental.ToAliasReference 23 | import Database.Esqueleto.Internal.Internal hiding (From(..), from, on) 24 | import Database.Esqueleto.Internal.PersistentImport (PersistValue) 25 | 26 | -- | Data type used to implement the SqlSetOperation language 27 | -- this type is implemented in the same way as a @From@ 28 | -- 29 | -- Semantically a @SqlSetOperation@ is always a @From@ but not vice versa 30 | -- 31 | -- @since 3.5.0.0 32 | newtype SqlSetOperation a = SqlSetOperation 33 | { unSqlSetOperation :: NeedParens -> SqlQuery (a, IdentInfo -> (TLB.Builder, [PersistValue]))} 34 | 35 | instance ToAliasReference a => ToFrom (SqlSetOperation a) a where 36 | toFrom setOperation = From $ do 37 | ident <- newIdentFor (DBName "u") 38 | (a, fromClause) <- unSqlSetOperation setOperation Never 39 | ref <- toAliasReference ident a 40 | pure (ref, \_ info -> (first parens $ fromClause info) <> (" AS " <> useIdent info ident, mempty)) 41 | 42 | -- | Type class to support direct use of @SqlQuery@ in a set operation tree 43 | -- 44 | -- @since 3.5.0.0 45 | class ToSqlSetOperation a r | a -> r where 46 | toSqlSetOperation :: a -> SqlSetOperation r 47 | instance ToSqlSetOperation (SqlSetOperation a) a where 48 | toSqlSetOperation = id 49 | instance (SqlSelect a r, ToAlias a, ToAliasReference a) => ToSqlSetOperation (SqlQuery a) a where 50 | toSqlSetOperation subquery = 51 | SqlSetOperation $ \p -> do 52 | (ret, sideData) <- Q $ W.censor (\_ -> mempty) $ W.listen $ unQ subquery 53 | aliasedValue <- toAlias ret 54 | let aliasedQuery = Q $ W.WriterT $ pure (aliasedValue, sideData) 55 | let p' = 56 | case p of 57 | Parens -> Parens 58 | Never -> 59 | if (sdLimitClause sideData) /= mempty 60 | || length (sdOrderByClause sideData) > 0 then 61 | Parens 62 | else 63 | Never 64 | pure (aliasedValue, \info -> first (parensM p') $ toRawSql SELECT info aliasedQuery) 65 | 66 | -- | Helper function for defining set operations 67 | -- @since 3.5.0.0 68 | mkSetOperation :: (ToSqlSetOperation a a', ToSqlSetOperation b a') 69 | => TLB.Builder -> a -> b -> SqlSetOperation a' 70 | mkSetOperation operation lhs rhs = SqlSetOperation $ \p -> do 71 | state <- Q $ lift S.get 72 | (leftValue, leftClause) <- unSqlSetOperation (toSqlSetOperation lhs) p 73 | Q $ lift $ S.put state 74 | (_, rightClause) <- unSqlSetOperation (toSqlSetOperation rhs) p 75 | pure (leftValue, \info -> leftClause info <> (operation, mempty) <> rightClause info) 76 | 77 | -- | Overloaded @union_@ function to support use in both 'SqlSetOperation' 78 | -- and 'withRecursive' 79 | -- 80 | -- @since 3.5.0.0 81 | class Union_ a where 82 | -- | @UNION@ SQL set operation. Can be used as an infix function between 'SqlQuery' values. 83 | union_ :: a 84 | 85 | instance (ToSqlSetOperation a c, ToSqlSetOperation b c, res ~ SqlSetOperation c) 86 | => Union_ (a -> b -> res) where 87 | union_ = mkSetOperation " UNION " 88 | 89 | -- | Overloaded @unionAll_@ function to support use in both 'SqlSetOperation' 90 | -- and 'withRecursive' 91 | -- 92 | -- @since 3.5.0.0 93 | class UnionAll_ a where 94 | -- | @UNION@ @ALL@ SQL set operation. Can be used as an infix function between 'SqlQuery' values. 95 | unionAll_ :: a 96 | instance (ToSqlSetOperation a c, ToSqlSetOperation b c, res ~ SqlSetOperation c) 97 | => UnionAll_ (a -> b -> res) where 98 | unionAll_ = mkSetOperation " UNION ALL " 99 | 100 | -- | @EXCEPT@ SQL set operation. Can be used as an infix function between 'SqlQuery' values. 101 | except_ :: (ToSqlSetOperation a a', ToSqlSetOperation b a') => a -> b -> SqlSetOperation a' 102 | except_ = mkSetOperation " EXCEPT " 103 | 104 | -- | @INTERSECT@ SQL set operation. Can be used as an infix function between 'SqlQuery' values. 105 | intersect_ :: (ToSqlSetOperation a a', ToSqlSetOperation b a') => a -> b -> SqlSetOperation a' 106 | intersect_ = mkSetOperation " INTERSECT " 107 | -------------------------------------------------------------------------------- /src/Database/Esqueleto/Experimental/ToAlias.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleInstances #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE TypeFamilies #-} 4 | 5 | module Database.Esqueleto.Experimental.ToAlias 6 | where 7 | 8 | import Database.Esqueleto.Internal.Internal hiding (From, from, on) 9 | import Database.Esqueleto.Internal.PersistentImport 10 | 11 | -- Tedious tuple magic 12 | class ToAlias a where 13 | toAlias :: a -> SqlQuery a 14 | 15 | instance ToAlias (SqlExpr (Value a)) where 16 | toAlias e@(ERaw m f) 17 | | Just _ <- sqlExprMetaAlias m = pure e 18 | | otherwise = do 19 | ident <- newIdentFor (DBName "v") 20 | pure $ ERaw noMeta{sqlExprMetaAlias = Just ident} f 21 | 22 | instance ToAlias (SqlExpr (Entity a)) where 23 | toAlias e@(ERaw m f) 24 | | Just _ <- sqlExprMetaAlias m = pure e 25 | | otherwise = do 26 | ident <- newIdentFor (DBName "v") 27 | pure $ ERaw m{sqlExprMetaIsReference = False, sqlExprMetaAlias = Just ident} f 28 | 29 | instance ToAlias (SqlExpr (Maybe (Entity a))) where 30 | -- FIXME: Code duplication because the compiler doesnt like half final encoding 31 | toAlias e@(ERaw m f) 32 | | Just _ <- sqlExprMetaAlias m = pure e 33 | | otherwise = do 34 | ident <- newIdentFor (DBName "v") 35 | pure $ ERaw m{sqlExprMetaIsReference = False, sqlExprMetaAlias = Just ident} f 36 | 37 | instance (ToAlias a, ToAlias b) => ToAlias (a,b) where 38 | toAlias (a,b) = (,) <$> toAlias a <*> toAlias b 39 | 40 | instance ( ToAlias a 41 | , ToAlias b 42 | , ToAlias c 43 | ) => ToAlias (a,b,c) where 44 | toAlias x = to3 <$> (toAlias $ from3 x) 45 | 46 | instance ( ToAlias a 47 | , ToAlias b 48 | , ToAlias c 49 | , ToAlias d 50 | ) => ToAlias (a,b,c,d) where 51 | toAlias x = to4 <$> (toAlias $ from4 x) 52 | 53 | instance ( ToAlias a 54 | , ToAlias b 55 | , ToAlias c 56 | , ToAlias d 57 | , ToAlias e 58 | ) => ToAlias (a,b,c,d,e) where 59 | toAlias x = to5 <$> (toAlias $ from5 x) 60 | 61 | instance ( ToAlias a 62 | , ToAlias b 63 | , ToAlias c 64 | , ToAlias d 65 | , ToAlias e 66 | , ToAlias f 67 | ) => ToAlias (a,b,c,d,e,f) where 68 | toAlias x = to6 <$> (toAlias $ from6 x) 69 | 70 | instance ( ToAlias a 71 | , ToAlias b 72 | , ToAlias c 73 | , ToAlias d 74 | , ToAlias e 75 | , ToAlias f 76 | , ToAlias g 77 | ) => ToAlias (a,b,c,d,e,f,g) where 78 | toAlias x = to7 <$> (toAlias $ from7 x) 79 | 80 | instance ( ToAlias a 81 | , ToAlias b 82 | , ToAlias c 83 | , ToAlias d 84 | , ToAlias e 85 | , ToAlias f 86 | , ToAlias g 87 | , ToAlias h 88 | ) => ToAlias (a,b,c,d,e,f,g,h) where 89 | toAlias x = to8 <$> (toAlias $ from8 x) 90 | 91 | instance ( ToAlias a 92 | , ToAlias b 93 | , ToAlias c 94 | , ToAlias d 95 | , ToAlias e 96 | , ToAlias f 97 | , ToAlias g 98 | , ToAlias h 99 | , ToAlias i 100 | ) => ToAlias (a,b,c,d,e,f,g,h,i) where 101 | toAlias x = to9 <$> (toAlias $ from9 x) 102 | 103 | instance ( ToAlias a 104 | , ToAlias b 105 | , ToAlias c 106 | , ToAlias d 107 | , ToAlias e 108 | , ToAlias f 109 | , ToAlias g 110 | , ToAlias h 111 | , ToAlias i 112 | , ToAlias j 113 | ) => ToAlias (a,b,c,d,e,f,g,h,i,j) where 114 | toAlias x = to10 <$> (toAlias $ from10 x) 115 | 116 | instance ( ToAlias a 117 | , ToAlias b 118 | , ToAlias c 119 | , ToAlias d 120 | , ToAlias e 121 | , ToAlias f 122 | , ToAlias g 123 | , ToAlias h 124 | , ToAlias i 125 | , ToAlias j 126 | , ToAlias k 127 | ) => ToAlias (a,b,c,d,e,f,g,h,i,j,k) where 128 | toAlias x = to11 <$> (toAlias $ from11 x) 129 | 130 | instance ( ToAlias a 131 | , ToAlias b 132 | , ToAlias c 133 | , ToAlias d 134 | , ToAlias e 135 | , ToAlias f 136 | , ToAlias g 137 | , ToAlias h 138 | , ToAlias i 139 | , ToAlias j 140 | , ToAlias k 141 | , ToAlias l 142 | ) => ToAlias (a,b,c,d,e,f,g,h,i,j,k,l) where 143 | toAlias x = to12 <$> (toAlias $ from12 x) 144 | 145 | instance ( ToAlias a 146 | , ToAlias b 147 | , ToAlias c 148 | , ToAlias d 149 | , ToAlias e 150 | , ToAlias f 151 | , ToAlias g 152 | , ToAlias h 153 | , ToAlias i 154 | , ToAlias j 155 | , ToAlias k 156 | , ToAlias l 157 | , ToAlias m 158 | ) => ToAlias (a,b,c,d,e,f,g,h,i,j,k,l,m) where 159 | toAlias x = to13 <$> (toAlias $ from13 x) 160 | 161 | instance ( ToAlias a 162 | , ToAlias b 163 | , ToAlias c 164 | , ToAlias d 165 | , ToAlias e 166 | , ToAlias f 167 | , ToAlias g 168 | , ToAlias h 169 | , ToAlias i 170 | , ToAlias j 171 | , ToAlias k 172 | , ToAlias l 173 | , ToAlias m 174 | , ToAlias n 175 | ) => ToAlias (a,b,c,d,e,f,g,h,i,j,k,l,m,n) where 176 | toAlias x = to14 <$> (toAlias $ from14 x) 177 | 178 | instance ( ToAlias a 179 | , ToAlias b 180 | , ToAlias c 181 | , ToAlias d 182 | , ToAlias e 183 | , ToAlias f 184 | , ToAlias g 185 | , ToAlias h 186 | , ToAlias i 187 | , ToAlias j 188 | , ToAlias k 189 | , ToAlias l 190 | , ToAlias m 191 | , ToAlias n 192 | , ToAlias o 193 | ) => ToAlias (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o) where 194 | toAlias x = to15 <$> (toAlias $ from15 x) 195 | 196 | instance ( ToAlias a 197 | , ToAlias b 198 | , ToAlias c 199 | , ToAlias d 200 | , ToAlias e 201 | , ToAlias f 202 | , ToAlias g 203 | , ToAlias h 204 | , ToAlias i 205 | , ToAlias j 206 | , ToAlias k 207 | , ToAlias l 208 | , ToAlias m 209 | , ToAlias n 210 | , ToAlias o 211 | , ToAlias p 212 | ) => ToAlias (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p) where 213 | toAlias x = to16 <$> (toAlias $ from16 x) 214 | -------------------------------------------------------------------------------- /src/Database/Esqueleto/Experimental/ToAliasReference.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleInstances #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE TypeFamilies #-} 4 | 5 | module Database.Esqueleto.Experimental.ToAliasReference 6 | where 7 | 8 | import Database.Esqueleto.Internal.Internal hiding (From, from, on) 9 | import Database.Esqueleto.Internal.PersistentImport 10 | 11 | -- more tedious tuple magic 12 | class ToAliasReference a where 13 | toAliasReference :: Ident -> a -> SqlQuery a 14 | 15 | instance ToAliasReference (SqlExpr (Value a)) where 16 | toAliasReference aliasSource (ERaw m _) 17 | | Just alias <- sqlExprMetaAlias m = pure $ ERaw m{sqlExprMetaIsReference = True} $ \_ info -> 18 | (useIdent info aliasSource <> "." <> useIdent info alias, []) 19 | toAliasReference _ e = pure e 20 | 21 | instance ToAliasReference (SqlExpr (Entity a)) where 22 | toAliasReference aliasSource (ERaw m _) 23 | | Just _ <- sqlExprMetaAlias m = 24 | pure $ ERaw m{sqlExprMetaIsReference = True} $ \_ info -> 25 | (useIdent info aliasSource, []) 26 | toAliasReference _ e = pure e 27 | 28 | instance ToAliasReference (SqlExpr (Maybe (Entity a))) where 29 | toAliasReference aliasSource e = 30 | veryUnsafeCoerceSqlExpr <$> toAliasReference aliasSource (veryUnsafeCoerceSqlExpr e :: SqlExpr (Entity a)) 31 | 32 | 33 | instance (ToAliasReference a, ToAliasReference b) => ToAliasReference (a, b) where 34 | toAliasReference ident (a,b) = (,) <$> (toAliasReference ident a) <*> (toAliasReference ident b) 35 | 36 | instance ( ToAliasReference a 37 | , ToAliasReference b 38 | , ToAliasReference c 39 | ) => ToAliasReference (a,b,c) where 40 | toAliasReference ident x = fmap to3 $ toAliasReference ident $ from3 x 41 | 42 | instance ( ToAliasReference a 43 | , ToAliasReference b 44 | , ToAliasReference c 45 | , ToAliasReference d 46 | ) => ToAliasReference (a,b,c,d) where 47 | toAliasReference ident x = fmap to4 $ toAliasReference ident $ from4 x 48 | 49 | instance ( ToAliasReference a 50 | , ToAliasReference b 51 | , ToAliasReference c 52 | , ToAliasReference d 53 | , ToAliasReference e 54 | ) => ToAliasReference (a,b,c,d,e) where 55 | toAliasReference ident x = fmap to5 $ toAliasReference ident $ from5 x 56 | 57 | instance ( ToAliasReference a 58 | , ToAliasReference b 59 | , ToAliasReference c 60 | , ToAliasReference d 61 | , ToAliasReference e 62 | , ToAliasReference f 63 | ) => ToAliasReference (a,b,c,d,e,f) where 64 | toAliasReference ident x = to6 <$> (toAliasReference ident $ from6 x) 65 | 66 | instance ( ToAliasReference a 67 | , ToAliasReference b 68 | , ToAliasReference c 69 | , ToAliasReference d 70 | , ToAliasReference e 71 | , ToAliasReference f 72 | , ToAliasReference g 73 | ) => ToAliasReference (a,b,c,d,e,f,g) where 74 | toAliasReference ident x = to7 <$> (toAliasReference ident $ from7 x) 75 | 76 | instance ( ToAliasReference a 77 | , ToAliasReference b 78 | , ToAliasReference c 79 | , ToAliasReference d 80 | , ToAliasReference e 81 | , ToAliasReference f 82 | , ToAliasReference g 83 | , ToAliasReference h 84 | ) => ToAliasReference (a,b,c,d,e,f,g,h) where 85 | toAliasReference ident x = to8 <$> (toAliasReference ident $ from8 x) 86 | 87 | instance ( ToAliasReference a 88 | , ToAliasReference b 89 | , ToAliasReference c 90 | , ToAliasReference d 91 | , ToAliasReference e 92 | , ToAliasReference f 93 | , ToAliasReference g 94 | , ToAliasReference h 95 | , ToAliasReference i 96 | ) => ToAliasReference (a,b,c,d,e,f,g,h,i) where 97 | toAliasReference ident x = to9 <$> (toAliasReference ident $ from9 x) 98 | 99 | instance ( ToAliasReference a 100 | , ToAliasReference b 101 | , ToAliasReference c 102 | , ToAliasReference d 103 | , ToAliasReference e 104 | , ToAliasReference f 105 | , ToAliasReference g 106 | , ToAliasReference h 107 | , ToAliasReference i 108 | , ToAliasReference j 109 | ) => ToAliasReference (a,b,c,d,e,f,g,h,i,j) where 110 | toAliasReference ident x = to10 <$> (toAliasReference ident $ from10 x) 111 | 112 | instance ( ToAliasReference a 113 | , ToAliasReference b 114 | , ToAliasReference c 115 | , ToAliasReference d 116 | , ToAliasReference e 117 | , ToAliasReference f 118 | , ToAliasReference g 119 | , ToAliasReference h 120 | , ToAliasReference i 121 | , ToAliasReference j 122 | , ToAliasReference k 123 | ) => ToAliasReference (a,b,c,d,e,f,g,h,i,j,k) where 124 | toAliasReference ident x = to11 <$> (toAliasReference ident $ from11 x) 125 | 126 | instance ( ToAliasReference a 127 | , ToAliasReference b 128 | , ToAliasReference c 129 | , ToAliasReference d 130 | , ToAliasReference e 131 | , ToAliasReference f 132 | , ToAliasReference g 133 | , ToAliasReference h 134 | , ToAliasReference i 135 | , ToAliasReference j 136 | , ToAliasReference k 137 | , ToAliasReference l 138 | ) => ToAliasReference (a,b,c,d,e,f,g,h,i,j,k,l) where 139 | toAliasReference ident x = to12 <$> (toAliasReference ident $ from12 x) 140 | 141 | instance ( ToAliasReference a 142 | , ToAliasReference b 143 | , ToAliasReference c 144 | , ToAliasReference d 145 | , ToAliasReference e 146 | , ToAliasReference f 147 | , ToAliasReference g 148 | , ToAliasReference h 149 | , ToAliasReference i 150 | , ToAliasReference j 151 | , ToAliasReference k 152 | , ToAliasReference l 153 | , ToAliasReference m 154 | ) => ToAliasReference (a,b,c,d,e,f,g,h,i,j,k,l,m) where 155 | toAliasReference ident x = to13 <$> (toAliasReference ident $ from13 x) 156 | 157 | instance ( ToAliasReference a 158 | , ToAliasReference b 159 | , ToAliasReference c 160 | , ToAliasReference d 161 | , ToAliasReference e 162 | , ToAliasReference f 163 | , ToAliasReference g 164 | , ToAliasReference h 165 | , ToAliasReference i 166 | , ToAliasReference j 167 | , ToAliasReference k 168 | , ToAliasReference l 169 | , ToAliasReference m 170 | , ToAliasReference n 171 | ) => ToAliasReference (a,b,c,d,e,f,g,h,i,j,k,l,m,n) where 172 | toAliasReference ident x = to14 <$> (toAliasReference ident $ from14 x) 173 | 174 | instance ( ToAliasReference a 175 | , ToAliasReference b 176 | , ToAliasReference c 177 | , ToAliasReference d 178 | , ToAliasReference e 179 | , ToAliasReference f 180 | , ToAliasReference g 181 | , ToAliasReference h 182 | , ToAliasReference i 183 | , ToAliasReference j 184 | , ToAliasReference k 185 | , ToAliasReference l 186 | , ToAliasReference m 187 | , ToAliasReference n 188 | , ToAliasReference o 189 | ) => ToAliasReference (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o) where 190 | toAliasReference ident x = to15 <$> (toAliasReference ident $ from15 x) 191 | 192 | instance ( ToAliasReference a 193 | , ToAliasReference b 194 | , ToAliasReference c 195 | , ToAliasReference d 196 | , ToAliasReference e 197 | , ToAliasReference f 198 | , ToAliasReference g 199 | , ToAliasReference h 200 | , ToAliasReference i 201 | , ToAliasReference j 202 | , ToAliasReference k 203 | , ToAliasReference l 204 | , ToAliasReference m 205 | , ToAliasReference n 206 | , ToAliasReference o 207 | , ToAliasReference p 208 | ) => ToAliasReference (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p) where 209 | toAliasReference ident x = to16 <$> (toAliasReference ident $ from16 x) 210 | -------------------------------------------------------------------------------- /src/Database/Esqueleto/Experimental/ToMaybe.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleInstances #-} 2 | {-# LANGUAGE TypeFamilies #-} 3 | 4 | module Database.Esqueleto.Experimental.ToMaybe 5 | ( module Database.Esqueleto.Experimental.ToMaybe 6 | , Nullable 7 | ) 8 | where 9 | 10 | import Database.Esqueleto.Internal.Internal hiding (From(..), from, on) 11 | import Database.Esqueleto.Internal.PersistentImport (Entity(..)) 12 | 13 | class ToMaybe a where 14 | type ToMaybeT a 15 | toMaybe :: a -> ToMaybeT a 16 | 17 | instance ToMaybe (SqlExpr (Maybe a)) where 18 | type ToMaybeT (SqlExpr (Maybe a)) = SqlExpr (Maybe a) 19 | toMaybe = id 20 | 21 | instance ToMaybe (SqlExpr (Entity a)) where 22 | type ToMaybeT (SqlExpr (Entity a)) = SqlExpr (Maybe (Entity a)) 23 | toMaybe (ERaw f m) = (ERaw f m) 24 | 25 | instance ToMaybe (SqlExpr (Value a)) where 26 | type ToMaybeT (SqlExpr (Value a)) = SqlExpr (Value (Maybe (Nullable a))) 27 | toMaybe = veryUnsafeCoerceSqlExprValue 28 | 29 | 30 | instance (ToMaybe a, ToMaybe b) => ToMaybe (a,b) where 31 | type ToMaybeT (a, b) = (ToMaybeT a, ToMaybeT b) 32 | toMaybe (a, b) = (toMaybe a, toMaybe b) 33 | 34 | instance ( ToMaybe a , ToMaybe b , ToMaybe c) => ToMaybe (a,b,c) where 35 | type ToMaybeT (a, b, c) = (ToMaybeT a, ToMaybeT b, ToMaybeT c) 36 | toMaybe = to3 . toMaybe . from3 37 | 38 | instance ( ToMaybe a , ToMaybe b , ToMaybe c , ToMaybe d) => ToMaybe (a,b,c,d) where 39 | type ToMaybeT (a, b, c, d) = (ToMaybeT a, ToMaybeT b, ToMaybeT c, ToMaybeT d) 40 | toMaybe = to4 . toMaybe . from4 41 | 42 | instance ( ToMaybe a , ToMaybe b , ToMaybe c , ToMaybe d , ToMaybe e) => ToMaybe (a,b,c,d,e) where 43 | type ToMaybeT (a, b, c, d, e) = (ToMaybeT a, ToMaybeT b, ToMaybeT c, ToMaybeT d, ToMaybeT e) 44 | toMaybe = to5 . toMaybe . from5 45 | 46 | instance ( ToMaybe a 47 | , ToMaybe b 48 | , ToMaybe c 49 | , ToMaybe d 50 | , ToMaybe e 51 | , ToMaybe f 52 | ) => ToMaybe (a,b,c,d,e,f) where 53 | type ToMaybeT (a, b, c, d, e, f) = (ToMaybeT a, ToMaybeT b, ToMaybeT c, ToMaybeT d, ToMaybeT e, ToMaybeT f) 54 | toMaybe = to6 . toMaybe . from6 55 | 56 | instance ( ToMaybe a 57 | , ToMaybe b 58 | , ToMaybe c 59 | , ToMaybe d 60 | , ToMaybe e 61 | , ToMaybe f 62 | , ToMaybe g 63 | ) => ToMaybe (a,b,c,d,e,f,g) where 64 | type ToMaybeT (a, b, c, d, e, f, g) = (ToMaybeT a, ToMaybeT b, ToMaybeT c, ToMaybeT d, ToMaybeT e, ToMaybeT f, ToMaybeT g) 65 | toMaybe = to7 . toMaybe . from7 66 | 67 | instance ( ToMaybe a 68 | , ToMaybe b 69 | , ToMaybe c 70 | , ToMaybe d 71 | , ToMaybe e 72 | , ToMaybe f 73 | , ToMaybe g 74 | , ToMaybe h 75 | ) => ToMaybe (a,b,c,d,e,f,g,h) where 76 | type ToMaybeT (a, b, c, d, e, f, g, h) = (ToMaybeT a, ToMaybeT b, ToMaybeT c, ToMaybeT d, ToMaybeT e, ToMaybeT f, ToMaybeT g, ToMaybeT h) 77 | toMaybe = to8 . toMaybe . from8 78 | 79 | -------------------------------------------------------------------------------- /src/Database/Esqueleto/Internal/ExprParser.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE RecordWildCards #-} 3 | 4 | -- | This is an internal module. This module may have breaking changes without 5 | -- a corresponding major version bump. If you use this module, please open an 6 | -- issue with your use-case so we can safely support it. 7 | module Database.Esqueleto.Internal.ExprParser where 8 | 9 | import Prelude hiding (takeWhile) 10 | 11 | import Control.Applicative ((<|>)) 12 | import Control.Monad (void) 13 | import Data.Attoparsec.Text 14 | import Data.Set (Set) 15 | import qualified Data.Set as Set 16 | import Data.Text (Text) 17 | import qualified Data.Text as Text 18 | import Database.Persist.Sql 19 | import Database.Persist.SqlBackend 20 | 21 | -- | A type representing the access of a table value. In Esqueleto, we get 22 | -- a guarantee that the access will look something like: 23 | -- 24 | -- @ 25 | -- escape-char [character] escape-char . escape-char [character] escape-char 26 | -- ^^^^^^^^^^^ ^^^^^^^^^^^ 27 | -- table name column name 28 | -- @ 29 | data TableAccess = TableAccess 30 | { tableAccessTable :: Text 31 | , tableAccessColumn :: Text 32 | } 33 | deriving (Eq, Ord, Show) 34 | 35 | -- | Parse a @SqlExpr (Value Bool)@'s textual representation into a list of 36 | -- 'TableAccess' 37 | parseOnExpr :: SqlBackend -> Text -> Either String (Set TableAccess) 38 | parseOnExpr sqlBackend text = do 39 | c <- mkEscapeChar sqlBackend 40 | parseOnly (onExpr c) text 41 | 42 | -- | This function uses the 'connEscapeName' function in the 'SqlBackend' with an 43 | -- empty identifier to pull out an escape character. This implementation works 44 | -- with postgresql, mysql, and sqlite backends. 45 | mkEscapeChar :: SqlBackend -> Either String Char 46 | mkEscapeChar sqlBackend = 47 | case Text.uncons (getEscapedRawName "" sqlBackend) of 48 | Nothing -> 49 | Left "Failed to get an escape character from the SQL backend." 50 | Just (c, _) -> 51 | Right c 52 | 53 | type ExprParser a = Char -> Parser a 54 | 55 | onExpr :: ExprParser (Set TableAccess) 56 | onExpr e = Set.fromList <$> many' tableAccesses 57 | where 58 | tableAccesses = do 59 | skipToEscape e "Skipping to an escape char" 60 | parseTableAccess e "Parsing a table access" 61 | 62 | skipToEscape :: ExprParser () 63 | skipToEscape escapeChar = void (takeWhile (/= escapeChar)) 64 | 65 | parseEscapedIdentifier :: ExprParser [Char] 66 | parseEscapedIdentifier escapeChar = do 67 | _ <- char escapeChar 68 | str <- parseEscapedChars escapeChar 69 | _ <- char escapeChar 70 | pure str 71 | 72 | parseTableAccess :: ExprParser TableAccess 73 | parseTableAccess ec = do 74 | tableAccessTable <- Text.pack <$> parseEscapedIdentifier ec 75 | _ <- char '.' 76 | tableAccessColumn <- Text.pack <$> parseEscapedIdentifier ec 77 | pure TableAccess {..} 78 | 79 | parseEscapedChars :: ExprParser [Char] 80 | parseEscapedChars escapeChar = go 81 | where 82 | twoEscapes = char escapeChar *> char escapeChar 83 | go = many' (notChar escapeChar <|> twoEscapes) 84 | -------------------------------------------------------------------------------- /src/Database/Esqueleto/Internal/PersistentImport.hs: -------------------------------------------------------------------------------- 1 | {-# language CPP #-} 2 | 3 | -- | Re-export "Database.Persist.Sql" without any clashes with 4 | -- @esqueleto@. 5 | module Database.Esqueleto.Internal.PersistentImport 6 | (module Database.Persist.Sql) where 7 | 8 | import Database.Persist.Sql hiding 9 | ( BackendSpecificFilter 10 | , Filter(..) 11 | , PersistQuery 12 | , SelectOpt(..) 13 | , Update(..) 14 | , count 15 | , delete 16 | , deleteWhereCount 17 | , exists 18 | , getPersistMap 19 | , limitOffsetOrder 20 | , listToJSON 21 | , mapToJSON 22 | , selectKeysList 23 | , selectList 24 | , selectSource 25 | , update 26 | , updateWhereCount 27 | , (!=.) 28 | , (*=.) 29 | , (+=.) 30 | , (-=.) 31 | , (/<-.) 32 | , (/=.) 33 | , (<-.) 34 | , (<.) 35 | , (<=.) 36 | , (=.) 37 | , (==.) 38 | , (>.) 39 | , (>=.) 40 | , (||.) 41 | ) 42 | -------------------------------------------------------------------------------- /src/Database/Esqueleto/Legacy.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE FlexibleInstances #-} 3 | {-# LANGUAGE GADTs #-} 4 | {-# LANGUAGE RankNTypes #-} 5 | 6 | -- | WARNING 7 | -- 8 | -- This module is introduced in version @3.5.0.0@ to provide a smooth migration 9 | -- experience from this legacy syntax to the new and improved syntax. If you've 10 | -- imported this module, it means you've decided to use the old syntax for 11 | -- a little bit longer, rather than migrate to the new stuff. That's fine! 12 | -- 13 | -- But you should know that this module, and all of the legacy syntax, will be 14 | -- completely removed from the library in version @4.0.0.0@. 15 | -- 16 | -- The @esqueleto@ EDSL (embedded domain specific language). 17 | -- This module replaces @Database.Persist@, so instead of 18 | -- importing that module you should just import this one: 19 | -- 20 | -- @ 21 | -- -- For a module using just esqueleto. 22 | -- import Database.Esqueleto 23 | -- @ 24 | -- 25 | -- If you need to use @persistent@'s default support for queries 26 | -- as well, either import it qualified: 27 | -- 28 | -- @ 29 | -- -- For a module that mostly uses esqueleto. 30 | -- import Database.Esqueleto 31 | -- import qualified Database.Persist as P 32 | -- @ 33 | -- 34 | -- or import @esqueleto@ itself qualified: 35 | -- 36 | -- @ 37 | -- -- For a module that uses esqueleto just on some queries. 38 | -- import Database.Persist 39 | -- import qualified Database.Esqueleto as E 40 | -- @ 41 | -- 42 | -- Other than identifier name clashes, @esqueleto@ does not 43 | -- conflict with @persistent@ in any way. 44 | module Database.Esqueleto.Legacy 45 | ( -- * Setup 46 | -- $setup 47 | 48 | -- * Introduction 49 | -- $introduction 50 | 51 | -- * Getting started 52 | -- $gettingstarted 53 | 54 | -- * @esqueleto@'s Language 55 | where_, on, groupBy, orderBy, asc, desc, limit, offset 56 | , distinct, distinctOn, don, distinctOnOrderBy, having, locking 57 | , (^.), (?.) 58 | , val, isNothing, just, just', nothing, joinV, joinV', withNonNull 59 | , countRows, count, countDistinct 60 | , not_, (==.), (>=.), (>.), (<=.), (<.), (!=.), (&&.), (||.) 61 | , between, (+.), (-.), (/.), (*.) 62 | , round_, ceiling_, floor_ 63 | , min_, max_, sum_, avg_, castNum, castNumM 64 | , coalesce, coalesceDefault 65 | , lower_, upper_, trim_, ltrim_, rtrim_, length_, left_, right_ 66 | , like, ilike, (%), concat_, (++.), castString 67 | , subList_select, valList, justList 68 | , in_, notIn, exists, notExists 69 | , set, (=.), (+=.), (-=.), (*=.), (/=.) 70 | , case_, toBaseId, fromBaseId, fromBaseIdMaybe, toBaseIdMaybe 71 | , subSelect 72 | , subSelectMaybe 73 | , subSelectCount 74 | , subSelectForeign 75 | , subSelectList 76 | , subSelectUnsafe 77 | , ToBaseId(..) 78 | , when_ 79 | , then_ 80 | , else_ 81 | , from 82 | , Value(..) 83 | , ValueList(..) 84 | , OrderBy 85 | , DistinctOn 86 | , LockingKind(..) 87 | , forUpdate 88 | , forUpdateSkipLocked 89 | , LockableEntity(..) 90 | , SqlString 91 | -- ** Joins 92 | , InnerJoin(..) 93 | , CrossJoin(..) 94 | , LeftOuterJoin(..) 95 | , RightOuterJoin(..) 96 | , FullOuterJoin(..) 97 | , JoinKind(..) 98 | , OnClauseWithoutMatchingJoinException(..) 99 | -- * SQL backend 100 | , SqlQuery 101 | , SqlExpr 102 | , SqlEntity 103 | , select 104 | , selectOne 105 | , selectSource 106 | , delete 107 | , deleteCount 108 | , update 109 | , updateCount 110 | , insertSelect 111 | , insertSelectCount 112 | , (<#) 113 | , (<&>) 114 | -- ** Rendering Queries 115 | , renderQueryToText 116 | , renderQuerySelect 117 | , renderQueryUpdate 118 | , renderQueryDelete 119 | , renderQueryInsertInto 120 | -- * Internal.Language 121 | , From 122 | -- * RDBMS-specific modules 123 | -- $rdbmsSpecificModules 124 | 125 | -- * Helpers 126 | , valkey 127 | , valJ 128 | , associateJoin 129 | 130 | -- * Re-exports 131 | -- $reexports 132 | , deleteKey 133 | , module Database.Esqueleto.Internal.PersistentImport 134 | ) where 135 | 136 | import Database.Esqueleto.Internal.Internal 137 | import Database.Esqueleto.Internal.PersistentImport 138 | 139 | 140 | -- $setup 141 | -- 142 | -- If you're already using @persistent@, then you're ready to use 143 | -- @esqueleto@, no further setup is needed. If you're just 144 | -- starting a new project and would like to use @esqueleto@, take 145 | -- a look at @persistent@'s book first 146 | -- () to learn how to 147 | -- define your schema. 148 | 149 | 150 | ---------------------------------------------------------------------- 151 | 152 | 153 | -- $introduction 154 | -- 155 | -- The main goals of @esqueleto@ are to: 156 | -- 157 | -- * Be easily translatable to SQL. When you take a look at a 158 | -- @esqueleto@ query, you should be able to know exactly how 159 | -- the SQL query will end up. (As opposed to being a 160 | -- relational algebra EDSL such as HaskellDB, which is 161 | -- non-trivial to translate into SQL.) 162 | -- 163 | -- * Support the most widely used SQL features. We'd like you to be 164 | -- able to use @esqueleto@ for all of your queries, no 165 | -- exceptions. Send a pull request or open an issue on our 166 | -- project page () if 167 | -- there's anything missing that you'd like to see. 168 | -- 169 | -- * Be as type-safe as possible. We strive to provide as many 170 | -- type checks as possible. If you get bitten by some invalid 171 | -- code that type-checks, please open an issue on our project 172 | -- page so we can take a look. 173 | -- 174 | -- However, it is /not/ a goal to be able to write portable SQL. 175 | -- We do not try to hide the differences between DBMSs from you, 176 | -- and @esqueleto@ code that works for one database may not work 177 | -- on another. This is a compromise we have to make in order to 178 | -- give you as much control over the raw SQL as possible without 179 | -- losing too much convenience. This also means that you may 180 | -- type-check a query that doesn't work on your DBMS. 181 | 182 | 183 | ---------------------------------------------------------------------- 184 | 185 | 186 | -- $gettingstarted 187 | -- 188 | -- We like clean, easy-to-read EDSLs. However, in order to 189 | -- achieve this goal we've used a lot of type hackery, leading to 190 | -- some hard-to-read type signatures. On this section, we'll try 191 | -- to build some intuition about the syntax. 192 | -- 193 | -- For the following examples, we'll use this example schema: 194 | -- 195 | -- @ 196 | -- share [mkPersist sqlSettings, mkMigrate \"migrateAll\"] [persist| 197 | -- Person 198 | -- name String 199 | -- age Int Maybe 200 | -- deriving Eq Show 201 | -- BlogPost 202 | -- title String 203 | -- authorId PersonId 204 | -- deriving Eq Show 205 | -- Follow 206 | -- follower PersonId 207 | -- followed PersonId 208 | -- deriving Eq Show 209 | -- |] 210 | -- @ 211 | -- 212 | -- Most of @esqueleto@ was created with @SELECT@ statements in 213 | -- mind, not only because they're the most common but also 214 | -- because they're the most complex kind of statement. The most 215 | -- simple kind of @SELECT@ would be: 216 | -- 217 | -- @ 218 | -- SELECT * 219 | -- FROM Person 220 | -- @ 221 | -- 222 | -- In @esqueleto@, we may write the same query above as: 223 | -- 224 | -- @ 225 | -- do people <- 'select' $ 226 | -- 'from' $ \\person -> do 227 | -- return person 228 | -- liftIO $ mapM_ (putStrLn . personName . entityVal) people 229 | -- @ 230 | -- 231 | -- The expression above has type @SqlPersist m ()@, while 232 | -- @people@ has type @[Entity Person]@. The query above will be 233 | -- translated into exactly the same query we wrote manually, but 234 | -- instead of @SELECT *@ it will list all entity fields (using 235 | -- @*@ is not robust). Note that @esqueleto@ knows that we want 236 | -- an @Entity Person@ just because of the @personName@ that we're 237 | -- printing later. 238 | -- 239 | -- However, most of the time we need to filter our queries using 240 | -- @WHERE@. For example: 241 | -- 242 | -- @ 243 | -- SELECT * 244 | -- FROM Person 245 | -- WHERE Person.name = \"John\" 246 | -- @ 247 | -- 248 | -- In @esqueleto@, we may write the same query above as: 249 | -- 250 | -- @ 251 | -- 'select' $ 252 | -- 'from' $ \\p -> do 253 | -- 'where_' (p '^.' PersonName '==.' 'val' \"John\") 254 | -- return p 255 | -- @ 256 | -- 257 | -- Although @esqueleto@'s code is a bit more noisy, it's has 258 | -- almost the same structure (save from the @return@). The 259 | -- @('^.')@ operator is used to project a field from an entity. 260 | -- The field name is the same one generated by @persistent@'s 261 | -- Template Haskell functions. We use 'val' to lift a constant 262 | -- Haskell value into the SQL query. 263 | -- 264 | -- Another example would be: 265 | -- 266 | -- @ 267 | -- SELECT * 268 | -- FROM Person 269 | -- WHERE Person.age >= 18 270 | -- @ 271 | -- 272 | -- In @esqueleto@, we may write the same query above as: 273 | -- 274 | -- @ 275 | -- 'select' $ 276 | -- 'from' $ \\p -> do 277 | -- 'where_' (p '^.' PersonAge '>=.' 'just' ('val' 18)) 278 | -- return p 279 | -- @ 280 | -- 281 | -- Since @age@ is an optional @Person@ field, we use 'just' to lift 282 | -- @'val' 18 :: SqlExpr (Value Int)@ into @just ('val' 18) :: 283 | -- SqlExpr (Value (Maybe Int))@. 284 | -- 285 | -- Implicit joins are represented by tuples. For example, to get 286 | -- the list of all blog posts and their authors, we could write: 287 | -- 288 | -- @ 289 | -- SELECT BlogPost.*, Person.* 290 | -- FROM BlogPost, Person 291 | -- WHERE BlogPost.authorId = Person.id 292 | -- ORDER BY BlogPost.title ASC 293 | -- @ 294 | -- 295 | -- In @esqueleto@, we may write the same query above as: 296 | -- 297 | -- @ 298 | -- 'select' $ 299 | -- 'from' $ \\(b, p) -> do 300 | -- 'where_' (b '^.' BlogPostAuthorId '==.' p '^.' PersonId) 301 | -- 'orderBy' ['asc' (b '^.' BlogPostTitle)] 302 | -- return (b, p) 303 | -- @ 304 | -- 305 | -- However, you may want your results to include people who don't 306 | -- have any blog posts as well using a @LEFT OUTER JOIN@: 307 | -- 308 | -- @ 309 | -- SELECT Person.*, BlogPost.* 310 | -- FROM Person LEFT OUTER JOIN BlogPost 311 | -- ON Person.id = BlogPost.authorId 312 | -- ORDER BY Person.name ASC, BlogPost.title ASC 313 | -- @ 314 | -- 315 | -- In @esqueleto@, we may write the same query above as: 316 | -- 317 | -- @ 318 | -- 'select' $ 319 | -- 'from' $ \\(p `'LeftOuterJoin`` mb) -> do 320 | -- 'on' ('just' (p '^.' PersonId) '==.' mb '?.' BlogPostAuthorId) 321 | -- 'orderBy' ['asc' (p '^.' PersonName), 'asc' (mb '?.' BlogPostTitle)] 322 | -- return (p, mb) 323 | -- @ 324 | -- 325 | -- On a @LEFT OUTER JOIN@ the entity on the right hand side may 326 | -- not exist (i.e. there may be a @Person@ without any 327 | -- @BlogPost@s), so while @p :: SqlExpr (Entity Person)@, we have 328 | -- @mb :: SqlExpr (Maybe (Entity BlogPost))@. The whole 329 | -- expression above has type @SqlPersist m [(Entity Person, Maybe 330 | -- (Entity BlogPost))]@. Instead of using @(^.)@, we used 331 | -- @('?.')@ to project a field from a @Maybe (Entity a)@. 332 | -- 333 | -- We are by no means limited to joins of two tables, nor by 334 | -- joins of different tables. For example, we may want a list 335 | -- of the @Follow@ entity: 336 | -- 337 | -- @ 338 | -- SELECT P1.*, Follow.*, P2.* 339 | -- FROM Person AS P1 340 | -- INNER JOIN Follow ON P1.id = Follow.follower 341 | -- INNER JOIN Person AS P2 ON P2.id = Follow.followed 342 | -- @ 343 | -- 344 | -- In @esqueleto@, we may write the same query above as: 345 | -- 346 | -- @ 347 | -- 'select' $ 348 | -- 'from' $ \\(p1 `'InnerJoin`` f `'InnerJoin`` p2) -> do 349 | -- 'on' (p1 '^.' PersonId '==.' f '^.' FollowFollower) 350 | -- 'on' (p2 '^.' PersonId '==.' f '^.' FollowFollowed) 351 | -- return (p1, f, p2) 352 | -- @ 353 | -- 354 | -- We also currently support @UPDATE@ and @DELETE@ statements. 355 | -- For example: 356 | -- 357 | -- @ 358 | -- do 'update' $ \\p -> do 359 | -- 'set' p [ PersonName '=.' 'val' \"João\" ] 360 | -- 'where_' (p '^.' PersonName '==.' 'val' \"Joao\") 361 | -- 'delete' $ 362 | -- 'from' $ \\p -> do 363 | -- 'where_' (p '^.' PersonAge '<.' 'just' ('val' 14)) 364 | -- @ 365 | -- 366 | -- The results of queries can also be used for insertions. 367 | -- In @SQL@, we might write the following, inserting a new blog 368 | -- post for every user: 369 | -- 370 | -- @ 371 | -- INSERT INTO BlogPost 372 | -- SELECT ('Group Blog Post', id) 373 | -- FROM Person 374 | -- @ 375 | -- 376 | -- In @esqueleto@, we may write the same query above as: 377 | -- 378 | -- @ 379 | -- 'insertSelect' $ 'from' $ \\p-> 380 | -- return $ BlogPost '<#' \"Group Blog Post\" '<&>' (p '^.' PersonId) 381 | -- @ 382 | -- 383 | -- Individual insertions can be performed through Persistent's 384 | -- 'insert' function, reexported for convenience. 385 | 386 | 387 | ---------------------------------------------------------------------- 388 | 389 | 390 | -- $reexports 391 | -- 392 | -- We re-export many symbols from @persistent@ for convenince: 393 | -- 394 | -- * \"Store functions\" from "Database.Persist". 395 | -- 396 | -- * Everything from "Database.Persist.Class" except for 397 | -- @PersistQuery@ and @delete@ (use 'deleteKey' instead). 398 | -- 399 | -- * Everything from "Database.Persist.Types" except for 400 | -- @Update@, @SelectOpt@, @BackendSpecificFilter@ and @Filter@. 401 | -- 402 | -- * Everything from "Database.Persist.Sql" except for 403 | -- @deleteWhereCount@ and @updateWhereCount@. 404 | 405 | 406 | ---------------------------------------------------------------------- 407 | 408 | 409 | -- $rdbmsSpecificModules 410 | -- 411 | -- There are many differences between SQL syntax and functions 412 | -- supported by different RDBMSs. Since version 2.2.8, 413 | -- @esqueleto@ includes modules containing functions that are 414 | -- specific to a given RDBMS. 415 | -- 416 | -- * PostgreSQL: "Database.Esqueleto.PostgreSQL". 417 | -- 418 | -- In order to use these functions, you need to explicitly import 419 | -- their corresponding modules, they're not re-exported here. 420 | -------------------------------------------------------------------------------- /src/Database/Esqueleto/MySQL.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | -- | This module contain MySQL-specific functions. 4 | -- 5 | -- @since 2.2.8 6 | module Database.Esqueleto.MySQL 7 | ( random_ 8 | , lockInShareMode 9 | ) where 10 | 11 | import Database.Esqueleto.Internal.Internal hiding (random_) 12 | import Database.Esqueleto.Internal.PersistentImport 13 | 14 | -- | (@random()@) Split out into database specific modules 15 | -- because MySQL uses `rand()`. 16 | -- 17 | -- @since 2.6.0 18 | random_ :: (PersistField a, Num a) => SqlExpr (Value a) 19 | random_ = unsafeSqlValue "RAND()" 20 | 21 | -- | @LOCK IN SHARE MODE@ syntax. 22 | -- 23 | -- Example: 24 | -- 25 | -- @ 26 | -- 'locking' 'lockInShareMode' 27 | -- @ 28 | -- 29 | -- @since 3.6.0.0 30 | lockInShareMode :: LockingKind 31 | lockInShareMode = LockInShareMode 32 | -------------------------------------------------------------------------------- /src/Database/Esqueleto/PostgreSQL/JSON/Instances.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveFoldable #-} 2 | {-# LANGUAGE DeriveFunctor #-} 3 | {-# LANGUAGE DeriveGeneric #-} 4 | {-# LANGUAGE DeriveTraversable #-} 5 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 6 | {-# LANGUAGE OverloadedStrings #-} 7 | {-# language DerivingStrategies #-} 8 | 9 | module Database.Esqueleto.PostgreSQL.JSON.Instances where 10 | 11 | import Data.Aeson (FromJSON(..), ToJSON(..), encode, eitherDecodeStrict) 12 | import Data.Bifunctor (first) 13 | import qualified Data.ByteString.Lazy as BSL (toStrict) 14 | import Data.String (IsString(..)) 15 | import Data.Text (Text) 16 | import qualified Data.Text as T (concat, pack) 17 | import qualified Data.Text.Encoding as TE (decodeUtf8, encodeUtf8) 18 | import Database.Esqueleto.Internal.PersistentImport 19 | import Database.Esqueleto.Internal.Internal (SqlExpr, Value, just, val) 20 | import GHC.Generics (Generic) 21 | 22 | -- | Newtype wrapper around any type with a JSON representation. 23 | -- 24 | -- @since 3.1.0 25 | newtype JSONB a = JSONB { unJSONB :: a } 26 | deriving stock 27 | ( Generic 28 | , Eq 29 | , Foldable 30 | , Functor 31 | , Ord 32 | , Read 33 | , Show 34 | , Traversable 35 | ) 36 | deriving newtype 37 | ( FromJSON 38 | , ToJSON 39 | ) 40 | 41 | -- | 'SqlExpr' of a NULL-able 'JSONB' value. Hence the 'Maybe'. 42 | -- 43 | -- Note: NULL here is a PostgreSQL NULL, not a JSON 'null' 44 | type JSONBExpr a = SqlExpr (Value (Maybe (JSONB a))) 45 | 46 | -- | Convenience function to lift a regular value into 47 | -- a 'JSONB' expression. 48 | jsonbVal :: (FromJSON a, ToJSON a) => a -> JSONBExpr a 49 | jsonbVal = just . val . JSONB 50 | 51 | -- | Used with certain JSON operators. 52 | -- 53 | -- This data type has 'Num' and 'IsString' instances 54 | -- for ease of use by using integer and string literals. 55 | -- 56 | -- >>> 3 :: JSONAccessor 57 | -- JSONIndex 3 58 | -- >>> -3 :: JSONAccessor 59 | -- JSONIndex -3 60 | -- 61 | -- >>> "name" :: JSONAccessor 62 | -- JSONKey "name" 63 | -- 64 | -- NOTE: DO NOT USE ANY OF THE 'Num' METHODS ON THIS TYPE! 65 | data JSONAccessor 66 | = JSONIndex Int 67 | | JSONKey Text 68 | deriving (Generic, Eq, Show) 69 | 70 | -- | I repeat, DO NOT use any method other than 'fromInteger'! 71 | instance Num JSONAccessor where 72 | fromInteger = JSONIndex . fromInteger 73 | negate (JSONIndex i) = JSONIndex $ negate i 74 | negate (JSONKey _) = error "Can not negate a JSONKey" 75 | (+) = numErr 76 | (-) = numErr 77 | (*) = numErr 78 | abs = numErr 79 | signum = numErr 80 | 81 | numErr :: a 82 | numErr = error "Do not use 'Num' methods on JSONAccessors" 83 | 84 | instance IsString JSONAccessor where 85 | fromString = JSONKey . T.pack 86 | 87 | -- | @since 3.1.0 88 | instance (FromJSON a, ToJSON a) => PersistField (JSONB a) where 89 | toPersistValue = PersistLiteralEscaped . BSL.toStrict . encode . unJSONB 90 | fromPersistValue pVal = fmap JSONB $ case pVal of 91 | PersistByteString bs -> first (badParse $ TE.decodeUtf8 bs) $ eitherDecodeStrict bs 92 | PersistText t -> first (badParse t) $ eitherDecodeStrict (TE.encodeUtf8 t) 93 | x -> Left $ fromPersistValueError "string or bytea" x 94 | 95 | -- | jsonb 96 | -- 97 | -- @since 3.1.0 98 | instance (FromJSON a, ToJSON a) => PersistFieldSql (JSONB a) where 99 | sqlType _ = SqlOther "JSONB" 100 | 101 | badParse :: Text -> String -> Text 102 | badParse t = fromPersistValueParseError t . T.pack 103 | 104 | fromPersistValueError 105 | :: Text -- ^ Database type(s), should appear different from Haskell name, e.g. "integer" or "INT", not "Int". 106 | -> PersistValue -- ^ Incorrect value 107 | -> Text -- ^ Error message 108 | fromPersistValueError databaseType received = T.concat 109 | [ "Failed to parse Haskell newtype `JSONB a`; " 110 | , "expected ", databaseType 111 | , " from database, but received: ", T.pack (show received) 112 | , ". Potential solution: Check that your database schema matches your Persistent model definitions." 113 | ] 114 | 115 | fromPersistValueParseError 116 | :: Text -- ^ Received value 117 | -> Text -- ^ Additional error 118 | -> Text -- ^ Error message 119 | fromPersistValueParseError received err = T.concat 120 | [ "Failed to parse Haskell type `JSONB a`, " 121 | , "but received ", received 122 | , " | with error: ", err 123 | ] 124 | -------------------------------------------------------------------------------- /src/Database/Esqueleto/SQLite.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | -- | This module contain SQLite-specific functions. 4 | -- 5 | -- @since 2.2.8 6 | module Database.Esqueleto.SQLite 7 | ( random_ 8 | ) where 9 | 10 | import Database.Esqueleto.Internal.Internal hiding (random_) 11 | import Database.Esqueleto.Internal.PersistentImport 12 | 13 | -- | (@random()@) Split out into database specific modules 14 | -- because MySQL uses `rand()`. 15 | -- 16 | -- /Since: 2.6.0/ 17 | random_ :: (PersistField a, Num a) => SqlExpr (Value a) 18 | random_ = unsafeSqlValue "RANDOM()" 19 | -------------------------------------------------------------------------------- /stack-8.10.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-18.28 2 | 3 | packages: 4 | - '.' 5 | - 'examples' 6 | 7 | allow-newer: true 8 | 9 | extra-deps: 10 | - lift-type-0.1.0.1 11 | - persistent-2.14.0.2 12 | 13 | nix: 14 | packages: [zlib, libmysqlclient, pcre, postgresql] 15 | -------------------------------------------------------------------------------- /stack-8.2.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-10.6 2 | 3 | packages: 4 | - '.' 5 | 6 | extra-deps: 7 | - aeson-1.4.1.0 8 | - aeson-compat-0.3.8 9 | - attoparsec-0.13.2.2 10 | - case-insensitive-1.2.0.11 11 | - conduit-1.3.0 12 | - conduit-extra-1.3.0 13 | - hashable-1.2.7.0 14 | - monad-logger-0.3.28.1 15 | - persistent-2.10.0 16 | - persistent-mysql-2.10.0 17 | - persistent-postgresql-2.10.0 18 | - persistent-sqlite-2.10.0 19 | - persistent-template-2.7.0 20 | - postgresql-libpq-0.9.4.2 21 | - postgresql-simple-0.6.1 22 | - resourcet-1.2.0 23 | - scientific-0.3.6.2 24 | - text-1.2.3.0 25 | - unliftio-0.2.0.0 26 | 27 | nix: 28 | packages: [zlib, libmysqlclient, pcre, postgresql] 29 | -------------------------------------------------------------------------------- /stack-8.4.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-12.2 2 | 3 | packages: 4 | - '.' 5 | 6 | extra-deps: 7 | - aeson-1.4.1.0 8 | - persistent-2.10.0 9 | - persistent-postgresql-2.10.0 10 | - persistent-sqlite-2.10.0 11 | - persistent-mysql-2.10.0 12 | - persistent-template-2.7.0 13 | - postgresql-libpq-0.9.4.2 14 | - postgresql-simple-0.6.1 15 | - transformers-0.5.5.2 16 | 17 | nix: 18 | packages: [zlib, libmysqlclient, pcre, postgresql] 19 | -------------------------------------------------------------------------------- /stack-8.6.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-13.6 2 | 3 | packages: 4 | - '.' 5 | - 'examples' 6 | 7 | allow-newer: true 8 | 9 | extra-deps: 10 | - persistent-2.14.0.1 11 | - persistent-postgresql-2.13.5.0 12 | - persistent-mysql-2.13.0.0 13 | - persistent-sqlite-2.13.0.0 14 | - postgresql-simple-0.6.4 15 | - postgresql-libpq-0.9.4.3 16 | - time-compat-1.9.6.1 17 | - base-orphans-0.8.6 18 | - hashable-1.2.7.0 19 | - lift-type-0.1.0.1 20 | - th-lift-instances-0.1.19 21 | - th-lift-0.8.2 22 | 23 | nix: 24 | packages: [zlib, libmysqlclient, pcre, postgresql] 25 | -------------------------------------------------------------------------------- /stack-8.8.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-16.31 2 | 3 | packages: 4 | - '.' 5 | - 'examples' 6 | 7 | extra-deps: 8 | - persistent-2.12.0.1 9 | - persistent-template-2.12.0.0 10 | - persistent-mysql-2.12.0.0 11 | - persistent-postgresql-2.12.0.0 12 | - persistent-sqlite-2.12.0.0 13 | 14 | nix: 15 | packages: [zlib, libmysqlclient, pcre, postgresql] 16 | -------------------------------------------------------------------------------- /stack-8.8.yaml.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by Stack. 2 | # You should not edit this file by hand. 3 | # For more information, please see the documentation at: 4 | # https://docs.haskellstack.org/en/stable/lock_files 5 | 6 | packages: 7 | - completed: 8 | subdir: persistent 9 | name: persistent 10 | version: 2.11.0.0 11 | git: https://github.com/yesodweb/persistent 12 | pantry-tree: 13 | size: 2099 14 | sha256: cd4d895557a60b40543c4a6804d32346a1c14c39e28658bb6852d8f4904ef1de 15 | commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702 16 | original: 17 | subdir: persistent 18 | git: https://github.com/yesodweb/persistent 19 | commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702 20 | - completed: 21 | subdir: persistent-template 22 | name: persistent-template 23 | version: '2.9' 24 | git: https://github.com/yesodweb/persistent 25 | pantry-tree: 26 | size: 620 27 | sha256: 0602872c9c38ccc6966b4a1fd1d102a345f94ad855077157d588536ee6803343 28 | commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702 29 | original: 30 | subdir: persistent-template 31 | git: https://github.com/yesodweb/persistent 32 | commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702 33 | - completed: 34 | subdir: persistent-mysql 35 | name: persistent-mysql 36 | version: 2.10.3 37 | git: https://github.com/yesodweb/persistent 38 | pantry-tree: 39 | size: 577 40 | sha256: a3b9d2ef77af25dca203a4dbe2857b6a1d4e421bbe376f261288e9a8ebfda28f 41 | commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702 42 | original: 43 | subdir: persistent-mysql 44 | git: https://github.com/yesodweb/persistent 45 | commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702 46 | - completed: 47 | subdir: persistent-postgresql 48 | name: persistent-postgresql 49 | version: 2.11.0.0 50 | git: https://github.com/yesodweb/persistent 51 | pantry-tree: 52 | size: 907 53 | sha256: 6f1ad1c5b0b22cf455c6b1b4551a749d21bb72042597450c8ef9ff1eb5a74782 54 | commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702 55 | original: 56 | subdir: persistent-postgresql 57 | git: https://github.com/yesodweb/persistent 58 | commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702 59 | - completed: 60 | subdir: persistent-sqlite 61 | name: persistent-sqlite 62 | version: 2.11.0.0 63 | git: https://github.com/yesodweb/persistent 64 | pantry-tree: 65 | size: 891 66 | sha256: fc9106077e16b406a5a823c732e3b543822a530f2befc446e49acf68797f6d42 67 | commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702 68 | original: 69 | subdir: persistent-sqlite 70 | git: https://github.com/yesodweb/persistent 71 | commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702 72 | snapshots: 73 | - completed: 74 | size: 532382 75 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/16/14.yaml 76 | sha256: 1ef27e36f38824abafc43224ca612211b3828fa9ffd31ba0fc2867ae2e19ba90 77 | original: lts-16.14 78 | -------------------------------------------------------------------------------- /stack-9.0.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-23.1 2 | 3 | packages: 4 | - '.' 5 | - 'examples' 6 | 7 | allow-newer: true 8 | 9 | extra-deps: 10 | - lift-type-0.1.0.1 11 | 12 | nix: 13 | packages: [zlib, libmysqlclient, pcre, postgresql] 14 | -------------------------------------------------------------------------------- /stack-nightly.yaml: -------------------------------------------------------------------------------- 1 | resolver: nightly-2022-03-29 2 | 3 | packages: 4 | - "." 5 | - 'examples' 6 | 7 | 8 | extra-deps: 9 | - time-1.12.1 10 | - base-compat-0.12.1 11 | - directory-1.3.7.0 12 | - process-1.6.14.0 13 | - Cabal-3.6.3.0 14 | - unix-2.7.2.2 15 | 16 | nix: 17 | packages: [zlib, libmysqlclient, pcre, postgresql] 18 | -------------------------------------------------------------------------------- /stack-nightly.yaml.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by Stack. 2 | # You should not edit this file by hand. 3 | # For more information, please see the documentation at: 4 | # https://docs.haskellstack.org/en/stable/lock_files 5 | 6 | packages: 7 | - completed: 8 | hackage: time-1.12.1@sha256:af1fafc1fb66e3d0afb66fb5ab8629f74c038bebd42c234b581aff7abc201089,6295 9 | pantry-tree: 10 | size: 7208 11 | sha256: 96205222b57c39156ee646d710a4100a119dc28f211c57cacaf741f6c1bb35da 12 | original: 13 | hackage: time-1.12.1 14 | - completed: 15 | hackage: base-compat-0.12.1@sha256:20e50848d9dfee1523fafe8950060b04fae43d402c15553da5c7cacd116f7846,6960 16 | pantry-tree: 17 | size: 9038 18 | sha256: 2f2c14615443954f117613d77835234b598718e611fb4cf4522e01980bf1bcbd 19 | original: 20 | hackage: base-compat-0.12.1 21 | - completed: 22 | hackage: directory-1.3.7.0@sha256:d44788eac41268d951679fdcc343adc8a65fcf5b016bdf6c1f996bf78dde798e,2940 23 | pantry-tree: 24 | size: 3433 25 | sha256: 2352834a6424cc8b462706c15e08bb721e120829b147b6d798eade4ebce425f5 26 | original: 27 | hackage: directory-1.3.7.0 28 | - completed: 29 | hackage: process-1.6.14.0@sha256:b6ad76fd3f4bf133cdc2dc9176e23447f2a0a8e9316047d53154cd11f871446d,2845 30 | pantry-tree: 31 | size: 1544 32 | sha256: 72300155a8fd5a91f6b25dfebb77db05aa27a0b866dbfb2d7098c5e4580ca105 33 | original: 34 | hackage: process-1.6.14.0 35 | - completed: 36 | hackage: Cabal-3.6.3.0@sha256:ff97c442b0c679c1c9876acd15f73ac4f602b973c45bde42b43ec28265ee48f4,12459 37 | pantry-tree: 38 | size: 19757 39 | sha256: b250a53bdb56844f047a2927833bb565b936a289abfa85dfc2a63148d776368a 40 | original: 41 | hackage: Cabal-3.6.3.0 42 | - completed: 43 | hackage: unix-2.7.2.2@sha256:15f5365c5995634e45de1772b9504761504a310184e676bc2ef60a14536dbef9,3496 44 | pantry-tree: 45 | size: 3536 46 | sha256: 36434ced74d679622d61b69e8d92e1bd632d9ef3e284c63094653b2e473b0553 47 | original: 48 | hackage: unix-2.7.2.2 49 | snapshots: 50 | - completed: 51 | size: 539378 52 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/nightly/2022/3/29.yaml 53 | sha256: c959441a05f6fa4d45ae6e258290f04d399245b8436263b4abb525c7f73da6a5 54 | original: nightly-2022-03-29 55 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | stack-9.0.yaml -------------------------------------------------------------------------------- /style-guide.md: -------------------------------------------------------------------------------- 1 | # Style Guide 2 | 3 | - Please use `stylish-haskell` on the project to keep imports consistent and 4 | clean. We have a custom [`.stylish-haskell.yaml`](.stylish-haskell.yaml) file. 5 | You can run `stylish-haskell` from vim with `:%! stylish-haskell`. 6 | - Four space indent. 7 | - Prefer indentation over any other form of alignment. 8 | - If text goes off the screen due to four space indentation, factor out 9 | functions and values into names to reduce indentation. 10 | -------------------------------------------------------------------------------- /test/Common/Test/CTE.hs: -------------------------------------------------------------------------------- 1 | {-# language TypeApplications #-} 2 | 3 | module Common.Test.CTE where 4 | 5 | import Common.Test.Models 6 | import Common.Test.Import 7 | import Database.Persist.TH 8 | 9 | testCTE :: SpecDb 10 | testCTE = describe "CTE" $ do 11 | itDb "can refer to the same CTE twice" $ do 12 | let q :: SqlQuery (SqlExpr (Value Int), SqlExpr (Value Int)) 13 | q = do 14 | bCte <- with $ do 15 | b <- from $ table @B 16 | pure b 17 | 18 | a :& b1 :& b2 <- from $ 19 | table @A 20 | `innerJoin` bCte 21 | `on` do 22 | \(a :& b) -> 23 | a ^. AK ==. b ^. BK 24 | `innerJoin` bCte 25 | `on` do 26 | \(a :& _ :& b2) -> 27 | a ^. AK ==. b2 ^. BK 28 | pure (a ^. AK, a ^. AV +. b1 ^. BV +. b2 ^. BV) 29 | insert_ $ A { aK = 1, aV = 2 } 30 | insert_ $ B { bK = 1, bV = 3 } 31 | ret <- select q 32 | asserting $ do 33 | ret `shouldMatchList` 34 | [ (Value 1, Value (2 + 3 + 3)) 35 | ] 36 | -------------------------------------------------------------------------------- /test/Common/Test/Import.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP, AllowAmbiguousTypes #-} 2 | {-# LANGUAGE ConstraintKinds #-} 3 | {-# LANGUAGE DataKinds #-} 4 | {-# LANGUAGE DeriveGeneric #-} 5 | {-# LANGUAGE DerivingStrategies #-} 6 | {-# LANGUAGE EmptyDataDecls #-} 7 | {-# LANGUAGE FlexibleContexts #-} 8 | {-# LANGUAGE FlexibleInstances #-} 9 | {-# LANGUAGE GADTs #-} 10 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 11 | {-# LANGUAGE MultiParamTypeClasses #-} 12 | {-# LANGUAGE OverloadedStrings #-} 13 | {-# LANGUAGE PartialTypeSignatures #-} 14 | {-# LANGUAGE QuasiQuotes #-} 15 | {-# LANGUAGE ScopedTypeVariables #-} 16 | {-# LANGUAGE StandaloneDeriving #-} 17 | {-# LANGUAGE TemplateHaskell #-} 18 | {-# LANGUAGE TypeApplications #-} 19 | {-# LANGUAGE TypeFamilies #-} 20 | {-# LANGUAGE TypeSynonymInstances #-} 21 | {-# LANGUAGE UndecidableInstances #-} 22 | 23 | module Common.Test.Import 24 | ( module Common.Test.Import 25 | , module X 26 | ) where 27 | 28 | import System.Environment 29 | import Control.Applicative 30 | import Common.Test.Models as X 31 | import Database.Esqueleto.Experimental as X hiding (random_) 32 | import Test.Hspec as X 33 | import UnliftIO as X 34 | import Control.Monad 35 | import Test.QuickCheck 36 | import Data.Text as X (Text) 37 | import Control.Monad.Trans.Reader as X (ReaderT, mapReaderT, ask) 38 | 39 | type SpecDb = SpecWith ConnectionPool 40 | 41 | asserting :: MonadIO f => IO () -> SqlPersistT f () 42 | asserting a = liftIO a 43 | 44 | noExceptions :: Expectation 45 | noExceptions = pure () 46 | 47 | itDb 48 | :: (HasCallStack) 49 | => String 50 | -> SqlPersistT IO x 51 | -> SpecDb 52 | itDb message action = do 53 | it message $ \connection -> do 54 | void $ testDb connection action 55 | 56 | propDb 57 | :: (HasCallStack, Testable a) 58 | => String 59 | -> ((SqlPersistT IO () -> IO ()) -> a ) 60 | -> SpecDb 61 | propDb message action = do 62 | it message $ \connection -> do 63 | property (action (testDb connection)) 64 | 65 | testDb :: ConnectionPool -> SqlPersistT IO a -> IO a 66 | testDb conn action = 67 | liftIO $ flip runSqlPool conn $ do 68 | a <- action 69 | transactionUndo 70 | pure a 71 | 72 | setDatabaseState 73 | :: SqlPersistT IO a 74 | -> SqlPersistT IO () 75 | -> SpecWith ConnectionPool 76 | -> SpecWith ConnectionPool 77 | setDatabaseState create clean test = 78 | beforeWith (\conn -> runSqlPool create conn >> pure conn) $ 79 | after (\conn -> runSqlPool clean conn) $ 80 | test 81 | 82 | isCI :: IO Bool 83 | isCI = do 84 | env <- getEnvironment 85 | return $ case lookup "TRAVIS" env <|> lookup "CI" env of 86 | Just "true" -> True 87 | _ -> False 88 | -------------------------------------------------------------------------------- /test/Common/Test/Models.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | {-# LANGUAGE ConstraintKinds #-} 3 | {-# LANGUAGE DataKinds #-} 4 | {-# LANGUAGE DeriveGeneric #-} 5 | {-# LANGUAGE DerivingStrategies #-} 6 | {-# LANGUAGE EmptyDataDecls #-} 7 | {-# LANGUAGE FlexibleContexts #-} 8 | {-# LANGUAGE FlexibleInstances #-} 9 | {-# LANGUAGE GADTs #-} 10 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 11 | {-# LANGUAGE MultiParamTypeClasses #-} 12 | {-# LANGUAGE OverloadedStrings #-} 13 | {-# LANGUAGE PartialTypeSignatures #-} 14 | {-# LANGUAGE QuasiQuotes #-} 15 | {-# LANGUAGE ScopedTypeVariables #-} 16 | {-# LANGUAGE StandaloneDeriving #-} 17 | {-# LANGUAGE TemplateHaskell #-} 18 | {-# LANGUAGE TypeApplications #-} 19 | {-# LANGUAGE TypeFamilies #-} 20 | {-# LANGUAGE TypeSynonymInstances #-} 21 | {-# LANGUAGE UndecidableInstances #-} 22 | 23 | module Common.Test.Models where 24 | 25 | import Data.Time 26 | import Database.Esqueleto.Experimental 27 | import Database.Persist.Sql 28 | import Database.Persist.TH 29 | 30 | share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistUpperCase| 31 | Foo 32 | name Int 33 | Primary name 34 | deriving Show Eq Ord 35 | Bar 36 | quux FooId 37 | deriving Show Eq Ord 38 | Baz 39 | blargh FooId 40 | deriving Show Eq 41 | Shoop 42 | baz BazId 43 | deriving Show Eq 44 | Asdf 45 | shoop ShoopId 46 | deriving Show Eq 47 | Another 48 | why BazId 49 | YetAnother 50 | argh ShoopId 51 | 52 | Person 53 | name String 54 | age Int Maybe 55 | weight Int Maybe 56 | favNum Int 57 | deriving Eq Show Ord 58 | BlogPost 59 | title String 60 | authorId PersonId 61 | deriving Eq Show 62 | Comment 63 | body String 64 | blog BlogPostId 65 | deriving Eq Show 66 | CommentReply 67 | body String 68 | comment CommentId 69 | Profile 70 | name String 71 | person PersonId 72 | deriving Eq Show 73 | Reply 74 | guy PersonId 75 | body String 76 | deriving Eq Show 77 | 78 | Lord 79 | county String maxlen=100 80 | dogs Int Maybe 81 | Primary county 82 | deriving Eq Show 83 | 84 | Deed 85 | contract String maxlen=100 86 | ownerId LordId maxlen=100 87 | Primary contract 88 | deriving Eq Show 89 | 90 | Follow 91 | follower PersonId 92 | followed PersonId 93 | deriving Eq Show 94 | 95 | CcList 96 | names [String] 97 | 98 | Frontcover 99 | number Int 100 | title String 101 | Primary number 102 | deriving Eq Show 103 | Article 104 | title String 105 | frontcoverNumber Int 106 | Foreign Frontcover fkfrontcover frontcoverNumber 107 | deriving Eq Show 108 | ArticleMetadata 109 | articleId ArticleId 110 | Primary articleId 111 | deriving Eq Show 112 | Tag 113 | name String maxlen=100 114 | Primary name 115 | deriving Eq Show 116 | ArticleTag 117 | articleId ArticleId 118 | tagId TagId maxlen=100 119 | Primary articleId tagId 120 | deriving Eq Show 121 | Article2 122 | title String 123 | frontcoverId FrontcoverId 124 | deriving Eq Show 125 | Point 126 | x Int 127 | y Int 128 | name String 129 | Primary x y 130 | deriving Eq Show 131 | Circle 132 | centerX Int 133 | centerY Int 134 | name String 135 | Foreign Point fkpoint centerX centerY 136 | deriving Eq Show 137 | Numbers 138 | int Int 139 | double Double 140 | deriving Eq Show 141 | 142 | JoinOne 143 | name String 144 | deriving Eq Show 145 | 146 | JoinTwo 147 | joinOne JoinOneId 148 | name String 149 | deriving Eq Show 150 | 151 | JoinThree 152 | joinTwo JoinTwoId 153 | name String 154 | deriving Eq Show 155 | 156 | JoinFour 157 | name String 158 | joinThree JoinThreeId 159 | deriving Eq Show 160 | 161 | JoinOther 162 | name String 163 | deriving Eq Show 164 | 165 | JoinMany 166 | name String 167 | joinOther JoinOtherId 168 | joinOne JoinOneId 169 | deriving Eq Show 170 | 171 | DateTruncTest 172 | created UTCTime 173 | deriving Eq Show 174 | 175 | User 176 | address AddressId Maybe 177 | name String 178 | deriving Show 179 | deriving Eq 180 | 181 | Address 182 | address String 183 | deriving Show 184 | deriving Eq 185 | 186 | A 187 | k Int 188 | v Int 189 | Primary k 190 | 191 | B 192 | k Int 193 | v Int 194 | Primary k 195 | |] 196 | 197 | -- Unique Test schema 198 | share [mkPersist sqlSettings, mkMigrate "migrateUnique"] [persistUpperCase| 199 | OneUnique 200 | name String 201 | value Int 202 | UniqueValue value 203 | deriving Eq Show 204 | |] 205 | 206 | 207 | instance ToBaseId ArticleMetadata where 208 | type BaseEnt ArticleMetadata = Article 209 | toBaseIdWitness articleId = ArticleMetadataKey articleId 210 | -------------------------------------------------------------------------------- /test/Common/Test/Select.hs: -------------------------------------------------------------------------------- 1 | module Common.Test.Select where 2 | 3 | import Common.Test.Import 4 | 5 | testSelect :: SpecDb 6 | testSelect = do 7 | describe "select" $ do 8 | itDb "works for a single value" $ do 9 | ret <- select $ return $ val (3 :: Int) 10 | asserting $ ret `shouldBe` [ Value 3 ] 11 | 12 | itDb "works for a pair of a single value and ()" $ do 13 | ret <- select $ return (val (3 :: Int), ()) 14 | asserting $ ret `shouldBe` [ (Value 3, ()) ] 15 | 16 | itDb "works for a single ()" $ do 17 | ret <- select $ return () 18 | asserting $ ret `shouldBe` [ () ] 19 | 20 | itDb "works for a single NULL value" $ do 21 | ret <- select $ return nothing 22 | asserting $ ret `shouldBe` [ Value (Nothing :: Maybe Int) ] 23 | -------------------------------------------------------------------------------- /test/MySQL/Test.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE RankNTypes #-} 3 | {-# LANGUAGE ScopedTypeVariables #-} 4 | {-# LANGUAGE TypeApplications #-} 5 | {-# LANGUAGE TypeFamilies #-} 6 | 7 | module MySQL.Test where 8 | 9 | import Common.Test.Import hiding (from, on) 10 | 11 | import Control.Applicative 12 | import Control.Monad (void) 13 | import Control.Monad.IO.Class (MonadIO(liftIO)) 14 | import Control.Monad.Logger (runNoLoggingT, runStderrLoggingT) 15 | import Control.Monad.Trans.Reader (ReaderT, mapReaderT) 16 | import qualified Control.Monad.Trans.Resource as R 17 | import Database.Esqueleto 18 | import Database.Esqueleto.Experimental hiding (from, on) 19 | import qualified Database.Esqueleto.Experimental as Experimental 20 | import Database.Persist.MySQL 21 | ( connectDatabase 22 | , connectHost 23 | , connectPassword 24 | , connectPort 25 | , connectUser 26 | , createMySQLPool 27 | , defaultConnectInfo 28 | , withMySQLConn 29 | ) 30 | 31 | import Test.Hspec 32 | 33 | import Common.Test 34 | import Data.Maybe (fromMaybe) 35 | import System.Environment (lookupEnv) 36 | 37 | testMysqlSum :: SpecDb 38 | testMysqlSum = do 39 | itDb "works with sum_" $ do 40 | _ <- insert' p1 41 | _ <- insert' p2 42 | _ <- insert' p3 43 | _ <- insert' p4 44 | ret <- select $ 45 | from $ \p-> 46 | return $ joinV $ sum_ (p ^. PersonAge) 47 | liftIO $ ret `shouldBe` [ Value $ Just (36 + 17 + 17 :: Double ) ] 48 | 49 | testMysqlTwoAscFields :: SpecDb 50 | testMysqlTwoAscFields = do 51 | itDb "works with two ASC fields (one call)" $ do 52 | p1e <- insert' p1 53 | p2e <- insert' p2 54 | p3e <- insert' p3 55 | p4e <- insert' p4 56 | ret <- select $ 57 | from $ \p -> do 58 | orderBy [asc (p ^. PersonAge), asc (p ^. PersonName)] 59 | return p 60 | liftIO $ ret `shouldBe` [ p2e, p4e, p3e, p1e ] 61 | 62 | testMysqlOneAscOneDesc :: SpecDb 63 | testMysqlOneAscOneDesc = do 64 | itDb "works with one ASC and one DESC field (two calls)" $ do 65 | p1e <- insert' p1 66 | p2e <- insert' p2 67 | p3e <- insert' p3 68 | p4e <- insert' p4 69 | ret <- select $ 70 | from $ \p -> do 71 | orderBy [desc (p ^. PersonAge)] 72 | orderBy [asc (p ^. PersonName)] 73 | return p 74 | liftIO $ ret `shouldBe` [ p1e, p4e, p3e, p2e ] 75 | 76 | 77 | 78 | 79 | testMysqlCoalesce :: SpecDb 80 | testMysqlCoalesce = do 81 | itDb "works on PostgreSQL and MySQL with <2 arguments" $ do 82 | _ :: [Value (Maybe Int)] <- 83 | select $ 84 | from $ \p -> do 85 | return (coalesce [p ^. PersonAge]) 86 | return () 87 | 88 | 89 | 90 | 91 | testMysqlUpdate :: SpecDb 92 | testMysqlUpdate = do 93 | itDb "works on a simple example" $ do 94 | p1k <- insert p1 95 | p2k <- insert p2 96 | p3k <- insert p3 97 | let anon = "Anonymous" 98 | () <- update $ \p -> do 99 | set p [ PersonName =. val anon 100 | , PersonAge *=. just (val 2) ] 101 | where_ (p ^. PersonName !=. val "Mike") 102 | n <- updateCount $ \p -> do 103 | set p [ PersonAge +=. just (val 1) ] 104 | where_ (p ^. PersonName !=. val "Mike") 105 | ret <- select $ 106 | from $ \p -> do 107 | orderBy [ asc (p ^. PersonName), asc (p ^. PersonAge) ] 108 | return p 109 | -- MySQL: nulls appear first, and update returns actual number 110 | -- of changed rows 111 | liftIO $ n `shouldBe` 1 112 | liftIO $ ret `shouldBe` [ Entity p2k (Person anon Nothing (Just 37) 2) 113 | , Entity p1k (Person anon (Just 73) Nothing 1) 114 | , Entity p3k p3 ] 115 | 116 | nameContains :: (SqlString s) 117 | => (SqlExpr (Value [Char]) 118 | -> SqlExpr (Value s) 119 | -> SqlExpr (Value Bool)) 120 | -> s 121 | -> [Entity Person] 122 | -> SqlPersistT IO () 123 | nameContains f t expected = do 124 | ret <- select $ 125 | from $ \p -> do 126 | where_ (f 127 | (p ^. PersonName) 128 | (concat_ [(%), val t, (%)])) 129 | orderBy [asc (p ^. PersonName)] 130 | return p 131 | liftIO $ ret `shouldBe` expected 132 | 133 | 134 | testMysqlTextFunctions :: SpecDb 135 | testMysqlTextFunctions = do 136 | describe "text functions" $ do 137 | itDb "like, (%) and (++.) work on a simple example" $ do 138 | [p1e, p2e, p3e, p4e] <- mapM insert' [p1, p2, p3, p4] 139 | nameContains like "h" [p1e, p2e] 140 | nameContains like "i" [p4e, p3e] 141 | nameContains like "iv" [p4e] 142 | 143 | 144 | testMysqlUnionWithLimits :: SpecDb 145 | testMysqlUnionWithLimits = do 146 | describe "MySQL Union" $ do 147 | itDb "supports limit/orderBy by parenthesizing" $ do 148 | mapM_ (insert . Foo) [1..6] 149 | 150 | let q1 = do 151 | foo <- Experimental.from $ Table @Foo 152 | where_ $ foo ^. FooName <=. val 3 153 | orderBy [asc $ foo ^. FooName] 154 | limit 2 155 | pure $ foo ^. FooName 156 | 157 | let q2 = do 158 | foo <- Experimental.from $ Table @Foo 159 | where_ $ foo ^. FooName >. val 3 160 | orderBy [asc $ foo ^. FooName] 161 | limit 2 162 | pure $ foo ^. FooName 163 | 164 | 165 | ret <- select $ Experimental.from $ q1 `union_` q2 166 | liftIO $ ret `shouldMatchList` [Value 1, Value 2, Value 4, Value 5] 167 | 168 | spec :: Spec 169 | spec = beforeAll mkConnectionPool $ do 170 | tests 171 | 172 | describe "MySQL specific tests" $ do 173 | -- definitely doesn't work at the moment 174 | -- testMysqlRandom 175 | testMysqlSum 176 | testMysqlTwoAscFields 177 | testMysqlOneAscOneDesc 178 | testMysqlCoalesce 179 | testMysqlUpdate 180 | testMysqlTextFunctions 181 | testMysqlUnionWithLimits 182 | 183 | verbose :: Bool 184 | verbose = False 185 | 186 | migrateIt :: R.MonadUnliftIO m => SqlPersistT m () 187 | migrateIt = do 188 | mapReaderT R.runResourceT $ void $ runMigrationSilent migrateAll 189 | cleanDB 190 | 191 | mkConnectionPool :: IO ConnectionPool 192 | mkConnectionPool = do 193 | ci <- isCI 194 | mysqlHost <- (fromMaybe "localhost" <$> lookupEnv "MYSQL_HOST") 195 | let connInfo 196 | | ci = 197 | defaultConnectInfo 198 | { connectHost = "127.0.0.1" 199 | , connectUser = "travis" 200 | , connectPassword = "esqutest" 201 | , connectDatabase = "esqutest" 202 | , connectPort = 33306 203 | } 204 | | otherwise = 205 | defaultConnectInfo 206 | { connectHost = mysqlHost 207 | , connectUser = "travis" 208 | , connectPassword = "esqutest" 209 | , connectDatabase = "esqutest" 210 | , connectPort = 3306 211 | } 212 | pool <- 213 | if verbose 214 | then 215 | runStderrLoggingT $ 216 | createMySQLPool connInfo 4 217 | else 218 | runNoLoggingT $ 219 | createMySQLPool connInfo 4 220 | 221 | 222 | flip runSqlPool pool $ do 223 | migrateIt 224 | cleanDB 225 | 226 | pure pool 227 | -------------------------------------------------------------------------------- /test/PostgreSQL/MigrateJSON.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE DerivingStrategies #-} 3 | {-# LANGUAGE FlexibleContexts #-} 4 | {-# LANGUAGE FlexibleInstances #-} 5 | {-# LANGUAGE GADTs #-} 6 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 7 | {-# LANGUAGE MultiParamTypeClasses #-} 8 | {-# LANGUAGE OverloadedStrings #-} 9 | {-# LANGUAGE QuasiQuotes #-} 10 | {-# LANGUAGE RankNTypes #-} 11 | {-# LANGUAGE ScopedTypeVariables #-} 12 | {-# LANGUAGE StandaloneDeriving #-} 13 | {-# LANGUAGE TemplateHaskell #-} 14 | {-# LANGUAGE TypeFamilies #-} 15 | {-# LANGUAGE UndecidableInstances #-} 16 | 17 | module PostgreSQL.MigrateJSON where 18 | 19 | import Common.Test.Import hiding (Value, from, on) 20 | 21 | import Data.Aeson (Value) 22 | import Database.Esqueleto.Legacy (from) 23 | import Database.Esqueleto.PostgreSQL.JSON (JSONB) 24 | import Database.Persist.TH 25 | 26 | -- JSON Table for PostgreSQL 27 | share [mkPersist sqlSettings, mkMigrate "migrateJSON"] [persistUpperCase| 28 | Json 29 | value (JSONB Value) 30 | deriving Show 31 | |] 32 | 33 | cleanJSON 34 | :: forall m. MonadIO m 35 | => SqlPersistT m () 36 | cleanJSON = delete $ from $ \(_ :: SqlExpr (Entity Json)) -> return () 37 | -------------------------------------------------------------------------------- /test/SQLite/Test.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RankNTypes #-} 4 | {-# LANGUAGE ScopedTypeVariables #-} 5 | {-# LANGUAGE TypeFamilies #-} 6 | 7 | module SQLite.Test where 8 | 9 | import Common.Test.Import hiding (from, on) 10 | 11 | import Control.Monad (void) 12 | import Control.Monad.Logger (runNoLoggingT, runStderrLoggingT) 13 | import Database.Esqueleto.Legacy hiding (random_) 14 | import Database.Esqueleto.SQLite (random_) 15 | import Database.Persist.Sqlite (createSqlitePool) 16 | import Database.Sqlite (SqliteException) 17 | 18 | import Common.Test 19 | 20 | testSqliteRandom :: SpecDb 21 | testSqliteRandom = do 22 | itDb "works with random_" $ do 23 | _ <- select $ return (random_ :: SqlExpr (Value Int)) 24 | asserting noExceptions 25 | 26 | testSqliteSum :: SpecDb 27 | testSqliteSum = do 28 | itDb "works with sum_" $ do 29 | _ <- insert' p1 30 | _ <- insert' p2 31 | _ <- insert' p3 32 | _ <- insert' p4 33 | ret <- select $ 34 | from $ \p-> 35 | return $ joinV $ sum_ (p ^. PersonAge) 36 | asserting $ ret `shouldBe` [ Value $ Just (36 + 17 + 17 :: Int) ] 37 | 38 | 39 | 40 | 41 | 42 | testSqliteTwoAscFields :: SpecDb 43 | testSqliteTwoAscFields = do 44 | itDb "works with two ASC fields (one call)" $ do 45 | p1e <- insert' p1 46 | p2e <- insert' p2 47 | p3e <- insert' p3 48 | p4e <- insert' p4 49 | ret <- select $ 50 | from $ \p -> do 51 | orderBy [asc (p ^. PersonAge), asc (p ^. PersonName)] 52 | return p 53 | -- in SQLite and MySQL, its the reverse 54 | asserting $ ret `shouldBe` [ p2e, p4e, p3e, p1e ] 55 | 56 | testSqliteOneAscOneDesc :: SpecDb 57 | testSqliteOneAscOneDesc = do 58 | itDb "works with one ASC and one DESC field (two calls)" $ do 59 | p1e <- insert' p1 60 | p2e <- insert' p2 61 | p3e <- insert' p3 62 | p4e <- insert' p4 63 | ret <- select $ 64 | from $ \p -> do 65 | orderBy [desc (p ^. PersonAge)] 66 | orderBy [asc (p ^. PersonName)] 67 | return p 68 | asserting $ ret `shouldBe` [ p1e, p4e, p3e, p2e ] 69 | 70 | testSqliteCoalesce :: SpecDb 71 | testSqliteCoalesce = do 72 | itDb "throws an exception on SQLite with <2 arguments" $ do 73 | eres <- try $ select $ 74 | from $ \p -> do 75 | return (coalesce [p ^. PersonAge]) :: SqlQuery (SqlExpr (Value (Maybe Int))) 76 | asserting $ case eres of 77 | Left (_ :: SqliteException) -> 78 | pure () 79 | Right _ -> 80 | expectationFailure "Expected SqliteException with <2 args to coalesce" 81 | 82 | testSqliteUpdate :: SpecDb 83 | testSqliteUpdate = do 84 | itDb "works on a simple example" $ do 85 | p1k <- insert p1 86 | p2k <- insert p2 87 | p3k <- insert p3 88 | let anon = "Anonymous" :: String 89 | () <- update $ \p -> do 90 | set p [ PersonName =. val anon 91 | , PersonAge *=. just (val 2) ] 92 | where_ (p ^. PersonName !=. val "Mike") 93 | n <- updateCount $ \p -> do 94 | set p [ PersonAge +=. just (val 1) ] 95 | where_ (p ^. PersonName !=. val "Mike") 96 | ret <- select $ 97 | from $ \p -> do 98 | orderBy [ asc (p ^. PersonName), asc (p ^. PersonAge) ] 99 | return p 100 | -- SQLite: nulls appear first, update returns matched rows. 101 | asserting $ do 102 | n `shouldBe` 2 103 | ret `shouldMatchList` 104 | [ Entity p2k (Person anon Nothing (Just 37) 2) 105 | , Entity p1k (Person anon (Just 73) Nothing 1) 106 | , Entity p3k p3 107 | ] 108 | 109 | testSqliteTextFunctions :: SpecDb 110 | testSqliteTextFunctions = do 111 | describe "text functions" $ do 112 | itDb "like, (%) and (++.) work on a simple example" $ do 113 | let query :: String -> SqlPersistT IO [Entity Person] 114 | query t = 115 | select $ 116 | from $ \p -> do 117 | where_ (like 118 | (p ^. PersonName) 119 | ((%) ++. val t ++. (%))) 120 | orderBy [asc (p ^. PersonName)] 121 | return p 122 | [p1e, p2e, p3e, p4e] <- mapM insert' [p1, p2, p3, p4] 123 | r0 <- query "h" 124 | r1 <- query "i" 125 | r2 <- query "iv" 126 | asserting $ do 127 | r0 `shouldBe` [p1e, p2e] 128 | r1 `shouldBe` [p4e, p3e] 129 | r2 `shouldBe` [p4e] 130 | 131 | spec :: HasCallStack => Spec 132 | spec = beforeAll mkConnectionPool $ do 133 | tests 134 | 135 | describe "SQLite specific tests" $ do 136 | testAscRandom random_ 137 | testSqliteRandom 138 | testSqliteSum 139 | testSqliteTwoAscFields 140 | testSqliteOneAscOneDesc 141 | testSqliteCoalesce 142 | testSqliteUpdate 143 | testSqliteTextFunctions 144 | 145 | mkConnectionPool :: IO ConnectionPool 146 | mkConnectionPool = do 147 | conn <- 148 | if verbose 149 | then runStderrLoggingT $ 150 | createSqlitePool ".esqueleto-test.sqlite" 4 151 | else runNoLoggingT $ 152 | createSqlitePool ".esqueleto-test.sqlite" 4 153 | flip runSqlPool conn $ do 154 | migrateIt 155 | 156 | pure conn 157 | 158 | verbose :: Bool 159 | verbose = False 160 | 161 | migrateIt :: MonadUnliftIO m => SqlPersistT m () 162 | migrateIt = do 163 | void $ runMigrationSilent migrateAll 164 | cleanDB 165 | -------------------------------------------------------------------------------- /test/Spec.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Test.Hspec 4 | import Test.Hspec.Core.Spec 5 | 6 | import qualified SQLite.Test as SQLite 7 | import qualified MySQL.Test as MySQL 8 | import qualified PostgreSQL.Test as Postgres 9 | 10 | main :: IO () 11 | main = hspec spec 12 | 13 | spec :: Spec 14 | spec = do 15 | parallel $ describe "Esqueleto" $ do 16 | describe "SQLite" $ do 17 | sequential $ SQLite.spec 18 | describe "MySQL" $ do 19 | sequential $ MySQL.spec 20 | describe "Postgresql" $ do 21 | sequential $ Postgres.spec 22 | -------------------------------------------------------------------------------- /test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # docker-compose file for running postgres and mysql DBMS 2 | 3 | # If using this to run the tests, 4 | # while these containers are running (i.e. after something like) 5 | # (cd test; docker-compose up -d) 6 | # the tests must be told to use the hostname via MYSQL_HOST environment variable 7 | # e.g. something like: 8 | # MYSQL_HOST=127.0.0.1 stack test 9 | 10 | version: '3' 11 | services: 12 | postgres: 13 | image: 'postgres:15.2-alpine' 14 | environment: 15 | POSTGRES_USER: esqutest 16 | POSTGRES_PASSWORD: esqutest 17 | POSTGRES_DB: esqutest 18 | ports: 19 | - 5432:5432 20 | mysql: 21 | image: 'mysql:8.0.32' 22 | environment: 23 | MYSQL_USER: travis 24 | MYSQL_PASSWORD: esqutest 25 | MYSQL_ROOT_PASSWORD: esqutest 26 | MYSQL_DATABASE: esqutest 27 | ports: 28 | - 3306:3306 29 | -------------------------------------------------------------------------------- /test/expected-compile-failures/.gitignore: -------------------------------------------------------------------------------- 1 | .stack-work/ 2 | expected-compile-failures.cabal 3 | *~ -------------------------------------------------------------------------------- /test/expected-compile-failures/README.md: -------------------------------------------------------------------------------- 1 | # expected-compile-failures 2 | 3 | This subdirectory contains a stack project for expected compilation failures. To 4 | add a new "test case", create a new `executable` stanza in the `package.yaml` 5 | file. The Travis CI test script ([`test.sh`](test.sh)) will attempt to compile 6 | the executable and will exit with an error if it successfully compiled. 7 | -------------------------------------------------------------------------------- /test/expected-compile-failures/Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /test/expected-compile-failures/package.yaml: -------------------------------------------------------------------------------- 1 | name: expected-compile-failures 2 | version: 0.1.0.0 3 | github: bitemyapp/esqueleto 4 | license: BSD3 5 | author: Matt Parsons 6 | maintainer: parsonsmatt@gmail.com 7 | copyright: 2018 Matt Parsons 8 | 9 | extra-source-files: 10 | - README.md 11 | 12 | description: Please see the README on GitHub at 13 | 14 | dependencies: 15 | - base >= 4.7 && < 5 16 | - esqueleto 17 | - persistent 18 | - persistent-template 19 | 20 | default-extensions: 21 | - FlexibleContexts 22 | - FlexibleInstances 23 | - GADTs 24 | - GeneralizedNewtypeDeriving 25 | - MultiParamTypeClasses 26 | - NoMonomorphismRestriction 27 | - OverloadedStrings 28 | - QuasiQuotes 29 | - ScopedTypeVariables 30 | - StandaloneDeriving 31 | - TemplateHaskell 32 | - TypeFamilies 33 | 34 | library: 35 | source-dirs: src 36 | 37 | executables: 38 | update-with-read-role: 39 | main: Main.hs 40 | source-dirs: update-read-role 41 | ghc-options: 42 | - -threaded 43 | - -rtsopts 44 | - -with-rtsopts=-N 45 | dependencies: 46 | - expected-compile-failures 47 | write-with-read-role: 48 | main: Main.hs 49 | source-dirs: write-read-role 50 | ghc-options: 51 | - -threaded 52 | - -rtsopts 53 | - -with-rtsopts=-N 54 | dependencies: 55 | - expected-compile-failures 56 | -------------------------------------------------------------------------------- /test/expected-compile-failures/src/Lib.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE UndecidableInstances, DerivingStrategies #-} 2 | module Lib where 3 | 4 | import Control.Monad.IO.Class (MonadIO) 5 | import Database.Persist 6 | import Database.Persist.Sql (SqlReadT) 7 | import Database.Esqueleto (SqlExpr, SqlQuery, from, 8 | val, (<#), insertSelect, (<&>), (^.)) 9 | import Database.Esqueleto.Internal.Language (Insertion) 10 | import Database.Persist.TH (mkDeleteCascade, 11 | mkMigrate, mkPersist, 12 | persistLowerCase, share, 13 | sqlSettings) 14 | 15 | share [ mkPersist sqlSettings 16 | , mkDeleteCascade sqlSettings 17 | , mkMigrate "migrateAll"] [persistLowerCase| 18 | Person 19 | name String 20 | age Int Maybe 21 | born Int Maybe 22 | deriving Eq Show 23 | BlogPost 24 | title String 25 | authorId PersonId 26 | deriving Eq Show 27 | Follow 28 | follower PersonId 29 | followed PersonId 30 | deriving Eq Show 31 | |] 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/expected-compile-failures/stack-8.2.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-10.6 2 | 3 | extra-deps: 4 | - aeson-1.4.1.0 5 | - aeson-compat-0.3.8 6 | - attoparsec-0.13.2.2 7 | - case-insensitive-1.2.0.11 8 | - conduit-1.3.0 9 | - conduit-extra-1.3.0 10 | - hashable-1.2.7.0 11 | - monad-logger-0.3.28.1 12 | - persistent-2.10.0 13 | - persistent-mysql-2.10.0 14 | - persistent-postgresql-2.10.0 15 | - persistent-sqlite-2.10.0 16 | - persistent-template-2.7.0 17 | - postgresql-libpq-0.9.4.2 18 | - postgresql-simple-0.6.1 19 | - resourcet-1.2.0 20 | - scientific-0.3.6.2 21 | - text-1.2.3.0 22 | - unliftio-0.2.0.0 23 | 24 | packages: 25 | - . 26 | - ../../../esqueleto 27 | -------------------------------------------------------------------------------- /test/expected-compile-failures/stack-8.4.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-12.24 2 | 3 | packages: 4 | - . 5 | - ../../../esqueleto 6 | 7 | extra-deps: 8 | - aeson-1.4.1.0 9 | - persistent-2.10.0 10 | - persistent-mysql-2.10.0 11 | - persistent-postgresql-2.10.0 12 | - persistent-sqlite-2.10.0 13 | - persistent-template-2.7.0 14 | - postgresql-libpq-0.9.4.2 15 | - postgresql-simple-0.6.1 16 | - transformers-0.5.5.2 17 | allow-newer: true 18 | -------------------------------------------------------------------------------- /test/expected-compile-failures/stack-8.6.yaml: -------------------------------------------------------------------------------- 1 | resolver: nightly-2018-12-18 2 | 3 | extra-deps: 4 | - persistent-2.10.0 5 | - persistent-mysql-2.10.0 6 | - persistent-postgresql-2.10.0 7 | - persistent-sqlite-2.10.0 8 | - persistent-template-2.7.0 9 | - postgresql-simple-0.6.1 10 | 11 | allow-newer: true 12 | 13 | packages: 14 | - . 15 | - ../../../esqueleto 16 | -------------------------------------------------------------------------------- /test/expected-compile-failures/stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-12.24 2 | 3 | packages: 4 | - . 5 | - ../../../esqueleto 6 | 7 | extra-deps: 8 | - aeson-1.4.1.0 9 | - persistent-2.10.0 10 | - persistent-mysql-2.10.0 11 | - persistent-postgresql-2.10.0 12 | - persistent-sqlite-2.10.0 13 | - persistent-template-2.7.0 14 | - postgresql-libpq-0.9.4.2 15 | - postgresql-simple-0.6.1 16 | - transformers-0.5.5.2 17 | -------------------------------------------------------------------------------- /test/expected-compile-failures/stack.yaml.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by Stack. 2 | # You should not edit this file by hand. 3 | # For more information, please see the documentation at: 4 | # https://docs.haskellstack.org/en/stable/lock_files 5 | 6 | packages: 7 | - completed: 8 | hackage: aeson-1.4.1.0@sha256:a72639fbf44d5c2d5270fb6d9484629ed332d3029987fafd7580b5204265fb8b,6372 9 | pantry-tree: 10 | size: 39767 11 | sha256: 3eee6f6a05e563ebdd45e93348240d79eb20c267e70683360758327745d3249d 12 | original: 13 | hackage: aeson-1.4.1.0 14 | - completed: 15 | hackage: persistent-2.10.0@sha256:6e4566c2cf8dda6bf3e00f4f813dd711e7796a1598f46c5c729491f4b643c91d,4708 16 | pantry-tree: 17 | size: 2094 18 | sha256: 5ae7466479cf300e9bf07dc9780a432b4338c4e1e56fc732399260303cdf2f84 19 | original: 20 | hackage: persistent-2.10.0 21 | - completed: 22 | hackage: persistent-mysql-2.10.0@sha256:4bf76721312104b726406d3cac4a30185e9e19898605615ded6cbfe5cdabda6a,2884 23 | pantry-tree: 24 | size: 460 25 | sha256: 15737a7f8af7085fa6f83f1c084ff4de4922f28576633aa9aab4a8e668ccc5c3 26 | original: 27 | hackage: persistent-mysql-2.10.0 28 | - completed: 29 | hackage: persistent-postgresql-2.10.0@sha256:87384a179e44b57af7b12b00ccfdfc4bc03010a438aad207b9f38def0147cda2,2829 30 | pantry-tree: 31 | size: 671 32 | sha256: 5a2b25b40cb440466792b9ae293de95b4fcfcd1410c1c7aed9ffc8001699f5dc 33 | original: 34 | hackage: persistent-postgresql-2.10.0 35 | - completed: 36 | hackage: persistent-sqlite-2.10.0@sha256:d41ad3e2d9b88ab31bfdcd15e76ad41cd495573937921026b3b13f010ff9b8cf,4664 37 | pantry-tree: 38 | size: 681 39 | sha256: 86ad7225024dbe74421b78ab6a6c3e05aeb94d0633cde413f7e91453bee3e7c0 40 | original: 41 | hackage: persistent-sqlite-2.10.0 42 | - completed: 43 | hackage: persistent-template-2.7.0@sha256:1855a36c7dbfa1554c1711c1d61c41e83495bcb1986851cf1b3340f44ed269af,2703 44 | pantry-tree: 45 | size: 560 46 | sha256: 073f355d9425b1553e8e4f8553bb06e63d185c0e113c75512f969eeb92bcb4db 47 | original: 48 | hackage: persistent-template-2.7.0 49 | - completed: 50 | hackage: postgresql-libpq-0.9.4.2@sha256:3a3f372cf72706f349104f73d4ea5dee9c3eeac1ff749301110dadb55e2ac66f,2804 51 | pantry-tree: 52 | size: 549 53 | sha256: b045b567464d6c86ecc23a3915a6aa81c52cfbaa1c51c7fe9649366185c9ce6b 54 | original: 55 | hackage: postgresql-libpq-0.9.4.2 56 | - completed: 57 | hackage: postgresql-simple-0.6.1@sha256:316e6424da50ec863c74dcf2d7c86cfe6ee00cb142c07a422eb118577dc1d3b7,5256 58 | pantry-tree: 59 | size: 4055 60 | sha256: c22e1f054f3be5eaad5eba5abc793504be85e441ff671bf203013ac8f72f9c79 61 | original: 62 | hackage: postgresql-simple-0.6.1 63 | - completed: 64 | hackage: transformers-0.5.5.2@sha256:c6a1dc5261d87de1d7d0876b670ca8782c43ac89e59ec2bafa1e32d25c7d3509,3122 65 | pantry-tree: 66 | size: 2365 67 | sha256: 5c38ca49a4b2468b6c61682a722611c8a54699bb94f8d6e0ee9f2c546477f116 68 | original: 69 | hackage: transformers-0.5.5.2 70 | snapshots: 71 | - completed: 72 | size: 508835 73 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/12/24.yaml 74 | sha256: b0a5564eb448e69b9f6a4f67fe72016d9e7ec24e37de1826e1a9cfd064a1b6a5 75 | original: lts-12.24 76 | -------------------------------------------------------------------------------- /test/expected-compile-failures/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/env bash 2 | 3 | # This script attempts to build each executable in the package, which should all 4 | # fail with a compiler error. If any executable builds successfully, then we exit 5 | # the script. 6 | 7 | # We have to use 2>&1 because `stack ide targets` outputs to stderr for some 8 | # reason. 9 | for target in $(stack ide targets 2>&1 | grep exe); do 10 | echo "Building target: $target" 11 | if stack build --fast $target; then 12 | exit 1 13 | fi 14 | done 15 | -------------------------------------------------------------------------------- /test/expected-compile-failures/update-read-role/Main.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -fno-warn-unused-top-binds #-} 2 | 3 | module Main where 4 | 5 | import Control.Monad.IO.Class (MonadIO) 6 | import Database.Esqueleto 7 | import Database.Esqueleto.Internal.Language (Insertion) 8 | import Database.Persist.Sql (SqlWriteT) 9 | import Database.Persist.TH (mkDeleteCascade, 10 | mkMigrate, mkPersist, 11 | persistLowerCase, share, 12 | sqlSettings) 13 | 14 | import Lib 15 | 16 | main :: IO () 17 | main = pure () 18 | 19 | updateQuery :: SqlExpr (Entity Person) -> SqlQuery () 20 | updateQuery = \p -> do 21 | set p [ PersonAge =. just (val 123) -. p ^. PersonBorn ] 22 | where_ $ isNothing (p ^. PersonAge) 23 | 24 | shouldFail :: MonadIO m => SqlReadT m () 25 | shouldFail = update updateQuery 26 | -------------------------------------------------------------------------------- /test/expected-compile-failures/write-read-role/Main.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -fno-warn-unused-top-binds #-} 2 | 3 | module Main where 4 | 5 | import Control.Monad.IO.Class (MonadIO) 6 | import Database.Esqueleto (SqlExpr, SqlQuery, from, 7 | insertSelect, val, (<#), 8 | (<&>), (^.)) 9 | import Database.Esqueleto.Internal.Language (Insertion) 10 | import Database.Persist.Sql (SqlReadT) 11 | import Database.Persist.TH (mkDeleteCascade, 12 | mkMigrate, mkPersist, 13 | persistLowerCase, share, 14 | sqlSettings) 15 | 16 | import Lib 17 | 18 | main :: IO () 19 | main = pure () 20 | 21 | insertQuery :: SqlQuery (SqlExpr (Insertion BlogPost)) 22 | insertQuery = 23 | from $ \p -> 24 | return $ BlogPost <# (val "Group Blog Post") <&> (p ^. PersonId) 25 | 26 | shouldFail :: MonadIO m => SqlReadT m () 27 | shouldFail = insertSelect insertQuery 28 | -------------------------------------------------------------------------------- /test/new-join-compiler-errors/README.md: -------------------------------------------------------------------------------- 1 | # expected-compile-failures 2 | 3 | This subdirectory contains a stack project for expected compilation failures. To 4 | add a new "test case", create a new `executable` stanza in the `package.yaml` 5 | file. The Travis CI test script ([`test.sh`](test.sh)) will attempt to compile 6 | the executable and will exit with an error if it successfully compiled. 7 | -------------------------------------------------------------------------------- /test/new-join-compiler-errors/bad-errors/Main.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -fno-warn-unused-top-binds #-} 2 | {-# LANGUAGE TypeApplications #-} 3 | module Main where 4 | 5 | import Control.Monad.IO.Class (MonadIO) 6 | import Database.Esqueleto hiding (from,on) 7 | import Database.Esqueleto.Experimental 8 | import Database.Esqueleto.Internal.Language (Insertion) 9 | import Database.Persist.Sql (SqlWriteT) 10 | import Database.Persist.TH (mkDeleteCascade, 11 | mkMigrate, mkPersist, 12 | persistLowerCase, share, 13 | sqlSettings) 14 | 15 | import Lib 16 | 17 | main :: IO () 18 | main = pure () 19 | 20 | -- Missing on condition leads to an unintelligeable error and points to the wrong spot 21 | missingOnConditionShouldFail :: MonadIO m => SqlPersistT m [(Entity Person, Entity BlogPost)] 22 | missingOnConditionShouldFail = select $ do 23 | (people :& blogPosts) <- 24 | from $ Table @Person 25 | `LeftOuterJoin` Table @BlogPost 26 | pure (people, blogPosts) 27 | 28 | -- Mismatched union when one part is returning a different shape than the other 29 | mismatchedUnion :: MonadIO m => SqlPersistT m [(Value String, Value (Maybe Int))] 30 | mismatchedUnion = select . from $ 31 | (SelectQuery $ do 32 | people <- from $ Table @Person 33 | pure (people ^. PersonName, people ^. PersonAge)) 34 | `Union` 35 | (SelectQuery $ do 36 | people <- from $ Table @Person 37 | pure (people ^. PersonName)) 38 | 39 | incorrectNumberOfOnElements = select . from $ 40 | Table @Person 41 | `LeftOuterJoin` Table @Follow 42 | `on` (\(people :& follows) -> just (people ^. PersonId) ==. follows ?. FollowFollowed) 43 | `LeftOuterJoin` Table @Person 44 | `on` (\(follows :& followers) -> followers ?. PersonId ==. follows ?. FollowFollower) 45 | 46 | -------------------------------------------------------------------------------- /test/new-join-compiler-errors/new-join-compiler-errors.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 1.12 2 | 3 | -- This file has been generated from package.yaml by hpack version 0.31.2. 4 | -- 5 | -- see: https://github.com/sol/hpack 6 | -- 7 | -- hash: 2bf9103f4701fb3c063743dbb88970ee68ecbeaeb87eea96ca21096da1264968 8 | 9 | name: new-join-compiler-errors 10 | version: 0.1.0.0 11 | description: Please see the README on GitHub at 12 | homepage: https://github.com/bitemyapp/esqueleto#readme 13 | bug-reports: https://github.com/bitemyapp/esqueleto/issues 14 | author: Ben Levy 15 | maintainer: benjaminlevy007@gmail.com 16 | copyright: 2020 Ben Levy 17 | license: BSD3 18 | build-type: Simple 19 | extra-source-files: 20 | README.md 21 | 22 | source-repository head 23 | type: git 24 | location: https://github.com/bitemyapp/esqueleto 25 | 26 | library 27 | exposed-modules: 28 | Lib 29 | other-modules: 30 | Paths_new_join_compiler_errors 31 | hs-source-dirs: 32 | src 33 | default-extensions: FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving MultiParamTypeClasses NoMonomorphismRestriction OverloadedStrings QuasiQuotes ScopedTypeVariables StandaloneDeriving TemplateHaskell TypeFamilies 34 | build-depends: 35 | base >=4.7 && <5 36 | , esqueleto 37 | , persistent 38 | , persistent-template 39 | default-language: Haskell2010 40 | 41 | executable bad-errors 42 | main-is: Main.hs 43 | other-modules: 44 | Paths_new_join_compiler_errors 45 | hs-source-dirs: 46 | bad-errors 47 | default-extensions: FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving MultiParamTypeClasses NoMonomorphismRestriction OverloadedStrings QuasiQuotes ScopedTypeVariables StandaloneDeriving TemplateHaskell TypeFamilies 48 | ghc-options: -threaded -rtsopts -with-rtsopts=-N 49 | build-depends: 50 | base >=4.7 && <5 51 | , esqueleto 52 | , new-join-compiler-errors 53 | , persistent 54 | , persistent-template 55 | default-language: Haskell2010 56 | -------------------------------------------------------------------------------- /test/new-join-compiler-errors/package.yaml: -------------------------------------------------------------------------------- 1 | name: new-join-compiler-errors 2 | version: 0.1.0.0 3 | github: bitemyapp/esqueleto 4 | license: BSD3 5 | author: Ben Levy 6 | maintainer: benjaminlevy007@gmail.com 7 | copyright: 2020 Ben Levy 8 | 9 | extra-source-files: 10 | - README.md 11 | 12 | description: Please see the README on GitHub at 13 | 14 | dependencies: 15 | - base >= 4.7 && < 5 16 | - esqueleto 17 | - persistent 18 | - persistent-template 19 | 20 | default-extensions: 21 | - FlexibleContexts 22 | - FlexibleInstances 23 | - GADTs 24 | - GeneralizedNewtypeDeriving 25 | - MultiParamTypeClasses 26 | - NoMonomorphismRestriction 27 | - OverloadedStrings 28 | - QuasiQuotes 29 | - ScopedTypeVariables 30 | - StandaloneDeriving 31 | - TemplateHaskell 32 | - TypeFamilies 33 | 34 | library: 35 | source-dirs: src 36 | 37 | executables: 38 | bad-errors: 39 | main: Main.hs 40 | source-dirs: bad-errors 41 | ghc-options: 42 | - -threaded 43 | - -rtsopts 44 | - -with-rtsopts=-N 45 | dependencies: 46 | - new-join-compiler-errors 47 | -------------------------------------------------------------------------------- /test/new-join-compiler-errors/src/Lib.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE UndecidableInstances, DerivingStrategies #-} 2 | module Lib where 3 | 4 | import Control.Monad.IO.Class (MonadIO) 5 | import Database.Persist 6 | import Database.Persist.Sql (SqlReadT) 7 | import Database.Esqueleto (SqlExpr, SqlQuery, from, 8 | val, (<#), insertSelect, (<&>), (^.)) 9 | import Database.Esqueleto.Internal.Language (Insertion) 10 | import Database.Persist.TH (mkDeleteCascade, 11 | mkMigrate, mkPersist, 12 | persistLowerCase, share, 13 | sqlSettings) 14 | 15 | share [ mkPersist sqlSettings 16 | , mkDeleteCascade sqlSettings 17 | , mkMigrate "migrateAll"] [persistLowerCase| 18 | Person 19 | name String 20 | age Int Maybe 21 | born Int Maybe 22 | deriving Eq Show 23 | BlogPost 24 | title String 25 | authorId PersonId 26 | deriving Eq Show 27 | Follow 28 | follower PersonId 29 | followed PersonId 30 | deriving Eq Show 31 | |] 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/new-join-compiler-errors/stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-13.6 2 | 3 | packages: 4 | - . 5 | - ../../../esqueleto 6 | 7 | extra-deps: 8 | - aeson-1.4.1.0 9 | - persistent-2.10.0 10 | - persistent-mysql-2.10.0 11 | - persistent-postgresql-2.10.0 12 | - persistent-sqlite-2.10.0 13 | - persistent-template-2.7.0 14 | - postgresql-libpq-0.9.4.2 15 | - postgresql-simple-0.6.1 16 | - transformers-0.5.5.2 17 | --------------------------------------------------------------------------------