Compare commits

...

114 Commits

Author SHA1 Message Date
Matt Parsons
e18dd125c5
Support OverloadedRecordDot (#310)
* Support OverloadedRecordDot

* stylish

* Haddocks

* Add comments to README
2022-03-31 09:14:48 -06:00
Matt Parsons
5e212049d4
Support time >= 10 (#309)
* support new time

* changelog, cabal

* ok
2022-03-29 19:32:11 -06:00
Matt Parsons
f883262dc2
GHC 9.2 support (#304)
* Add GHC 9.2 to CI

* empty??
2022-03-14 13:31:07 -06:00
Jappie Klooster
8f591832d9
Add docs for experimental delete (#303)
* Add docs for experimental delete

the implementation seem shared,
it took me a couple of minutes to figure this out,
it seems wise to add a seperate header showing how,
it ought to work for the new API.

Add changes to change log

bump version number

fixup pr link

* Update esqueleto.cabal

Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>

* Update changelog.md

Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>

Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>
2022-03-11 17:21:08 -07:00
Maximilian Tagher
101a87f936
Add @localhost to last MySQL command (#296)
It's been a long time since I remembered MySQL user naming convention stuff, but, just empirically I get the following error following the README instructions:

```
mysql -u root
```

```
mysql> GRANT ALL ON esqutest.* TO 'travis';
ERROR 1410 (42000): You are not allowed to create a user with GRANT
mysql> GRANT ALL ON esqutest.* TO 'travis'@'localhost';
Query OK, 0 rows affected (0.00 sec)
```

MySQL version:

```
~/D/C/H/esqueleto> mysql --version                                                                                                                                                                                                                                                                                                                                  12:38:37
mysql  Ver 8.0.22 for osx10.15 on x86_64 (Homebrew)
```
2022-03-10 07:34:08 -07:00
Matt Parsons
c70799be09
esqueleto-3.5.3.0 (#292) 2021-09-30 11:59:30 -06:00
m4dc4p
ed4e98f96b
Add missing instances to (:&) (#291)
* Add missing instances to (:&)

The (:&) operator has an instance for `SqlSelect`, but none
for `ToAlias` and `ToAliasReference`. Adding those for parity.

* Updates based on review.

* Update test/Common/Test.hs

Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>

* Update test/Common/Test.hs

Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>

Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>
2021-09-30 11:43:04 -06:00
Nikita Razmakhnin
2a44844f75
Add support of PostgreSQL-specific VALUES(..) expression (#284)
* Add PostgreSQL-specific support of VALUES(..)
scalar expression of values-list for `from` targets.

* Bump version and update changelog

* Align identation for Postgres `values` func

* Use direct `From` data-type instead
of `ToFrom` typeclass for postgres `values` expression.
2021-09-30 10:11:28 -06:00
Matt Parsons
982b354c7e
Use haskell/actions/setup for CI (#285) 2021-09-22 08:30:09 -06:00
Nikita Razmakhnin
18951b280b
Fix distinctOnOrderBy with nested expression (#278)
* Fix stripped part of nested expression during
assembling of `distinctOnOrderBy` subexpression

* Bump version number and update changelog
2021-09-02 12:39:29 -06:00
Matt Parsons
f03bba5bf9
Better documentation on associateJoin (#281)
* Better documentation on associateJoin

* fix format
2021-09-02 12:38:38 -06:00
Chris Parks
e8271a00d6
Avoid generating an empty list as the left operand to NOT IN (#273)
* Avoid generating an empty list as the left operand to NOT IN

Postgres treats 'x in ()' and 'x not in ()' as syntax errors. Instead
translate:

  x in ()      ⇒  FALSE
  x not in ()  ⇒  TRUE

Older versions of esqueleto did this, but apparently the latter got
lost.

* Bump version and update changelog
2021-07-08 19:27:32 -05:00
Isaac van Bakel
3a12a15d00
Add SqlSelect instance for :& (#268)
* Add SqlSelect instance for (:&)

Motivation is given in bitemyapp/esqueleto#267 - this instance allows
for polymorphic use of the new Experimental API, where it otherwise
wouldn't be possible to split `a :& b` into `(a, b)`.

* Bump version to 3.5.2.0
2021-06-23 11:03:38 -06:00
Esteban Ibarra
33128042c4
Add selectOne (#265)
* Add `selectSingle`

* Clean up and add to test execution :/

* Import library's `selectFirst` rather than re-export it from `persistent`

* Add haddock and update test name

* Add missing comments for haddock :)

* Rename to `selectOne` and add @since

* Bump version number

* Add missing refs for `table` function

* Update to experimental syntax
2021-06-17 13:17:44 -06:00
Matt Parsons
34047e1f5f
Pass ConnectionPool to tests (#262)
* rewriting tests

* tests now run in 1.45 seconds

* tests pass

* fix json

* fix tests

* add helper for setting the database state

* clean things up a bit
2021-05-28 15:34:56 -06:00
Matt Parsons
e145be999a
Consolidate Tests (#261)
* Consolidate Tests

* stylish-haskell

* woops

* lol
2021-05-27 14:38:02 -06:00
Matt Parsons
b295bc6a5f
Esqueleto.Legacy (#259)
* Esqueleto.Legacy

* Add changelog entry

* Delete deprecated modules

* a bit more

* ghc 9 support, clean warns

* yes

* okkk
2021-05-26 14:27:04 -06:00
Ben Levy
ea4ff33b93
Destroy all GADTs; Removes the From GADT and SqlExpr GADT (#228)
* Explode the From GADT. Move runFrom into the ToFrom typeclass removing the need for the intermediate structure. Extract the parts of the Experimental module into submodules.

* Reorganize Experimental folder. Move Subquery into core Experimental.From module.

* Cleanup hackage documentation. Make sure stylish ran correctly. Update changelog and bump version

* Update ERaw to change the direction of NeedParens (parent now tells child context). Removed need for composite key constructor

* Get rid of AliasedValue and ValueReference; added sqlExprMetaAlias to SqlExprMeta

* Remove EList and EEmptyList; ERaw is now technically possible in each case since it is generalized to all

* Remove entity specific constructors from SqlExpr

* Remove EOrderBy, EDistinctOn; Change PreprocessedFrom a to just be an independent datatype

* Remove EOrderByRandom, calling distinctOnOrderBy with rand will choke the db but you shouldnt be using rand anyway. distinctOnOrderBy seems dangerous though

* Remove ESet

* Remove EInsert and EInsertFinal

* Make postgres tests pass

* Change aliased val to be legal value by waiting until expr materialization in select clause before adding AS <alias>

* Cleanup ToAliasRefernce; Add isReference meta to value reference even though that info isnt currently used anywhere

* Expose Experimental submodules

* Update changelog

* Create a FromRaw to replace FromSubquery and FromIdent in from clause. Modify Experimental to only use FromRaw.

* Convert all of experimental to use new From type instead of From type class. Make the data constructors second class, functions should be used. Introduce *Lateral functions, using the same type for lateral and non lateral queries was probably a mistake.

* Expose the new functions and fix the mysql test compilation error (type inference was wonky with `Union` replaced with `union_`

* Bump version and add more comments

* ValidOnClause was too restrictive, ToFrom is actually the correct amount of leniency. ValidOnClause would not catch use of on for a cross join but would prevent nested joins

* Unbreak lateral joins by introducing a completely different ValidOnClause constraint

* Fixe error introduced in merge with master

* Dont realias alias references

* Never realias an already aliased Entity or Value

* reindex value references to the latest 'source'
2021-05-26 12:12:11 -06:00
Matt Parsons
e39c62990e
Fix group by composite (#255)
* remove failing test with bad behavior

* test case

* changelog, vbump
2021-05-20 16:33:01 -06:00
parsonsmatt
b5c0d84cad Merge branch 'master' of github.com:bitemyapp/esqueleto 2021-05-20 10:10:26 -06:00
parsonsmatt
129b1734c3 relax attoparsec upper bound 2021-05-20 10:10:22 -06:00
Esteban Ibarra
bbaa0595e0
Update status badge with GH actions (#252) 2021-05-13 09:45:55 -06:00
Felix Yan
bd6da6eb3b
Allow attoparsec 0.14 (#244)
Tested to build fine here.
2021-05-06 09:19:38 -06:00
Matt Parsons
cd16b2b22f
Support upcoming persistent-2.13 (#245)
* stack-8.8.yaml now does GHC 8.8

* support ghc 8.10.4, upgrade to cabal 3.4

* do it

* use stack 8.10 by default, support pers2.13

* sqlite tests are failing???

* build with cabal

* gitignore

* tidy up

* work with persistent-2.13

* giddyup

* keep cabal file in repo

* fixx

* changelog, vbump

* update cache keys
2021-05-05 16:23:53 -06:00
parsonsmatt
9fba3e33e4 bump resolver 2021-03-30 09:07:27 -06:00
parsonsmatt
f96daae3b5 remove stuff from cabal 2021-03-30 09:01:04 -06:00
Matt Parsons
96331257e4
get persistent 2.12 going (#243)
* run mysql tests

* uhhh why are you like this

* stuff

* tests pass locally

* make the example work

* minor bump

* fix gha

* k

* no persistent-template dependency please

* it passed?

* ci nonsense

* uh

* i think that should do it

* ok no really

* i miss file-watch

* sigh

* come on pls

* stylish haskell

* i hate this
2021-03-29 14:47:20 -06:00
Maximilian Tagher
c4ec95874f
Improve recommended hlints for catching x = NULL SQL (#240)
* Improve recommended hlints for catching `x = NULL` SQL

The current hints work fine for unqualified imports, but I realized they don't work with qualified ones, such as `import qualified Database.Esqueleto as E`.

I tested on our codebase that these with the `Database.Esqueleto.` addition to `hlint.yaml`, this now works in unqualified and qualified code

* Update changelog.md
2021-03-26 17:24:14 -06:00
Arthur Xavier
a61f5527e8
Fix non-exhaustive patterns in 'unsafeSqlAggregateFunction' (#238)
* Fix non-exhaustive patterns in 'unsafeSqlAggregateFunction'

* Update changelog

* Abstract 'UnexpectedValueError' in 'valueToRawSqlParens'

Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>
2021-02-21 13:50:03 -06:00
Felix Paulusma
8fb9a1fe24
Also export ValidOnClauseValue (#232)
* Also export ValidOnClauseValue

This is a constraint on `on`, but not exported, so you have to go into the source to see what it does. Exporting makes this easier.

* add to ChangeLog and bumped version
2021-02-05 14:41:40 -07:00
Matt Parsons
da72f428d1
fix ci (#233) 2020-12-04 10:51:14 -07:00
parsonsmatt
305b11e58e esqueleto-3.4.0.1 2020-11-04 14:01:49 -07:00
Matt Parsons
521ac01488
Support persistent-2.11 (#226)
* Support persistent-2.11

* sigh

* woop woop

* use hackage

* cpp so we don't have to tighten bounds

* add changelog entry

* lmao timing attacks

* no
2020-11-04 14:01:23 -07:00
Ben Levy
eb034458de
Simplify ToFromT (#225)
* Simplify ToFromT. Converted most closed type families to be associated type families with the exception of IsLateral, Nullable and the two new FromOnClause and FromCrossJoin type families that handle the overlaps instead of ToFromT
2020-11-04 11:15:17 -06:00
Georgi Lyubenov
eb91208e94
Use type families instead of empty classes (#220)
* Use type families instead of empty classes

* It's not possible to expand a closed type family.

Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>

Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>
2020-10-29 18:04:28 -06:00
Matt Parsons
b35713c09f
Autoformatting + Stylish Haskell Config (#218)
* Add stylish-haskell.yaml, update spacing to 4 in configs

* update travis

* lol

* major formatting stuff

* fix parse error

* fix

* warnings, more tidying up

* Add style guide [ci skip]

* faster build perhaps

* cabbal

* sigh
2020-10-29 16:20:52 -06:00
Arthur Xavier
4f6b02298c
Deprecate ToAliasT and ToAliasReferenceT (#221)
* Make ToAliasT and ToAliasReferenceT associated types of the corresponding classes

* Update changelog

* Remove ToAliasT and ToAliasReferenceT

* Update changelog and deprecate type families instead of deleting them

* Apply suggestions from code review

Co-authored-by: Ben Levy <benjaminlevy007@gmail.com>

Co-authored-by: Ben Levy <benjaminlevy007@gmail.com>
2020-10-29 15:35:18 -06:00
Matt Parsons
4ea3d5da59
Github Actions (#223)
* Create haskell.yml
2020-10-29 15:10:54 -06:00
Georgi Lyubenov
d2925e227c Remove a double adverb usage 2020-10-29 07:16:56 -06:00
Ben Levy
a319d13bee
[Experimental] More powerful queries (#215)
* Initial attempt at Lateral joins

* Fix lateral queries for Inner and Left joins. Remove for Full and Right as this is apparently illegal(who knew). Add TypeError on Full and Right joins. Update on clause to use a custom constraint instead of relying on ToFrom.

* Fix typo leading to erroneous ToFrom instance

* Implement non-recursive CTE's

* add withRecursive; cleanup whitespace

* Fix multiple recursive CTEs. Apparently the spec just wants RECURSIVE if any of the queries are recursive.

* Add test to verify that a CTE can reference a previously defined CTE

* Update with/Recursive to return an element of a from clause to allow for joins against CTEs

* Modify set operations to use a custom data type + typeclass + typefamily to allow direct use of SqlQuery a in set operation and to allow recursive cte's to unify syntax with SqlSetOperation. Added lowercase names for set operations. If we can migrate off the constructor names we may be able to simplify the implementation.

* Fixed haddock documentation issue from v3.3.4.0 and added documentation
for new features introduced by v3.4.0.0

* fixed comments that were changed while debugging haddock build

* Cleanup formatting in From per PR. Cleanup ValidOnClause, added documentation and reduced the number of instances

* Update src/Database/Esqueleto/Experimental.hs

Co-authored-by: charukiewicz <charukiewicz@protonmail.com>
Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>
2020-10-28 21:37:17 -06:00
Maximilian Tagher
8adab239df
Add recommended hlint rules for proper isNothing usage (#214)
* Add recommended hlint rules for proper isNothing usage

* Update changelog.md
2020-10-28 10:42:35 -06:00
Matt Parsons
56d1e348c3
update nightly resolver (#208)
* update nightly resolver

* hmm
2020-10-28 10:40:22 -06:00
Matt Parsons
4887bc19fe
add GHC 8.8 to testing (#206) 2020-09-17 15:47:54 -06:00
Matt Parsons
583167adb0
Make the Experimental module more prominent (#205)
* update README

* add comments

* update cabal

* update changelog
2020-09-17 14:52:38 -06:00
Ben Levy
f9a8088170
Bugfix rollup: Fix issue with extra characters in generated SQL; Fix ToAliasReference for already referenced values; Fix Alias/Reference for Maybe Entity (#191)
* Fix issue with extra characters. Extra parens in valueList caused issues in mysql. Extra backticks in value reference names

* update changelog and bump version number

* Fix issue caused by toAliasReference failing to reindex an alias reference by its new alias source

* Add support for SqlExpr (Maybe (Entity a))) to aliasing in Experimental. Fix #193

* Update changelog with new PR name. Fix name shadowing in test.

* Fix binary operations(i.e. ==.) on aliased values.

* no need for version bump since 3.3.3.3 hasnt been released yet

Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>
2020-08-30 13:16:37 -06:00
Ben Levy
2b5b561f6e
Add new SetOperation constructor for parenthesized query (#195)
* Add new SetOperation constructor for parenthesized query. Automatically detect when parentheses are needed on SelectQuery usage (only works for MySQL).

* Add Parens to SelectQueryP and create a pattern synonym for SelectQuery. SelectQueryP is hidden as end users should only be using SelectQuery.
2020-08-30 13:15:11 -06:00
Maximilian Tagher
dd16400d64
Document isNothing (#203)
* Document isNothing

I have two goals with this documentation:

1. Surface how to do `IS NOT NULL`. This PR makes a search for that string turn up a result, and directs you to combine `isNothing` with `not_`.
2. Documents a major gotcha where behavior between Persistent and Esqueleto is different. I haven't tested this in awhile, but we run into this gotcha occassionally, so I'm pretty confident it's still an issue.

* ..

* ..

Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>
2020-08-26 12:36:41 -06:00
tim
1de1ee9e6e Removes irrelevant copy pasta from some haddocks 2020-08-26 11:35:19 -06:00
Sebastián Estrella
29eb443fac Remove whitespaces from Database.Esqueleto.Internal.Internal 2020-08-26 11:34:48 -06:00
Sebastián Estrella
91ab01d76f [#197] Allow PostgreSQL aggregate functions to take a filter clause 2020-08-26 11:34:25 -06:00
Maximilian Tagher
4dbd5339ad
Test and further document ToBaseId (#190)
* Test and further document ToBaseId

My coworker Lev was adding this typeclass to our codebase and we hadn't used it before. I added a little more documentation that I think would help clarify things, particularly what the point of the witness function was. More importantly I added a test for this typeclass.

* 3.3.3.2
2020-06-22 09:43:17 -06:00
Ben Levy
7f769cc673
Add test for calling sql functions on aliased values; Fixed unsafeSql… (#189)
* Add test for calling sql functions on aliased values; Fixed unsafeSqlFunction to handle aliaed values properly

* version bump and changelog
2020-06-21 10:00:12 -06:00
Mitchell Vitez
9a4813d422
Remove old note about ON clause ordering (#186)
#156 fixed the issue
2020-05-26 14:28:19 -06:00
parsonsmatt
2cd6460260 travis pls 2020-03-30 13:37:22 -06:00
Matt Parsons
9e643acb3d
27 date trunc postgres (#181)
* write test case

* weird

* better error message

* rename, fix
2020-03-30 12:55:19 -06:00
Matt Parsons
b6279ca9f2
Postgresql Date Truncation (#180)
* write test case

* weird

* better error message
2020-03-30 12:11:27 -06:00
parsonsmatt
0484dfb8d4 v3.3.3.0 2020-03-29 10:42:23 -06:00
Ben Levy
56e4b83e5c
New syntax for Joins (Subquery + Union/Intersect/...) (#172)
* It works?

* Add multiple return values back in

* Allow order by alias

* Support groupBy and count, Returning value from a fromQuery now will make it into an alias

* Eliminate Alias type, TODO: finish implementing all the functions on Value for the alias constructors

* Add entity support to subqueries

* Cleanup duplication; Cleanup warnings and finish implementing all the cases for aliased values and entities.

* Cleanup fromQuery and add comments

* Modify EValueReference to support aliased entity fields instead of having to use opaque ERaw in field access

* Implement SQL Set Operations

* Add test to show novel use of fromQuery

* Cleanup unsafe case statements

* Add type annotations to helper queries to satisfy the typechecker on older GHC

* New syntax for joins, using placeholder names with ' in them to avoid name conflict with existing join types.
New api properly enforces Maybe on outer joins and requires an on clause for all joins in their construction.

* Add some more test queries using the new syntax

* Add test to verify that delete works with the new syntax

* Add cross join and implicit cross join using comma examples to test code for new from syntax

* Comment out use of CrossJoin in common tests since postgres cant handle it with the current implementation of the CrossJoin kind

* Add typeclass machinery to support the use of the old Join data types used in the existing from clause

* Fix bug with CrossJoin and add on_ syntax sugar

* move new from syntax into Database.Esqueleto.Experimental

* Merge subqueries and unions with the new join syntax, they all seem to play nicely together

* Cleanup somehow copies of ToAlias ended up staying in Internal and a swp file made it in to the branch.

* Fix compilation errors

* Swith tuple to using a TypeOperator

* Make operator only 2 characters

* added up to 8-tuple instances for ToMaybe, ToAlias, and ToAliasReference

* Add compiler error tests for new syntax to support making better errors

* Use closed data families to allow for catching missing on statements in joins.

* Convert ToAliasReferenceT to be a closed type family matching the other classes in the Experimental module

* added Esqueleto.Experimental documentation: added introduction and several examples of old vs. new syntax

* added more usage examples to module introduction; added documentation to SqlSetOperation, From, on, from, and (:&)

* Update (^.) to only treat natural keys with more than one component as ECompositeKey. Fixes #176.

* Update article metadata test to ensure the correct response was being returned instead of just check if an exception was thrown

* Add article metadata to cleanDB before deleting all articles to fix foreign key constraint errors

* Bump version number and add changelog entry

* Fix issue with ToMaybeT for Values, Maybe was going in the wrong place compared to the rest of the library. Add test to prove that Left joining into a subquery that returns a maybe flattens the maybe properly to avoid needing to call joinV.

* Fix common test for postgres, needed to add dogCounts to the group by since postgres is strict on only agregates for non grouped columns; I really need to set up a local postgresql

* Revert ToFromT changes. Only accept functions that return a SqlExpr (Value Bool) in ToFromT

* escaped use of '@' in TypeApplications in documentation

* Add more specific type signature to `on`

per parsonsmatt review suggestion. Improves type inference significantly.

Co-Authored-By: Matt Parsons <parsonsmatt@gmail.com>

Co-authored-by: charukiewicz <c.charukiewicz@gmail.com>
Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>
2020-03-29 10:40:49 -06:00
Ben Levy
9a762e9f20
Update (^.) to fix natural key handling (#177)
* Update (^.) to only treat natural keys with more than one component as ECompositeKey. Fixes #176.

* Update article metadata test to ensure the correct response was being returned instead of just check if an exception was thrown

* Add article metadata to cleanDB before deleting all articles to fix foreign key constraint errors

* Bump version number and add changelog entry
2020-03-22 09:30:45 -06:00
Matt Parsons
951bb21c1b
remove upper bounds (#171)
* remove upper bounds

* work with stackage nightly

* Add nightly build
2020-01-24 13:23:26 -07:00
Matt Parsons
3fcc965de7
Group by documentation (#170)
* Group by documentation
2020-01-16 10:03:53 -07:00
Matt Parsons
aded2932e9
Merge pull request #167 from charukiewicz/master
Exposed new SQL string functions added in v3.3.0 and bumped to v3.3.1
2019-12-31 09:39:27 -07:00
charukiewicz
a7435bac06 Exposed new SQL string functions added in v3.3.0 and bumped to v3.3.1 2019-12-12 22:42:26 -06:00
Matt Parsons
c0dd6c70ef
Merge pull request #166 from charukiewicz/master
Add several common SQL string functions
2019-12-12 15:35:15 -08:00
charukiewicz
ca385665dd added several common SQL string functions: UPPER, TRIM, LTRIM, RTRIM, LENGTH, LEFT, RIGHT
Co-authored-by: charukiewicz <c.charukiewicz@gmail.com>
Co-authored-by: belevy <benjaminlevy007@gmail.com>
Co-authored-by: joemalin95 <joemalin95@gmail.com>
2019-12-12 14:15:16 -06:00
Matt Parsons
a94fb6d9a8
Merge pull request #163 from hdgarrood/more-unsafe-sql-function-args
Allow unsafeSqlFunction to take up to 10 args
2019-11-25 08:48:48 -07:00
Matt Parsons
3eb2b181ac
Merge pull request #164 from felixonmars/patch-1
Remove duplicated dependency
2019-11-25 08:48:09 -07:00
Felix Yan
99c1bbc8fe
Remove duplicated dependency 2019-11-24 17:21:54 +08:00
Harry Garrood
0c96ee6af4 update changelog.md for #163 2019-11-21 23:33:55 +00:00
Harry Garrood
d889476bdf Allow unsafeSqlFunction to take up to 10 args
... without needing to nest tuples. Fixes #162
2019-11-21 23:29:09 +00:00
Matt Parsons
04a73ed92d
Merge pull request #161 from bitemyapp/matt/test-on-clause
Fix on clause nesting
2019-10-31 14:38:16 -06:00
Jose Duran
f9f953c89e Add unsafe documentation (#158)
* Add unsafe documentation

* Update README.md

Co-Authored-By: Matt Parsons <parsonsmatt@gmail.com>

* Update README.md

Co-Authored-By: Matt Parsons <parsonsmatt@gmail.com>

* Update README.md

Co-Authored-By: Matt Parsons <parsonsmatt@gmail.com>

* Update README.md

Co-Authored-By: Matt Parsons <parsonsmatt@gmail.com>

* Update README.md

Co-Authored-By: Matt Parsons <parsonsmatt@gmail.com>

* Update README.md

Co-Authored-By: Matt Parsons <parsonsmatt@gmail.com>

* Update README.md

Co-Authored-By: Matt Parsons <parsonsmatt@gmail.com>

* updated Readme.md

* Update README.md

Co-Authored-By: Matt Parsons <parsonsmatt@gmail.com>

* add safety
2019-10-31 14:34:31 -06:00
parsonsmatt
1627feafa3 aha! 2019-10-29 16:54:58 -06:00
parsonsmatt
ae3b96e0f6 cleaner diff 2019-10-29 14:29:27 -06:00
parsonsmatt
f84945fb04 add cabal 2019-10-29 14:26:48 -06:00
parsonsmatt
edc7db8f3f remove debug 2019-10-29 14:26:09 -06:00
parsonsmatt
1ea6e709d2 Merge branch 'master' into matt/test-on-clause 2019-10-29 14:13:27 -06:00
parsonsmatt
0c9b41a87d lord 2019-10-29 14:07:53 -06:00
parsonsmatt
6a8239ac93 Add test cases 2019-10-29 11:50:10 -06:00
Matt Parsons
096a251c39
Add instance of UnsafeSqlFunctionArgument () (#159)
* Add instance of UnsafeSqlFunctionArgument ()

* Use now, clean a warn
2019-10-29 10:03:42 -06:00
parsonsmatt
214f1906da Use now, clean a warn 2019-10-29 08:53:13 -06:00
parsonsmatt
55fec71ed4 Add instance of UnsafeSqlFunctionArgument () 2019-10-29 08:50:51 -06:00
parsonsmatt
c2ecf9c1a4 v3.2.0 2019-10-28 17:26:40 -06:00
Matt Parsons
e0489988c8
sub_select fix #2 (#153)
* Deprecation notice

* Better message, changelog

* thanks @philonous for the typo find!

* Add subSelectCount

* Add subSelectList

* Add subSelectForeign

* Flip the warning back on

* Add subSelect test

* Write tests demonstrating usage

* fix

* sigh
2019-10-28 17:26:09 -06:00
Matt Parsons
91fa258193
Fix the On Clause Ordering issue (#156)
* Add failing test

* Refactor newIdentFor to not have an error case

* annotation for warning

* refactoring

* Expression parser

* holy shit it works

* Add a shitload of tests

* cross join

* Find a failing case

* Account for that one case

* works

* Composability test

* okay now it tests something

* Documentation updates

* Add since, changelog

* fix
2019-10-28 14:06:01 -06:00
parsonsmatt
5c1f0f65fa v3.1.3 2019-10-28 11:58:31 -06:00
Jose Duran
40f7a0ca97 Insert Select With Conflict for postgres (#155)
* add insertSelectWithConflict to allow insert with conflict resolution

* insertSelectWithConflictCount does nothing when no updates given and add tests

* no longer require undefined for insertSelectWithConflict

* Update src/Database/Esqueleto/PostgreSQL.hs

Co-Authored-By: Matt Parsons <parsonsmatt@gmail.com>

* Update src/Database/Esqueleto/PostgreSQL.hs

Co-Authored-By: Matt Parsons <parsonsmatt@gmail.com>

* Update src/Database/Esqueleto/PostgreSQL.hs

Co-Authored-By: Matt Parsons <parsonsmatt@gmail.com>

* Update src/Database/Esqueleto/PostgreSQL.hs

Co-Authored-By: Matt Parsons <parsonsmatt@gmail.com>

* Move non postgres related functions out of postgres module to internal.

* add entry to changelog
2019-10-28 11:56:34 -06:00
parsonsmatt
7608d716a1 Add pull request template 2019-10-28 10:21:24 -06:00
Matt Parsons
56420e1c34
Merge pull request #157 from tippenein/bmo/associated-join
add associateJoin function
2019-10-25 16:01:40 -06:00
brady.ouren
7b3cb37131 move function and bump version
- moves associateJoin to Database.Esqueleto
- relaxes bounds on containers dep
-
2019-10-25 11:58:54 -07:00
brady.ouren
e94808856f add associateJoin function
this helps avoid N+1 queries
2019-10-23 15:31:15 -07:00
Matt Parsons
53515b868f
Merge pull request #137 from rnons/patch-1
Fix code example of LeftOuterJoin and ?.
2019-10-18 09:04:59 -06:00
parsonsmatt
45f5a2ba6f 3.1.1 2019-10-17 15:06:07 -06:00
Chris Allen
5384ab7bf1
Merge pull request #149 from JoseD92/147-upsert-support
147 upsert support
2019-10-10 10:10:34 -05:00
Jose Duran
9512cbe270 add changelog entry 2019-10-10 09:23:24 -05:00
Jose Duran
5ff34fc8f8 fix build on 8.4 and 8.2 2019-10-09 21:04:14 -05:00
Jose Duran
ced45b0c4e style fix 2019-09-30 15:19:20 -05:00
Jose Duran
ba650748f0 add upsert postgres test 2019-09-30 15:12:15 -05:00
Jose Duran
3ebb31af58 made upsert and upsertby postgres specific 2019-09-30 14:11:39 -05:00
Jose Duran
6acb8f0732 add unique postgres tests 2019-09-30 14:10:41 -05:00
Jose Duran
07d9730dc4 add EsqueletoUpsert class and SqlBackend instance 2019-09-27 11:02:10 -05:00
Chris Allen
4f48df0484
Merge pull request #146 from bitemyapp/bitemyapp/support-time-1.9
Support time 1.9
2019-09-24 10:27:23 -05:00
Chris Allen
b4bfe538f9
Merge pull request #133 from bitemyapp/matt/render-query
Render queries as Text
2019-09-24 10:12:05 -05:00
Chris Allen
9775af6f3c Merge branch 'master' into patch-1 2019-09-24 09:55:19 -05:00
parsonsmatt
30cba15094 Fix test 2019-09-24 08:50:52 -06:00
parsonsmatt
3801155f1b Merge branch 'matt/render-query' of github.com:bitemyapp/esqueleto into matt/render-query 2019-09-20 10:41:29 -06:00
parsonsmatt
c7a24bd968 add github note 2019-09-20 09:09:51 -05:00
parsonsmatt
330a36b27e update note 2019-09-20 09:09:51 -05:00
parsonsmatt
a36f3f7bfe renderQueryToText 2019-09-20 09:09:51 -05:00
Ollie Charles
624d44eefd
Support time-1.9 2019-09-18 14:02:11 +01:00
Ping Chen
806fe763c9
Fix code example of LeftOuterJoin and ?. 2019-09-07 08:56:57 +09:00
parsonsmatt
b4a92ed33a add github note 2019-08-28 09:41:39 -06:00
parsonsmatt
677868b07c update note 2019-08-28 09:40:57 -06:00
parsonsmatt
6d82106b68 renderQueryToText 2019-08-28 09:40:01 -06:00
60 changed files with 9376 additions and 3292 deletions

View File

@ -11,8 +11,8 @@ insert_final_newline = true
[*.{hs,md,php}]
indent_style = space
indent_size = 2
tab_width = 2
indent_size = 4
tab_width = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true

19
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,19 @@
Before submitting your PR, check that you've:
- [ ] Bumped the version number.
- [ ] Documented new APIs with [Haddock markup](https://www.haskell.org/haddock/doc/html/index.html).
- [ ] Added [`@since` declarations](http://haskell-haddock.readthedocs.io/en/latest/markup.html#since) to the Haddock.
- [ ] Ran `stylish-haskell` and otherwise adhered to the [style guide](https://github.com/bitemyapp/esqueleto/blob/master/style-guide.yaml).
After submitting your PR:
- [ ] Update the Changelog.md file with a link to your PR.
- [ ] Check that CI passes (or if it fails, for reasons unrelated to your change, like CI timeouts).
<!---Thanks so much for contributing! :)
_If these checkboxes don't apply to your PR, you can delete them_
If you're unsure on what the new version number should be, feel free to ask.
-->

76
.github/workflows/haskell.yml vendored Normal file
View File

@ -0,0 +1,76 @@
name: CI
on:
push:
branches:
- master
pull_request:
types:
- opened
- synchronize
jobs:
build:
runs-on: ubuntu-latest
services:
# mysql-service Label used to access the service container
mysql-service:
# Docker Hub image (also with version)
image: mysql:8.0
env:
## Accessing to Github secrets, where you can store your configuration
MYSQL_USER: travis
MYSQL_PASSWORD: esqutest
MYSQL_ROOT_PASSWORD: esqutest
MYSQL_DATABASE: esqutest
## map the "external" 33306 port with the "internal" 3306
ports:
- 33306:3306
# Set health checks to wait until mysql database has started (it takes some seconds to start)
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
strategy:
matrix:
cabal: ["3.6"]
ghc: ["8.6.5", "8.8.4", "8.10.4", "9.0.2", "9.2.2"]
env:
CONFIG: "--enable-tests --enable-benchmarks "
steps:
- uses: actions/checkout@v2
- uses: haskell/actions/setup@v1
id: setup-haskell-cabal
with:
ghc-version: ${{ matrix.ghc }}
cabal-version: ${{ matrix.cabal }}
- uses: harmon758/postgresql-action@v1
with:
postgresql version: '12' # See https://hub.docker.com/_/postgres for available versions
postgresql user: esqutest
postgresql password: esqutest
postgresql db: esqutest
- name: Create MySQL
run: mysql -utravis -pesqutest -h127.0.0.1 --port=33306 esqutest -e "SELECT 1;"
# - name: Shutdown Ubuntu MySQL (SUDO)
# run: sudo service mysql stop
# - uses: mirromutth/mysql-action@v1.1
# with:
# mysql version: '8.0' # Optional, default value is "latest". The version of the MySQL
# mysql database: 'esqutest' # Optional, default value is "test". The specified database which will be create
# mysql user: 'travis' # Required if "mysql root password" is empty, default is empty. The superuser for the specified database. Can use secrets, too
# mysql password: 'esqutest' # Required if "mysql user" exists. The password for the "mysql user"
- run: cabal v2-update
- run: cabal v2-freeze $CONFIG
- uses: actions/cache@v2
with:
path: |
${{ steps.setup-haskell-cabal.outputs.cabal-store }}
dist-newstyle
key: ${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }}
restore-keys: |
${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }}
${{ runner.os }}-${{ matrix.ghc }}-
- run: cabal v2-build --disable-optimization -j $CONFIG
- run: cabal v2-test --disable-optimization -j $CONFIG --test-options "--fail-on-focus"
- run: cabal v2-haddock -j $CONFIG
- run: cabal v2-sdist

3
.gitignore vendored
View File

@ -1,7 +1,10 @@
.stack-work
stack.yaml.lock
*.yaml.lock
/dist*
*~
.cabal-sandbox/
cabal.sandbox.config
.hspec-failures
*.sqlite*
cabal.project.freeze

39
.stylish-haskell.yaml Normal file
View File

@ -0,0 +1,39 @@
steps:
- imports:
align: none
list_align: with_module_name
pad_module_names: false
long_list_align: new_line_multiline
empty_list_align: inherit
list_padding: 7 # length "import "
separate_lists: false
space_surround: false
- language_pragmas:
style: vertical
align: false
remove_redundant: true
- simple_align:
cases: false
top_level_patterns: false
records: false
- trailing_whitespace: {}
indent: 4
columns: 80
newline: native
language_extensions:
- BlockArguments
- DataKinds
- DeriveGeneric
- DerivingStrategies
- DerivingVia
- ExplicitForAll
- FlexibleContexts
- MultiParamTypeClasses
- NamedFieldPuns
- OverloadedStrings
- QuantifiedConstraints
- RecordWildCards
- ScopedTypeVariables
- TemplateHaskell
- TypeApplications
- ViewPatterns

View File

@ -1,7 +1,5 @@
language: c
sudo: false
services:
- mysql
@ -21,6 +19,13 @@ env:
- GHCVER=8.2
- GHCVER=8.4
- GHCVER=8.6
- GHCVER=8.8
- GHCVER=nightly
jobs:
fast_finish: true
allow_failures:
- env: GHCVER=nightly
install:
- export STACK_YAML=stack-$GHCVER.yaml
@ -31,12 +36,11 @@ install:
- psql -c "CREATE USER esqutest WITH PASSWORD 'esqutest';" -U postgres
- createdb -O esqutest esqutest
- mysql -e 'CREATE DATABASE esqutest;'
- stack setup
script:
- stack setup
- stack update
- stack build
- stack test
- stack build --fast $(if [[ $GHCVER == "nightly" ]]; then echo "--resolver nightly"; fi)
- stack test --fast $(if [[ $GHCVER == "nightly" ]]; then echo "--resolver nightly"; fi)
- cd test/expected-compile-failures/
- bash test.sh

View File

@ -21,7 +21,19 @@ test-ghci:
stack ghci esqueleto:test:sqlite
test-ghcid:
ghcid -c "stack ghci --ghci-options -fobject-code esqueleto:test:sqlite"
ghcid -c "stack ghci --ghci-options -fobject-code esqueleto --test" \
--warnings \
--restart "stack.yaml" \
--restart "esqueleto.cabal" \
--test main
test-ghcid-build:
ghcid -c "stack ghci --ghci-options -fobject-code esqueleto --test" \
--warnings \
--restart "stack.yaml" \
--restart "esqueleto.cabal"
init-pgsql:
sudo -u postgres -- createuser -s esqutest

280
README.md
View File

@ -1,4 +1,4 @@
Esqueleto [![TravisCI](https://travis-ci.org/bitemyapp/esqueleto.svg)](https://travis-ci.org/bitemyapp/esqueleto)
Esqueleto [![CI](https://github.com/bitemyapp/esqueleto/actions/workflows/haskell.yml/badge.svg?branch=master)](https://github.com/bitemyapp/esqueleto/actions/workflows/haskell.yml)
==========
![Skeleton](./esqueleto.png)
@ -127,9 +127,106 @@ FROM Person
WHERE Person.age >= 18
```
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))`.
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))`.
## Joins
### Alternative Field Projections
The `(^.)` operator works on an `EntityField` value, which are generated by
`persistent` as the table name + the field name. This can get a little bit
verbose. As of `persistent-2.11`, you can use `OverloadedLabels` to make this a
bit more concise:
```haskell
{-# LANGUAGE OverloadedLabels #-}
select $ do
p <- from $ table @Person
pure
( p ^. PersonName
, p ^. #name
)
```
The `OverloadedLabels` support uses the `fieldName` as given by the Persistent
entity definition syntax - no type name prefix necessary. Additionally, these
field accesses are *polymorphic* - the following query filters any table that
has a `name` column:
```haskell
rowsByName
:: forall rec.
( PersistEntity rec
, PersistEntityBackend rec ~ SqlBackend
, SymbolToField "name" rec Text
)
=> SqlExpr (Value Text)
-> SqlQuery (SqlExpr (Entity rec))
rowsByName name = do
rec <- from $ table @rec
where_ $ rec ^. #name ==. name
pure rec
```
GHC 9.2 introduces the `OverloadedRecordDot` language extension, and `esqueleto`
supports this on `SqlExpr (Entity rec)` and `SqlExpr (Maybe (Entity rec))`. It
looks like this:
```haskell
select $ do
(person, blogPost) <-
from $
table @Person
`leftJoin` table @BlogPost
`on` do
\(person :& blogPost) ->
just person.id ==. blogPost.authorId
pure (person.name, blogPost.title)
```
## Experimental/New Joins
There's a new way to write `JOIN`s in esqueleto! It has less potential for
runtime errors and is much more powerful than the old syntax. To opt in to the
new syntax, import:
```haskell
import Database.Esqueleto.Experimental
```
This will conflict with the definition of `from` and `on` in the
`Database.Esqueleto` module, so you'll want to remove that import.
This style will become the new "default" in esqueleto-4.0.0.0, so it's a good
idea to port your code to using it soon.
The module documentation in `Database.Esqueleto.Experimental` has many examples,
and they won't be repeated here. Here's a quick sample:
```haskell
select $ do
(a :& b) <-
from $
Table @BlogPost
`InnerJoin`
Table @Person
`on` do \(bp :& a) ->
bp ^. BlogPostAuthorId ==. a ^. PersonId
pure (a, b)
```
Advantages:
- `ON` clause is attached directly to the relevant join, so you never need to
worry about how they're ordered, nor will you ever run into bugs where the
`on` clause is on the wrong `JOIN`
- The `ON` clause lambda will all the available tables in it. This forbids
runtime errors where an `ON` clause refers to a table that isn't in scope yet.
- You can join on a table twice, and the aliases work out fine with the `ON`
clause.
- You can use `UNION`, `EXCEPT`, `INTERSECTION` etc with this new syntax!
- You can reuse subqueries more easily.
## Legacy Joins
Implicit joins are represented by tuples.
@ -195,8 +292,6 @@ INNER JOIN Follow ON P1.id = Follow.follower
INNER JOIN Person AS P2 ON P2.id = Follow.followed
```
Note carefully that the order of the ON clauses is reversed! You're required to write your `on`s in reverse order because that helps composability (see the documentation of `on` for more details).
## Update and Delete
```haskell
@ -243,12 +338,159 @@ There are many differences between SQL syntax and functions supported by differe
In order to use these functions, you need to explicitly import their corresponding modules.
### Unsafe functions, operators and values
### Tests and Postgres
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.
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!
The most common use of these functions is for calling RDBMS specific or custom functions,
for that end we use `unsafeSqlFunction`. For example, if we wish to consult the postgres
`now` function we could so as follow:
```haskell
postgresTime :: (MonadIO m, MonadLogger m) => SqlWriteT m UTCTime
postgresTime =
result <- select (pure now)
case result of
[x] -> pure x
_ -> error "now() is guaranteed to return a single result"
where
now :: SqlExpr (Value UTCTime)
now = unsafeSqlFunction "now" ()
```
which generates this SQL:
```sql
SELECT now()
```
With the `now` function we could now use the current time of the postgres RDBMS on any query.
Do notice that `now` does not use any arguments, so we use `()` that is an instance of
`UnsafeSqlFunctionArgument` to represent no arguments, an empty list cast to a correct value
will yield the same result as `()`.
We can also use `unsafeSqlFunction` for more complex functions with customs values using
`unsafeSqlValue` which turns any string into a sql value of whatever type we want, disclaimer:
if you use it badly you will cause a runtime error. For example, say we want to try postgres'
`date_part` function and get the day of a timestamp, we could use:
```haskell
postgresTimestampDay :: (MonadIO m, MonadLogger m) => SqlWriteT m Int
postgresTimestampDay =
result <- select (return $ dayPart date)
case result of
[x] -> pure x
_ -> error "dayPart is guaranteed to return a single result"
where
dayPart :: SqlExpr (Value UTCTime) -> SqlExpr (Value Int)
dayPart s = unsafeSqlFunction "date_part" (unsafeSqlValue "\'day\'" :: SqlExpr (Value String) ,s)
date :: SqlExpr (Value UTCTime)
date = unsafeSqlValue "TIMESTAMP \'2001-02-16 20:38:40\'"
```
which generates this SQL:
```sql
SELECT date_part('day', TIMESTAMP '2001-02-16 20:38:40')
```
Using `unsafeSqlValue` we were required to also define the type of the value.
Another useful unsafe function is `unsafeSqlCastAs`, which allows us to cast any type
to another within a query. For example, say we want to use our previews `dayPart` function
on the current system time, we could:
```haskell
postgresTimestampDay :: (MonadIO m, MonadLogger m) => SqlWriteT m Int
postgresTimestampDay = do
currentTime <- liftIO getCurrentTime
result <- select (return $ dayPart (toTIMESTAMP $ val currentTime))
case result of
[x] -> pure x
_ -> error "dayPart is guaranteed to return a single result"
where
dayPart :: SqlExpr (Value UTCTime) -> SqlExpr (Value Int)
dayPart s = unsafeSqlFunction "date_part" (unsafeSqlValue "\'day\'" :: SqlExpr (Value String) ,s)
toTIMESTAMP :: SqlExpr (Value UTCTime) -> SqlExpr (Value UTCTime)
toTIMESTAMP = unsafeSqlCastAs "TIMESTAMP"
```
which generates this SQL:
```sql
SELECT date_part('day', CAST('2019-10-28 23:19:39.400898344Z' AS TIMESTAMP))
```
### SQL injection
Esqueleto uses parameterization to prevent sql injections on values and arguments
on all queries, for example, if we have:
```haskell
myEvilQuery :: (MonadIO m, MonadLogger m) => SqlWriteT m ()
myEvilQuery =
select (return $ val ("hi\'; DROP TABLE foo; select \'bye\'" :: String)) >>= liftIO . print
```
which generates this SQL(when using postgres):
```sql
SELECT 'hi''; DROP TABLE foo; select ''bye'''
```
And the printed value is `hi\'; DROP TABLE foo; select \'bye\'` and no table is dropped. This is good
and makes the use of strings values safe. Unfortunately this is not the case when using unsafe functions.
Let's see an example of defining a new evil `now` function:
```haskell
myEvilQuery :: (MonadIO m, MonadLogger m) => SqlWriteT m ()
myEvilQuery =
select (return nowWithInjection) >>= liftIO . print
where
nowWithInjection :: SqlExpr (Value UTCTime)
nowWithInjection = unsafeSqlFunction "0; DROP TABLE bar; select now" ([] :: [SqlExpr (Value Int)])
```
which generates this SQL:
```sql
SELECT 0; DROP TABLE bar; select now()
```
If we were to run the above code we would see the postgres time printed but the table `bar`
will be erased with no indication whatsoever. Another example of this behavior is seen when using
`unsafeSqlValue`:
```haskell
myEvilQuery :: (MonadIO m, MonadLogger m) => SqlWriteT m ()
myEvilQuery =
select (return $ dayPart dateWithInjection) >>= liftIO . print
where
dayPart :: SqlExpr (Value UTCTime) -> SqlExpr (Value Int)
dayPart s = unsafeSqlFunction "date_part" (unsafeSqlValue "\'day\'" :: SqlExpr (Value String) ,s)
dateWithInjection :: SqlExpr (Value UTCTime)
dateWithInjection = unsafeSqlValue "TIMESTAMP \'2001-02-16 20:38:40\');DROP TABLE bar; select (16"
```
which generates this SQL:
```sql
SELECT date_part('day', TIMESTAMP '2001-02-16 20:38:40');DROP TABLE bar; select (16)
```
This will print 16 and also erase the `bar` table. The main take away of this examples is to
never use any user or third party input inside an unsafe function without first parsing it or
heavily sanitizing the input.
### Tests
To run the tests, do `stack test`. This tests all the backends, so you'll need
to have MySQL and Postgresql installed.
#### Postgres
Using apt-get, you should be able to do:
```
@ -274,23 +516,31 @@ withConn =
You can change these if you like but to just get them working set up as follows on linux:
```$ sudo -u postgres createuser esqutest```
```$ sudo -u postgres createdb esqutest```
```
$ sudo -u postgres createuser esqutest
$ sudo -u postgres createdb esqutest
$ sudo -u postgres psql
postgres=# \password esqutest
```
And on osx
```$ createuser esqutest```
```$ createdb esqutest```
```
$ createuser esqutest
$ createdb esqutest
$ psql postgres
postgres=# \password esqutest
```
#### MySQL
To test MySQL, you'll need to have a MySQL server installation.
Then, you'll need to create a database `esqutest` and a `'travis'@'localhost'`
user which can access it:
```
mysql> CREATE DATABASE esqutest;
mysql> CREATE USER 'travis'@'localhost';
mysql> ALTER USER 'travis'@'localhost' IDENTIFIED BY 'esqutest';
mysql> GRANT ALL ON esqutest.* TO 'travis'@'localhost';
```

5
cabal.project Normal file
View File

@ -0,0 +1,5 @@
-- Generated by stackage-to-hackage
packages:
./
, examples/

View File

@ -1,5 +1,277 @@
Unreleased
3.5.4.0
=======
- @parsonsmatt
- [#310](https://github.com/bitemyapp/esqueleto/pull/310)
- Add instances of `HasField` for `SqlExpr (Entity rec)` and `SqlExpr
(Maybe (Entity rec))`. These instances allow you to use the
`OverloadedRecordDot` language extension in GHC 9.2 with SQL
representations of database entities.
3.5.3.2
=======
- @parsonsmatt
- [#309](https://github.com/bitemyapp/esqueleto/pull/309)
- Bump `time` version bound
3.5.3.1
=======
- @jappeace
- [#303](https://github.com/bitemyapp/esqueleto/pull/303)
- Added docs for delete function for new experimental API.
3.5.3.0
=======
- @m4dc4p
- [#291](https://github.com/bitemyapp/esqueleto/pull/291)
- Added `ToAlias` and `ToAliasReference` instaces to the `:&` type, mirroring
the tuple instances for the same classes. See [Issue #290](https://github.com/bitemyapp/esqueleto/issues/290)
for discussion.
- @NikitaRazmakhnin
- [#284](https://github.com/bitemyapp/esqueleto/pull/284)
- Add PostgreSQL-specific support of VALUES(..) literals
3.5.2.2
=======
- @NikitaRazmakhnin
- [#278](https://github.com/bitemyapp/esqueleto/pull/278)
- Fix generating of bad sql using nexted expressions with `distinctOnOrderBy`.
3.5.2.1
=======
- @cdparks
- [#273](https://github.com/bitemyapp/esqueleto/pull/273)
- Avoid generating an empty list as the left operand to `NOT IN`.
3.5.2.0
=======
- @ivanbakel
- [#268](https://github.com/bitemyapp/esqueleto/pull/268)
- Added `SqlSelect` instance for `(:&)`, allowing it to be returned from
queries just like `(,)` tuples.
3.5.1.0
=======
- @ibarrae
- [#265](https://github.com/bitemyapp/esqueleto/pull/265)
- Added `selectOne`
3.5.0.0
=======
- @belevy
- [#228](https://github.com/bitemyapp/esqueleto/pull/228)
- Destroy all GADTs; Removes the From GADT and SqlExpr GADT
- From GADT is replaced with a From data type and FromRaw
- SqlExpr is now all defined in terms of ERaw
- Modified ERaw to contain a SqlExprMeta with any extra information
that may be needed
- Experimental top level is now strictly for documentation and all the
implementation details are in Experimental.* modules
- @parsonsmatt
- [#259](https://github.com/bitemyapp/esqueleto/pull/259)
- Create the `Database.Esqueleto.Legacy` module. The
`Database.Esqueleto` module now emits a warning, directing users to
either import `Database.Esqueleto.Legacy` to keep the old behavior or
to import `Database.Esqueleto.Experimental` to opt in to the new
behavior.
- Deleted the deprecated modules
`Database.Esqueleto.Internal.{Language,Sql}`. Please use
`Database.Esqueleto.Internal.Internal` instead, or ideally post what
you need from the library so we can support you safely.
- Support GHC 9
3.4.2.2
=======
- @parsonsmatt
- [#255](https://github.com/bitemyapp/esqueleto/pull/255)
- Fix a bug where a composite primary key in a `groupBy` clause would break.
3.4.2.1
=======
- @parsonsmatt
- [#245](https://github.com/bitemyapp/esqueleto/pull/245)
- Support `persistent-2.13`
3.4.2.0
=======
- @parsonsmatt
- [#243](https://github.com/bitemyapp/esqueleto/pull/243)
- Support `persistent-2.12`
3.4.1.1
=======
- @MaxGabriel
- [#240](https://github.com/bitemyapp/esqueleto/pull/240/files)
- Improve recommend hlint to avoid doing `x = NULL` SQL queries
3.4.1.0
=======
- @arthurxavierx
- [#238](https://github.com/bitemyapp/esqueleto/pull/238)
- Fix non-exhaustive patterns in `unsafeSqlAggregateFunction`
- @Vlix
- [#232](https://github.com/bitemyapp/esqueleto/pull/232)
- Export the `ValidOnClauseValue` type family
3.4.0.1
=======
- @arthurxavierx
- [#221](https://github.com/bitemyapp/esqueleto/pull/221)
- Deprecate `ToAliasT` and `ToAliasReferenceT`
- @parsonsmatt
- [#226](https://github.com/bitemyapp/esqueleto/pull/226)
- Support `persistent-2.11`
- @belevy
- [#225](https://github.com/bitemyapp/esqueleto/pull/225)
- Simplify `ToFromT` extracting the overlapping and type error instances
- Make `ToFromT` and associated type family of `ToFrom`
3.4.0.0
=======
- @belevy, @charukiewicz
- [#215](https://github.com/bitemyapp/esqueleto/pull/215)
- Added support for common table expressions (`with`, `withRecursive`)
- Added support for lateral JOINs with updated example (Example #6)
- Deprecated `SelectQuery`, removing the neccessity to tag `SqlQuery` values
- Deprecated use of data constructors for SQL set operations (replaced with functions)
- Refactored module structure to fix haddock build (fixes build from `3.3.4.0`)
3.3.4.1
=======
- @maxgabriel
- [#214](https://github.com/bitemyapp/esqueleto/pull/214)
- Add suggested hlint rules for proper `isNothing` usage
3.3.4.0
=======
- @parsonsmatt
- [#205](https://github.com/bitemyapp/esqueleto/pull/205)
- More documentation on the `Experimental` module
- `Database.Esqueleto.Experimental` now reexports `Database.Esqueleto`, so
the new "approved" import syntax is less verbose. Before, you'd write:
```haskell
import Database.Esqueleto hiding (from, on)
import Database.Esqueleto.Experimental
```
Now you can merely write:
```haskell
import Database.Esqueleto.Experimental
```
Users will get 'redundant import' warnings if they followed the original
syntax, the solution is evident from the error message provided.
3.3.3.3
=======
- @belevy
- [#191](https://github.com/bitemyapp/esqueleto/pull/191) - Bugfix rollup:
Fix issue with extra characters in generated SQL;
Fix ToAliasReference for already referenced values;
Fix Alias/Reference for Maybe Entity
- @maxgabriel
- [#203](https://github.com/bitemyapp/esqueleto/pull/203) Document `isNothing`
- @sestrella
- [#198](https://github.com/bitemyapp/esqueleto/pull/198) - Allow PostgreSQL aggregate functions to take a filter clause
3.3.3.2
========
- @maxgabriel
- [#190](https://github.com/bitemyapp/esqueleto/pull/190) Further document and test `ToBaseId`
3.3.3.1
========
- @belevy
- [#189](https://github.com/bitemyapp/esqueleto/pull/189) - Fix bug in function calls with
aliased values introduced by SubQuery joins.
3.3.3.0
========
- @belevy
- [#172](https://github.com/bitemyapp/esqueleto/pull/172) - Introduce new
experimental module for joins, set operations (eg UNION), and safer queries
from outer joins.
3.3.2
========
- @belevy
- [#177](https://github.com/bitemyapp/esqueleto/pull/177) Fix natural key handling in (^.)
3.3.1.1
========
- @parsonsmatt
- [#170](https://github.com/bitemyapp/esqueleto/pull/170) Add documentation to `groupBy` to explain tuple nesting.
3.3.1
========
- @charukiewicz, @belevy, @joemalin95
- [#167](https://github.com/bitemyapp/esqueleto/pull/167): Exposed functions that were added in `3.3.0`
3.3.0
========
- @charukiewicz, @belevy, @joemalin95
- [#166](https://github.com/bitemyapp/esqueleto/pull/166): Add several common SQL string functions: `upper_`, `trim_`, `ltrim_`, `rtrim_`, `length_`, `left_`, `right_`
3.2.3
========
- @hdgarrood
- [#163](https://github.com/bitemyapp/esqueleto/pull/163): Allow `unsafeSqlFunction` to take up to 10 arguments without needing to nest tuples.
3.2.2
========
- @parsonsmatt
- [#161](https://github.com/bitemyapp/esqueleto/pull/161/): Fix an issue where
nested joins didn't get the right on clause.
3.2.1
========
- @parsonsmatt
- [#159](https://github.com/bitemyapp/esqueleto/pull/159): Add an instance of `UnsafeSqlFunction ()` for 0-argument SQL
functions.
3.2.0
========
- @parsonsmatt
- [#153](https://github.com/bitemyapp/esqueleto/pull/153): Deprecate
`sub_select` and introduce `subSelect`, `subSelectMaybe`, and
`subSelectUnsafe`.
- @parsonsmatt
- [#156](https://github.com/bitemyapp/esqueleto/pull/156): Remove the
restriction that `on` clauses must appear in reverse order to the joining
tables.
3.1.3
========
- @JoseD92
- [#155](https://github.com/bitemyapp/esqueleto/pull/149): Added `insertSelectWithConflict` postgres function.
3.1.2
========
- @tippenein
- [#149](https://github.com/bitemyapp/esqueleto/pull/157): Added `associateJoin` query helpers.
3.1.1
=======
- @JoseD92
- [#149](https://github.com/bitemyapp/esqueleto/pull/149): Added `upsert` support.
- @parsonsmatt
- [#133](https://github.com/bitemyapp/esqueleto/pull/133): Added `renderQueryToText` and related functions.
3.1.0
=======

View File

@ -1,7 +1,7 @@
cabal-version: 1.12
name: esqueleto
version: 3.1.0
version: 3.5.4.0
synopsis: Type-safe EDSL for SQL queries on persistent backends.
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.
.
@ -23,142 +23,106 @@ extra-source-files:
changelog.md
source-repository head
type: git
location: git://github.com/bitemyapp/esqueleto.git
type: git
location: git://github.com/bitemyapp/esqueleto.git
library
exposed-modules:
Database.Esqueleto
Database.Esqueleto.Internal.Language
Database.Esqueleto.Internal.Sql
Database.Esqueleto.MySQL
Database.Esqueleto.PostgreSQL
Database.Esqueleto.PostgreSQL.JSON
Database.Esqueleto.SQLite
other-modules:
Database.Esqueleto.Internal.Internal
Database.Esqueleto.Internal.PersistentImport
Database.Esqueleto.PostgreSQL.JSON.Instances
Paths_esqueleto
hs-source-dirs:
src/
build-depends:
base >=4.8 && <5.0
, aeson >=1.0
, blaze-html
, bytestring
, conduit >=1.3
, monad-logger
, persistent >=2.10.0 && <2.11
, resourcet >=1.2
, tagged >=0.2
, text >=0.11 && <1.3
, time >=1.5.0.1 && <=1.8.0.2
, transformers >=0.2
, unliftio
, unordered-containers >=0.2
if impl(ghc >=8.0)
ghc-options: -Wall -Wno-redundant-constraints
else
ghc-options: -Wall
default-language: Haskell2010
exposed-modules:
Database.Esqueleto
Database.Esqueleto.Legacy
Database.Esqueleto.Experimental
Database.Esqueleto.Internal.Internal
Database.Esqueleto.Internal.ExprParser
Database.Esqueleto.MySQL
Database.Esqueleto.PostgreSQL
Database.Esqueleto.PostgreSQL.JSON
Database.Esqueleto.SQLite
Database.Esqueleto.Experimental.From
Database.Esqueleto.Experimental.From.CommonTableExpression
Database.Esqueleto.Experimental.From.Join
Database.Esqueleto.Experimental.From.SqlSetOperation
Database.Esqueleto.Experimental.ToAlias
Database.Esqueleto.Experimental.ToAliasReference
Database.Esqueleto.Experimental.ToMaybe
other-modules:
Database.Esqueleto.PostgreSQL.JSON.Instances
Database.Esqueleto.Internal.PersistentImport
Paths_esqueleto
hs-source-dirs:
src/
build-depends:
base >=4.8 && <5.0
, aeson >=1.0
, attoparsec >= 0.13 && < 0.15
, blaze-html
, bytestring
, conduit >=1.3
, containers
, monad-logger
, persistent >=2.13 && <3
, resourcet >=1.2
, tagged >=0.2
, text >=0.11 && <1.3
, time >=1.5.0.1 && <=1.13
, transformers >=0.2
, unliftio
, unordered-containers >=0.2
ghc-options:
-Wall
-Wno-redundant-constraints
-Wincomplete-uni-patterns
-Wincomplete-record-updates
-Wpartial-fields
-Wmissing-home-modules
-Widentities
-Wcpp-undef
-Wcpp-undef
default-language: Haskell2010
test-suite mysql
type: exitcode-stdio-1.0
main-is: MySQL/Test.hs
other-modules:
Common.Test
Paths_esqueleto
hs-source-dirs:
test
ghc-options: -Wall
build-depends:
base >=4.8 && <5.0
, blaze-html
, bytestring
, conduit >=1.3
, containers
, esqueleto
, exceptions
, hspec
, monad-logger
, mysql
, mysql-simple
, persistent >=2.8.0 && <2.11
, persistent-mysql
, persistent-template
, resourcet >=1.2
, tagged >=0.2
, text >=0.11 && <1.3
, time >=1.5.0.1 && <=1.8.0.2
, transformers >=0.2
, unliftio
, unordered-containers >=0.2
default-language: Haskell2010
test-suite postgresql
type: exitcode-stdio-1.0
main-is: PostgreSQL/Test.hs
other-modules:
Common.Test
PostgreSQL.MigrateJSON
Paths_esqueleto
hs-source-dirs:
test
ghc-options: -Wall
build-depends:
base >=4.8 && <5.0
, aeson
, blaze-html
, bytestring
, conduit >=1.3
, containers
, esqueleto
, exceptions
, hspec
, monad-logger
, persistent >=2.10.0 && <2.11
, persistent-postgresql >= 2.10.0 && <2.11
, persistent-template
, postgresql-libpq
, postgresql-simple
, resourcet >=1.2
, tagged >=0.2
, text >=0.11 && <1.3
, time >=1.5.0.1 && <=1.8.0.2
, transformers >=0.2
, unliftio
, unordered-containers >=0.2
, vector
default-language: Haskell2010
test-suite sqlite
type: exitcode-stdio-1.0
main-is: SQLite/Test.hs
other-modules:
Common.Test
Paths_esqueleto
hs-source-dirs:
test
ghc-options: -Wall
build-depends:
base >=4.8 && <5.0
, blaze-html
, bytestring
, conduit >=1.3
, containers
, esqueleto
, exceptions
, hspec
, monad-logger
, persistent >=2.8.0 && <2.11
, persistent-sqlite
, persistent-template
, resourcet >=1.2
, tagged >=0.2
, text >=0.11 && <1.3
, time >=1.5.0.1 && <=1.8.0.2
, transformers >=0.2
, unliftio
, unordered-containers >=0.2
default-language: Haskell2010
test-suite specs
type: exitcode-stdio-1.0
main-is: Spec.hs
other-modules:
Common.Test
Common.Test.Models
Common.Test.Import
Common.Test.Select
PostgreSQL.MigrateJSON
SQLite.Test
PostgreSQL.Test
MySQL.Test
default-extensions:
RankNTypes
hs-source-dirs:
test
ghc-options: -Wall -threaded
build-depends:
base >=4.8 && <5.0
, aeson
, attoparsec
, blaze-html
, bytestring
, conduit
, containers
, esqueleto
, exceptions
, hspec
, hspec-core
, monad-logger
, mtl
, mysql
, mysql-simple
, persistent
, persistent-mysql
, persistent-postgresql
, persistent-sqlite
, postgresql-simple
, QuickCheck
, resourcet
, tagged
, text
, time
, transformers
, unliftio
, unordered-containers
default-language: Haskell2010

1
examples/.gitignore vendored
View File

@ -1 +0,0 @@
*.cabal

View File

@ -11,7 +11,7 @@ module Blog
import Control.Monad.Base (MonadBase (..))
import Control.Monad.IO.Unlift (MonadUnliftIO(..), wrappedWithRunInIO)
import Control.Monad.Logger (MonadLogger, NoLoggingT (..))
import Control.Monad.Logger (MonadLoggerIO, MonadLogger, NoLoggingT (..))
import Control.Monad.Reader
import Control.Monad.Trans.Control (ComposeSt, MonadBaseControl (..),
MonadTransControl (..),
@ -26,6 +26,7 @@ newtype BlogT m a = BlogT { unBlogT :: NoLoggingT (ReaderT ConnectionString m) a
, MonadLogger
, MonadReader ConnectionString
, MonadIO
, MonadLoggerIO
)
instance MonadUnliftIO m => MonadUnliftIO (BlogT m) where

View File

@ -1,42 +1,44 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-unused-top-binds #-}
module Main
( main
) where
import Blog
import Control.Monad (void)
import Control.Monad (forM_)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.IO.Unlift (MonadUnliftIO)
import Control.Monad.Logger (MonadLogger)
import Control.Monad.Reader (MonadReader (..), runReaderT)
import Control.Monad.Trans.Control (MonadBaseControl)
import Data.Monoid ((<>))
import Database.Esqueleto
import Database.Persist.Postgresql (ConnectionString,
withPostgresqlConn)
import Database.Persist.TH ( AtLeastOneUniqueKey(..)
, OnlyOneUniqueKey(..)
, mkDeleteCascade
, mkMigrate
, mkPersist
, persistLowerCase
, share
, sqlSettings
)
import Blog
import Control.Monad (void)
import Control.Monad (forM_)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.IO.Unlift (MonadUnliftIO)
import Control.Monad.Logger (MonadLogger, MonadLoggerIO)
import Control.Monad.Reader (MonadReader(..), runReaderT)
import Control.Monad.Trans.Control (MonadBaseControl)
import Data.Monoid ((<>))
import Database.Esqueleto
import Database.Persist.Postgresql (ConnectionString, withPostgresqlConn)
import Database.Persist.TH
( AtLeastOneUniqueKey(..)
, OnlyOneUniqueKey(..)
, mkDeleteCascade
, mkMigrate
, mkPersist
, persistLowerCase
, share
, sqlSettings
)
share [ mkPersist sqlSettings
@ -163,6 +165,7 @@ runDB :: (MonadReader ConnectionString m,
MonadIO m,
MonadBaseControl IO m,
MonadUnliftIO m,
MonadLoggerIO m,
MonadLogger m)
=> SqlPersistT m a -> m a
runDB query = do

View File

@ -0,0 +1,49 @@
cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.34.4.
--
-- see: https://github.com/sol/hpack
--
-- hash: d5fddaf37d0c2f27fb2446f5038899d766102efd74ccfe4c7bcd02c61837e6b6
name: esqueleto-examples
version: 0.0.0.0
category: Database
homepage: https://github.com/bitemyapp/esqueleto#readme
bug-reports: https://github.com/bitemyapp/esqueleto/issues
author: Fintan Halpenny
maintainer: cma@bitemyapp.com
copyright: 2019, Chris Allen
license: BSD3
license-file: LICENSE
build-type: Simple
extra-source-files:
README.md
source-repository head
type: git
location: https://github.com/bitemyapp/esqueleto
flag werror
description: Treat warnings as errors
manual: True
default: False
executable blog-example
main-is: Main.hs
other-modules:
Blog
ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
build-depends:
base
, esqueleto
, monad-control
, monad-logger
, mtl
, persistent >=2.12
, persistent-postgresql
, transformers-base
, unliftio-core
if flag(werror)
ghc-options: -Werror
default-language: Haskell2010

View File

@ -13,8 +13,7 @@ extra-source-files:
dependencies:
- base
- esqueleto
- persistent
- persistent-template
- persistent >= 2.12
- persistent-postgresql
- mtl
- monad-logger

View File

@ -1,4 +1,8 @@
{-# LANGUAGE FlexibleContexts, FlexibleInstances, GADTs #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RankNTypes #-}
-- | The @esqueleto@ EDSL (embedded domain specific language).
-- This module replaces @Database.Persist@, so instead of
-- importing that module you should just import this one:
@ -27,7 +31,16 @@
--
-- Other than identifier name clashes, @esqueleto@ does not
-- conflict with @persistent@ in any way.
module Database.Esqueleto
--
-- Note that the facilities for @JOIN@ have been significantly improved in the
-- "Database.Esqueleto.Experimental" module. The definition of 'from' and 'on'
-- in this module will be replaced with those at the 4.0.0.0 version, so you are
-- encouraged to migrate to the new method.
--
-- This module has an attached WARNING message indicating that the Experimental
-- syntax will become the default. If you want to continue using the old syntax,
-- please refer to "Database.Esqueleto.Legacy" as a drop-in replacement.
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." #-}
( -- * Setup
-- $setup
@ -48,11 +61,18 @@ module Database.Esqueleto
, random_, round_, ceiling_, floor_
, min_, max_, sum_, avg_, castNum, castNumM
, coalesce, coalesceDefault
, lower_, like, ilike, (%), concat_, (++.), castString
, lower_, upper_, trim_, ltrim_, rtrim_, length_, left_, right_
, like, ilike, (%), concat_, (++.), castString
, subList_select, valList, justList
, in_, notIn, exists, notExists
, set, (=.), (+=.), (-=.), (*=.), (/=.)
, case_, toBaseId
, subSelect
, subSelectMaybe
, subSelectCount
, subSelectForeign
, subSelectList
, subSelectUnsafe
, ToBaseId(..)
, when_
, then_
@ -70,13 +90,14 @@ module Database.Esqueleto
, LeftOuterJoin(..)
, RightOuterJoin(..)
, FullOuterJoin(..)
, JoinKind(..)
, OnClauseWithoutMatchingJoinException(..)
-- * SQL backend
, SqlQuery
, SqlExpr
, SqlEntity
, select
, selectOne
, selectSource
, delete
, deleteCount
@ -86,6 +107,12 @@ module Database.Esqueleto
, insertSelectCount
, (<#)
, (<&>)
-- ** Rendering Queries
, renderQueryToText
, renderQuerySelect
, renderQueryUpdate
, renderQueryDelete
, renderQueryInsertInto
-- * Internal.Language
, From
-- * RDBMS-specific modules
@ -94,6 +121,7 @@ module Database.Esqueleto
-- * Helpers
, valkey
, valJ
, associateJoin
-- * Re-exports
-- $reexports
@ -101,13 +129,8 @@ module Database.Esqueleto
, module Database.Esqueleto.Internal.PersistentImport
) where
import Control.Monad.IO.Class (MonadIO)
import Control.Monad.Trans.Reader (ReaderT)
import Data.Int (Int64)
import Database.Esqueleto.Internal.Language
import Database.Esqueleto.Internal.Sql
import Database.Esqueleto.Legacy
import Database.Esqueleto.Internal.PersistentImport
import qualified Database.Persist
-- $setup
@ -319,16 +342,11 @@ import qualified Database.Persist
-- @
-- 'select' $
-- 'from' $ \\(p1 `'InnerJoin`` f `'InnerJoin`` p2) -> do
-- 'on' (p2 '^.' PersonId '==.' f '^.' FollowFollowed)
-- 'on' (p1 '^.' PersonId '==.' f '^.' FollowFollower)
-- 'on' (p2 '^.' PersonId '==.' f '^.' FollowFollowed)
-- return (p1, f, p2)
-- @
--
-- /Note carefully that the order of the ON clauses is/
-- /reversed!/ You're required to write your 'on's in reverse
-- order because that helps composability (see the documentation
-- of 'on' for more details).
--
-- We also currently support @UPDATE@ and @DELETE@ statements.
-- For example:
--
@ -395,41 +413,3 @@ import qualified Database.Persist
--
-- In order to use these functions, you need to explicitly import
-- their corresponding modules, they're not re-exported here.
----------------------------------------------------------------------
-- | @valkey i = 'val' . 'toSqlKey'@
-- (<https://github.com/prowdsponsor/esqueleto/issues/9>).
valkey :: (ToBackendKey SqlBackend entity, PersistField (Key entity)) =>
Int64 -> SqlExpr (Value (Key entity))
valkey = val . toSqlKey
-- | @valJ@ is like @val@ but for something that is already a @Value@. The use
-- case it was written for was, given a @Value@ lift the @Key@ for that @Value@
-- into the query expression in a type safe way. However, the implementation is
-- more generic than that so we call it @valJ@.
--
-- Its important to note that the input entity and the output entity are
-- constrained to be the same by the type signature on the function
-- (<https://github.com/prowdsponsor/esqueleto/pull/69>).
--
-- /Since: 1.4.2/
valJ :: (PersistField (Key entity)) =>
Value (Key entity) -> SqlExpr (Value (Key entity))
valJ = val . unValue
----------------------------------------------------------------------
-- | Synonym for 'Database.Persist.Store.delete' that does not
-- clash with @esqueleto@'s 'delete'.
deleteKey :: ( PersistStore backend
, BaseBackend backend ~ PersistEntityBackend val
, MonadIO m
, PersistEntity val )
=> Key val -> ReaderT backend m ()
deleteKey = Database.Persist.delete

View File

@ -0,0 +1,563 @@
{-# LANGUAGE PatternSynonyms #-}
-- | This module contains a new way (introduced in 3.3.3.0) of using @FROM@ in
-- Haskell. The old method was a bit finicky and could permit runtime errors,
-- and this new way is both significantly safer and much more powerful.
--
-- This syntax will become the default syntax exported from the library in
-- version @3.6.0.0@. To use the old syntax, see "Database.Esqueleto.Legacy".
module Database.Esqueleto.Experimental
( -- * Setup
-- $setup
-- * Introduction
-- $introduction
-- * A New Syntax
-- $new-syntax
-- * Documentation
-- ** Basic Queries
from
, table
, Table(..)
, SubQuery(..)
, selectQuery
-- ** Joins
, (:&)(..)
, on
, innerJoin
, innerJoinLateral
, leftJoin
, leftJoinLateral
, rightJoin
, fullOuterJoin
, crossJoin
, crossJoinLateral
-- ** Set Operations
-- $sql-set-operations
, union_
, Union(..)
, unionAll_
, UnionAll(..)
, except_
, Except(..)
, intersect_
, Intersect(..)
, pattern SelectQuery
-- ** Common Table Expressions
, with
, withRecursive
-- ** Internals
, From(..)
, ToMaybe(..)
, ToAlias(..)
, ToAliasT
, ToAliasReference(..)
, ToAliasReferenceT
, ToSqlSetOperation(..)
-- * The Normal Stuff
, where_
, groupBy
, orderBy
, rand
, asc
, desc
, limit
, offset
, distinct
, distinctOn
, don
, distinctOnOrderBy
, having
, locking
, sub_select
, (^.)
, (?.)
, val
, isNothing
, just
, nothing
, joinV
, withNonNull
, countRows
, count
, countDistinct
, not_
, (==.)
, (>=.)
, (>.)
, (<=.)
, (<.)
, (!=.)
, (&&.)
, (||.)
, between
, (+.)
, (-.)
, (/.)
, (*.)
, random_
, round_
, ceiling_
, floor_
, min_
, max_
, sum_
, avg_
, castNum
, castNumM
, coalesce
, coalesceDefault
, lower_
, upper_
, trim_
, ltrim_
, rtrim_
, length_
, left_
, right_
, like
, ilike
, (%)
, concat_
, (++.)
, castString
, subList_select
, valList
, justList
, in_
, notIn
, exists
, notExists
, set
, (=.)
, (+=.)
, (-=.)
, (*=.)
, (/=.)
, case_
, toBaseId
, subSelect
, subSelectMaybe
, subSelectCount
, subSelectForeign
, subSelectList
, subSelectUnsafe
, ToBaseId(..)
, when_
, then_
, else_
, Value(..)
, ValueList(..)
, OrderBy
, DistinctOn
, LockingKind(..)
, SqlString
-- ** Joins
, InnerJoin(..)
, CrossJoin(..)
, LeftOuterJoin(..)
, RightOuterJoin(..)
, FullOuterJoin(..)
, JoinKind(..)
, OnClauseWithoutMatchingJoinException(..)
-- ** SQL backend
, SqlQuery
, SqlExpr
, SqlEntity
, select
, selectOne
, selectSource
, delete
, deleteCount
, update
, updateCount
, insertSelect
, insertSelectCount
, (<#)
, (<&>)
-- ** Rendering Queries
, renderQueryToText
, renderQuerySelect
, renderQueryUpdate
, renderQueryDelete
, renderQueryInsertInto
-- ** Helpers
, valkey
, valJ
, associateJoin
-- ** Re-exports
-- $reexports
, deleteKey
, module Database.Esqueleto.Internal.PersistentImport
) where
import Database.Esqueleto.Internal.Internal hiding (From, from, on)
import Database.Esqueleto.Internal.PersistentImport
import Database.Esqueleto.Experimental.From
import Database.Esqueleto.Experimental.From.CommonTableExpression
import Database.Esqueleto.Experimental.From.Join
import Database.Esqueleto.Experimental.From.SqlSetOperation
import Database.Esqueleto.Experimental.ToAlias
import Database.Esqueleto.Experimental.ToAliasReference
import Database.Esqueleto.Experimental.ToMaybe
-- $setup
--
-- If you're already using "Database.Esqueleto", then you can get
-- started using this module just by changing your imports slightly,
-- as well as enabling the [TypeApplications](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#extension-TypeApplications) extension.
--
-- @
-- {-\# LANGUAGE TypeApplications \#-}
--
-- ...
--
-- import Database.Esqueleto.Experimental
-- @
--
-- Note: Prior to @esqueleto-3.3.4.0@, the @Database.Esqueleto.Experimental@
-- module did not reexport @Data.Esqueleto@.
----------------------------------------------------------------------
-- $introduction
--
-- This module is fully backwards-compatible extension to the @esqueleto@
-- EDSL that expands subquery functionality and enables
-- [SQL set operations](https://en.wikipedia.org/wiki/Set_operations_(SQL\))
-- to be written directly in Haskell. Specifically, this enables:
--
-- * Subqueries in 'JOIN' statements
-- * 'UNION'
-- * 'UNION' 'ALL'
-- * 'INTERSECT'
-- * 'EXCEPT'
--
-- As a consequence of this, several classes of runtime errors are now
-- caught at compile time. This includes missing 'on' clauses and improper
-- handling of @Maybe@ values in outer joins.
--
-- This module can be used in conjunction with the main "Database.Esqueleto"
-- module, but doing so requires qualified imports to avoid ambiguous
-- definitions of 'on' and 'from', which are defined in both modules.
--
-- Below we will give an overview of how to use this module and the
-- features it enables.
----------------------------------------------------------------------
-- $new-syntax
--
-- This module introduces a new syntax that serves to enable the aforementioned
-- features. This new syntax also changes how joins written in the @esqueleto@
-- EDSL to more closely resemble the underlying SQL.
--
-- For our examples, we'll use a schema similar to the one in the Getting Started
-- section of "Database.Esqueleto":
--
-- @
-- share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
-- Person
-- name String
-- age Int Maybe
-- deriving Eq Show
-- BlogPost
-- title String
-- authorId PersonId
-- deriving Eq Show
-- Follow
-- follower PersonId
-- followed PersonId
-- deriving Eq Show
-- |]
-- @
--
-- === Example 1: Simple select
--
-- Let's select all people who are named \"John\".
--
-- ==== "Database.Esqueleto":
--
-- @
-- select $
-- from $ \\people -> do
-- where_ (people ^. PersonName ==. val \"John\")
-- pure people
-- @
--
-- ==== "Database.Esqueleto.Experimental":
--
-- @
-- select $ do
-- people <- from $ table \@Person
-- where_ (people ^. PersonName ==. val \"John\")
-- pure people
-- @
--
--
-- === Example 2: Select with join
--
-- Let's select all people and their blog posts who are over
-- the age of 18.
--
-- ==== "Database.Esqueleto":
--
-- @
-- select $
-- from $ \\(people \`LeftOuterJoin\` blogPosts) -> do
-- on (people ^. PersonId ==. blogPosts ?. BlogPostAuthorId)
-- where_ (people ^. PersonAge >. val 18)
-- pure (people, blogPosts)
-- @
--
-- ==== "Database.Esqueleto.Experimental":
--
-- Here we use the ':&' operator to pattern match against the joined tables.
--
-- @
-- select $ do
-- (people :& blogPosts) <-
-- from $ table \@Person
-- \`leftJoin\` table \@BlogPost
-- \`on\` (\\(people :& blogPosts) ->
-- people ^. PersonId ==. blogPosts ?. BlogPostAuthorId)
-- where_ (people ^. PersonAge >. val 18)
-- pure (people, blogPosts)
-- @
--
-- === Example 3: Select with multi-table join
--
-- Let's select all people who follow a person named \"John\", including
-- the name of each follower.
--
-- ==== "Database.Esqueleto":
--
-- @
-- select $
-- from $ \\(
-- people1
-- \`InnerJoin\` followers
-- \`InnerJoin\` people2
-- ) -> do
-- on (people1 ^. PersonId ==. followers ^. FollowFollowed)
-- on (followers ^. FollowFollower ==. people2 ^. PersonId)
-- where_ (people1 ^. PersonName ==. val \"John\")
-- pure (followers, people2)
-- @
--
-- ==== "Database.Esqueleto.Experimental":
--
-- In this version, with each successive 'on' clause, only the tables
-- we have already joined into are in scope, so we must pattern match
-- accordingly. In this case, in the second 'innerJoin', we do not use
-- the first `Person` reference, so we use @_@ as a placeholder to
-- ignore it. This prevents a possible runtime error where a table
-- is referenced before it appears in the sequence of 'JOIN's.
--
-- @
-- select $ do
-- (people1 :& followers :& people2) <-
-- from $ table \@Person
-- \`innerJoin` table \@Follow
-- \`on\` (\\(people1 :& followers) ->
-- people1 ^. PersonId ==. followers ^. FollowFollowed)
-- \`innerJoin` table \@Person
-- \`on\` (\\(_ :& followers :& people2) ->
-- followers ^. FollowFollower ==. people2 ^. PersonId)
-- where_ (people1 ^. PersonName ==. val \"John\")
-- pure (followers, people2)
-- @
--
-- === Example 4: Counting results of a subquery
--
-- Let's count the number of people who have posted at least 10 posts
--
-- ==== "Database.Esqueleto":
--
-- @
-- select $ pure $ subSelectCount $
-- from $ \\(
-- people
-- \`InnerJoin\` blogPosts
-- ) -> do
-- on (people ^. PersonId ==. blogPosts ^. BlogPostAuthorId)
-- groupBy (people ^. PersonId)
-- having ((count $ blogPosts ^. BlogPostId) >. val 10)
-- pure people
-- @
--
-- ==== "Database.Esqueleto.Experimental":
--
-- @
-- select $ do
-- peopleWithPosts <-
-- from $ do
-- (people :& blogPosts) <-
-- from $ table \@Person
-- \`innerJoin\` table \@BlogPost
-- \`on\` (\\(p :& bP) ->
-- p ^. PersonId ==. bP ^. BlogPostAuthorId)
-- groupBy (people ^. PersonId)
-- having ((count $ blogPosts ^. BlogPostId) >. val 10)
-- pure people
-- pure $ count (peopleWithPosts ^. PersonId)
-- @
--
-- We now have the ability to refactor this
--
-- === Example 5: Sorting the results of a UNION with limits
--
-- Out of all of the posts created by a person and the people they follow,
-- generate a list of the first 25 posts, sorted alphabetically.
--
-- ==== "Database.Esqueleto":
--
-- Since 'UNION' is not supported, this requires using `Database.Esqueleto.rawSql`. (Not shown)
--
-- ==== "Database.Esqueleto.Experimental":
--
-- Since this module supports all set operations (see `SqlSetOperation`), we can use
-- `Union` to write this query.
--
-- @
-- select $ do
-- (authors, blogPosts) <- from $
-- (do
-- (author :& blogPost) <-
-- from $ table \@Person
-- \`innerJoin\` table \@BlogPost
-- \`on\` (\\(a :& bP) ->
-- a ^. PersonId ==. bP ^. BlogPostAuthorId)
-- where_ (author ^. PersonId ==. val currentPersonId)
-- pure (author, blogPost)
-- )
-- \`union_\`
-- (do
-- (follow :& blogPost :& author) <-
-- from $ table \@Follow
-- \`innerJoin\` table \@BlogPost
-- \`on\` (\\(f :& bP) ->
-- f ^. FollowFollowed ==. bP ^. BlogPostAuthorId)
-- \`innerJoin\` table \@Person
-- \`on\` (\\(_ :& bP :& a) ->
-- bP ^. BlogPostAuthorId ==. a ^. PersonId)
-- where_ (follow ^. FollowFollower ==. val currentPersonId)
-- pure (author, blogPost)
-- )
-- orderBy [ asc (blogPosts ^. BlogPostTitle) ]
-- limit 25
-- pure (authors, blogPosts)
-- @
--
-- === Example 6: LATERAL JOIN
--
-- As of version @3.4.0.0@, lateral subquery joins are supported.
--
--
-- @
-- select $ do
-- (salesPerson :& maxSaleAmount :& maxSaleCustomerName) <-
-- from $ table \@SalesPerson
-- \`crossJoinLateral\` (\\salesPerson -> do
-- sales <- from $ table \@Sale
-- where_ $ sales ^. SaleSalesPersonId ==. salesPerson ^. SalesPersonId
-- pure $ max_ (sales ^. SaleAmount)
-- )
-- \`crossJoinLateral\` (\\(salesPerson :& maxSaleAmount) -> do
-- sales <- from $ table \@Sale
-- where_ $ sales ^. SaleSalesPersonId ==. salesPerson ^. SalesPersonId
-- &&. sales ^. SaleAmount ==. maxSaleAmount
-- pure $ sales ^. SaleCustomerName)
-- )
-- pure (salesPerson ^. SalesPersonName, maxSaleAmount, maxSaleCustomerName)
-- @
--
-- This is the equivalent to the following SQL (example taken from the
-- [MySQL Lateral Derived Table](https://dev.mysql.com/doc/refman/8.0/en/lateral-derived-tables.html)
-- documentation):
--
-- @
-- SELECT
-- salesperson.name,
-- max_sale.amount,
-- max_sale_customer.customer_name
-- FROM
-- salesperson,
-- -- calculate maximum size, cache it in transient derived table max_sale
-- LATERAL
-- (SELECT MAX(amount) AS amount
-- FROM all_sales
-- WHERE all_sales.salesperson_id = salesperson.id)
-- AS max_sale,
-- LATERAL
-- (SELECT customer_name
-- FROM all_sales
-- WHERE all_sales.salesperson_id = salesperson.id
-- AND all_sales.amount =
-- -- the cached maximum size
-- max_sale.amount)
-- AS max_sale_customer;
-- @
-- $sql-set-operations
--
-- Data type that represents SQL set operations. This includes
-- 'UNION', 'UNION' 'ALL', 'EXCEPT', and 'INTERSECT'. These types form
-- a binary tree, with @SqlQuery@ values on the leaves.
--
-- Each function corresponding to the aforementioned set operations
-- can be used as an infix in a @from@ to help with readability
-- and lead to code that closely resembles the underlying SQL. For example,
--
-- @
-- select $ from $
-- (do
-- a <- from $ table @A
-- pure $ a ^. ASomeCol
-- )
-- \`union_\`
-- (do
-- b <- from $ table @B
-- pure $ b ^. BSomeCol
-- )
-- @
--
-- is translated into
--
-- @
-- SELECT * FROM (
-- (SELECT a.some_col FROM a)
-- UNION
-- (SELECT b.some_col FROM b)
-- )
-- @
--

View File

@ -0,0 +1,145 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
module Database.Esqueleto.Experimental.From
where
import qualified Control.Monad.Trans.Writer as W
import Data.Coerce (coerce)
import Data.Proxy
import qualified Data.Text.Lazy.Builder as TLB
import Database.Esqueleto.Experimental.ToAlias
import Database.Esqueleto.Experimental.ToAliasReference
import Database.Esqueleto.Internal.Internal hiding (From(..), from, on)
import Database.Esqueleto.Internal.PersistentImport
import Database.Persist.Names (EntityNameDB(..))
-- | 'FROM' clause, used to bring entities into scope.
--
-- Internally, this function uses the `From` datatype.
-- Unlike the old `Database.Esqueleto.from`, this does not
-- take a function as a parameter, but rather a value that
-- represents a 'JOIN' tree constructed out of instances of `From`.
-- This implementation eliminates certain
-- types of runtime errors by preventing the construction of
-- invalid SQL (e.g. illegal nested-@from@).
from :: ToFrom a a' => a -> SqlQuery a'
from f = do
(a, clause) <- unFrom (toFrom f)
Q $ W.tell mempty{sdFromClause=[FromRaw $ clause]}
pure a
type RawFn = NeedParens -> IdentInfo -> (TLB.Builder, [PersistValue])
-- | Data type defining the "From" language. This should not
-- constructed directly in application code.
--
-- A @From@ is a SqlQuery which returns a reference to the result of calling from
-- and a function that produces a portion of a FROM clause. This gets passed to
-- the FromRaw FromClause constructor directly when converting
-- from a @From@ to a @SqlQuery@ using @from@
--
-- @since 3.5.0.0
newtype From a = From
{ unFrom :: SqlQuery (a, RawFn)}
-- | A helper class primarily designed to allow using @SqlQuery@ directly in
-- a From expression. This is also useful for embedding a @SqlSetOperation@,
-- as well as supporting backwards compatibility for the
-- data constructor join tree used prior to /3.5.0.0/
--
-- @since 3.5.0.0
class ToFrom a r | a -> r where
toFrom :: a -> From r
instance ToFrom (From a) a where
toFrom = id
{-# DEPRECATED Table "@since 3.5.0.0 - use 'table' instead" #-}
data Table a = Table
instance PersistEntity ent => ToFrom (Table ent) (SqlExpr (Entity ent)) where
toFrom _ = table
-- | Bring a PersistEntity into scope from a table
--
-- @
-- select $ from $ table \@People
-- @
--
-- @since 3.5.0.0
table :: forall ent. PersistEntity ent => From (SqlExpr (Entity ent))
table = From $ do
let ed = entityDef (Proxy @ent)
ident <- newIdentFor (coerce $ getEntityDBName ed)
let entity = unsafeSqlEntity ident
pure $ ( entity, const $ base ident ed )
where
base ident@(I identText) def info =
let db = coerce $ getEntityDBName def
in ( (fromDBName info (coerce db)) <>
if db == identText
then mempty
else " AS " <> useIdent info ident
, mempty
)
{-# DEPRECATED SubQuery "/Since: 3.4.0.0/ - It is no longer necessary to tag 'SqlQuery' values with @SubQuery@" #-}
newtype SubQuery a = SubQuery a
instance (SqlSelect a r, ToAlias a, ToAliasReference a) => ToFrom (SubQuery (SqlQuery a)) a where
toFrom (SubQuery q) = selectQuery q
instance (SqlSelect a r, ToAlias a, ToAliasReference a) => ToFrom (SqlQuery a) a where
toFrom = selectQuery
-- | Select from a subquery, often used in conjuction with joins but can be
-- used without any joins. Because @SqlQuery@ has a @ToFrom@ instance you probably
-- dont need to use this function directly.
--
-- @
-- select $
-- p <- from $
-- selectQuery do
-- p <- from $ table \@Person
-- limit 5
-- orderBy [ asc p ^. PersonAge ]
-- ...
-- @
--
-- @since 3.5.0.0
selectQuery :: (SqlSelect a r, ToAlias a, ToAliasReference a) => SqlQuery a -> From a
selectQuery subquery = From $ do
-- We want to update the IdentState without writing the query to side data
(ret, sideData) <- Q $ W.censor (\_ -> mempty) $ W.listen $ unQ subquery
aliasedValue <- toAlias ret
-- Make a fake query with the aliased results, this allows us to ensure that the query is only run once
let aliasedQuery = Q $ W.WriterT $ pure (aliasedValue, sideData)
-- Add the FromQuery that renders the subquery to our side data
subqueryAlias <- newIdentFor (DBName "q")
-- Pass the aliased results of the subquery to the outer query
-- create aliased references from the outer query results (e.g value from subquery will be `subquery`.`value`),
-- this is probably overkill as the aliases should already be unique but seems to be good practice.
ref <- toAliasReference subqueryAlias aliasedValue
pure (ref, \_ info ->
let (queryText,queryVals) = toRawSql SELECT info aliasedQuery
in
( (parens queryText) <> " AS " <> useIdent info subqueryAlias
, queryVals
)
)

View File

@ -0,0 +1,111 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
module Database.Esqueleto.Experimental.From.CommonTableExpression
where
import qualified Control.Monad.Trans.Writer as W
import qualified Data.Text.Lazy.Builder as TLB
import Database.Esqueleto.Experimental.From
import Database.Esqueleto.Experimental.From.SqlSetOperation
import Database.Esqueleto.Experimental.ToAlias
import Database.Esqueleto.Experimental.ToAliasReference
import Database.Esqueleto.Internal.Internal hiding (From(..), from, on)
-- | @WITH@ clause used to introduce a [Common Table Expression (CTE)](https://en.wikipedia.org/wiki/Hierarchical_and_recursive_queries_in_SQL#Common_table_expression).
-- CTEs are supported in most modern SQL engines and can be useful
-- in performance tuning. In Esqueleto, CTEs should be used as a
-- subquery memoization tactic. When writing plain SQL, CTEs
-- are sometimes used to organize the SQL code, in Esqueleto, this
-- is better achieved through function that return 'SqlQuery' values.
--
-- @
-- select $ do
-- cte <- with subQuery
-- cteResult <- from cte
-- where_ $ cteResult ...
-- pure cteResult
-- @
--
-- __WARNING__: In some SQL engines using a CTE can diminish performance.
-- In these engines the CTE is treated as an optimization fence. You should
-- always verify that using a CTE will in fact improve your performance
-- over a regular subquery.
--
-- /Since: 3.4.0.0/
with :: ( ToAlias a
, ToAliasReference a
, SqlSelect a r
) => SqlQuery a -> SqlQuery (From a)
with query = do
(ret, sideData) <- Q $ W.censor (\_ -> mempty) $ W.listen $ unQ query
aliasedValue <- toAlias ret
let aliasedQuery = Q $ W.WriterT $ pure (aliasedValue, sideData)
ident <- newIdentFor (DBName "cte")
let clause = CommonTableExpressionClause NormalCommonTableExpression ident (\info -> toRawSql SELECT info aliasedQuery)
Q $ W.tell mempty{sdCteClause = [clause]}
ref <- toAliasReference ident aliasedValue
pure $ From $ pure (ref, (\_ info -> (useIdent info ident, mempty)))
-- | @WITH@ @RECURSIVE@ allows one to make a recursive subquery, which can
-- reference itself. Like @WITH@, this is supported in most modern SQL engines.
-- Useful for hierarchical, self-referential data, like a tree of data.
--
-- @
-- select $ do
-- cte <- withRecursive
-- (do
-- person <- from $ table \@Person
-- where_ $ person ^. PersonId ==. val personId
-- pure person
-- )
-- unionAll_
-- (\\self -> do
-- (p :& f :& p2 :& pSelf) <- from self
-- \`innerJoin\` $ table \@Follow
-- \`on\` (\\(p :& f) ->
-- p ^. PersonId ==. f ^. FollowFollower)
-- \`innerJoin\` $ table \@Person
-- \`on\` (\\(p :& f :& p2) ->
-- f ^. FollowFollowed ==. p2 ^. PersonId)
-- \`leftJoin\` self
-- \`on\` (\\(_ :& _ :& p2 :& pSelf) ->
-- just (p2 ^. PersonId) ==. pSelf ?. PersonId)
-- where_ $ isNothing (pSelf ?. PersonId)
-- groupBy (p2 ^. PersonId)
-- pure p2
-- )
-- from cte
-- @
--
-- /Since: 3.4.0.0/
withRecursive :: ( ToAlias a
, ToAliasReference a
, SqlSelect a r
)
=> SqlQuery a
-> UnionKind
-> (From a -> SqlQuery a)
-> SqlQuery (From a)
withRecursive baseCase unionKind recursiveCase = do
(ret, sideData) <- Q $ W.censor (\_ -> mempty) $ W.listen $ unQ baseCase
aliasedValue <- toAlias ret
let aliasedQuery = Q $ W.WriterT $ pure (aliasedValue, sideData)
ident <- newIdentFor (DBName "cte")
ref <- toAliasReference ident aliasedValue
let refFrom = From (pure (ref, (\_ info -> (useIdent info ident, mempty))))
let recursiveQuery = recursiveCase refFrom
let clause = CommonTableExpressionClause RecursiveCommonTableExpression ident
(\info -> (toRawSql SELECT info aliasedQuery)
<> ("\n" <> (unUnionKind unionKind) <> "\n", mempty)
<> (toRawSql SELECT info recursiveQuery)
)
Q $ W.tell mempty{sdCteClause = [clause]}
pure refFrom
newtype UnionKind = UnionKind { unUnionKind :: TLB.Builder }
instance Union_ UnionKind where
union_ = UnionKind "UNION"
instance UnionAll_ UnionKind where
unionAll_ = UnionKind "UNION ALL"

View File

@ -0,0 +1,425 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Database.Esqueleto.Experimental.From.Join
where
import Data.Bifunctor (first)
import Data.Kind (Constraint)
import Data.Proxy
import qualified Data.Text.Lazy.Builder as TLB
import Database.Esqueleto.Experimental.From
import Database.Esqueleto.Experimental.ToAlias
import Database.Esqueleto.Experimental.ToAliasReference
import Database.Esqueleto.Experimental.ToMaybe
import Database.Esqueleto.Internal.Internal hiding
(From(..), from, fromJoin, on)
import GHC.TypeLits
-- | A left-precedence pair. Pronounced \"and\". Used to represent expressions
-- that have been joined together.
--
-- The precedence behavior can be demonstrated by:
--
-- @
-- a :& b :& c == ((a :& b) :& c)
-- @
--
-- See the examples at the beginning of this module to see how this
-- operator is used in 'JOIN' operations.
data (:&) a b = a :& b
infixl 2 :&
instance (ToMaybe a, ToMaybe b) => ToMaybe (a :& b) where
type ToMaybeT (a :& b) = (ToMaybeT a :& ToMaybeT b)
toMaybe (a :& b) = (toMaybe a :& toMaybe b)
class ValidOnClause a
instance {-# OVERLAPPABLE #-} ToFrom a a' => ValidOnClause a
instance ValidOnClause (a -> SqlQuery b)
-- | You may return joined values from a 'select' query - this is
-- identical to the tuple instance, but is provided for convenience.
--
-- @since 3.5.2.0
instance (SqlSelect a ra, SqlSelect b rb) => SqlSelect (a :& b) (ra :& rb) where
sqlSelectCols esc (a :& b) = sqlSelectCols esc (a, b)
sqlSelectColCount = sqlSelectColCount . toTuple
where
toTuple :: Proxy (a :& b) -> Proxy (a, b)
toTuple = const Proxy
sqlSelectProcessRow = fmap (uncurry (:&)) . sqlSelectProcessRow
-- | Identical to the tuple instance and provided for convenience.
--
-- @since 3.5.3.0
instance (ToAlias a, ToAlias b) => ToAlias (a :& b) where
toAlias (a :& b) = (:&) <$> toAlias a <*> toAlias b
-- | Identical to the tuple instance and provided for convenience.
--
-- @since 3.5.3.0
instance (ToAliasReference a, ToAliasReference b) => ToAliasReference (a :& b) where
toAliasReference ident (a :& b) = (:&) <$> (toAliasReference ident a) <*> (toAliasReference ident b)
-- | An @ON@ clause that describes how two tables are related. This should be
-- used as an infix operator after a 'JOIN'. For example,
--
-- @
-- select $
-- from $ table \@Person
-- \`innerJoin\` table \@BlogPost
-- \`on\` (\\(p :& bP) ->
-- p ^. PersonId ==. bP ^. BlogPostAuthorId)
-- @
on :: ValidOnClause a => a -> (b -> SqlExpr (Value Bool)) -> (a, b -> SqlExpr (Value Bool))
on = (,)
infix 9 `on`
type family ErrorOnLateral a :: Constraint where
ErrorOnLateral (a -> SqlQuery b) = TypeError ('Text "LATERAL can only be used for INNER, LEFT, and CROSS join kinds.")
ErrorOnLateral _ = ()
fromJoin :: TLB.Builder -> RawFn -> RawFn -> Maybe (SqlExpr (Value Bool)) -> RawFn
fromJoin joinKind lhs rhs monClause =
\paren info ->
first (parensM paren) $
mconcat [ lhs Never info
, (joinKind, mempty)
, rhs Parens info
, maybe mempty (makeOnClause info) monClause
]
where
makeOnClause info (ERaw _ f) = first (" ON " <>) (f Never info)
type family HasOnClause actual expected :: Constraint where
HasOnClause (a, b -> SqlExpr (Value Bool)) c = () -- Let the compiler handle the type mismatch
HasOnClause a expected =
TypeError ( 'Text "Missing ON clause for join with"
':$$: 'ShowType a
':$$: 'Text ""
':$$: 'Text "Expected: "
':$$: 'ShowType a
':$$: 'Text "`on` " ':<>: 'ShowType (expected -> SqlExpr (Value Bool))
':$$: 'Text ""
)
-- | INNER JOIN
--
-- Used as an infix operator \`innerJoin\`
--
-- @
-- select $
-- from $ table \@Person
-- \`innerJoin\` table \@BlogPost
-- \`on\` (\\(p :& bp) ->
-- p ^. PersonId ==. bp ^. BlogPostAuthorId)
-- @
--
-- @since 3.5.0.0
innerJoin :: ( ToFrom a a'
, ToFrom b b'
, HasOnClause rhs (a' :& b')
, rhs ~ (b, (a' :& b') -> SqlExpr (Value Bool))
) => a -> rhs -> From (a' :& b')
innerJoin lhs (rhs, on') = From $ do
(leftVal, leftFrom) <- unFrom (toFrom lhs)
(rightVal, rightFrom) <- unFrom (toFrom rhs)
let ret = leftVal :& rightVal
pure $ (ret, fromJoin " INNER JOIN " leftFrom rightFrom (Just $ on' ret))
-- | INNER JOIN LATERAL
--
-- A Lateral subquery join allows the joined query to reference entities from the
-- left hand side of the join. Discards rows that don't match the on clause
--
-- Used as an infix operator \`innerJoinLateral\`
--
-- See example 6
--
-- @since 3.5.0.0
innerJoinLateral :: ( ToFrom a a'
, HasOnClause rhs (a' :& b)
, SqlSelect b r
, ToAlias b
, ToAliasReference b
, rhs ~ (a' -> SqlQuery b, (a' :& b) -> SqlExpr (Value Bool))
)
=> a -> rhs -> From (a' :& b)
innerJoinLateral lhs (rhsFn, on') = From $ do
(leftVal, leftFrom) <- unFrom (toFrom lhs)
(rightVal, rightFrom) <- unFrom (selectQuery (rhsFn leftVal))
let ret = leftVal :& rightVal
pure $ (ret, fromJoin " INNER JOIN LATERAL " leftFrom rightFrom (Just $ on' ret))
-- | CROSS JOIN
--
-- Used as an infix \`crossJoin\`
--
-- @
-- select $ do
-- from $ table \@Person
-- \`crossJoin\` table \@BlogPost
-- @
--
-- @since 3.5.0.0
crossJoin :: ( ToFrom a a'
, ToFrom b b'
) => a -> b -> From (a' :& b')
crossJoin lhs rhs = From $ do
(leftVal, leftFrom) <- unFrom (toFrom lhs)
(rightVal, rightFrom) <- unFrom (toFrom rhs)
let ret = leftVal :& rightVal
pure $ (ret, fromJoin " CROSS JOIN " leftFrom rightFrom Nothing)
-- | CROSS JOIN LATERAL
--
-- A Lateral subquery join allows the joined query to reference entities from the
-- left hand side of the join.
--
-- Used as an infix operator \`crossJoinLateral\`
--
-- See example 6
--
-- @since 3.5.0.0
crossJoinLateral :: ( ToFrom a a'
, SqlSelect b r
, ToAlias b
, ToAliasReference b
)
=> a -> (a' -> SqlQuery b) -> From (a' :& b)
crossJoinLateral lhs rhsFn = From $ do
(leftVal, leftFrom) <- unFrom (toFrom lhs)
(rightVal, rightFrom) <- unFrom (selectQuery (rhsFn leftVal))
let ret = leftVal :& rightVal
pure $ (ret, fromJoin " CROSS JOIN LATERAL " leftFrom rightFrom Nothing)
-- | LEFT OUTER JOIN
--
-- Join where the right side may not exist.
-- If the on clause fails then the right side will be NULL'ed
-- Because of this the right side needs to be handled as a Maybe
--
-- Used as an infix operator \`leftJoin\`
--
-- @
-- select $
-- from $ table \@Person
-- \`leftJoin\` table \@BlogPost
-- \`on\` (\\(p :& bp) ->
-- p ^. PersonId ==. bp ?. BlogPostAuthorId)
-- @
--
-- @since 3.5.0.0
leftJoin :: ( ToFrom a a'
, ToFrom b b'
, ToMaybe b'
, HasOnClause rhs (a' :& ToMaybeT b')
, rhs ~ (b, (a' :& ToMaybeT b') -> SqlExpr (Value Bool))
) => a -> rhs -> From (a' :& ToMaybeT b')
leftJoin lhs (rhs, on') = From $ do
(leftVal, leftFrom) <- unFrom (toFrom lhs)
(rightVal, rightFrom) <- unFrom (toFrom rhs)
let ret = leftVal :& toMaybe rightVal
pure $ (ret, fromJoin " LEFT OUTER JOIN " leftFrom rightFrom (Just $ on' ret))
-- | LEFT OUTER JOIN LATERAL
--
-- Lateral join where the right side may not exist.
-- In the case that the query returns nothing or the on clause fails the right
-- side of the join will be NULL'ed
-- Because of this the right side needs to be handled as a Maybe
--
-- Used as an infix operator \`leftJoinLateral\`
--
-- See example 6 for how to use LATERAL
--
-- @since 3.5.0.0
leftJoinLateral :: ( ToFrom a a'
, SqlSelect b r
, HasOnClause rhs (a' :& ToMaybeT b)
, ToAlias b
, ToAliasReference b
, ToMaybe b
, rhs ~ (a' -> SqlQuery b, (a' :& ToMaybeT b) -> SqlExpr (Value Bool))
)
=> a -> rhs -> From (a' :& ToMaybeT b)
leftJoinLateral lhs (rhsFn, on') = From $ do
(leftVal, leftFrom) <- unFrom (toFrom lhs)
(rightVal, rightFrom) <- unFrom (selectQuery (rhsFn leftVal))
let ret = leftVal :& toMaybe rightVal
pure $ (ret, fromJoin " LEFT OUTER JOIN LATERAL " leftFrom rightFrom (Just $ on' ret))
-- | RIGHT OUTER JOIN
--
-- Join where the left side may not exist.
-- If the on clause fails then the left side will be NULL'ed
-- Because of this the left side needs to be handled as a Maybe
--
-- Used as an infix operator \`rightJoin\`
--
-- @
-- select $
-- from $ table \@Person
-- \`rightJoin\` table \@BlogPost
-- \`on\` (\\(p :& bp) ->
-- p ?. PersonId ==. bp ^. BlogPostAuthorId)
-- @
--
-- @since 3.5.0.0
rightJoin :: ( ToFrom a a'
, ToFrom b b'
, ToMaybe a'
, HasOnClause rhs (ToMaybeT a' :& b')
, rhs ~ (b, (ToMaybeT a' :& b') -> SqlExpr (Value Bool))
) => a -> rhs -> From (ToMaybeT a' :& b')
rightJoin lhs (rhs, on') = From $ do
(leftVal, leftFrom) <- unFrom (toFrom lhs)
(rightVal, rightFrom) <- unFrom (toFrom rhs)
let ret = toMaybe leftVal :& rightVal
pure $ (ret, fromJoin " RIGHT OUTER JOIN " leftFrom rightFrom (Just $ on' ret))
-- | FULL OUTER JOIN
--
-- Join where both sides of the join may not exist.
-- Because of this the result needs to be handled as a Maybe
--
-- Used as an infix operator \`fullOuterJoin\`
--
-- @
-- select $
-- from $ table \@Person
-- \`fullOuterJoin\` table \@BlogPost
-- \`on\` (\\(p :& bp) ->
-- p ?. PersonId ==. bp ?. BlogPostAuthorId)
-- @
--
-- @since 3.5.0.0
fullOuterJoin :: ( ToFrom a a'
, ToFrom b b'
, ToMaybe a'
, ToMaybe b'
, HasOnClause rhs (ToMaybeT a' :& ToMaybeT b')
, rhs ~ (b, (ToMaybeT a' :& ToMaybeT b') -> SqlExpr (Value Bool))
) => a -> rhs -> From (ToMaybeT a' :& ToMaybeT b')
fullOuterJoin lhs (rhs, on') = From $ do
(leftVal, leftFrom) <- unFrom (toFrom lhs)
(rightVal, rightFrom) <- unFrom (toFrom rhs)
let ret = toMaybe leftVal :& toMaybe rightVal
pure $ (ret, fromJoin " FULL OUTER JOIN " leftFrom rightFrom (Just $ on' ret))
infixl 2 `innerJoin`,
`innerJoinLateral`,
`leftJoin`,
`leftJoinLateral`,
`crossJoin`,
`crossJoinLateral`,
`rightJoin`,
`fullOuterJoin`
------ Compatibility for old syntax
data Lateral
data NotLateral
type family IsLateral a where
IsLateral (a -> SqlQuery b, c) = Lateral
IsLateral (a -> SqlQuery b) = Lateral
IsLateral a = NotLateral
class DoInnerJoin lateral lhs rhs res | lateral rhs lhs -> res where
doInnerJoin :: Proxy lateral -> lhs -> rhs -> From res
instance ( ToFrom a a'
, ToFrom b b'
, HasOnClause rhs (a' :& b')
, rhs ~ (b, (a' :& b') -> SqlExpr (Value Bool))
) => DoInnerJoin NotLateral a rhs (a' :& b') where
doInnerJoin _ = innerJoin
instance ( ToFrom a a'
, SqlSelect b r
, ToAlias b
, ToAliasReference b
, d ~ (a' :& b)
) => DoInnerJoin Lateral a (a' -> SqlQuery b, d -> SqlExpr (Value Bool)) d where
doInnerJoin _ = innerJoinLateral
instance ( DoInnerJoin lateral lhs rhs r, lateral ~ IsLateral rhs )
=> ToFrom (InnerJoin lhs rhs) r where
toFrom (InnerJoin a b) = doInnerJoin (Proxy @lateral) a b
class DoLeftJoin lateral lhs rhs res | lateral rhs lhs -> res where
doLeftJoin :: Proxy lateral -> lhs -> rhs -> From res
instance ( ToFrom a a'
, ToFrom b b'
, ToMaybe b'
, ToMaybeT b' ~ mb
, HasOnClause rhs (a' :& mb)
, rhs ~ (b, (a' :& mb) -> SqlExpr (Value Bool))
) => DoLeftJoin NotLateral a rhs (a' :& mb) where
doLeftJoin _ = leftJoin
instance ( ToFrom a a'
, ToMaybe b
, d ~ (a' :& ToMaybeT b)
, SqlSelect b r
, ToAlias b
, ToAliasReference b
) => DoLeftJoin Lateral a (a' -> SqlQuery b, d -> SqlExpr (Value Bool)) d where
doLeftJoin _ = leftJoinLateral
instance ( DoLeftJoin lateral lhs rhs r, lateral ~ IsLateral rhs )
=> ToFrom (LeftOuterJoin lhs rhs) r where
toFrom (LeftOuterJoin a b) = doLeftJoin (Proxy @lateral) a b
class DoCrossJoin lateral lhs rhs res | lateral lhs rhs -> res where
doCrossJoin :: Proxy lateral -> lhs -> rhs -> From res
instance (ToFrom a a', ToFrom b b') => DoCrossJoin NotLateral a b (a' :& b') where
doCrossJoin _ = crossJoin
instance (ToFrom a a', SqlSelect b r, ToAlias b, ToAliasReference b)
=> DoCrossJoin Lateral a (a' -> SqlQuery b) (a' :& b) where
doCrossJoin _ = crossJoinLateral
instance (DoCrossJoin lateral lhs rhs r, IsLateral rhs ~ lateral)
=> ToFrom (CrossJoin lhs rhs) r where
toFrom (CrossJoin a b) = doCrossJoin (Proxy @lateral) a b
instance ( ToFrom a a'
, ToFrom b b'
, ToMaybe a'
, ToMaybeT a' ~ ma
, HasOnClause rhs (ma :& b')
, ErrorOnLateral b
, rhs ~ (b, (ma :& b') -> SqlExpr (Value Bool))
) => ToFrom (RightOuterJoin a rhs) (ma :& b') where
toFrom (RightOuterJoin a b) = rightJoin a b
instance ( ToFrom a a'
, ToFrom b b'
, ToMaybe a'
, ToMaybeT a' ~ ma
, ToMaybe b'
, ToMaybeT b' ~ mb
, HasOnClause rhs (ma :& mb)
, ErrorOnLateral b
, rhs ~ (b, (ma :& mb) -> SqlExpr (Value Bool))
) => ToFrom (FullOuterJoin a rhs) (ma :& mb) where
toFrom (FullOuterJoin a b) = fullOuterJoin a b

View File

@ -0,0 +1,130 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
module Database.Esqueleto.Experimental.From.SqlSetOperation
where
import Control.Arrow (first)
import Control.Monad.Trans.Class (lift)
import qualified Control.Monad.Trans.State as S
import qualified Control.Monad.Trans.Writer as W
import qualified Data.Text.Lazy.Builder as TLB
import Database.Esqueleto.Experimental.From
import Database.Esqueleto.Experimental.ToAlias
import Database.Esqueleto.Experimental.ToAliasReference
import Database.Esqueleto.Internal.Internal hiding (From(..), from, on)
import Database.Esqueleto.Internal.PersistentImport (PersistValue)
-- | Data type used to implement the SqlSetOperation language
-- this type is implemented in the same way as a @From@
--
-- Semantically a @SqlSetOperation@ is always a @From@ but not vice versa
--
-- @since 3.5.0.0
newtype SqlSetOperation a = SqlSetOperation
{ unSqlSetOperation :: NeedParens -> SqlQuery (a, IdentInfo -> (TLB.Builder, [PersistValue]))}
instance ToAliasReference a => ToFrom (SqlSetOperation a) a where
toFrom setOperation = From $ do
ident <- newIdentFor (DBName "u")
(a, fromClause) <- unSqlSetOperation setOperation Never
ref <- toAliasReference ident a
pure (ref, \_ info -> (first parens $ fromClause info) <> (" AS " <> useIdent info ident, mempty))
-- | Type class to support direct use of @SqlQuery@ in a set operation tree
--
-- @since 3.5.0.0
class ToSqlSetOperation a r | a -> r where
toSqlSetOperation :: a -> SqlSetOperation r
instance ToSqlSetOperation (SqlSetOperation a) a where
toSqlSetOperation = id
instance (SqlSelect a r, ToAlias a, ToAliasReference a) => ToSqlSetOperation (SqlQuery a) a where
toSqlSetOperation subquery =
SqlSetOperation $ \p -> do
(ret, sideData) <- Q $ W.censor (\_ -> mempty) $ W.listen $ unQ subquery
state <- Q $ lift S.get
aliasedValue <- toAlias ret
Q $ lift $ S.put state
let aliasedQuery = Q $ W.WriterT $ pure (aliasedValue, sideData)
let p' =
case p of
Parens -> Parens
Never ->
if (sdLimitClause sideData) /= mempty
|| length (sdOrderByClause sideData) > 0 then
Parens
else
Never
pure (aliasedValue, \info -> first (parensM p') $ toRawSql SELECT info aliasedQuery)
-- | Helper function for defining set operations
-- @since 3.5.0.0
mkSetOperation :: (ToSqlSetOperation a a', ToSqlSetOperation b a')
=> TLB.Builder -> a -> b -> SqlSetOperation a'
mkSetOperation operation lhs rhs = SqlSetOperation $ \p -> do
(leftValue, leftClause) <- unSqlSetOperation (toSqlSetOperation lhs) p
(_, rightClause) <- unSqlSetOperation (toSqlSetOperation rhs) p
pure (leftValue, \info -> leftClause info <> (operation, mempty) <> rightClause info)
{-# DEPRECATED Union "/Since: 3.4.0.0/ - Use the 'union_' function instead of the 'Union' data constructor" #-}
data Union a b = a `Union` b
instance ToSqlSetOperation a a' => ToSqlSetOperation (Union a a) a' where
toSqlSetOperation (Union a b) = union_ a b
-- | Overloaded @union_@ function to support use in both 'SqlSetOperation'
-- and 'withRecursive'
--
-- @since 3.5.0.0
class Union_ a where
-- | @UNION@ SQL set operation. Can be used as an infix function between 'SqlQuery' values.
union_ :: a
instance (ToSqlSetOperation a c, ToSqlSetOperation b c, res ~ SqlSetOperation c)
=> Union_ (a -> b -> res) where
union_ = mkSetOperation " UNION "
-- | Overloaded @unionAll_@ function to support use in both 'SqlSetOperation'
-- and 'withRecursive'
--
-- @since 3.5.0.0
class UnionAll_ a where
-- | @UNION@ @ALL@ SQL set operation. Can be used as an infix function between 'SqlQuery' values.
unionAll_ :: a
instance (ToSqlSetOperation a c, ToSqlSetOperation b c, res ~ SqlSetOperation c)
=> UnionAll_ (a -> b -> res) where
unionAll_ = mkSetOperation " UNION ALL "
{-# DEPRECATED UnionAll "/Since: 3.4.0.0/ - Use the 'unionAll_' function instead of the 'UnionAll' data constructor" #-}
data UnionAll a b = a `UnionAll` b
instance ToSqlSetOperation a a' => ToSqlSetOperation (UnionAll a a) a' where
toSqlSetOperation (UnionAll a b) = unionAll_ a b
{-# DEPRECATED Except "/Since: 3.4.0.0/ - Use the 'except_' function instead of the 'Except' data constructor" #-}
data Except a b = a `Except` b
instance ToSqlSetOperation a a' => ToSqlSetOperation (Except a a) a' where
toSqlSetOperation (Except a b) = except_ a b
-- | @EXCEPT@ SQL set operation. Can be used as an infix function between 'SqlQuery' values.
except_ :: (ToSqlSetOperation a a', ToSqlSetOperation b a') => a -> b -> SqlSetOperation a'
except_ = mkSetOperation " EXCEPT "
{-# DEPRECATED Intersect "/Since: 3.4.0.0/ - Use the 'intersect_' function instead of the 'Intersect' data constructor" #-}
data Intersect a b = a `Intersect` b
instance ToSqlSetOperation a a' => ToSqlSetOperation (Intersect a a) a' where
toSqlSetOperation (Intersect a b) = intersect_ a b
-- | @INTERSECT@ SQL set operation. Can be used as an infix function between 'SqlQuery' values.
intersect_ :: (ToSqlSetOperation a a', ToSqlSetOperation b a') => a -> b -> SqlSetOperation a'
intersect_ = mkSetOperation " INTERSECT "
{-# DEPRECATED SelectQuery "/Since: 3.4.0.0/ - It is no longer necessary to tag 'SqlQuery' values with @SelectQuery@" #-}
pattern SelectQuery a = a

View File

@ -0,0 +1,92 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
module Database.Esqueleto.Experimental.ToAlias
where
import Database.Esqueleto.Internal.Internal hiding (From, from, on)
import Database.Esqueleto.Internal.PersistentImport
{-# DEPRECATED ToAliasT "This type alias doesn't do anything. Please delete it. Will be removed in the next release." #-}
type ToAliasT a = a
-- Tedious tuple magic
class ToAlias a where
toAlias :: a -> SqlQuery a
instance ToAlias (SqlExpr (Value a)) where
toAlias e@(ERaw m f)
| Just _ <- sqlExprMetaAlias m = pure e
| otherwise = do
ident <- newIdentFor (DBName "v")
pure $ ERaw noMeta{sqlExprMetaAlias = Just ident} f
instance ToAlias (SqlExpr (Entity a)) where
toAlias e@(ERaw m f)
| Just _ <- sqlExprMetaAlias m = pure e
| otherwise = do
ident <- newIdentFor (DBName "v")
pure $ ERaw m{sqlExprMetaIsReference = False, sqlExprMetaAlias = Just ident} f
instance ToAlias (SqlExpr (Maybe (Entity a))) where
-- FIXME: Code duplication because the compiler doesnt like half final encoding
toAlias e@(ERaw m f)
| Just _ <- sqlExprMetaAlias m = pure e
| otherwise = do
ident <- newIdentFor (DBName "v")
pure $ ERaw m{sqlExprMetaIsReference = False, sqlExprMetaAlias = Just ident} f
instance (ToAlias a, ToAlias b) => ToAlias (a,b) where
toAlias (a,b) = (,) <$> toAlias a <*> toAlias b
instance ( ToAlias a
, ToAlias b
, ToAlias c
) => ToAlias (a,b,c) where
toAlias x = to3 <$> (toAlias $ from3 x)
instance ( ToAlias a
, ToAlias b
, ToAlias c
, ToAlias d
) => ToAlias (a,b,c,d) where
toAlias x = to4 <$> (toAlias $ from4 x)
instance ( ToAlias a
, ToAlias b
, ToAlias c
, ToAlias d
, ToAlias e
) => ToAlias (a,b,c,d,e) where
toAlias x = to5 <$> (toAlias $ from5 x)
instance ( ToAlias a
, ToAlias b
, ToAlias c
, ToAlias d
, ToAlias e
, ToAlias f
) => ToAlias (a,b,c,d,e,f) where
toAlias x = to6 <$> (toAlias $ from6 x)
instance ( ToAlias a
, ToAlias b
, ToAlias c
, ToAlias d
, ToAlias e
, ToAlias f
, ToAlias g
) => ToAlias (a,b,c,d,e,f,g) where
toAlias x = to7 <$> (toAlias $ from7 x)
instance ( ToAlias a
, ToAlias b
, ToAlias c
, ToAlias d
, ToAlias e
, ToAlias f
, ToAlias g
, ToAlias h
) => ToAlias (a,b,c,d,e,f,g,h) where
toAlias x = to8 <$> (toAlias $ from8 x)

View File

@ -0,0 +1,90 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
module Database.Esqueleto.Experimental.ToAliasReference
where
import Data.Coerce
import Database.Esqueleto.Internal.Internal hiding (From, from, on)
import Database.Esqueleto.Internal.PersistentImport
{-# DEPRECATED ToAliasReferenceT "This type alias doesn't do anything. Please delete it. Will be removed in the next release." #-}
type ToAliasReferenceT a = a
-- more tedious tuple magic
class ToAliasReference a where
toAliasReference :: Ident -> a -> SqlQuery a
instance ToAliasReference (SqlExpr (Value a)) where
toAliasReference aliasSource (ERaw m _)
| Just alias <- sqlExprMetaAlias m = pure $ ERaw m{sqlExprMetaIsReference = True} $ \_ info ->
(useIdent info aliasSource <> "." <> useIdent info alias, [])
toAliasReference _ e = pure e
instance ToAliasReference (SqlExpr (Entity a)) where
toAliasReference aliasSource (ERaw m _)
| Just _ <- sqlExprMetaAlias m =
pure $ ERaw m{sqlExprMetaIsReference = True} $ \_ info ->
(useIdent info aliasSource, [])
toAliasReference _ e = pure e
instance ToAliasReference (SqlExpr (Maybe (Entity a))) where
toAliasReference aliasSource e =
coerce <$> toAliasReference aliasSource (coerce e :: SqlExpr (Entity a))
instance (ToAliasReference a, ToAliasReference b) => ToAliasReference (a, b) where
toAliasReference ident (a,b) = (,) <$> (toAliasReference ident a) <*> (toAliasReference ident b)
instance ( ToAliasReference a
, ToAliasReference b
, ToAliasReference c
) => ToAliasReference (a,b,c) where
toAliasReference ident x = fmap to3 $ toAliasReference ident $ from3 x
instance ( ToAliasReference a
, ToAliasReference b
, ToAliasReference c
, ToAliasReference d
) => ToAliasReference (a,b,c,d) where
toAliasReference ident x = fmap to4 $ toAliasReference ident $ from4 x
instance ( ToAliasReference a
, ToAliasReference b
, ToAliasReference c
, ToAliasReference d
, ToAliasReference e
) => ToAliasReference (a,b,c,d,e) where
toAliasReference ident x = fmap to5 $ toAliasReference ident $ from5 x
instance ( ToAliasReference a
, ToAliasReference b
, ToAliasReference c
, ToAliasReference d
, ToAliasReference e
, ToAliasReference f
) => ToAliasReference (a,b,c,d,e,f) where
toAliasReference ident x = to6 <$> (toAliasReference ident $ from6 x)
instance ( ToAliasReference a
, ToAliasReference b
, ToAliasReference c
, ToAliasReference d
, ToAliasReference e
, ToAliasReference f
, ToAliasReference g
) => ToAliasReference (a,b,c,d,e,f,g) where
toAliasReference ident x = to7 <$> (toAliasReference ident $ from7 x)
instance ( ToAliasReference a
, ToAliasReference b
, ToAliasReference c
, ToAliasReference d
, ToAliasReference e
, ToAliasReference f
, ToAliasReference g
, ToAliasReference h
) => ToAliasReference (a,b,c,d,e,f,g,h) where
toAliasReference ident x = to8 <$> (toAliasReference ident $ from8 x)

View File

@ -0,0 +1,79 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}
module Database.Esqueleto.Experimental.ToMaybe
where
import Database.Esqueleto.Internal.Internal hiding (From(..), from, on)
import Database.Esqueleto.Internal.PersistentImport (Entity(..))
type family Nullable a where
Nullable (Maybe a) = a
Nullable a = a
class ToMaybe a where
type ToMaybeT a
toMaybe :: a -> ToMaybeT a
instance ToMaybe (SqlExpr (Maybe a)) where
type ToMaybeT (SqlExpr (Maybe a)) = SqlExpr (Maybe a)
toMaybe = id
instance ToMaybe (SqlExpr (Entity a)) where
type ToMaybeT (SqlExpr (Entity a)) = SqlExpr (Maybe (Entity a))
toMaybe (ERaw f m) = (ERaw f m)
instance ToMaybe (SqlExpr (Value a)) where
type ToMaybeT (SqlExpr (Value a)) = SqlExpr (Value (Maybe (Nullable a)))
toMaybe = veryUnsafeCoerceSqlExprValue
instance (ToMaybe a, ToMaybe b) => ToMaybe (a,b) where
type ToMaybeT (a, b) = (ToMaybeT a, ToMaybeT b)
toMaybe (a, b) = (toMaybe a, toMaybe b)
instance ( ToMaybe a , ToMaybe b , ToMaybe c) => ToMaybe (a,b,c) where
type ToMaybeT (a, b, c) = (ToMaybeT a, ToMaybeT b, ToMaybeT c)
toMaybe = to3 . toMaybe . from3
instance ( ToMaybe a , ToMaybe b , ToMaybe c , ToMaybe d) => ToMaybe (a,b,c,d) where
type ToMaybeT (a, b, c, d) = (ToMaybeT a, ToMaybeT b, ToMaybeT c, ToMaybeT d)
toMaybe = to4 . toMaybe . from4
instance ( ToMaybe a , ToMaybe b , ToMaybe c , ToMaybe d , ToMaybe e) => ToMaybe (a,b,c,d,e) where
type ToMaybeT (a, b, c, d, e) = (ToMaybeT a, ToMaybeT b, ToMaybeT c, ToMaybeT d, ToMaybeT e)
toMaybe = to5 . toMaybe . from5
instance ( ToMaybe a
, ToMaybe b
, ToMaybe c
, ToMaybe d
, ToMaybe e
, ToMaybe f
) => ToMaybe (a,b,c,d,e,f) where
type ToMaybeT (a, b, c, d, e, f) = (ToMaybeT a, ToMaybeT b, ToMaybeT c, ToMaybeT d, ToMaybeT e, ToMaybeT f)
toMaybe = to6 . toMaybe . from6
instance ( ToMaybe a
, ToMaybe b
, ToMaybe c
, ToMaybe d
, ToMaybe e
, ToMaybe f
, ToMaybe g
) => ToMaybe (a,b,c,d,e,f,g) where
type ToMaybeT (a, b, c, d, e, f, g) = (ToMaybeT a, ToMaybeT b, ToMaybeT c, ToMaybeT d, ToMaybeT e, ToMaybeT f, ToMaybeT g)
toMaybe = to7 . toMaybe . from7
instance ( ToMaybe a
, ToMaybe b
, ToMaybe c
, ToMaybe d
, ToMaybe e
, ToMaybe f
, ToMaybe g
, ToMaybe h
) => ToMaybe (a,b,c,d,e,f,g,h) where
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)
toMaybe = to8 . toMaybe . from8

View File

@ -0,0 +1,83 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
-- | This is an internal module. This module may have breaking changes without
-- a corresponding major version bump. If you use this module, please open an
-- issue with your use-case so we can safely support it.
module Database.Esqueleto.Internal.ExprParser where
import Prelude hiding (takeWhile)
import Control.Applicative ((<|>))
import Control.Monad (void)
import Data.Attoparsec.Text
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Text (Text)
import qualified Data.Text as Text
import Database.Persist.Sql
import Database.Persist.SqlBackend
-- | A type representing the access of a table value. In Esqueleto, we get
-- a guarantee that the access will look something like:
--
-- @
-- escape-char [character] escape-char . escape-char [character] escape-char
-- ^^^^^^^^^^^ ^^^^^^^^^^^
-- table name column name
-- @
data TableAccess = TableAccess
{ tableAccessTable :: Text
, tableAccessColumn :: Text
}
deriving (Eq, Ord, Show)
-- | Parse a @SqlExpr (Value Bool)@'s textual representation into a list of
-- 'TableAccess'
parseOnExpr :: SqlBackend -> Text -> Either String (Set TableAccess)
parseOnExpr sqlBackend text = do
c <- mkEscapeChar sqlBackend
parseOnly (onExpr c) text
-- | This function uses the 'connEscapeName' function in the 'SqlBackend' with an
-- empty identifier to pull out an escape character. This implementation works
-- with postgresql, mysql, and sqlite backends.
mkEscapeChar :: SqlBackend -> Either String Char
mkEscapeChar sqlBackend =
case Text.uncons (getEscapedRawName "" sqlBackend) of
Nothing ->
Left "Failed to get an escape character from the SQL backend."
Just (c, _) ->
Right c
type ExprParser a = Char -> Parser a
onExpr :: ExprParser (Set TableAccess)
onExpr e = Set.fromList <$> many' tableAccesses
where
tableAccesses = do
skipToEscape e <?> "Skipping to an escape char"
parseTableAccess e <?> "Parsing a table access"
skipToEscape :: ExprParser ()
skipToEscape escapeChar = void (takeWhile (/= escapeChar))
parseEscapedIdentifier :: ExprParser [Char]
parseEscapedIdentifier escapeChar = do
_ <- char escapeChar
str <- parseEscapedChars escapeChar
_ <- char escapeChar
pure str
parseTableAccess :: ExprParser TableAccess
parseTableAccess ec = do
tableAccessTable <- Text.pack <$> parseEscapedIdentifier ec
_ <- char '.'
tableAccessColumn <- Text.pack <$> parseEscapedIdentifier ec
pure TableAccess {..}
parseEscapedChars :: ExprParser [Char]
parseEscapedChars escapeChar = go
where
twoEscapes = char escapeChar *> char escapeChar
go = many' (notChar escapeChar <|> twoEscapes)

File diff suppressed because it is too large Load Diff

View File

@ -1,62 +0,0 @@
{-# LANGUAGE DeriveDataTypeable
, EmptyDataDecls
, FlexibleContexts
, FlexibleInstances
, FunctionalDependencies
, MultiParamTypeClasses
, TypeFamilies
, UndecidableInstances
, GADTs
#-}
-- | This is an internal module, anything exported by this module
-- may change without a major version bump. Please use only
-- "Database.Esqueleto" if possible.
module Database.Esqueleto.Internal.Language
( -- * The pretty face
from
, Value(..)
, ValueList(..)
, SomeValue(..)
, ToSomeValues(..)
, InnerJoin(..)
, CrossJoin(..)
, LeftOuterJoin(..)
, RightOuterJoin(..)
, FullOuterJoin(..)
, OnClauseWithoutMatchingJoinException(..)
, OrderBy
, DistinctOn
, Update
, Insertion
, LockingKind(..)
, SqlString
, ToBaseId(..)
-- * The guts
, JoinKind(..)
, IsJoinKind(..)
, BackendCompatible(..)
, PreprocessedFrom
, From
, FromPreprocess
, when_
, then_
, else_
, where_, on, groupBy, orderBy, rand, asc, desc, limit, offset
, distinct, distinctOn, don, distinctOnOrderBy, having, locking
, sub_select, (^.), (?.)
, val, isNothing, just, nothing, joinV, withNonNull
, countRows, count, countDistinct
, not_, (==.), (>=.), (>.), (<=.), (<.), (!=.), (&&.), (||.)
, between, (+.), (-.), (/.), (*.)
, random_, round_, ceiling_, floor_
, min_, max_, sum_, avg_, castNum, castNumM
, coalesce, coalesceDefault
, lower_, like, ilike, (%), concat_, (++.), castString
, subList_select, valList, justList
, in_, notIn, exists, notExists
, set, (=.), (+=.), (-=.), (*=.), (/=.)
, case_, toBaseId, (<#), (<&>)
) where
import Database.Esqueleto.Internal.PersistentImport
import Database.Esqueleto.Internal.Internal

View File

@ -3,148 +3,175 @@
module Database.Esqueleto.Internal.PersistentImport
-- NOTE: switch back to a module export once https://gitlab.haskell.org/ghc/ghc/merge_requests/276
-- has been merged. See https://github.com/bitemyapp/esqueleto/issues/110 for more details
( toJsonText,
entityIdFromJSON,
entityIdToJSON,
entityValues,
fromPersistValueJSON,
keyValueEntityFromJSON,
keyValueEntityToJSON,
toPersistValueJSON,
selectKeys,
belongsTo,
belongsToJust,
getEntity,
getJust,
getJustEntity,
insertEntity,
insertRecord,
liftPersist,
checkUnique,
getByValue,
insertBy,
insertUniqueEntity,
onlyUnique,
replaceUnique,
transactionSave,
transactionUndo,
defaultAttribute,
mkColumns,
getMigration,
migrate,
parseMigration,
parseMigration',
printMigration,
runMigration,
runMigrationSilent,
runMigrationUnsafe,
showMigration,
decorateSQLWithLimitOffset,
fieldDBName,
fromSqlKey,
getFieldName,
getTableName,
tableDBName,
toSqlKey,
withRawQuery,
getStmtConn,
rawExecute,
rawExecuteCount,
rawQuery,
rawQueryRes,
rawSql,
askLogFunc,
close',
createSqlPool,
liftSqlPersistMPool,
runSqlConn,
runSqlPersistM,
runSqlPersistMPool,
runSqlPool,
withSqlConn,
withSqlPool,
readToUnknown,
readToWrite,
writeToUnknown,
entityKeyFields,
entityPrimary,
fromPersistValueText,
keyAndEntityFields,
toEmbedEntityDef,
PersistStore,
PersistUnique,
DeleteCascade(..),
PersistConfig(..),
BackendSpecificUpdate,
Entity(..),
PersistEntity(..),
PersistField(..),
SomePersistField(..),
PersistQueryRead(..),
PersistQueryWrite(..),
BackendCompatible(..),
BackendKey(..),
HasPersistBackend(..),
IsPersistBackend,
PersistCore(..),
PersistRecordBackend,
PersistStoreRead(..),
PersistStoreWrite(..),
ToBackendKey(..),
PersistUniqueRead(..),
PersistUniqueWrite(..),
PersistFieldSql(..),
RawSql(..),
CautiousMigration,
Column(..),
ConnectionPool,
Migration,
PersistentSqlException(..),
Single(..),
Sql,
SqlPersistM,
SqlPersistT,
InsertSqlResult(..),
IsSqlBackend,
LogFunc,
SqlBackend(..),
SqlBackendCanRead,
SqlBackendCanWrite,
SqlReadBackend(..),
SqlReadT,
SqlWriteBackend(..),
SqlWriteT,
Statement(..),
Attr,
Checkmark(..),
CompositeDef(..),
DBName(..),
EmbedEntityDef(..),
EmbedFieldDef(..),
EntityDef(..),
ExtraLine,
FieldDef(..),
FieldType(..),
ForeignDef(..),
ForeignFieldDef,
HaskellName(..),
IsNullable(..),
OnlyUniqueException(..),
PersistException(..),
PersistFilter(..),
PersistUpdate(..),
PersistValue(..),
ReferenceDef(..),
SqlType(..),
UniqueDef(..),
UpdateException(..),
WhyNullable(..)
) where
( toJsonText
, entityIdFromJSON
, entityIdToJSON
, entityValues
, fromPersistValueJSON
, keyValueEntityFromJSON
, keyValueEntityToJSON
, toPersistValueJSON
, selectKeys
, belongsTo
, belongsToJust
, getEntity
, getJust
, getJustEntity
, insertEntity
, insertRecord
, liftPersist
, checkUnique
, getByValue
, insertBy
, insertUniqueEntity
, onlyUnique
, replaceUnique
, transactionSave
, transactionUndo
, defaultAttribute
, mkColumns
, getMigration
, migrate
, parseMigration
, parseMigration'
, printMigration
, runMigration
, runMigrationSilent
, runMigrationUnsafe
, showMigration
, decorateSQLWithLimitOffset
, fieldDBName
, fromSqlKey
, getFieldName
, getTableName
, tableDBName
, toSqlKey
, withRawQuery
, getStmtConn
, rawExecute
, rawExecuteCount
, rawQuery
, rawQueryRes
, rawSql
, close'
, createSqlPool
, liftSqlPersistMPool
, runSqlConn
, runSqlPersistM
, runSqlPersistMPool
, runSqlPool
, withSqlConn
, withSqlPool
, readToUnknown
, readToWrite
, writeToUnknown
, getEntityKeyFields
, entityPrimary
, keyAndEntityFields
, PersistStore
, PersistUnique
, DeleteCascade(..)
, PersistConfig(..)
, BackendSpecificUpdate
, Entity(..)
, PersistEntity(..)
, PersistField(..)
, SomePersistField(..)
, PersistQueryRead(..)
, PersistQueryWrite(..)
, BackendCompatible(..)
, BackendKey(..)
, HasPersistBackend(..)
, IsPersistBackend
, PersistCore(..)
, PersistRecordBackend
, PersistStoreRead(..)
, PersistStoreWrite(..)
, ToBackendKey(..)
, PersistUniqueRead(..)
, PersistUniqueWrite(..)
, PersistFieldSql(..)
, RawSql(..)
, CautiousMigration
, Column(..)
, ConnectionPool
, Migration
, PersistentSqlException(..)
, Single(..)
, Sql
, SqlPersistM
, SqlPersistT
, InsertSqlResult(..)
, IsSqlBackend
, LogFunc
, SqlBackend
, SqlBackendCanRead
, SqlBackendCanWrite
, SqlReadBackend(..)
, SqlReadT
, SqlWriteBackend(..)
, SqlWriteT
, Statement(..)
, Attr
, Checkmark(..)
, CompositeDef(..)
, EmbedEntityDef(..)
, EmbedFieldDef(..)
, EntityDef
, EntityIdDef(..)
, ExtraLine
, FieldDef(..)
, FieldType(..)
, ForeignDef(..)
, ForeignFieldDef
, IsNullable(..)
, PersistException(..)
, PersistFilter(..)
, PersistUpdate(..)
, PersistValue(..)
, ReferenceDef(..)
, SqlType(..)
, UniqueDef(..)
, UpdateException(..)
, WhyNullable(..)
, getEntityFields
, getEntityId
, getEntityDBName
, getEntityUniques
) where
import Database.Persist.Sql hiding
( BackendSpecificFilter, Filter(..), PersistQuery, SelectOpt(..)
, Update(..), delete, deleteWhereCount, updateWhereCount, selectList
, selectKeysList, deleteCascadeWhere, (=.), (+=.), (-=.), (*=.), (/=.)
, (==.), (!=.), (<.), (>.), (<=.), (>=.), (<-.), (/<-.), (||.)
, listToJSON, mapToJSON, getPersistMap, limitOffsetOrder, selectSource
, update , count )
( BackendSpecificFilter
, Filter(..)
, PersistQuery
, SelectOpt(..)
, Update(..)
, count
, delete
, deleteCascadeWhere
, deleteWhereCount
, exists
, getPersistMap
, limitOffsetOrder
, listToJSON
, mapToJSON
, selectKeysList
, selectList
, selectSource
, update
, updateWhereCount
, (!=.)
, (*=.)
, (+=.)
, (-=.)
, (/<-.)
, (/=.)
, (<-.)
, (<.)
, (<=.)
, (=.)
, (==.)
, (>.)
, (>=.)
, (||.)
)

View File

@ -1,71 +0,0 @@
{-# LANGUAGE DeriveDataTypeable
, EmptyDataDecls
, FlexibleContexts
, FlexibleInstances
, FunctionalDependencies
, MultiParamTypeClasses
, TypeFamilies
, UndecidableInstances
, GADTs
#-}
{-# LANGUAGE ConstraintKinds
, EmptyDataDecls
, FlexibleContexts
, FlexibleInstances
, FunctionalDependencies
, GADTs
, MultiParamTypeClasses
, OverloadedStrings
, UndecidableInstances
, ScopedTypeVariables
, InstanceSigs
, Rank2Types
, CPP
#-}
-- | This is an internal module, anything exported by this module
-- may change without a major version bump. Please use only
-- "Database.Esqueleto" if possible.
module Database.Esqueleto.Internal.Sql
( -- * The pretty face
SqlQuery
, SqlExpr(..)
, SqlEntity
, select
, selectSource
, delete
, deleteCount
, update
, updateCount
, insertSelect
, insertSelectCount
-- * The guts
, unsafeSqlCase
, unsafeSqlBinOp
, unsafeSqlBinOpComposite
, unsafeSqlValue
, unsafeSqlCastAs
, unsafeSqlFunction
, unsafeSqlExtractSubField
, UnsafeSqlFunctionArgument
, OrderByClause
, rawSelectSource
, runSource
, rawEsqueleto
, toRawSql
, Mode(..)
, NeedParens(..)
, IdentState
, initialIdentState
, IdentInfo
, SqlSelect(..)
, veryUnsafeCoerceSqlExprValue
, veryUnsafeCoerceSqlExprValueList
-- * Helper functions
, makeOrderByNoNewline
, uncommas'
, parens
, toArgList
, builderToText
) where
import Database.Esqueleto.Internal.Internal

View File

@ -0,0 +1,416 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RankNTypes #-}
-- | WARNING
--
-- This module is introduced in version @3.5.0.0@ to provide a smooth migration
-- experience from this legacy syntax to the new and improved syntax. If you've
-- imported this module, it means you've decided to use the old syntax for
-- a little bit longer, rather than migrate to the new stuff. That's fine!
--
-- But you should know that this module, and all of the legacy syntax, will be
-- completely removed from the library in version @4.0.0.0@.
--
-- The @esqueleto@ EDSL (embedded domain specific language).
-- This module replaces @Database.Persist@, so instead of
-- importing that module you should just import this one:
--
-- @
-- -- For a module using just esqueleto.
-- import Database.Esqueleto
-- @
--
-- If you need to use @persistent@'s default support for queries
-- as well, either import it qualified:
--
-- @
-- -- For a module that mostly uses esqueleto.
-- import Database.Esqueleto
-- import qualified Database.Persist as P
-- @
--
-- or import @esqueleto@ itself qualified:
--
-- @
-- -- For a module that uses esqueleto just on some queries.
-- import Database.Persist
-- import qualified Database.Esqueleto as E
-- @
--
-- Other than identifier name clashes, @esqueleto@ does not
-- conflict with @persistent@ in any way.
module Database.Esqueleto.Legacy
( -- * Setup
-- $setup
-- * Introduction
-- $introduction
-- * Getting started
-- $gettingstarted
-- * @esqueleto@'s Language
where_, on, groupBy, orderBy, rand, asc, desc, limit, offset
, distinct, distinctOn, don, distinctOnOrderBy, having, locking
, sub_select, (^.), (?.)
, val, isNothing, just, nothing, joinV, withNonNull
, countRows, count, countDistinct
, not_, (==.), (>=.), (>.), (<=.), (<.), (!=.), (&&.), (||.)
, between, (+.), (-.), (/.), (*.)
, random_, round_, ceiling_, floor_
, min_, max_, sum_, avg_, castNum, castNumM
, coalesce, coalesceDefault
, lower_, upper_, trim_, ltrim_, rtrim_, length_, left_, right_
, like, ilike, (%), concat_, (++.), castString
, subList_select, valList, justList
, in_, notIn, exists, notExists
, set, (=.), (+=.), (-=.), (*=.), (/=.)
, case_, toBaseId
, subSelect
, subSelectMaybe
, subSelectCount
, subSelectForeign
, subSelectList
, subSelectUnsafe
, ToBaseId(..)
, when_
, then_
, else_
, from
, Value(..)
, ValueList(..)
, OrderBy
, DistinctOn
, LockingKind(..)
, SqlString
-- ** Joins
, InnerJoin(..)
, CrossJoin(..)
, LeftOuterJoin(..)
, RightOuterJoin(..)
, FullOuterJoin(..)
, JoinKind(..)
, OnClauseWithoutMatchingJoinException(..)
-- * SQL backend
, SqlQuery
, SqlExpr
, SqlEntity
, select
, selectOne
, selectSource
, delete
, deleteCount
, update
, updateCount
, insertSelect
, insertSelectCount
, (<#)
, (<&>)
-- ** Rendering Queries
, renderQueryToText
, renderQuerySelect
, renderQueryUpdate
, renderQueryDelete
, renderQueryInsertInto
-- * Internal.Language
, From
-- * RDBMS-specific modules
-- $rdbmsSpecificModules
-- * Helpers
, valkey
, valJ
, associateJoin
-- * Re-exports
-- $reexports
, deleteKey
, module Database.Esqueleto.Internal.PersistentImport
) where
import Database.Esqueleto.Internal.Internal
import Database.Esqueleto.Internal.PersistentImport
-- $setup
--
-- 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 first
-- (<http://www.yesodweb.com/book/persistent>) to learn how to
-- define your schema.
----------------------------------------------------------------------
-- $introduction
--
-- The main goals of @esqueleto@ are to:
--
-- * Be easily translatable to SQL. When you take a look at a
-- @esqueleto@ query, you should be able to know exactly how
-- the SQL query will end up. (As opposed to being a
-- relational algebra EDSL such as HaskellDB, which is
-- non-trivial to translate into SQL.)
--
-- * Support the most widely used SQL features. We'd like you to be
-- able to use @esqueleto@ for all of your queries, no
-- exceptions. Send a pull request or open an issue on our
-- project page (<https://github.com/prowdsponsor/esqueleto>) if
-- there's anything missing that you'd like to see.
--
-- * Be as type-safe as possible. We strive to provide as many
-- type checks as possible. If you get bitten by some invalid
-- code that type-checks, please open an issue on our project
-- page so we can take a look.
--
-- However, it is /not/ a goal to be able to write portable SQL.
-- We do not try to hide the differences between DBMSs from you,
-- and @esqueleto@ code that works for one database may not work
-- on another. This is a compromise we have to make in order to
-- give you as much control over the raw SQL as possible without
-- losing too much convenience. This also means that you may
-- type-check a query that doesn't work on your DBMS.
----------------------------------------------------------------------
-- $gettingstarted
--
-- We like clean, easy-to-read EDSLs. However, in order to
-- achieve this goal we've used a lot of type hackery, leading to
-- some hard-to-read type signatures. On this section, we'll try
-- to build some intuition about the syntax.
--
-- For the following examples, we'll use this example schema:
--
-- @
-- share [mkPersist sqlSettings, mkMigrate \"migrateAll\"] [persist|
-- Person
-- name String
-- age Int Maybe
-- deriving Eq Show
-- BlogPost
-- title String
-- authorId PersonId
-- deriving Eq Show
-- Follow
-- follower PersonId
-- followed PersonId
-- deriving Eq Show
-- |]
-- @
--
-- 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:
--
-- @
-- SELECT *
-- FROM Person
-- @
--
-- In @esqueleto@, we may write the same query above as:
--
-- @
-- do people <- 'select' $
-- 'from' $ \\person -> do
-- return person
-- liftIO $ mapM_ (putStrLn . personName . entityVal) people
-- @
--
-- The expression above has type @SqlPersist m ()@, while
-- @people@ has type @[Entity Person]@. The query above will be
-- translated into exactly the same query we wrote manually, but
-- instead of @SELECT *@ it will list all entity fields (using
-- @*@ is not robust). Note that @esqueleto@ knows that we want
-- an @Entity Person@ just because of the @personName@ that we're
-- printing later.
--
-- However, most of the time we need to filter our queries using
-- @WHERE@. For example:
--
-- @
-- SELECT *
-- FROM Person
-- WHERE Person.name = \"John\"
-- @
--
-- In @esqueleto@, we may write the same query above as:
--
-- @
-- 'select' $
-- 'from' $ \\p -> do
-- 'where_' (p '^.' PersonName '==.' 'val' \"John\")
-- return p
-- @
--
-- Although @esqueleto@'s code is a bit more noisy, it's has
-- almost the same structure (save from the @return@). 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.
--
-- Another example would be:
--
-- @
-- SELECT *
-- FROM Person
-- WHERE Person.age >= 18
-- @
--
-- In @esqueleto@, we may write the same query above as:
--
-- @
-- 'select' $
-- 'from' $ \\p -> do
-- 'where_' (p '^.' PersonAge '>=.' 'just' ('val' 18))
-- return p
-- @
--
-- 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))@.
--
-- Implicit joins are represented by tuples. For example, to get
-- the list of all blog posts and their authors, we could write:
--
-- @
-- SELECT BlogPost.*, Person.*
-- FROM BlogPost, Person
-- WHERE BlogPost.authorId = Person.id
-- ORDER BY BlogPost.title ASC
-- @
--
-- In @esqueleto@, we may write the same query above as:
--
-- @
-- 'select' $
-- 'from' $ \\(b, p) -> do
-- 'where_' (b '^.' BlogPostAuthorId '==.' p '^.' PersonId)
-- 'orderBy' ['asc' (b '^.' BlogPostTitle)]
-- return (b, p)
-- @
--
-- However, you may want your results to include people who don't
-- have any blog posts as well using a @LEFT OUTER JOIN@:
--
-- @
-- SELECT Person.*, BlogPost.*
-- FROM Person LEFT OUTER JOIN BlogPost
-- ON Person.id = BlogPost.authorId
-- ORDER BY Person.name ASC, BlogPost.title ASC
-- @
--
-- In @esqueleto@, we may write the same query above as:
--
-- @
-- 'select' $
-- 'from' $ \\(p `'LeftOuterJoin`` mb) -> do
-- 'on' ('just' (p '^.' PersonId) '==.' mb '?.' BlogPostAuthorId)
-- 'orderBy' ['asc' (p '^.' PersonName), 'asc' (mb '?.' BlogPostTitle)]
-- return (p, mb)
-- @
--
-- 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)@.
--
-- 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:
--
-- @
-- SELECT P1.*, Follow.*, P2.*
-- FROM Person AS P1
-- INNER JOIN Follow ON P1.id = Follow.follower
-- INNER JOIN Person AS P2 ON P2.id = Follow.followed
-- @
--
-- In @esqueleto@, we may write the same query above as:
--
-- @
-- 'select' $
-- 'from' $ \\(p1 `'InnerJoin`` f `'InnerJoin`` p2) -> do
-- 'on' (p1 '^.' PersonId '==.' f '^.' FollowFollower)
-- 'on' (p2 '^.' PersonId '==.' f '^.' FollowFollowed)
-- return (p1, f, p2)
-- @
--
-- We also currently support @UPDATE@ and @DELETE@ statements.
-- For example:
--
-- @
-- do 'update' $ \\p -> do
-- 'set' p [ PersonName '=.' 'val' \"João\" ]
-- 'where_' (p '^.' PersonName '==.' 'val' \"Joao\")
-- 'delete' $
-- 'from' $ \\p -> do
-- 'where_' (p '^.' PersonAge '<.' 'just' ('val' 14))
-- @
--
-- 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:
--
-- @
-- INSERT INTO BlogPost
-- SELECT ('Group Blog Post', id)
-- FROM Person
-- @
--
-- In @esqueleto@, we may write the same query above as:
--
-- @
-- 'insertSelect' $ 'from' $ \\p->
-- return $ BlogPost '<#' \"Group Blog Post\" '<&>' (p '^.' PersonId)
-- @
--
-- Individual insertions can be performed through Persistent's
-- 'insert' function, reexported for convenience.
----------------------------------------------------------------------
-- $reexports
--
-- We re-export many symbols from @persistent@ for convenince:
--
-- * \"Store functions\" from "Database.Persist".
--
-- * Everything from "Database.Persist.Class" except for
-- @PersistQuery@ and @delete@ (use 'deleteKey' instead).
--
-- * Everything from "Database.Persist.Types" except for
-- @Update@, @SelectOpt@, @BackendSpecificFilter@ and @Filter@.
--
-- * Everything from "Database.Persist.Sql" except for
-- @deleteWhereCount@ and @updateWhereCount@.
----------------------------------------------------------------------
-- $rdbmsSpecificModules
--
-- 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.
--
-- * PostgreSQL: "Database.Esqueleto.PostgreSQL".
--
-- In order to use these functions, you need to explicitly import
-- their corresponding modules, they're not re-exported here.

View File

@ -1,14 +1,14 @@
{-# LANGUAGE OverloadedStrings #-}
-- | This module contain MySQL-specific functions.
--
-- /Since: 2.2.8/
-- @since 2.2.8
module Database.Esqueleto.MySQL
( random_
) where
( random_
) where
import Database.Esqueleto.Internal.Language hiding (random_)
import Database.Esqueleto.Internal.Internal hiding (random_)
import Database.Esqueleto.Internal.PersistentImport
import Database.Esqueleto.Internal.Sql
-- | (@random()@) Split out into database specific modules
-- because MySQL uses `rand()`.

View File

@ -1,40 +1,65 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings
, GADTs, CPP
#-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | This module contain PostgreSQL-specific functions.
--
-- /Since: 2.2.8/
-- @since: 2.2.8
module Database.Esqueleto.PostgreSQL
( AggMode(..)
, arrayAggDistinct
, arrayAgg
, arrayAggWith
, arrayRemove
, arrayRemoveNull
, stringAgg
, stringAggWith
, maybeArray
, chr
, now_
, random_
-- * Internal
, unsafeSqlAggregateFunction
) where
( AggMode(..)
, arrayAggDistinct
, arrayAgg
, arrayAggWith
, arrayRemove
, arrayRemoveNull
, stringAgg
, stringAggWith
, maybeArray
, chr
, now_
, random_
, upsert
, upsertBy
, insertSelectWithConflict
, insertSelectWithConflictCount
, filterWhere
, values
-- * Internal
, unsafeSqlAggregateFunction
) where
#if __GLASGOW_HASKELL__ < 804
import Data.Semigroup
import Data.Semigroup
#endif
import qualified Data.Text.Internal.Builder as TLB
import Data.Time.Clock (UTCTime)
import Database.Esqueleto.Internal.Language hiding (random_)
import Database.Esqueleto.Internal.PersistentImport
import Database.Esqueleto.Internal.Sql
import Control.Arrow (first)
import Control.Exception (throw)
import Control.Monad (void)
import Control.Monad.IO.Class (MonadIO(..))
import qualified Control.Monad.Trans.Reader as R
import Data.Int (Int64)
import qualified Data.List.NonEmpty as NE
import Data.Maybe
import Data.Proxy (Proxy(..))
import qualified Data.Text.Internal.Builder as TLB
import qualified Data.Text.Lazy as TL
import Data.Time.Clock (UTCTime)
import qualified Database.Esqueleto.Experimental as Ex
import qualified Database.Esqueleto.Experimental.From as Ex
import Database.Esqueleto.Internal.Internal hiding (random_)
import Database.Esqueleto.Internal.PersistentImport hiding (upsert, upsertBy)
import Database.Persist.Class (OnlyOneUniqueKey)
import Database.Persist (ConstraintNameDB(..), EntityNameDB(..))
import Database.Persist.SqlBackend
-- | (@random()@) Split out into database specific modules
-- because MySQL uses `rand()`.
--
-- /Since: 2.6.0/
-- @since 2.6.0
random_ :: (PersistField a, Num a) => SqlExpr (Value a)
random_ = unsafeSqlValue "RANDOM()"
@ -50,45 +75,48 @@ maybeArray ::
maybeArray x = coalesceDefault [x] (emptyArray)
-- | Aggregate mode
data AggMode = AggModeAll -- ^ ALL
| AggModeDistinct -- ^ DISTINCT
deriving (Show)
data AggMode
= AggModeAll -- ^ ALL
| AggModeDistinct -- ^ DISTINCT
deriving (Show)
-- | (Internal) Create a custom aggregate functions with aggregate mode
--
-- /Do/ /not/ use this function directly, instead define a new function and give
-- it a type (see `unsafeSqlBinOp`)
unsafeSqlAggregateFunction ::
UnsafeSqlFunctionArgument a
=> TLB.Builder
-> AggMode
-> a
-> [OrderByClause]
-> SqlExpr (Value b)
unsafeSqlAggregateFunction name mode args orderByClauses =
ERaw Never $ \info ->
unsafeSqlAggregateFunction
:: UnsafeSqlFunctionArgument a
=> TLB.Builder
-> AggMode
-> a
-> [OrderByClause]
-> SqlExpr (Value b)
unsafeSqlAggregateFunction name mode args orderByClauses = ERaw noMeta $ \_ info ->
let (orderTLB, orderVals) = makeOrderByNoNewline info orderByClauses
-- Don't add a space if we don't have order by clauses
orderTLBSpace = case orderByClauses of
[] -> ""
(_:_) -> " "
orderTLBSpace =
case orderByClauses of
[] -> ""
(_:_) -> " "
(argsTLB, argsVals) =
uncommas' $ map (\(ERaw _ f) -> f info) $ toArgList args
aggMode = case mode of
AggModeAll -> "" -- ALL is the default, so we don't need to
-- specify it
AggModeDistinct -> "DISTINCT "
uncommas' $ map (\(ERaw _ f) -> f Never info) $ toArgList args
aggMode =
case mode of
AggModeAll -> ""
-- ALL is the default, so we don't need to
-- specify it
AggModeDistinct -> "DISTINCT "
in ( name <> parens (aggMode <> argsTLB <> orderTLBSpace <> orderTLB)
, argsVals <> orderVals
)
--- | (@array_agg@) Concatenate input values, including @NULL@s,
--- into an array.
arrayAggWith ::
AggMode
-> SqlExpr (Value a)
-> [OrderByClause]
-> SqlExpr (Value (Maybe [a]))
arrayAggWith
:: AggMode
-> SqlExpr (Value a)
-> [OrderByClause]
-> SqlExpr (Value (Maybe [a]))
arrayAggWith = unsafeSqlAggregateFunction "array_agg"
--- | (@array_agg@) Concatenate input values, including @NULL@s,
@ -99,18 +127,17 @@ arrayAgg x = arrayAggWith AggModeAll x []
-- | (@array_agg@) Concatenate distinct input values, including @NULL@s, into
-- an array.
--
-- /Since: 2.5.3/
arrayAggDistinct ::
(PersistField a, PersistField [a])
=> SqlExpr (Value a)
-> SqlExpr (Value (Maybe [a]))
-- @since 2.5.3
arrayAggDistinct
:: (PersistField a, PersistField [a])
=> SqlExpr (Value a)
-> SqlExpr (Value (Maybe [a]))
arrayAggDistinct x = arrayAggWith AggModeDistinct x []
-- | (@array_remove@) Remove all elements equal to the given value from the
-- array.
--
-- /Since: 2.5.3/
-- @since 2.5.3
arrayRemove :: SqlExpr (Value [a]) -> SqlExpr (Value a) -> SqlExpr (Value [a])
arrayRemove arr elem' = unsafeSqlFunction "array_remove" (arr, elem')
@ -135,7 +162,7 @@ stringAggWith mode expr delim os =
-- | (@string_agg@) Concatenate input values separated by a
-- delimiter.
--
-- /Since: 2.2.8/
-- @since 2.2.8
stringAgg ::
SqlString s
=> SqlExpr (Value s) -- ^ Input values.
@ -146,9 +173,266 @@ stringAgg expr delim = stringAggWith AggModeAll expr delim []
-- | (@chr@) Translate the given integer to a character. (Note the result will
-- depend on the character set of your database.)
--
-- /Since: 2.2.11/
-- @since 2.2.11
chr :: SqlString s => SqlExpr (Value Int) -> SqlExpr (Value s)
chr = unsafeSqlFunction "chr"
now_ :: SqlExpr (Value UTCTime)
now_ = unsafeSqlValue "NOW()"
now_ = unsafeSqlFunction "NOW" ()
upsert
::
( MonadIO m
, PersistEntity record
, OnlyOneUniqueKey record
, PersistRecordBackend record SqlBackend
, IsPersistBackend (PersistEntityBackend record)
)
=> record
-- ^ new record to insert
-> [SqlExpr (Entity record) -> SqlExpr Update]
-- ^ updates to perform if the record already exists
-> R.ReaderT SqlBackend m (Entity record)
-- ^ the record in the database after the operation
upsert record updates = do
uniqueKey <- onlyUnique record
upsertBy uniqueKey record updates
upsertBy
::
(MonadIO m
, PersistEntity record
, IsPersistBackend (PersistEntityBackend record)
)
=> Unique record
-- ^ uniqueness constraint to find by
-> record
-- ^ new record to insert
-> [SqlExpr (Entity record) -> SqlExpr Update]
-- ^ updates to perform if the record already exists
-> R.ReaderT SqlBackend m (Entity record)
-- ^ the record in the database after the operation
upsertBy uniqueKey record updates = do
sqlB <- R.ask
case getConnUpsertSql sqlB of
Nothing ->
-- Postgres backend should have connUpsertSql, if this error is
-- thrown, check changes on persistent
throw (UnexpectedCaseErr OperationNotSupported)
Just upsertSql ->
handler sqlB upsertSql
where
addVals l = map toPersistValue (toPersistFields record) ++ l ++ persistUniqueToValues uniqueKey
entDef = entityDef (Just record)
updatesText conn = first builderToText $ renderUpdates conn updates
#if MIN_VERSION_persistent(2,11,0)
uniqueFields = persistUniqueToFieldNames uniqueKey
handler sqlB upsertSql = do
let (updateText, updateVals) =
updatesText sqlB
queryText =
upsertSql entDef uniqueFields updateText
queryVals =
addVals updateVals
xs <- rawSql queryText queryVals
pure (head xs)
#else
uDef = toUniqueDef uniqueKey
handler conn f = fmap head $ uncurry rawSql $
(***) (f entDef (uDef :| [])) addVals $ updatesText conn
#endif
-- | Inserts into a table the results of a query similar to 'insertSelect' but allows
-- to update values that violate a constraint during insertions.
--
-- Example of usage:
--
-- @
-- share [ mkPersist sqlSettings
-- , mkDeleteCascade sqlSettings
-- , mkMigrate "migrate"
-- ] [persistLowerCase|
-- Bar
-- num Int
-- deriving Eq Show
-- Foo
-- num Int
-- UniqueFoo num
-- deriving Eq Show
-- |]
--
-- insertSelectWithConflict
-- UniqueFoo -- (UniqueFoo undefined) or (UniqueFoo anyNumber) would also work
-- (from $ \b ->
-- return $ Foo <# (b ^. BarNum)
-- )
-- (\current excluded ->
-- [FooNum =. (current ^. FooNum) +. (excluded ^. FooNum)]
-- )
-- @
--
-- Inserts to table Foo all Bar.num values and in case of conflict SomeFooUnique,
-- the conflicting value is updated to the current plus the excluded.
--
-- @since 3.1.3
insertSelectWithConflict
:: forall a m val
. (FinalResult a, KnowResult a ~ Unique val, MonadIO m, PersistEntity val)
=> a
-- ^ Unique constructor or a unique, this is used just to get the name of
-- the postgres constraint, the value(s) is(are) never used, so if you have
-- a unique "MyUnique 0", "MyUnique undefined" would work as well.
-> SqlQuery (SqlExpr (Insertion val))
-- ^ Insert query.
-> (SqlExpr (Entity val) -> SqlExpr (Entity val) -> [SqlExpr (Entity val) -> SqlExpr Update])
-- ^ A list of updates to be applied in case of the constraint being
-- violated. The expression takes the current and excluded value to produce
-- the updates.
-> SqlWriteT m ()
insertSelectWithConflict unique query a =
void $ insertSelectWithConflictCount unique query a
-- | Same as 'insertSelectWithConflict' but returns the number of rows affected.
--
-- @since 3.1.3
insertSelectWithConflictCount
:: forall a val m
. (FinalResult a, KnowResult a ~ Unique val, MonadIO m, PersistEntity val)
=> a
-> SqlQuery (SqlExpr (Insertion val))
-> (SqlExpr (Entity val) -> SqlExpr (Entity val) -> [SqlExpr (Entity val) -> SqlExpr Update])
-> SqlWriteT m Int64
insertSelectWithConflictCount unique query conflictQuery = do
conn <- R.ask
uncurry rawExecuteCount $
combine
(toRawSql INSERT_INTO (conn, initialIdentState) query)
(conflict conn)
where
proxy :: Proxy val
proxy = Proxy
updates = conflictQuery entCurrent entExcluded
combine (tlb1,vals1) (tlb2,vals2) = (builderToText (tlb1 `mappend` tlb2), vals1 ++ vals2)
entExcluded = unsafeSqlEntity (I "excluded")
tableName = unEntityNameDB . getEntityDBName . entityDef
entCurrent = unsafeSqlEntity (I (tableName proxy))
uniqueDef = toUniqueDef unique
constraint = TLB.fromText . unConstraintNameDB . uniqueDBName $ uniqueDef
renderedUpdates :: (BackendCompatible SqlBackend backend) => backend -> (TLB.Builder, [PersistValue])
renderedUpdates conn = renderUpdates conn updates
conflict conn = (mconcat ([
TLB.fromText "ON CONFLICT ON CONSTRAINT \"",
constraint,
TLB.fromText "\" DO "
] ++ if null updates then [TLB.fromText "NOTHING"] else [
TLB.fromText "UPDATE SET ",
updatesTLB
]),values)
where
(updatesTLB,values) = renderedUpdates conn
-- | Allow aggregate functions to take a filter clause.
--
-- Example of usage:
--
-- @
-- share [mkPersist sqlSettings] [persistLowerCase|
-- User
-- name Text
-- deriving Eq Show
-- Task
-- userId UserId
-- completed Bool
-- deriving Eq Show
-- |]
--
-- select $ from $ \(users `InnerJoin` tasks) -> do
-- on $ users ^. UserId ==. tasks ^. TaskUserId
-- groupBy $ users ^. UserId
-- return
-- ( users ^. UserId
-- , count (tasks ^. TaskId) `filterWhere` (tasks ^. TaskCompleted ==. val True)
-- , count (tasks ^. TaskId) `filterWhere` (tasks ^. TaskCompleted ==. val False)
-- )
-- @
--
-- @since 3.3.3.3
filterWhere
:: SqlExpr (Value a)
-- ^ Aggregate function
-> SqlExpr (Value Bool)
-- ^ Filter clause
-> SqlExpr (Value a)
filterWhere aggExpr clauseExpr = ERaw noMeta $ \_ info ->
let (aggBuilder, aggValues) = case aggExpr of
ERaw _ aggF -> aggF Never info
(clauseBuilder, clauseValues) = case clauseExpr of
ERaw _ clauseF -> clauseF Never info
in ( aggBuilder <> " FILTER (WHERE " <> clauseBuilder <> ")"
, aggValues <> clauseValues
)
-- | Allows to use `VALUES (..)` in-memory set of values
-- in RHS of `from` expressions. Useful for JOIN's on
-- known values which also can be additionally preprocessed
-- somehow on db side with usage of inner PostgreSQL capabilities.
--
--
-- Example of usage:
--
-- @
-- share [mkPersist sqlSettings] [persistLowerCase|
-- User
-- name Text
-- age Int
-- deriving Eq Show
--
-- select $ do
-- bound :& user <- from $
-- values ( (val (10 :: Int), val ("ten" :: Text))
-- :| [ (val 20, val "twenty")
-- , (val 30, val "thirty") ]
-- )
-- `InnerJoin` table User
-- `on` (\((bound, _boundName) :& user) -> user^.UserAge >=. bound)
-- groupBy bound
-- pure (bound, count @Int $ user^.UserName)
-- @
--
-- @since 3.5.2.3
values :: (ToSomeValues a, Ex.ToAliasReference a, Ex.ToAlias a) => NE.NonEmpty a -> Ex.From a
values exprs = Ex.From $ do
ident <- newIdentFor $ DBName "vq"
alias <- Ex.toAlias $ NE.head exprs
ref <- Ex.toAliasReference ident alias
let aliasIdents = mapMaybe (\someVal -> case someVal of
SomeValue (ERaw aliasMeta _) -> sqlExprMetaAlias aliasMeta
) $ toSomeValues ref
pure (ref, const $ mkExpr ident aliasIdents)
where
someValueToSql :: IdentInfo -> SomeValue -> (TLB.Builder, [PersistValue])
someValueToSql info (SomeValue expr) = materializeExpr info expr
mkValuesRowSql :: IdentInfo -> [SomeValue] -> (TLB.Builder, [PersistValue])
mkValuesRowSql info vs =
let materialized = someValueToSql info <$> vs
valsSql = TLB.toLazyText . fst <$> materialized
params = concatMap snd materialized
in (TLB.fromLazyText $ "(" <> TL.intercalate "," valsSql <> ")", params)
-- (VALUES (v11, v12,..), (v21, v22,..)) as "vq"("v1", "v2",..)
mkExpr :: Ident -> [Ident] -> IdentInfo -> (TLB.Builder, [PersistValue])
mkExpr valsIdent colIdents info =
let materialized = mkValuesRowSql info . toSomeValues <$> NE.toList exprs
(valsSql, params) =
( TL.intercalate "," $ map (TLB.toLazyText . fst) materialized
, concatMap snd materialized
)
colsAliases = TL.intercalate "," (map (TLB.toLazyText . useIdent info) colIdents)
in
( "(VALUES " <> TLB.fromLazyText valsSql <> ") AS "
<> useIdent info valsIdent
<> "(" <> TLB.fromLazyText colsAliases <> ")"
, params
)

View File

@ -1,4 +1,5 @@
{-# LANGUAGE OverloadedStrings #-}
{-|
This module contains PostgreSQL-specific JSON functions.
@ -22,130 +23,127 @@
@since 3.1.0
-}
module Database.Esqueleto.PostgreSQL.JSON
( -- * JSONB Newtype
( -- * JSONB Newtype
--
-- | With 'JSONB', you can use your Haskell types in your
-- database table models as long as your type has 'FromJSON'
-- and 'ToJSON' instances.
--
-- @
-- import Database.Persist.TH
--
-- share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistUpperCase|
-- Example
-- json (JSONB MyType)
-- |]
-- @
--
-- CAUTION: Remember that changing the 'FromJSON' instance
-- of your type might result in old data becoming unparsable!
-- You can use (@JSONB Data.Aeson.Value@) for unstructured/variable JSON.
JSONB(..)
, JSONBExpr
, jsonbVal
-- * JSONAccessor
, JSONAccessor(..)
-- * Arrow operators
--
-- | With 'JSONB', you can use your Haskell types in your
-- database table models as long as your type has 'FromJSON'
-- and 'ToJSON' instances.
-- | /Better documentation included with individual functions/
--
-- The arrow operators are selection functions to select values
-- from JSON arrays or objects.
--
-- === PostgreSQL Documentation
--
-- /Requires PostgreSQL version >= 9.3/
--
-- @
-- import Database.Persist.TH
-- | Type | Description | Example | Example Result
-- -----+--------+--------------------------------------------+--------------------------------------------------+----------------
-- -> | int | Get JSON array element (indexed from zero, | '[{"a":"foo"},{"b":"bar"},{"c":"baz"}]'::json->2 | {"c":"baz"}
-- | | negative integers count from the end) | |
-- -> | text | Get JSON object field by key | '{"a": {"b":"foo"}}'::json->'a' | {"b":"foo"}
-- ->> | int | Get JSON array element as text | '[1,2,3]'::json->>2 | 3
-- ->> | text | Get JSON object field as text | '{"a":1,"b":2}'::json->>'b' | 2
-- \#> | text[] | Get JSON object at specified path | '{"a": {"b":{"c": "foo"}}}'::json#>'{a,b}' | {"c": "foo"}
-- \#>> | text[] | Get JSON object at specified path as text | '{"a":[1,2,3],"b":[4,5,6]}'::json#>>'{a,2}' | 3
-- @
, (->.)
, (->>.)
, (#>.)
, (#>>.)
-- * Filter operators
--
-- share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistUpperCase|
-- Example
-- json (JSONB MyType)
-- |]
-- | /Better documentation included with individual functions/
--
-- These functions test certain properties of JSON values
-- and return booleans, so are mainly used in WHERE clauses.
--
-- === PostgreSQL Documentation
--
-- /Requires PostgreSQL version >= 9.4/
--
-- @
-- | Type | Description | Example
-- ----+--------+-----------------------------------------------------------------+---------------------------------------------------
-- \@> | jsonb | Does the left JSON value contain within it the right value? | '{"a":1, "b":2}'::jsonb \@> '{"b":2}'::jsonb
-- <\@ | jsonb | Is the left JSON value contained within the right value? | '{"b":2}'::jsonb <\@ '{"a":1, "b":2}'::jsonb
-- ? | text | Does the string exist as a top-level key within the JSON value? | '{"a":1, "b":2}'::jsonb ? 'b'
-- ?| | text[] | Do any of these array strings exist as top-level keys? | '{"a":1, "b":2, "c":3}'::jsonb ?| array['b', 'c']
-- ?& | text[] | Do all of these array strings exist as top-level keys? | '["a", "b"]'::jsonb ?& array['a', 'b']
-- @
, (@>.)
, (<@.)
, (?.)
, (?|.)
, (?&.)
-- * Deletion and concatenation operators
--
-- | /Better documentation included with individual functions/
--
-- These operators change the shape of the JSON value and
-- also have the highest risk of throwing an exception.
-- Please read the descriptions carefully before using these functions.
--
-- === PostgreSQL Documentation
--
-- /Requires PostgreSQL version >= 9.5/
--
-- @
-- | Type | Description | Example
-- ----+---------+------------------------------------------------------------------------+-------------------------------------------------
-- || | jsonb | Concatenate two jsonb values into a new jsonb value | '["a", "b"]'::jsonb || '["c", "d"]'::jsonb
-- - | text | Delete key/value pair or string element from left operand. | '{"a": "b"}'::jsonb - 'a'
-- | | Key/value pairs are matched based on their key value. |
-- - | integer | Delete the array element with specified index (Negative integers count | '["a", "b"]'::jsonb - 1
-- | | from the end). Throws an error if top level container is not an array. |
-- \#- | text[] | Delete the field or element with specified path | '["a", {"b":1}]'::jsonb \#- '{1,b}'
-- | | (for JSON arrays, negative integers count from the end) |
-- @
--
-- CAUTION: Remember that changing the 'FromJSON' instance
-- of your type might result in old data becoming unparsable!
-- You can use (@JSONB Data.Aeson.Value@) for unstructured/variable JSON.
JSONB(..)
, JSONBExpr
, jsonbVal
-- * JSONAccessor
, JSONAccessor(..)
-- * Arrow operators
--
-- | /Better documentation included with individual functions/
--
-- The arrow operators are selection functions to select values
-- from JSON arrays or objects.
--
-- === PostgreSQL Documentation
--
-- /Requires PostgreSQL version >= 9.3/
--
-- @
-- | Type | Description | Example | Example Result
-- -----+--------+--------------------------------------------+--------------------------------------------------+----------------
-- -> | int | Get JSON array element (indexed from zero, | '[{"a":"foo"},{"b":"bar"},{"c":"baz"}]'::json->2 | {"c":"baz"}
-- | | negative integers count from the end) | |
-- -> | text | Get JSON object field by key | '{"a": {"b":"foo"}}'::json->'a' | {"b":"foo"}
-- ->> | int | Get JSON array element as text | '[1,2,3]'::json->>2 | 3
-- ->> | text | Get JSON object field as text | '{"a":1,"b":2}'::json->>'b' | 2
-- \#> | text[] | Get JSON object at specified path | '{"a": {"b":{"c": "foo"}}}'::json#>'{a,b}' | {"c": "foo"}
-- \#>> | text[] | Get JSON object at specified path as text | '{"a":[1,2,3],"b":[4,5,6]}'::json#>>'{a,2}' | 3
-- @
, (->.)
, (->>.)
, (#>.)
, (#>>.)
-- * Filter operators
--
-- | /Better documentation included with individual functions/
--
-- These functions test certain properties of JSON values
-- and return booleans, so are mainly used in WHERE clauses.
--
-- === PostgreSQL Documentation
--
-- /Requires PostgreSQL version >= 9.4/
--
-- @
-- | Type | Description | Example
-- ----+--------+-----------------------------------------------------------------+---------------------------------------------------
-- \@> | jsonb | Does the left JSON value contain within it the right value? | '{"a":1, "b":2}'::jsonb \@> '{"b":2}'::jsonb
-- <\@ | jsonb | Is the left JSON value contained within the right value? | '{"b":2}'::jsonb <\@ '{"a":1, "b":2}'::jsonb
-- ? | text | Does the string exist as a top-level key within the JSON value? | '{"a":1, "b":2}'::jsonb ? 'b'
-- ?| | text[] | Do any of these array strings exist as top-level keys? | '{"a":1, "b":2, "c":3}'::jsonb ?| array['b', 'c']
-- ?& | text[] | Do all of these array strings exist as top-level keys? | '["a", "b"]'::jsonb ?& array['a', 'b']
-- @
, (@>.)
, (<@.)
, (?.)
, (?|.)
, (?&.)
-- * Deletion and concatenation operators
--
-- | /Better documentation included with individual functions/
--
-- These operators change the shape of the JSON value and
-- also have the highest risk of throwing an exception.
-- Please read the descriptions carefully before using these functions.
--
-- === PostgreSQL Documentation
--
-- /Requires PostgreSQL version >= 9.5/
--
-- @
-- | Type | Description | Example
-- ----+---------+------------------------------------------------------------------------+-------------------------------------------------
-- || | jsonb | Concatenate two jsonb values into a new jsonb value | '["a", "b"]'::jsonb || '["c", "d"]'::jsonb
-- - | text | Delete key/value pair or string element from left operand. | '{"a": "b"}'::jsonb - 'a'
-- | | Key/value pairs are matched based on their key value. |
-- - | integer | Delete the array element with specified index (Negative integers count | '["a", "b"]'::jsonb - 1
-- | | from the end). Throws an error if top level container is not an array. |
-- \#- | text[] | Delete the field or element with specified path | '["a", {"b":1}]'::jsonb \#- '{1,b}'
-- | | (for JSON arrays, negative integers count from the end) |
-- @
--
-- /Requires PostgreSQL version >= 10/
--
-- @
-- | Type | Description | Example
-- ----+---------+------------------------------------------------------------------------+-------------------------------------------------
-- - | text[] | Delete multiple key/value pairs or string elements from left operand. | '{"a": "b", "c": "d"}'::jsonb - '{a,c}'::text[]
-- | | Key/value pairs are matched based on their key value. |
-- @
, (-.)
, (--.)
, (#-.)
, (||.)
) where
-- /Requires PostgreSQL version >= 10/
--
-- @
-- | Type | Description | Example
-- ----+---------+------------------------------------------------------------------------+-------------------------------------------------
-- - | text[] | Delete multiple key/value pairs or string elements from left operand. | '{"a": "b", "c": "d"}'::jsonb - '{a,c}'::text[]
-- | | Key/value pairs are matched based on their key value. |
-- @
, (-.)
, (--.)
, (#-.)
, (||.)
) where
import Data.Text (Text)
import Database.Esqueleto.Internal.Language hiding ((?.), (-.), (||.))
import Database.Esqueleto.Internal.Internal hiding ((-.), (?.), (||.))
import Database.Esqueleto.Internal.PersistentImport
import Database.Esqueleto.Internal.Sql
import Database.Esqueleto.PostgreSQL.JSON.Instances
infixl 6 ->., ->>., #>., #>>.
infixl 6 @>., <@., ?., ?|., ?&.
infixl 6 ||., -., --., #-.
-- | /Requires PostgreSQL version >= 9.3/
--
-- This function extracts the jsonb value from a JSON array or object,

View File

@ -4,6 +4,8 @@
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# language DerivingStrategies #-}
module Database.Esqueleto.PostgreSQL.JSON.Instances where
import Data.Aeson (FromJSON(..), ToJSON(..), encode, eitherDecodeStrict)
@ -13,28 +15,28 @@ import Data.String (IsString(..))
import Data.Text (Text)
import qualified Data.Text as T (concat, pack)
import qualified Data.Text.Encoding as TE (decodeUtf8, encodeUtf8)
import Database.Esqueleto (Value, just, val)
import Database.Esqueleto.Internal.PersistentImport
import Database.Esqueleto.Internal.Sql (SqlExpr)
import Database.Esqueleto.Internal.Internal (SqlExpr, Value, just, val)
import GHC.Generics (Generic)
-- | Newtype wrapper around any type with a JSON representation.
--
-- @since 3.1.0
newtype JSONB a = JSONB { unJSONB :: a }
deriving
( Generic
, FromJSON
, ToJSON
, Eq
, Foldable
, Functor
, Ord
, Read
, Show
, Traversable
)
deriving stock
( Generic
, Eq
, Foldable
, Functor
, Ord
, Read
, Show
, Traversable
)
deriving newtype
( FromJSON
, ToJSON
)
-- | 'SqlExpr' of a NULL-able 'JSONB' value. Hence the 'Maybe'.
--
@ -60,48 +62,49 @@ jsonbVal = just . val . JSONB
-- JSONKey "name"
--
-- NOTE: DO NOT USE ANY OF THE 'Num' METHODS ON THIS TYPE!
data JSONAccessor = JSONIndex Int
| JSONKey Text
deriving (Generic, Eq, Show)
data JSONAccessor
= JSONIndex Int
| JSONKey Text
deriving (Generic, Eq, Show)
-- | I repeat, DO NOT use any method other than 'fromInteger'!
instance Num JSONAccessor where
fromInteger = JSONIndex . fromInteger
negate (JSONIndex i) = JSONIndex $ negate i
negate (JSONKey _) = error "Can not negate a JSONKey"
(+) = numErr
(-) = numErr
(*) = numErr
abs = numErr
signum = numErr
fromInteger = JSONIndex . fromInteger
negate (JSONIndex i) = JSONIndex $ negate i
negate (JSONKey _) = error "Can not negate a JSONKey"
(+) = numErr
(-) = numErr
(*) = numErr
abs = numErr
signum = numErr
numErr :: a
numErr = error "Do not use 'Num' methods on JSONAccessors"
instance IsString JSONAccessor where
fromString = JSONKey . T.pack
fromString = JSONKey . T.pack
-- | @since 3.1.0
instance (FromJSON a, ToJSON a) => PersistField (JSONB a) where
toPersistValue = PersistDbSpecific . BSL.toStrict . encode . unJSONB
fromPersistValue pVal = fmap JSONB $ case pVal of
PersistByteString bs -> first (badParse $ TE.decodeUtf8 bs) $ eitherDecodeStrict bs
PersistText t -> first (badParse t) $ eitherDecodeStrict (TE.encodeUtf8 t)
x -> Left $ fromPersistValueError "string or bytea" x
toPersistValue = PersistLiteralEscaped . BSL.toStrict . encode . unJSONB
fromPersistValue pVal = fmap JSONB $ case pVal of
PersistByteString bs -> first (badParse $ TE.decodeUtf8 bs) $ eitherDecodeStrict bs
PersistText t -> first (badParse t) $ eitherDecodeStrict (TE.encodeUtf8 t)
x -> Left $ fromPersistValueError "string or bytea" x
-- | jsonb
--
-- @since 3.1.0
instance (FromJSON a, ToJSON a) => PersistFieldSql (JSONB a) where
sqlType _ = SqlOther "JSONB"
sqlType _ = SqlOther "JSONB"
badParse :: Text -> String -> Text
badParse t = fromPersistValueParseError t . T.pack
fromPersistValueError
:: Text -- ^ Database type(s), should appear different from Haskell name, e.g. "integer" or "INT", not "Int".
-> PersistValue -- ^ Incorrect value
-> Text -- ^ Error message
:: Text -- ^ Database type(s), should appear different from Haskell name, e.g. "integer" or "INT", not "Int".
-> PersistValue -- ^ Incorrect value
-> Text -- ^ Error message
fromPersistValueError databaseType received = T.concat
[ "Failed to parse Haskell newtype `JSONB a`; "
, "expected ", databaseType
@ -110,9 +113,9 @@ fromPersistValueError databaseType received = T.concat
]
fromPersistValueParseError
:: Text -- ^ Received value
-> Text -- ^ Additional error
-> Text -- ^ Error message
:: Text -- ^ Received value
-> Text -- ^ Additional error
-> Text -- ^ Error message
fromPersistValueParseError received err = T.concat
[ "Failed to parse Haskell type `JSONB a`, "
, "but received ", received

View File

@ -1,14 +1,14 @@
{-# LANGUAGE OverloadedStrings #-}
-- | This module contain SQLite-specific functions.
--
-- /Since: 2.2.8/
-- @since 2.2.8
module Database.Esqueleto.SQLite
( random_
) where
import Database.Esqueleto.Internal.Language hiding (random_)
import Database.Esqueleto.Internal.Internal hiding (random_)
import Database.Esqueleto.Internal.PersistentImport
import Database.Esqueleto.Internal.Sql
-- | (@random()@) Split out into database specific modules
-- because MySQL uses `rand()`.

12
stack-8.10.yaml Normal file
View File

@ -0,0 +1,12 @@
resolver: lts-17.8
packages:
- '.'
- 'examples'
extra-deps:
- lift-type-0.1.0.1
- persistent-2.13.0.0
- persistent-sqlite-2.13.0.0
- persistent-mysql-2.13.0.0
- persistent-postgresql-2.13.0.0

View File

@ -5,9 +5,11 @@ packages:
- 'examples'
extra-deps:
- persistent-2.10.0
- persistent-template-2.7.0
- persistent-mysql-2.10.0
- persistent-postgresql-2.10.0
- persistent-sqlite-2.10.0
- postgresql-simple-0.6.1
- git: https://github.com/yesodweb/persistent
commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702
subdirs:
- persistent
- persistent-template
- persistent-mysql
- persistent-postgresql
- persistent-sqlite

12
stack-8.8.yaml Normal file
View File

@ -0,0 +1,12 @@
resolver: lts-16.31
packages:
- '.'
- 'examples'
extra-deps:
- persistent-2.12.0.1
- persistent-template-2.12.0.0
- persistent-mysql-2.12.0.0
- persistent-postgresql-2.12.0.0
- persistent-sqlite-2.12.0.0

77
stack-8.8.yaml.lock Normal file
View File

@ -0,0 +1,77 @@
# This file was autogenerated by Stack.
# You should not edit this file by hand.
# For more information, please see the documentation at:
# https://docs.haskellstack.org/en/stable/lock_files
packages:
- completed:
subdir: persistent
name: persistent
version: 2.11.0.0
git: https://github.com/yesodweb/persistent
pantry-tree:
size: 2099
sha256: cd4d895557a60b40543c4a6804d32346a1c14c39e28658bb6852d8f4904ef1de
commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702
original:
subdir: persistent
git: https://github.com/yesodweb/persistent
commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702
- completed:
subdir: persistent-template
name: persistent-template
version: '2.9'
git: https://github.com/yesodweb/persistent
pantry-tree:
size: 620
sha256: 0602872c9c38ccc6966b4a1fd1d102a345f94ad855077157d588536ee6803343
commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702
original:
subdir: persistent-template
git: https://github.com/yesodweb/persistent
commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702
- completed:
subdir: persistent-mysql
name: persistent-mysql
version: 2.10.3
git: https://github.com/yesodweb/persistent
pantry-tree:
size: 577
sha256: a3b9d2ef77af25dca203a4dbe2857b6a1d4e421bbe376f261288e9a8ebfda28f
commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702
original:
subdir: persistent-mysql
git: https://github.com/yesodweb/persistent
commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702
- completed:
subdir: persistent-postgresql
name: persistent-postgresql
version: 2.11.0.0
git: https://github.com/yesodweb/persistent
pantry-tree:
size: 907
sha256: 6f1ad1c5b0b22cf455c6b1b4551a749d21bb72042597450c8ef9ff1eb5a74782
commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702
original:
subdir: persistent-postgresql
git: https://github.com/yesodweb/persistent
commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702
- completed:
subdir: persistent-sqlite
name: persistent-sqlite
version: 2.11.0.0
git: https://github.com/yesodweb/persistent
pantry-tree:
size: 891
sha256: fc9106077e16b406a5a823c732e3b543822a530f2befc446e49acf68797f6d42
commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702
original:
subdir: persistent-sqlite
git: https://github.com/yesodweb/persistent
commit: 0b8f9f3305c9b60c947565de882abfbfd8cb5702
snapshots:
- completed:
size: 532382
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/16/14.yaml
sha256: 1ef27e36f38824abafc43224ca612211b3828fa9ffd31ba0fc2867ae2e19ba90
original: lts-16.14

14
stack-nightly.yaml Normal file
View File

@ -0,0 +1,14 @@
resolver: nightly-2022-03-29
packages:
- "."
- 'examples'
extra-deps:
- time-1.12.1
- base-compat-0.12.1
- directory-1.3.7.0
- process-1.6.14.0
- Cabal-3.6.3.0
- unix-2.7.2.2

54
stack-nightly.yaml.lock Normal file
View File

@ -0,0 +1,54 @@
# This file was autogenerated by Stack.
# You should not edit this file by hand.
# For more information, please see the documentation at:
# https://docs.haskellstack.org/en/stable/lock_files
packages:
- completed:
hackage: time-1.12.1@sha256:af1fafc1fb66e3d0afb66fb5ab8629f74c038bebd42c234b581aff7abc201089,6295
pantry-tree:
size: 7208
sha256: 96205222b57c39156ee646d710a4100a119dc28f211c57cacaf741f6c1bb35da
original:
hackage: time-1.12.1
- completed:
hackage: base-compat-0.12.1@sha256:20e50848d9dfee1523fafe8950060b04fae43d402c15553da5c7cacd116f7846,6960
pantry-tree:
size: 9038
sha256: 2f2c14615443954f117613d77835234b598718e611fb4cf4522e01980bf1bcbd
original:
hackage: base-compat-0.12.1
- completed:
hackage: directory-1.3.7.0@sha256:d44788eac41268d951679fdcc343adc8a65fcf5b016bdf6c1f996bf78dde798e,2940
pantry-tree:
size: 3433
sha256: 2352834a6424cc8b462706c15e08bb721e120829b147b6d798eade4ebce425f5
original:
hackage: directory-1.3.7.0
- completed:
hackage: process-1.6.14.0@sha256:b6ad76fd3f4bf133cdc2dc9176e23447f2a0a8e9316047d53154cd11f871446d,2845
pantry-tree:
size: 1544
sha256: 72300155a8fd5a91f6b25dfebb77db05aa27a0b866dbfb2d7098c5e4580ca105
original:
hackage: process-1.6.14.0
- completed:
hackage: Cabal-3.6.3.0@sha256:ff97c442b0c679c1c9876acd15f73ac4f602b973c45bde42b43ec28265ee48f4,12459
pantry-tree:
size: 19757
sha256: b250a53bdb56844f047a2927833bb565b936a289abfa85dfc2a63148d776368a
original:
hackage: Cabal-3.6.3.0
- completed:
hackage: unix-2.7.2.2@sha256:15f5365c5995634e45de1772b9504761504a310184e676bc2ef60a14536dbef9,3496
pantry-tree:
size: 3536
sha256: 36434ced74d679622d61b69e8d92e1bd632d9ef3e284c63094653b2e473b0553
original:
hackage: unix-2.7.2.2
snapshots:
- completed:
size: 539378
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/nightly/2022/3/29.yaml
sha256: c959441a05f6fa4d45ae6e258290f04d399245b8436263b4abb525c7f73da6a5
original: nightly-2022-03-29

View File

@ -1 +1 @@
stack-8.6.yaml
stack-8.10.yaml

9
style-guide.md Normal file
View File

@ -0,0 +1,9 @@
# Style Guide
- Please use `stylish-haskell` on the project to keep imports consistent and
clean. We have a custom [`.stylish-haskell.yaml`](.stylish-haskell.yaml) file.
You can run `stylish-haskell` from vim with `:%! stylish-haskell`.
- Four space indent.
- Prefer indentation over any other form of alignment.
- If text goes off the screen due to four space indentation, factor out
functions and values into names to reduce indentation.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,87 @@
{-# LANGUAGE CPP, AllowAmbiguousTypes #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE UndecidableInstances #-}
module Common.Test.Import
( module Common.Test.Import
, module X
) where
import System.Environment
import Control.Applicative
import Common.Test.Models as X
import Database.Esqueleto.Experimental as X hiding (random_)
import Test.Hspec as X
import UnliftIO as X
import Control.Monad
import Test.QuickCheck
import Data.Text as X (Text)
import Control.Monad.Trans.Reader as X (ReaderT, mapReaderT, ask)
type SpecDb = SpecWith ConnectionPool
asserting :: MonadIO f => IO () -> SqlPersistT f ()
asserting a = liftIO a
noExceptions :: Expectation
noExceptions = pure ()
itDb
:: (HasCallStack)
=> String
-> SqlPersistT IO x
-> SpecDb
itDb message action = do
it message $ \connection -> do
void $ testDb connection action
propDb
:: (HasCallStack, Testable a)
=> String
-> ((SqlPersistT IO () -> IO ()) -> a )
-> SpecDb
propDb message action = do
it message $ \connection -> do
property (action (testDb connection))
testDb :: ConnectionPool -> SqlPersistT IO a -> IO a
testDb conn action =
liftIO $ flip runSqlPool conn $ do
a <- action
transactionUndo
pure a
setDatabaseState
:: SqlPersistT IO a
-> SqlPersistT IO ()
-> SpecWith ConnectionPool
-> SpecWith ConnectionPool
setDatabaseState create clean test =
beforeWith (\conn -> runSqlPool create conn >> pure conn) $
after (\conn -> runSqlPool clean conn) $
test
isCI :: IO Bool
isCI = do
env <- getEnvironment
return $ case lookup "TRAVIS" env <|> lookup "CI" env of
Just "true" -> True
_ -> False

189
test/Common/Test/Models.hs Normal file
View File

@ -0,0 +1,189 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE UndecidableInstances #-}
module Common.Test.Models where
import Data.Time
import Database.Esqueleto.Experimental
import Database.Persist.Sql
import Database.Persist.TH
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistUpperCase|
Foo
name Int
Primary name
deriving Show Eq Ord
Bar
quux FooId
deriving Show Eq Ord
Baz
blargh FooId
deriving Show Eq
Shoop
baz BazId
deriving Show Eq
Asdf
shoop ShoopId
deriving Show Eq
Another
why BazId
YetAnother
argh ShoopId
Person
name String
age Int Maybe
weight Int Maybe
favNum Int
deriving Eq Show Ord
BlogPost
title String
authorId PersonId
deriving Eq Show
Comment
body String
blog BlogPostId
deriving Eq Show
CommentReply
body String
comment CommentId
Profile
name String
person PersonId
deriving Eq Show
Reply
guy PersonId
body String
deriving Eq Show
Lord
county String maxlen=100
dogs Int Maybe
Primary county
deriving Eq Show
Deed
contract String maxlen=100
ownerId LordId maxlen=100
Primary contract
deriving Eq Show
Follow
follower PersonId
followed PersonId
deriving Eq Show
CcList
names [String]
Frontcover
number Int
title String
Primary number
deriving Eq Show
Article
title String
frontcoverNumber Int
Foreign Frontcover fkfrontcover frontcoverNumber
deriving Eq Show
ArticleMetadata
articleId ArticleId
Primary articleId
deriving Eq Show
Tag
name String maxlen=100
Primary name
deriving Eq Show
ArticleTag
articleId ArticleId
tagId TagId maxlen=100
Primary articleId tagId
deriving Eq Show
Article2
title String
frontcoverId FrontcoverId
deriving Eq Show
Point
x Int
y Int
name String
Primary x y
deriving Eq Show
Circle
centerX Int
centerY Int
name String
Foreign Point fkpoint centerX centerY
deriving Eq Show
Numbers
int Int
double Double
deriving Eq Show
JoinOne
name String
deriving Eq Show
JoinTwo
joinOne JoinOneId
name String
deriving Eq Show
JoinThree
joinTwo JoinTwoId
name String
deriving Eq Show
JoinFour
name String
joinThree JoinThreeId
deriving Eq Show
JoinOther
name String
deriving Eq Show
JoinMany
name String
joinOther JoinOtherId
joinOne JoinOneId
deriving Eq Show
DateTruncTest
created UTCTime
deriving Eq Show
|]
-- Unique Test schema
share [mkPersist sqlSettings, mkMigrate "migrateUnique"] [persistUpperCase|
OneUnique
name String
value Int
UniqueValue value
deriving Eq Show
|]
instance ToBaseId ArticleMetadata where
type BaseEnt ArticleMetadata = Article
toBaseIdWitness articleId = ArticleMetadataKey articleId

View File

@ -0,0 +1,22 @@
module Common.Test.Select where
import Common.Test.Import
testSelect :: SpecDb
testSelect = do
describe "select" $ do
itDb "works for a single value" $ do
ret <- select $ return $ val (3 :: Int)
asserting $ ret `shouldBe` [ Value 3 ]
itDb "works for a pair of a single value and ()" $ do
ret <- select $ return (val (3 :: Int), ())
asserting $ ret `shouldBe` [ (Value 3, ()) ]
itDb "works for a single ()" $ do
ret <- select $ return ()
asserting $ ret `shouldBe` [ () ]
itDb "works for a single NULL value" $ do
ret <- select $ return nothing
asserting $ ret `shouldBe` [ Value (Nothing :: Maybe Int) ]

View File

@ -1,45 +1,40 @@
{-# LANGUAGE ScopedTypeVariables
, FlexibleContexts
, RankNTypes
, TypeFamilies
#-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
module Main (main) where
module MySQL.Test where
import Common.Test.Import hiding (from, on)
import Control.Applicative
import Control.Monad (void)
import Control.Monad.IO.Class (MonadIO(liftIO))
import Control.Monad.Logger (runStderrLoggingT, runNoLoggingT)
import Control.Monad.Trans.Reader (ReaderT)
import Database.Persist.MySQL ( withMySQLConn
, connectHost
, connectDatabase
, connectUser
, connectPassword
, defaultConnectInfo)
import Database.Esqueleto
import Control.Monad.Logger (runNoLoggingT, runStderrLoggingT)
import Control.Monad.Trans.Reader (ReaderT, mapReaderT)
import qualified Control.Monad.Trans.Resource as R
import Database.Esqueleto
import Database.Esqueleto.Experimental hiding (from, on)
import qualified Database.Esqueleto.Experimental as Experimental
import Database.Persist.MySQL
( connectDatabase
, connectHost
, connectPassword
, connectPort
, connectUser
, defaultConnectInfo
, withMySQLConn
, createMySQLPool
)
import Test.Hspec
import Common.Test
-- testMysqlRandom :: Spec
-- testMysqlRandom = do
-- -- This is known not to work until
-- -- we can differentiate behavior by database
-- it "works with random_" $
-- run $ do
-- _ <- select $ return (random_ :: SqlExpr (Value Double))
-- return ()
testMysqlSum :: Spec
testMysqlSum :: SpecDb
testMysqlSum = do
it "works with sum_" $
run $ do
itDb "works with sum_" $ do
_ <- insert' p1
_ <- insert' p2
_ <- insert' p3
@ -49,13 +44,9 @@ testMysqlSum = do
return $ joinV $ sum_ (p ^. PersonAge)
liftIO $ ret `shouldBe` [ Value $ Just (36 + 17 + 17 :: Double ) ]
testMysqlTwoAscFields :: Spec
testMysqlTwoAscFields :: SpecDb
testMysqlTwoAscFields = do
it "works with two ASC fields (one call)" $
run $ do
itDb "works with two ASC fields (one call)" $ do
p1e <- insert' p1
p2e <- insert' p2
p3e <- insert' p3
@ -66,13 +57,9 @@ testMysqlTwoAscFields = do
return p
liftIO $ ret `shouldBe` [ p2e, p4e, p3e, p1e ]
testMysqlOneAscOneDesc :: Spec
testMysqlOneAscOneDesc :: SpecDb
testMysqlOneAscOneDesc = do
it "works with one ASC and one DESC field (two calls)" $
run $ do
itDb "works with one ASC and one DESC field (two calls)" $ do
p1e <- insert' p1
p2e <- insert' p2
p3e <- insert' p3
@ -87,10 +74,9 @@ testMysqlOneAscOneDesc = do
testMysqlCoalesce :: Spec
testMysqlCoalesce :: SpecDb
testMysqlCoalesce = do
it "works on PostgreSQL and MySQL with <2 arguments" $
run $ do
itDb "works on PostgreSQL and MySQL with <2 arguments" $ do
_ :: [Value (Maybe Int)] <-
select $
from $ \p -> do
@ -100,10 +86,9 @@ testMysqlCoalesce = do
testMysqlUpdate :: Spec
testMysqlUpdate :: SpecDb
testMysqlUpdate = do
it "works on a simple example" $
run $ do
itDb "works on a simple example" $ do
p1k <- insert p1
p2k <- insert p2
p3k <- insert p3
@ -126,20 +111,13 @@ testMysqlUpdate = do
, Entity p1k (Person anon (Just 73) Nothing 1)
, Entity p3k p3 ]
nameContains :: (BaseBackend backend ~ SqlBackend,
BackendCompatible SqlBackend backend,
MonadIO m, SqlString s,
IsPersistBackend backend, PersistQueryRead backend,
PersistUniqueRead backend)
nameContains :: (SqlString s)
=> (SqlExpr (Value [Char])
-> SqlExpr (Value s)
-> SqlExpr (Value Bool))
-> s
-> [Entity Person]
-> ReaderT backend m ()
-> SqlPersistT IO ()
nameContains f t expected = do
ret <- select $
from $ \p -> do
@ -151,68 +129,95 @@ nameContains f t expected = do
liftIO $ ret `shouldBe` expected
testMysqlTextFunctions :: Spec
testMysqlTextFunctions :: SpecDb
testMysqlTextFunctions = do
describe "text functions" $ do
it "like, (%) and (++.) work on a simple example" $
run $ do
itDb "like, (%) and (++.) work on a simple example" $ do
[p1e, p2e, p3e, p4e] <- mapM insert' [p1, p2, p3, p4]
nameContains like "h" [p1e, p2e]
nameContains like "i" [p4e, p3e]
nameContains like "iv" [p4e]
testMysqlUnionWithLimits :: SpecDb
testMysqlUnionWithLimits = do
describe "MySQL Union" $ do
itDb "supports limit/orderBy by parenthesizing" $ do
mapM_ (insert . Foo) [1..6]
main :: IO ()
main = do
hspec $ do
tests run
let q1 = do
foo <- Experimental.from $ Table @Foo
where_ $ foo ^. FooName <=. val 3
orderBy [asc $ foo ^. FooName]
limit 2
pure $ foo ^. FooName
describe "Test MySQL locking" $ do
testLocking withConn
let q2 = do
foo <- Experimental.from $ Table @Foo
where_ $ foo ^. FooName >. val 3
orderBy [asc $ foo ^. FooName]
limit 2
pure $ foo ^. FooName
ret <- select $ Experimental.from $ q1 `union_` q2
liftIO $ ret `shouldMatchList` [Value 1, Value 2, Value 4, Value 5]
spec :: Spec
spec = beforeAll mkConnectionPool $ do
tests
describe "MySQL specific tests" $ do
-- definitely doesn't work at the moment
-- testMysqlRandom
testMysqlSum
testMysqlTwoAscFields
testMysqlOneAscOneDesc
testMysqlCoalesce
testMysqlUpdate
testMysqlTextFunctions
run, runSilent, runVerbose :: Run
runSilent act = runNoLoggingT $ run_worker act
runVerbose act = runStderrLoggingT $ run_worker act
run =
if verbose
then runVerbose
else runSilent
-- definitely doesn't work at the moment
-- testMysqlRandom
testMysqlSum
testMysqlTwoAscFields
testMysqlOneAscOneDesc
testMysqlCoalesce
testMysqlUpdate
testMysqlTextFunctions
testMysqlUnionWithLimits
verbose :: Bool
verbose = False
run_worker :: RunDbMonad m => SqlPersistT (R.ResourceT m) a -> m a
run_worker act = withConn $ runSqlConn (migrateIt >> act)
migrateIt :: RunDbMonad m => SqlPersistT (R.ResourceT m) ()
migrateIt :: R.MonadUnliftIO m => SqlPersistT m ()
migrateIt = do
void $ runMigrationSilent migrateAll
mapReaderT R.runResourceT $ void $ runMigrationSilent migrateAll
cleanDB
mkConnectionPool :: IO ConnectionPool
mkConnectionPool = do
ci <- isCI
let connInfo
| ci =
defaultConnectInfo
{ connectHost = "127.0.0.1"
, connectUser = "travis"
, connectPassword = "esqutest"
, connectDatabase = "esqutest"
, connectPort = 33306
}
| otherwise =
defaultConnectInfo
{ connectHost = "localhost"
, connectUser = "travis"
, connectPassword = "esqutest"
, connectDatabase = "esqutest"
, connectPort = 3306
}
pool <-
if verbose
then
runStderrLoggingT $
createMySQLPool connInfo 4
else
runNoLoggingT $
createMySQLPool connInfo 4
withConn :: RunDbMonad m => (SqlBackend -> R.ResourceT m a) -> m a
withConn =
R.runResourceT .
withMySQLConn defaultConnectInfo
{ connectHost = "localhost"
, connectUser = "travis"
, connectPassword = ""
, connectDatabase = "esqutest"
}
flip runSqlPool pool $ do
migrateIt
cleanDB
pure pool

View File

@ -1,34 +1,36 @@
{-# LANGUAGE FlexibleContexts
, GADTs
, GeneralizedNewtypeDeriving
, MultiParamTypeClasses
, OverloadedStrings
, QuasiQuotes
, RankNTypes
, ScopedTypeVariables
, TemplateHaskell
, TypeFamilies
, UndecidableInstances
#-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
module PostgreSQL.MigrateJSON where
import Control.Monad.Trans.Resource (ResourceT)
import Data.Aeson (Value)
import Database.Esqueleto (SqlExpr, delete, from)
import Database.Esqueleto.PostgreSQL.JSON (JSONB)
import Database.Persist (Entity)
import Database.Persist.Sql (SqlPersistT)
import Database.Persist.TH
import Common.Test.Import hiding (Value, from, on)
import Common.Test (RunDbMonad)
import Data.Aeson (Value)
import Database.Esqueleto.Legacy (from)
import Database.Esqueleto.PostgreSQL.JSON (JSONB)
import Database.Persist.TH
-- JSON Table for PostgreSQL
share [mkPersist sqlSettings, mkMigrate "migrateJSON"] [persistUpperCase|
Json
value (JSONB Value)
deriving Show
|]
cleanJSON
:: (forall m. RunDbMonad m
=> SqlPersistT (ResourceT m) ())
:: forall m. MonadIO m
=> SqlPersistT m ()
cleanJSON = delete $ from $ \(_ :: SqlExpr (Entity Json)) -> return ()

File diff suppressed because it is too large Load Diff

View File

@ -1,181 +1,140 @@
{-# LANGUAGE ScopedTypeVariables
, FlexibleContexts
, RankNTypes
, TypeFamilies
, OverloadedStrings
#-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
module Main (main) where
module SQLite.Test where
import Common.Test.Import hiding (from, on)
import Control.Monad (void)
import Control.Monad.IO.Class (MonadIO(liftIO))
import Control.Monad.Logger (runStderrLoggingT, runNoLoggingT)
import Control.Monad.Trans.Reader (ReaderT)
import Database.Persist.Sqlite (withSqliteConn)
import Database.Sqlite (SqliteException)
import Database.Esqueleto hiding (random_)
import Control.Monad.Logger (runNoLoggingT, runStderrLoggingT)
import Database.Esqueleto.Legacy hiding (random_)
import Database.Esqueleto.SQLite (random_)
import qualified Control.Monad.Trans.Resource as R
import Test.Hspec
import Database.Persist.Sqlite (createSqlitePool)
import Database.Sqlite (SqliteException)
import Common.Test
testSqliteRandom :: Spec
testSqliteRandom :: SpecDb
testSqliteRandom = do
it "works with random_" $
run $ do
_ <- select $ return (random_ :: SqlExpr (Value Int))
return ()
itDb "works with random_" $ do
_ <- select $ return (random_ :: SqlExpr (Value Int))
asserting noExceptions
testSqliteSum :: Spec
testSqliteSum :: SpecDb
testSqliteSum = do
it "works with sum_" $
run $ do
_ <- insert' p1
_ <- insert' p2
_ <- insert' p3
_ <- insert' p4
ret <- select $
from $ \p->
return $ joinV $ sum_ (p ^. PersonAge)
liftIO $ ret `shouldBe` [ Value $ Just (36 + 17 + 17 :: Int) ]
itDb "works with sum_" $ do
_ <- insert' p1
_ <- insert' p2
_ <- insert' p3
_ <- insert' p4
ret <- select $
from $ \p->
return $ joinV $ sum_ (p ^. PersonAge)
asserting $ ret `shouldBe` [ Value $ Just (36 + 17 + 17 :: Int) ]
testSqliteTwoAscFields :: Spec
testSqliteTwoAscFields :: SpecDb
testSqliteTwoAscFields = do
it "works with two ASC fields (one call)" $
run $ do
p1e <- insert' p1
p2e <- insert' p2
p3e <- insert' p3
p4e <- insert' p4
ret <- select $
from $ \p -> do
orderBy [asc (p ^. PersonAge), asc (p ^. PersonName)]
return p
-- in SQLite and MySQL, its the reverse
liftIO $ ret `shouldBe` [ p2e, p4e, p3e, p1e ]
itDb "works with two ASC fields (one call)" $ do
p1e <- insert' p1
p2e <- insert' p2
p3e <- insert' p3
p4e <- insert' p4
ret <- select $
from $ \p -> do
orderBy [asc (p ^. PersonAge), asc (p ^. PersonName)]
return p
-- in SQLite and MySQL, its the reverse
asserting $ ret `shouldBe` [ p2e, p4e, p3e, p1e ]
testSqliteOneAscOneDesc :: Spec
testSqliteOneAscOneDesc :: SpecDb
testSqliteOneAscOneDesc = do
it "works with one ASC and one DESC field (two calls)" $
run $ do
p1e <- insert' p1
p2e <- insert' p2
p3e <- insert' p3
p4e <- insert' p4
ret <- select $
from $ \p -> do
orderBy [desc (p ^. PersonAge)]
orderBy [asc (p ^. PersonName)]
return p
liftIO $ ret `shouldBe` [ p1e, p4e, p3e, p2e ]
itDb "works with one ASC and one DESC field (two calls)" $ do
p1e <- insert' p1
p2e <- insert' p2
p3e <- insert' p3
p4e <- insert' p4
ret <- select $
from $ \p -> do
orderBy [desc (p ^. PersonAge)]
orderBy [asc (p ^. PersonName)]
return p
asserting $ ret `shouldBe` [ p1e, p4e, p3e, p2e ]
testSqliteCoalesce :: Spec
testSqliteCoalesce :: SpecDb
testSqliteCoalesce = do
it "throws an exception on SQLite with <2 arguments" $
run (select $
from $ \p -> do
return (coalesce [p ^. PersonAge]) :: SqlQuery (SqlExpr (Value (Maybe Int))))
`shouldThrow` (\(_ :: SqliteException) -> True)
itDb "throws an exception on SQLite with <2 arguments" $ do
eres <- try $ select $
from $ \p -> do
return (coalesce [p ^. PersonAge]) :: SqlQuery (SqlExpr (Value (Maybe Int)))
asserting $ case eres of
Left (_ :: SqliteException) ->
pure ()
Right _ ->
expectationFailure "Expected SqliteException with <2 args to coalesce"
testSqliteUpdate :: Spec
testSqliteUpdate :: SpecDb
testSqliteUpdate = do
it "works on a simple example" $
run $ do
p1k <- insert p1
p2k <- insert p2
p3k <- insert p3
let anon = "Anonymous"
() <- update $ \p -> do
set p [ PersonName =. val anon
, PersonAge *=. just (val 2) ]
where_ (p ^. PersonName !=. val "Mike")
n <- updateCount $ \p -> do
set p [ PersonAge +=. just (val 1) ]
where_ (p ^. PersonName !=. val "Mike")
ret <- select $
from $ \p -> do
orderBy [ asc (p ^. PersonName), asc (p ^. PersonAge) ]
return p
-- SQLite: nulls appear first, update returns matched rows.
liftIO $ n `shouldBe` 2
liftIO $ ret `shouldBe` [ Entity p2k (Person anon Nothing (Just 37) 2)
, Entity p1k (Person anon (Just 73) Nothing 1)
, Entity p3k p3 ]
itDb "works on a simple example" $ do
p1k <- insert p1
p2k <- insert p2
p3k <- insert p3
let anon = "Anonymous" :: String
() <- update $ \p -> do
set p [ PersonName =. val anon
, PersonAge *=. just (val 2) ]
where_ (p ^. PersonName !=. val "Mike")
n <- updateCount $ \p -> do
set p [ PersonAge +=. just (val 1) ]
where_ (p ^. PersonName !=. val "Mike")
ret <- select $
from $ \p -> do
orderBy [ asc (p ^. PersonName), asc (p ^. PersonAge) ]
return p
-- SQLite: nulls appear first, update returns matched rows.
asserting $ do
n `shouldBe` 2
ret `shouldMatchList`
[ Entity p2k (Person anon Nothing (Just 37) 2)
, Entity p1k (Person anon (Just 73) Nothing 1)
, Entity p3k p3
]
nameContains :: (BaseBackend backend ~ SqlBackend,
BackendCompatible SqlBackend backend,
MonadIO m, SqlString s,
IsPersistBackend backend, PersistQueryRead backend,
PersistUniqueRead backend)
=> (SqlExpr (Value [Char])
-> SqlExpr (Value s)
-> SqlExpr (Value Bool))
-> s
-> [Entity Person]
-> ReaderT backend m ()
nameContains f t expected = do
ret <- select $
from $ \p -> do
where_ (f
(p ^. PersonName)
((%) ++. val t ++. (%)))
orderBy [asc (p ^. PersonName)]
return p
liftIO $ ret `shouldBe` expected
testSqliteTextFunctions :: Spec
testSqliteTextFunctions :: SpecDb
testSqliteTextFunctions = do
describe "text functions" $ do
it "like, (%) and (++.) work on a simple example" $
run $ do
[p1e, p2e, p3e, p4e] <- mapM insert' [p1, p2, p3, p4]
nameContains like "h" [p1e, p2e]
nameContains like "i" [p4e, p3e]
nameContains like "iv" [p4e]
describe "text functions" $ do
itDb "like, (%) and (++.) work on a simple example" $ do
let query :: String -> SqlPersistT IO [Entity Person]
query t =
select $
from $ \p -> do
where_ (like
(p ^. PersonName)
((%) ++. val t ++. (%)))
orderBy [asc (p ^. PersonName)]
return p
[p1e, p2e, p3e, p4e] <- mapM insert' [p1, p2, p3, p4]
r0 <- query "h"
r1 <- query "i"
r2 <- query "iv"
asserting $ do
r0 `shouldBe` [p1e, p2e]
r1 `shouldBe` [p4e, p3e]
r2 `shouldBe` [p4e]
main :: IO ()
main = do
hspec $ do
tests run
describe "Test SQLite locking" $ do
testLocking withConn
spec :: HasCallStack => Spec
spec = beforeAll mkConnectionPool $ do
tests
describe "SQLite specific tests" $ do
testAscRandom random_ run
testRandomMath run
testAscRandom random_
testRandomMath
testSqliteRandom
testSqliteSum
testSqliteTwoAscFields
@ -184,32 +143,23 @@ main = do
testSqliteUpdate
testSqliteTextFunctions
mkConnectionPool :: IO ConnectionPool
mkConnectionPool = do
conn <-
if verbose
then runStderrLoggingT $
createSqlitePool ".esqueleto-test.sqlite" 4
else runNoLoggingT $
createSqlitePool ".esqueleto-test.sqlite" 4
flip runSqlPool conn $ do
migrateIt
run, runSilent, runVerbose :: Run
runSilent act = runNoLoggingT $ run_worker act
runVerbose act = runStderrLoggingT $ run_worker act
run =
if verbose
then runVerbose
else runSilent
pure conn
verbose :: Bool
verbose = False
run_worker :: RunDbMonad m => SqlPersistT (R.ResourceT m) a -> m a
run_worker act = withConn $ runSqlConn (migrateIt >> act)
migrateIt :: RunDbMonad m => SqlPersistT (R.ResourceT m) ()
migrateIt :: MonadUnliftIO m => SqlPersistT m ()
migrateIt = do
void $ runMigrationSilent migrateAll
withConn :: RunDbMonad m => (SqlBackend -> R.ResourceT m a) -> m a
withConn =
R.runResourceT . withSqliteConn ":memory:"
cleanDB

22
test/Spec.hs Normal file
View File

@ -0,0 +1,22 @@
module Main where
import Test.Hspec
import Test.Hspec.Core.Spec
import qualified SQLite.Test as SQLite
import qualified MySQL.Test as MySQL
import qualified PostgreSQL.Test as Postgres
main :: IO ()
main = hspec spec
spec :: Spec
spec = do
parallel $ describe "Esqueleto" $ do
describe "SQLite" $ do
sequential $ SQLite.spec
describe "MySQL" $ do
sequential $ MySQL.spec
describe "Postgresql" $ do
sequential $ Postgres.spec

View File

@ -1,4 +1,4 @@
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE UndecidableInstances, DerivingStrategies #-}
module Lib where
import Control.Monad.IO.Class (MonadIO)

View File

@ -0,0 +1,6 @@
# expected-compile-failures
This subdirectory contains a stack project for expected compilation failures. To
add a new "test case", create a new `executable` stanza in the `package.yaml`
file. The Travis CI test script ([`test.sh`](test.sh)) will attempt to compile
the executable and will exit with an error if it successfully compiled.

View File

@ -0,0 +1,45 @@
{-# OPTIONS_GHC -fno-warn-unused-top-binds #-}
{-# LANGUAGE TypeApplications #-}
module Main where
import Control.Monad.IO.Class (MonadIO)
import Database.Esqueleto hiding (from,on)
import Database.Esqueleto.Experimental
import Database.Esqueleto.Internal.Language (Insertion)
import Database.Persist.Sql (SqlWriteT)
import Database.Persist.TH (mkDeleteCascade,
mkMigrate, mkPersist,
persistLowerCase, share,
sqlSettings)
import Lib
main :: IO ()
main = pure ()
-- Missing on condition leads to an unintelligeable error and points to the wrong spot
missingOnConditionShouldFail :: MonadIO m => SqlPersistT m [(Entity Person, Entity BlogPost)]
missingOnConditionShouldFail = select $ do
(people :& blogPosts) <-
from $ Table @Person
`LeftOuterJoin` Table @BlogPost
pure (people, blogPosts)
-- Mismatched union when one part is returning a different shape than the other
mismatchedUnion :: MonadIO m => SqlPersistT m [(Value String, Value (Maybe Int))]
mismatchedUnion = select . from $
(SelectQuery $ do
people <- from $ Table @Person
pure (people ^. PersonName, people ^. PersonAge))
`Union`
(SelectQuery $ do
people <- from $ Table @Person
pure (people ^. PersonName))
incorrectNumberOfOnElements = select . from $
Table @Person
`LeftOuterJoin` Table @Follow
`on` (\(people :& follows) -> just (people ^. PersonId) ==. follows ?. FollowFollowed)
`LeftOuterJoin` Table @Person
`on` (\(follows :& followers) -> followers ?. PersonId ==. follows ?. FollowFollower)

View File

@ -0,0 +1,55 @@
cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.31.2.
--
-- see: https://github.com/sol/hpack
--
-- hash: 2bf9103f4701fb3c063743dbb88970ee68ecbeaeb87eea96ca21096da1264968
name: new-join-compiler-errors
version: 0.1.0.0
description: Please see the README on GitHub at <https://github.com/bitemyapp/esqueleto/test/new-join-compiler-errors#readme>
homepage: https://github.com/bitemyapp/esqueleto#readme
bug-reports: https://github.com/bitemyapp/esqueleto/issues
author: Ben Levy
maintainer: benjaminlevy007@gmail.com
copyright: 2020 Ben Levy
license: BSD3
build-type: Simple
extra-source-files:
README.md
source-repository head
type: git
location: https://github.com/bitemyapp/esqueleto
library
exposed-modules:
Lib
other-modules:
Paths_new_join_compiler_errors
hs-source-dirs:
src
default-extensions: FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving MultiParamTypeClasses NoMonomorphismRestriction OverloadedStrings QuasiQuotes ScopedTypeVariables StandaloneDeriving TemplateHaskell TypeFamilies
build-depends:
base >=4.7 && <5
, esqueleto
, persistent
, persistent-template
default-language: Haskell2010
executable bad-errors
main-is: Main.hs
other-modules:
Paths_new_join_compiler_errors
hs-source-dirs:
bad-errors
default-extensions: FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving MultiParamTypeClasses NoMonomorphismRestriction OverloadedStrings QuasiQuotes ScopedTypeVariables StandaloneDeriving TemplateHaskell TypeFamilies
ghc-options: -threaded -rtsopts -with-rtsopts=-N
build-depends:
base >=4.7 && <5
, esqueleto
, new-join-compiler-errors
, persistent
, persistent-template
default-language: Haskell2010

View File

@ -0,0 +1,46 @@
name: new-join-compiler-errors
version: 0.1.0.0
github: bitemyapp/esqueleto
license: BSD3
author: Ben Levy
maintainer: benjaminlevy007@gmail.com
copyright: 2020 Ben Levy
extra-source-files:
- README.md
description: Please see the README on GitHub at <https://github.com/bitemyapp/esqueleto/test/new-join-compiler-errors#readme>
dependencies:
- base >= 4.7 && < 5
- esqueleto
- persistent
- persistent-template
default-extensions:
- FlexibleContexts
- FlexibleInstances
- GADTs
- GeneralizedNewtypeDeriving
- MultiParamTypeClasses
- NoMonomorphismRestriction
- OverloadedStrings
- QuasiQuotes
- ScopedTypeVariables
- StandaloneDeriving
- TemplateHaskell
- TypeFamilies
library:
source-dirs: src
executables:
bad-errors:
main: Main.hs
source-dirs: bad-errors
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- new-join-compiler-errors

View File

@ -0,0 +1,33 @@
{-# LANGUAGE UndecidableInstances, DerivingStrategies #-}
module Lib where
import Control.Monad.IO.Class (MonadIO)
import Database.Persist
import Database.Persist.Sql (SqlReadT)
import Database.Esqueleto (SqlExpr, SqlQuery, from,
val, (<#), insertSelect, (<&>), (^.))
import Database.Esqueleto.Internal.Language (Insertion)
import Database.Persist.TH (mkDeleteCascade,
mkMigrate, mkPersist,
persistLowerCase, share,
sqlSettings)
share [ mkPersist sqlSettings
, mkDeleteCascade sqlSettings
, mkMigrate "migrateAll"] [persistLowerCase|
Person
name String
age Int Maybe
born Int Maybe
deriving Eq Show
BlogPost
title String
authorId PersonId
deriving Eq Show
Follow
follower PersonId
followed PersonId
deriving Eq Show
|]

View File

@ -0,0 +1,16 @@
resolver: lts-13.6
packages:
- .
- ../../../esqueleto
extra-deps:
- aeson-1.4.1.0
- persistent-2.10.0
- persistent-mysql-2.10.0
- persistent-postgresql-2.10.0
- persistent-sqlite-2.10.0
- persistent-template-2.7.0
- postgresql-libpq-0.9.4.2
- postgresql-simple-0.6.1
- transformers-0.5.5.2