Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e18dd125c5 | ||
|
|
5e212049d4 | ||
|
|
f883262dc2 | ||
|
|
8f591832d9 | ||
|
|
101a87f936 | ||
|
|
c70799be09 | ||
|
|
ed4e98f96b | ||
|
|
2a44844f75 | ||
|
|
982b354c7e | ||
|
|
18951b280b | ||
|
|
f03bba5bf9 | ||
|
|
e8271a00d6 | ||
|
|
3a12a15d00 | ||
|
|
33128042c4 | ||
|
|
34047e1f5f | ||
|
|
e145be999a | ||
|
|
b295bc6a5f | ||
|
|
ea4ff33b93 | ||
|
|
e39c62990e | ||
|
|
b5c0d84cad | ||
|
|
129b1734c3 | ||
|
|
bbaa0595e0 | ||
|
|
bd6da6eb3b | ||
|
|
cd16b2b22f | ||
|
|
9fba3e33e4 | ||
|
|
f96daae3b5 | ||
|
|
96331257e4 | ||
|
|
c4ec95874f | ||
|
|
a61f5527e8 |
9
.github/workflows/haskell.yml
vendored
9
.github/workflows/haskell.yml
vendored
@ -32,13 +32,13 @@ jobs:
|
||||
--health-retries=3
|
||||
strategy:
|
||||
matrix:
|
||||
cabal: ["3.2"]
|
||||
ghc: ["8.6.5", "8.8.3", "8.10.1"]
|
||||
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: actions/setup-haskell@v1.1.4
|
||||
- uses: haskell/actions/setup@v1
|
||||
id: setup-haskell-cabal
|
||||
with:
|
||||
ghc-version: ${{ matrix.ghc }}
|
||||
@ -68,8 +68,9 @@ jobs:
|
||||
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
|
||||
- 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
3
.gitignore
vendored
@ -1,7 +1,10 @@
|
||||
.stack-work
|
||||
stack.yaml.lock
|
||||
*.yaml.lock
|
||||
/dist*
|
||||
*~
|
||||
.cabal-sandbox/
|
||||
cabal.sandbox.config
|
||||
.hspec-failures
|
||||
*.sqlite*
|
||||
cabal.project.freeze
|
||||
|
||||
14
Makefile
14
Makefile
@ -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
|
||||
|
||||
61
README.md
61
README.md
@ -1,4 +1,4 @@
|
||||
Esqueleto [](https://travis-ci.org/bitemyapp/esqueleto)
|
||||
Esqueleto [](https://github.com/bitemyapp/esqueleto/actions/workflows/haskell.yml)
|
||||
==========
|
||||
|
||||

|
||||
@ -127,7 +127,61 @@ 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))`.
|
||||
|
||||
### 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
|
||||
|
||||
@ -487,5 +541,6 @@ user which can access it:
|
||||
```
|
||||
mysql> CREATE DATABASE esqutest;
|
||||
mysql> CREATE USER 'travis'@'localhost';
|
||||
mysql> GRANT ALL ON esqutest.* TO 'travis';
|
||||
mysql> ALTER USER 'travis'@'localhost' IDENTIFIED BY 'esqutest';
|
||||
mysql> GRANT ALL ON esqutest.* TO 'travis'@'localhost';
|
||||
```
|
||||
|
||||
@ -1 +1,5 @@
|
||||
packages: .
|
||||
-- Generated by stackage-to-hackage
|
||||
|
||||
packages:
|
||||
./
|
||||
, examples/
|
||||
|
||||
107
changelog.md
107
changelog.md
@ -1,17 +1,114 @@
|
||||
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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
|
||||
116
esqueleto.cabal
116
esqueleto.cabal
@ -1,7 +1,7 @@
|
||||
cabal-version: 1.12
|
||||
|
||||
name: esqueleto
|
||||
version: 3.5.0.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.
|
||||
.
|
||||
@ -29,11 +29,8 @@ source-repository head
|
||||
library
|
||||
exposed-modules:
|
||||
Database.Esqueleto
|
||||
Database.Esqueleto.Legacy
|
||||
Database.Esqueleto.Experimental
|
||||
Database.Esqueleto.Experimental.Aggregates
|
||||
Database.Esqueleto.Experimental.WindowFunctions
|
||||
Database.Esqueleto.Internal.Language
|
||||
Database.Esqueleto.Internal.Sql
|
||||
Database.Esqueleto.Internal.Internal
|
||||
Database.Esqueleto.Internal.ExprParser
|
||||
Database.Esqueleto.MySQL
|
||||
@ -56,17 +53,17 @@ library
|
||||
build-depends:
|
||||
base >=4.8 && <5.0
|
||||
, aeson >=1.0
|
||||
, attoparsec >= 0.13 && < 0.14
|
||||
, attoparsec >= 0.13 && < 0.15
|
||||
, blaze-html
|
||||
, bytestring
|
||||
, conduit >=1.3
|
||||
, containers
|
||||
, monad-logger
|
||||
, persistent >=2.10.0 && <2.12
|
||||
, persistent >=2.13 && <3
|
||||
, resourcet >=1.2
|
||||
, tagged >=0.2
|
||||
, text >=0.11 && <1.3
|
||||
, time >=1.5.0.1 && <=1.10
|
||||
, time >=1.5.0.1 && <=1.13
|
||||
, transformers >=0.2
|
||||
, unliftio
|
||||
, unordered-containers >=0.2
|
||||
@ -78,54 +75,24 @@ library
|
||||
-Wpartial-fields
|
||||
-Wmissing-home-modules
|
||||
-Widentities
|
||||
-Wredundant-constraints
|
||||
-Wcpp-undef
|
||||
-Wcpp-undef
|
||||
-Wmonomorphism-restriction
|
||||
default-language: Haskell2010
|
||||
|
||||
test-suite mysql
|
||||
test-suite specs
|
||||
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
|
||||
, attoparsec
|
||||
, blaze-html
|
||||
, bytestring
|
||||
, conduit >=1.3
|
||||
, containers
|
||||
, esqueleto
|
||||
, exceptions
|
||||
, hspec
|
||||
, monad-logger
|
||||
, mtl
|
||||
, mysql
|
||||
, mysql-simple
|
||||
, persistent
|
||||
, persistent-mysql
|
||||
, persistent-template
|
||||
, resourcet >=1.2
|
||||
, tagged >=0.2
|
||||
, text >=0.11 && <1.3
|
||||
, time
|
||||
, transformers >=0.2
|
||||
, unliftio
|
||||
, unordered-containers >=0.2
|
||||
default-language: Haskell2010
|
||||
|
||||
test-suite postgresql
|
||||
type: exitcode-stdio-1.0
|
||||
main-is: PostgreSQL/Test.hs
|
||||
main-is: Spec.hs
|
||||
other-modules:
|
||||
Common.Test
|
||||
Common.Test.Models
|
||||
Common.Test.Import
|
||||
Common.Test.Select
|
||||
PostgreSQL.MigrateJSON
|
||||
Paths_esqueleto
|
||||
SQLite.Test
|
||||
PostgreSQL.Test
|
||||
MySQL.Test
|
||||
default-extensions:
|
||||
RankNTypes
|
||||
hs-source-dirs:
|
||||
test
|
||||
ghc-options: -Wall -threaded
|
||||
@ -135,56 +102,27 @@ test-suite postgresql
|
||||
, attoparsec
|
||||
, blaze-html
|
||||
, bytestring
|
||||
, conduit >=1.3
|
||||
, conduit
|
||||
, containers
|
||||
, esqueleto
|
||||
, exceptions
|
||||
, hspec
|
||||
, hspec-core
|
||||
, monad-logger
|
||||
, mtl
|
||||
, mysql
|
||||
, mysql-simple
|
||||
, persistent
|
||||
, persistent-mysql
|
||||
, persistent-postgresql
|
||||
, persistent-template
|
||||
, postgresql-libpq
|
||||
, postgresql-simple
|
||||
, resourcet >=1.2
|
||||
, tagged >=0.2
|
||||
, text >=0.11 && <1.3
|
||||
, time
|
||||
, 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
|
||||
build-depends:
|
||||
base >=4.8 && <5.0
|
||||
, attoparsec
|
||||
, blaze-html
|
||||
, bytestring
|
||||
, conduit >=1.3
|
||||
, containers
|
||||
, esqueleto
|
||||
, exceptions
|
||||
, hspec
|
||||
, monad-logger
|
||||
, mtl
|
||||
, persistent
|
||||
, persistent-sqlite
|
||||
, persistent-template
|
||||
, resourcet >=1.2
|
||||
, tagged >=0.2
|
||||
, text >=0.11 && <1.3
|
||||
, postgresql-simple
|
||||
, QuickCheck
|
||||
, resourcet
|
||||
, tagged
|
||||
, text
|
||||
, time
|
||||
, transformers >=0.2
|
||||
, transformers
|
||||
, unliftio
|
||||
, unordered-containers >=0.2
|
||||
, unordered-containers
|
||||
default-language: Haskell2010
|
||||
|
||||
1
examples/.gitignore
vendored
1
examples/.gitignore
vendored
@ -1 +0,0 @@
|
||||
*.cabal
|
||||
@ -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
|
||||
|
||||
@ -10,7 +10,6 @@
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unused-top-binds #-}
|
||||
@ -20,14 +19,15 @@ module Main
|
||||
) where
|
||||
|
||||
import Blog
|
||||
import Control.Monad (forM_, void)
|
||||
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.Logger (MonadLogger, MonadLoggerIO)
|
||||
import Control.Monad.Reader (MonadReader(..), runReaderT)
|
||||
import Control.Monad.Trans.Control (MonadBaseControl)
|
||||
import Data.Monoid ((<>))
|
||||
import Database.Esqueleto.Experimental
|
||||
import Database.Esqueleto
|
||||
import Database.Persist.Postgresql (ConnectionString, withPostgresqlConn)
|
||||
import Database.Persist.TH
|
||||
( AtLeastOneUniqueKey(..)
|
||||
@ -62,9 +62,9 @@ putPersons :: (MonadIO m, MonadLogger m)
|
||||
=> SqlPersistT m ()
|
||||
putPersons = do
|
||||
-- | Select all values from the `person` table
|
||||
people <- select $ do
|
||||
person <- from $ table @Person
|
||||
return person
|
||||
people <- select $
|
||||
from $ \person -> do
|
||||
return person
|
||||
|
||||
-- | entityVal extracts the Person value, which we then extract
|
||||
-- | the person name from the record and print it
|
||||
@ -75,8 +75,8 @@ getJohns :: (MonadIO m, MonadLogger m)
|
||||
=> SqlReadT m [Entity Person]
|
||||
getJohns =
|
||||
-- | Select all persons where their name is equal to "John"
|
||||
select $ do
|
||||
p <- from $ table @Person
|
||||
select $
|
||||
from $ \p -> do
|
||||
where_ (p ^. PersonName ==. val "John")
|
||||
return p
|
||||
|
||||
@ -85,8 +85,8 @@ getAdults :: (MonadIO m, MonadLogger m)
|
||||
=> SqlReadT m [Entity Person]
|
||||
getAdults =
|
||||
-- | Select any Person where their age is >= 18 and NOT NULL
|
||||
select $ do
|
||||
p <- from $ table @Person
|
||||
select $
|
||||
from $ \p -> do
|
||||
where_ (p ^. PersonAge >=. just (val 18))
|
||||
return p
|
||||
|
||||
@ -95,10 +95,8 @@ getBlogPostsByAuthors :: (MonadIO m, MonadLogger m)
|
||||
=> SqlReadT m [(Entity BlogPost, Entity Person)]
|
||||
getBlogPostsByAuthors =
|
||||
-- | Select all persons and their blogposts, ordering by title
|
||||
select $ do
|
||||
p :& b <-
|
||||
from $ table @Person
|
||||
`crossJoin` table @BlogPost
|
||||
select $
|
||||
from $ \(b, p) -> do
|
||||
where_ (b ^. BlogPostAuthorId ==. p ^. PersonId)
|
||||
orderBy [asc (b ^. BlogPostTitle)]
|
||||
return (b, p)
|
||||
@ -110,11 +108,9 @@ getAuthorMaybePosts =
|
||||
-- | Select all persons doing a left outer join on blogposts
|
||||
-- | Since a person may not have any blogposts the BlogPost Entity is wrapped
|
||||
-- | in a Maybe
|
||||
select $ do
|
||||
(p :& mb) <-
|
||||
from $ table @Person
|
||||
`leftJoin` table @BlogPost
|
||||
`on` (\(p :& mb) -> (just (p ^. PersonId) ==. mb ?. BlogPostAuthorId))
|
||||
select $
|
||||
from $ \(p `LeftOuterJoin` mb) -> do
|
||||
on (just (p ^. PersonId) ==. mb ?. BlogPostAuthorId)
|
||||
orderBy [asc (p ^. PersonName), asc (mb ?. BlogPostTitle)]
|
||||
return (p, mb)
|
||||
|
||||
@ -126,13 +122,10 @@ followers =
|
||||
-- | Note carefully that the order of the ON clauses is reversed!
|
||||
-- | You're required to write your ons in reverse order because that helps composability
|
||||
-- | (see the documentation of on for more details).
|
||||
select $ do
|
||||
(p1 :& f :& p2) <-
|
||||
from $ table @Person
|
||||
`innerJoin` table @Follow
|
||||
`on` (\(p1 :& f) -> p1 ^. PersonId ==. f ^. FollowFollower)
|
||||
`innerJoin` table @Person
|
||||
`on` (\(_ :& f :& p2) -> p2 ^. PersonId ==. f ^. FollowFollowed)
|
||||
select $
|
||||
from $ \(p1 `InnerJoin` f `InnerJoin` p2) -> do
|
||||
on (p2 ^. PersonId ==. f ^. FollowFollowed)
|
||||
on (p1 ^. PersonId ==. f ^. FollowFollower)
|
||||
return (p1, f, p2)
|
||||
|
||||
|
||||
@ -153,8 +146,8 @@ deleteYoungsters = do
|
||||
-- | In this case where `ON DELETE CASCADE` is not generated by migration
|
||||
-- | we select all the entities we want to delete and then for each one
|
||||
-- | one we extract the key and use Persistent's `deleteCascade`
|
||||
youngsters <- select $ do
|
||||
p <- from $ table @Person
|
||||
youngsters <- select $
|
||||
from $ \p -> do
|
||||
where_ (p ^. PersonAge <. just (val 14))
|
||||
pure p
|
||||
forM_ youngsters (deleteCascade . entityKey)
|
||||
@ -164,8 +157,7 @@ insertBlogPosts :: (MonadIO m, MonadLogger m)
|
||||
=> SqlWriteT m ()
|
||||
insertBlogPosts =
|
||||
-- | Insert a new blogpost for every person
|
||||
insertSelect $ do
|
||||
p <- from $ table @Person
|
||||
insertSelect $ from $ \p ->
|
||||
return $ BlogPost <# (val "Group Blog Post") <&> (p ^. PersonId)
|
||||
|
||||
|
||||
@ -173,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
|
||||
|
||||
49
examples/esqueleto-examples.cabal
Normal file
49
examples/esqueleto-examples.cabal
Normal 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
|
||||
@ -13,8 +13,7 @@ extra-source-files:
|
||||
dependencies:
|
||||
- base
|
||||
- esqueleto
|
||||
- persistent
|
||||
- persistent-template
|
||||
- persistent >= 2.12
|
||||
- persistent-postgresql
|
||||
- mtl
|
||||
- monad-logger
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# 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:
|
||||
@ -32,11 +32,15 @@
|
||||
-- Other than identifier name clashes, @esqueleto@ does not
|
||||
-- conflict with @persistent@ in any way.
|
||||
--
|
||||
-- Note that the faciliites for @JOIN@ have been significantly improved in the
|
||||
-- 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.
|
||||
module Database.Esqueleto
|
||||
--
|
||||
-- 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
|
||||
|
||||
@ -75,8 +79,6 @@ module Database.Esqueleto
|
||||
, else_
|
||||
, from
|
||||
, Value(..)
|
||||
, pattern Value
|
||||
, unValue
|
||||
, ValueList(..)
|
||||
, OrderBy
|
||||
, DistinctOn
|
||||
@ -95,6 +97,7 @@ module Database.Esqueleto
|
||||
, SqlExpr
|
||||
, SqlEntity
|
||||
, select
|
||||
, selectOne
|
||||
, selectSource
|
||||
, delete
|
||||
, deleteCount
|
||||
@ -126,14 +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 qualified Data.Map.Strict as Map
|
||||
import Database.Esqueleto.Internal.Language
|
||||
import Database.Esqueleto.Legacy
|
||||
import Database.Esqueleto.Internal.PersistentImport
|
||||
import Database.Esqueleto.Internal.Sql
|
||||
import qualified Database.Persist
|
||||
|
||||
|
||||
-- $setup
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
-- 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.
|
||||
--
|
||||
-- Esqueleto users are encouraged to migrate to this module, as it will become
|
||||
-- the default in a new major version @4.0.0.0@.
|
||||
-- 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
|
||||
@ -90,7 +90,7 @@ module Database.Esqueleto.Experimental
|
||||
, joinV
|
||||
, withNonNull
|
||||
|
||||
, countRows_
|
||||
, countRows
|
||||
, count
|
||||
, countDistinct
|
||||
|
||||
@ -170,7 +170,6 @@ module Database.Esqueleto.Experimental
|
||||
, then_
|
||||
, else_
|
||||
, Value(..)
|
||||
, pattern Value
|
||||
, ValueList(..)
|
||||
, OrderBy
|
||||
, DistinctOn
|
||||
@ -191,6 +190,7 @@ module Database.Esqueleto.Experimental
|
||||
, SqlExpr
|
||||
, SqlEntity
|
||||
, select
|
||||
, selectOne
|
||||
, selectSource
|
||||
, delete
|
||||
, deleteCount
|
||||
@ -219,13 +219,9 @@ module Database.Esqueleto.Experimental
|
||||
, module Database.Esqueleto.Internal.PersistentImport
|
||||
) where
|
||||
|
||||
import Data.Coerce
|
||||
import Database.Esqueleto.Internal.Internal hiding
|
||||
(From, from, groupBy, on, sum_, (?.), (^.))
|
||||
import qualified Database.Esqueleto.Internal.Internal as I ((?.), (^.))
|
||||
import Database.Esqueleto.Internal.Internal hiding (From, from, on)
|
||||
import Database.Esqueleto.Internal.PersistentImport
|
||||
|
||||
import Database.Esqueleto.Experimental.Aggregates
|
||||
import Database.Esqueleto.Experimental.From
|
||||
import Database.Esqueleto.Experimental.From.CommonTableExpression
|
||||
import Database.Esqueleto.Experimental.From.Join
|
||||
@ -565,4 +561,3 @@ import Database.Esqueleto.Experimental.ToMaybe
|
||||
-- )
|
||||
-- @
|
||||
--
|
||||
--
|
||||
|
||||
@ -1,153 +0,0 @@
|
||||
{-# LANGUAGE DerivingVia #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE FunctionalDependencies #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
|
||||
module Database.Esqueleto.Experimental.Aggregates
|
||||
where
|
||||
|
||||
import Control.Monad.IO.Class
|
||||
import qualified Control.Monad.Trans.Writer as W
|
||||
import Data.Coerce (Coercible, coerce)
|
||||
import Data.Proxy
|
||||
import Database.Esqueleto.Internal.Internal
|
||||
( GroupByClause(..)
|
||||
, SideData(..)
|
||||
, SqlExpr(..)
|
||||
, SqlQuery(..)
|
||||
, SqlQueryHaving(..)
|
||||
, SqlSelect(..)
|
||||
, ToSomeValues(..)
|
||||
, noMeta
|
||||
, select
|
||||
, unsafeSqlFunction
|
||||
)
|
||||
import qualified Database.Esqueleto.Internal.Internal as I
|
||||
import Database.Esqueleto.Internal.PersistentImport
|
||||
( Entity
|
||||
, EntityField
|
||||
, Key
|
||||
, PersistEntity
|
||||
, PersistField
|
||||
, SqlReadT
|
||||
, persistIdField
|
||||
)
|
||||
|
||||
class SqlExprEntity expr where
|
||||
(^.) :: (PersistEntity val, PersistField typ)
|
||||
=> expr (Entity val)
|
||||
-> EntityField val typ
|
||||
-> expr typ
|
||||
(?.) :: (PersistEntity val, PersistField typ)
|
||||
=> expr (Maybe (Entity val))
|
||||
-> EntityField val typ
|
||||
-> expr (Maybe typ)
|
||||
|
||||
-- | Project a field of an entity.
|
||||
instance SqlExprEntity SqlExpr where
|
||||
(^.) = (I.^.)
|
||||
(?.) = (I.?.)
|
||||
|
||||
newtype SqlAggregate source a = SqlAggregate { unsafeSqlAggregate :: SqlExpr a }
|
||||
deriving via SqlExpr instance SqlExprEntity (SqlAggregate source)
|
||||
instance forall a source. PersistField a => SqlSelect (SqlAggregate source a) a where
|
||||
sqlSelectCols info (SqlAggregate e) = sqlSelectCols info e
|
||||
sqlSelectColCount = const 1
|
||||
sqlSelectProcessRow _ = sqlSelectProcessRow (Proxy :: Proxy (SqlExpr a))
|
||||
instance SqlQueryHaving (SqlAggregate source Bool) where
|
||||
having expr = Q $ W.tell mempty { sdHavingClause = I.Where (coerce expr) }
|
||||
instance SqlQueryHaving (SqlAggregate source (Maybe Bool)) where
|
||||
having expr = Q $ W.tell mempty { sdHavingClause = I.Where (coerce expr) }
|
||||
|
||||
test :: (PersistEntity ent, PersistField a, PersistField b, PersistField c)
|
||||
=> SqlExpr (Maybe (Entity ent))
|
||||
-> EntityField ent a
|
||||
-> SqlExpr b
|
||||
-> SqlExpr c
|
||||
-> SqlQuery (SqlExpr (Maybe a), SqlExpr b, SqlExpr (Maybe Int), SqlExpr Int)
|
||||
test ent field y other = do
|
||||
groupBy (ent, y) $ \(ent', y') ->
|
||||
pure (ent' ?. field, y', sum_ other, countRows_)
|
||||
|
||||
|
||||
-- Tuple magic, only SqlExprs are on the leaves.
|
||||
-- The Coercible instance from the SqlExpr a -> SqlExpr b allows 0 cost casting
|
||||
class Coercible a r => Aggregateable a r | a -> r, r -> a where
|
||||
toAggregate :: a -> r
|
||||
toAggregate = coerce
|
||||
|
||||
fromAggregate :: r -> a
|
||||
fromAggregate = coerce
|
||||
|
||||
data GroupedValue
|
||||
instance Aggregateable () () where
|
||||
instance Aggregateable (SqlExpr a) (SqlAggregate GroupedValue a) where
|
||||
instance (Aggregateable a ra, Aggregateable b rb) => Aggregateable (a,b) (ra, rb) where
|
||||
instance
|
||||
( Aggregateable a ra
|
||||
, Aggregateable b rb
|
||||
, Aggregateable c rc
|
||||
) => Aggregateable (a,b,c) (ra,rb,rc) where
|
||||
instance
|
||||
( Aggregateable a ra
|
||||
, Aggregateable b rb
|
||||
, Aggregateable c rc
|
||||
, Aggregateable d rd
|
||||
) => Aggregateable (a,b,c,d) (ra,rb,rc,rd) where
|
||||
instance
|
||||
( Aggregateable a ra
|
||||
, Aggregateable b rb
|
||||
, Aggregateable c rc
|
||||
, Aggregateable d rd
|
||||
, Aggregateable e re
|
||||
) => Aggregateable (a,b,c,d,e) (ra,rb,rc,rd,re) where
|
||||
instance
|
||||
( Aggregateable a ra
|
||||
, Aggregateable b rb
|
||||
, Aggregateable c rc
|
||||
, Aggregateable d rd
|
||||
, Aggregateable e re
|
||||
, Aggregateable f rf
|
||||
) => Aggregateable (a,b,c,d,e,f) (ra,rb,rc,rd,re,rf) where
|
||||
instance
|
||||
( Aggregateable a ra
|
||||
, Aggregateable b rb
|
||||
, Aggregateable c rc
|
||||
, Aggregateable d rd
|
||||
, Aggregateable e re
|
||||
, Aggregateable f rf
|
||||
, Aggregateable g rg
|
||||
) => Aggregateable (a,b,c,d,e,f,g) (ra,rb,rc,rd,re,rf,rg) where
|
||||
instance
|
||||
( Aggregateable a ra
|
||||
, Aggregateable b rb
|
||||
, Aggregateable c rc
|
||||
, Aggregateable d rd
|
||||
, Aggregateable e re
|
||||
, Aggregateable f rf
|
||||
, Aggregateable g rg
|
||||
, Aggregateable h rh
|
||||
) => Aggregateable (a,b,c,d,e,f,g,h) (ra,rb,rc,rd,re,rf,rg,rh) where
|
||||
|
||||
groupBy :: ( ToSomeValues a
|
||||
, Aggregateable a a'
|
||||
, Aggregateable b b'
|
||||
) => a -> (a' -> SqlQuery b') -> SqlQuery b
|
||||
groupBy a f = do
|
||||
Q $ W.tell $ mempty{sdGroupByClause = GroupBy $ toSomeValues a }
|
||||
fmap fromAggregate $ f $ toAggregate a
|
||||
|
||||
-- Aggregation Functions
|
||||
countRows_ :: forall n s. (PersistField n, Integral n) => SqlAggregate s n
|
||||
countRows_ = SqlAggregate $ ERaw noMeta $ \_ _ -> ("COUNT(*)", [])
|
||||
|
||||
sum_ :: forall n a w. (PersistField a, PersistField n, Integral n) => SqlExpr a -> SqlAggregate w (Maybe n)
|
||||
sum_ = coerce . unsafeSqlFunction "SUM"
|
||||
|
||||
@ -18,9 +18,8 @@
|
||||
module Database.Esqueleto.Experimental.From
|
||||
where
|
||||
|
||||
import Control.Arrow (first)
|
||||
import Control.Monad (ap)
|
||||
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
|
||||
@ -28,6 +27,8 @@ 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.
|
||||
@ -53,7 +54,7 @@ type RawFn = NeedParens -> IdentInfo -> (TLB.Builder, [PersistValue])
|
||||
-- the FromRaw FromClause constructor directly when converting
|
||||
-- from a @From@ to a @SqlQuery@ using @from@
|
||||
--
|
||||
-- /Since: 3.5.0.0/
|
||||
-- @since 3.5.0.0
|
||||
newtype From a = From
|
||||
{ unFrom :: SqlQuery (a, RawFn)}
|
||||
|
||||
@ -63,13 +64,13 @@ newtype From a = From
|
||||
-- as well as supporting backwards compatibility for the
|
||||
-- data constructor join tree used prior to /3.5.0.0/
|
||||
--
|
||||
-- /Since: 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" #-}
|
||||
{-# DEPRECATED Table "@since 3.5.0.0 - use 'table' instead" #-}
|
||||
data Table a = Table
|
||||
|
||||
instance PersistEntity ent => ToFrom (Table ent) (SqlExpr (Entity ent)) where
|
||||
@ -81,18 +82,18 @@ instance PersistEntity ent => ToFrom (Table ent) (SqlExpr (Entity ent)) where
|
||||
-- select $ from $ table \@People
|
||||
-- @
|
||||
--
|
||||
-- /Since: 3.5.0.0/
|
||||
-- @since 3.5.0.0
|
||||
table :: forall ent. PersistEntity ent => From (SqlExpr (Entity ent))
|
||||
table = From $ do
|
||||
let ed = entityDef (Proxy @ent)
|
||||
ident <- newIdentFor (entityDB ed)
|
||||
ident <- newIdentFor (coerce $ getEntityDBName ed)
|
||||
let entity = unsafeSqlEntity ident
|
||||
pure $ ( entity, const $ base ident ed )
|
||||
where
|
||||
base ident@(I identText) def info =
|
||||
let db@(DBName dbText) = entityDB def
|
||||
in ( fromDBName info db <>
|
||||
if dbText == identText
|
||||
let db = coerce $ getEntityDBName def
|
||||
in ( (fromDBName info (coerce db)) <>
|
||||
if db == identText
|
||||
then mempty
|
||||
else " AS " <> useIdent info ident
|
||||
, mempty
|
||||
@ -120,7 +121,7 @@ instance (SqlSelect a r, ToAlias a, ToAliasReference a) => ToFrom (SqlQuery a) a
|
||||
-- ...
|
||||
-- @
|
||||
--
|
||||
-- /Since: 3.5.0.0/
|
||||
-- @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
|
||||
|
||||
@ -12,7 +12,6 @@ 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)
|
||||
import Database.Esqueleto.Internal.PersistentImport (DBName(..))
|
||||
|
||||
-- | @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
|
||||
|
||||
@ -10,12 +10,12 @@
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
|
||||
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
|
||||
module Database.Esqueleto.Experimental.From.Join
|
||||
where
|
||||
|
||||
import Control.Arrow (first)
|
||||
import Data.Bifunctor (first)
|
||||
import Data.Kind (Constraint)
|
||||
import Data.Proxy
|
||||
import qualified Data.Text.Lazy.Builder as TLB
|
||||
@ -39,46 +39,59 @@ import GHC.TypeLits
|
||||
-- See the examples at the beginning of this module to see how this
|
||||
-- operator is used in 'JOIN' operations.
|
||||
data (:&) a b = a :& b
|
||||
deriving (Show, Eq)
|
||||
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)
|
||||
|
||||
fromInductiveTupleP :: Proxy (a :& b) -> Proxy (a, b)
|
||||
fromInductiveTupleP = const Proxy
|
||||
toInductiveTuple :: (a, b) -> (a :& b)
|
||||
toInductiveTuple (a, b) = a :& b
|
||||
|
||||
instance (SqlSelect a a', SqlSelect b b') => SqlSelect (a :& b) (a' :& b') where
|
||||
sqlSelectCols esc (a :& b) = sqlSelectCols esc (a, b)
|
||||
sqlSelectColCount = sqlSelectColCount . fromInductiveTupleP
|
||||
sqlSelectProcessRow p = fmap toInductiveTuple . sqlSelectProcessRow (fromInductiveTupleP p)
|
||||
|
||||
class ValidOnClause a where
|
||||
-- | 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 :: a -> (b -> SqlExpr Bool) -> (a, b -> SqlExpr Bool)
|
||||
on = (,)
|
||||
infix 9 `on`
|
||||
|
||||
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 Bool) -> RawFn
|
||||
fromJoin :: TLB.Builder -> RawFn -> RawFn -> Maybe (SqlExpr (Value Bool)) -> RawFn
|
||||
fromJoin joinKind lhs rhs monClause =
|
||||
\paren info ->
|
||||
first (parensM paren) $
|
||||
@ -91,14 +104,14 @@ fromJoin joinKind lhs rhs monClause =
|
||||
makeOnClause info (ERaw _ f) = first (" ON " <>) (f Never info)
|
||||
|
||||
type family HasOnClause actual expected :: Constraint where
|
||||
HasOnClause (a, b -> SqlExpr Bool) c = () -- Let the compiler handle the type mismatch
|
||||
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 Bool)
|
||||
':$$: 'Text "`on` " ':<>: 'ShowType (expected -> SqlExpr (Value Bool))
|
||||
':$$: 'Text ""
|
||||
)
|
||||
|
||||
@ -115,11 +128,11 @@ type family HasOnClause actual expected :: Constraint where
|
||||
-- p ^. PersonId ==. bp ^. BlogPostAuthorId)
|
||||
-- @
|
||||
--
|
||||
-- /Since: 3.5.0.0/
|
||||
-- @since 3.5.0.0
|
||||
innerJoin :: ( ToFrom a a'
|
||||
, ToFrom b b'
|
||||
, HasOnClause rhs (a' :& b')
|
||||
, rhs ~ (b, (a' :& b') -> SqlExpr Bool)
|
||||
, rhs ~ (b, (a' :& b') -> SqlExpr (Value Bool))
|
||||
) => a -> rhs -> From (a' :& b')
|
||||
innerJoin lhs (rhs, on') = From $ do
|
||||
(leftVal, leftFrom) <- unFrom (toFrom lhs)
|
||||
@ -137,13 +150,13 @@ innerJoin lhs (rhs, on') = From $ do
|
||||
--
|
||||
-- See example 6
|
||||
--
|
||||
-- /Since: 3.5.0.0/
|
||||
-- @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 Bool)
|
||||
, rhs ~ (a' -> SqlQuery b, (a' :& b) -> SqlExpr (Value Bool))
|
||||
)
|
||||
=> a -> rhs -> From (a' :& b)
|
||||
innerJoinLateral lhs (rhsFn, on') = From $ do
|
||||
@ -162,7 +175,7 @@ innerJoinLateral lhs (rhsFn, on') = From $ do
|
||||
-- \`crossJoin\` table \@BlogPost
|
||||
-- @
|
||||
--
|
||||
-- /Since: 3.5.0.0/
|
||||
-- @since 3.5.0.0
|
||||
crossJoin :: ( ToFrom a a'
|
||||
, ToFrom b b'
|
||||
) => a -> b -> From (a' :& b')
|
||||
@ -181,7 +194,7 @@ crossJoin lhs rhs = From $ do
|
||||
--
|
||||
-- See example 6
|
||||
--
|
||||
-- /Since: 3.5.0.0/
|
||||
-- @since 3.5.0.0
|
||||
crossJoinLateral :: ( ToFrom a a'
|
||||
, SqlSelect b r
|
||||
, ToAlias b
|
||||
@ -210,12 +223,12 @@ crossJoinLateral lhs rhsFn = From $ do
|
||||
-- p ^. PersonId ==. bp ?. BlogPostAuthorId)
|
||||
-- @
|
||||
--
|
||||
-- /Since: 3.5.0.0/
|
||||
-- @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 Bool)
|
||||
, 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)
|
||||
@ -234,14 +247,14 @@ leftJoin lhs (rhs, on') = From $ do
|
||||
--
|
||||
-- See example 6 for how to use LATERAL
|
||||
--
|
||||
-- /Since: 3.5.0.0/
|
||||
-- @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 Bool)
|
||||
, rhs ~ (a' -> SqlQuery b, (a' :& ToMaybeT b) -> SqlExpr (Value Bool))
|
||||
)
|
||||
=> a -> rhs -> From (a' :& ToMaybeT b)
|
||||
leftJoinLateral lhs (rhsFn, on') = From $ do
|
||||
@ -266,12 +279,12 @@ leftJoinLateral lhs (rhsFn, on') = From $ do
|
||||
-- p ?. PersonId ==. bp ^. BlogPostAuthorId)
|
||||
-- @
|
||||
--
|
||||
-- /Since: 3.5.0.0/
|
||||
-- @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 Bool)
|
||||
, 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)
|
||||
@ -294,13 +307,13 @@ rightJoin lhs (rhs, on') = From $ do
|
||||
-- p ?. PersonId ==. bp ?. BlogPostAuthorId)
|
||||
-- @
|
||||
--
|
||||
-- /Since: 3.5.0.0/
|
||||
-- @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 Bool)
|
||||
, 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)
|
||||
@ -334,7 +347,7 @@ class DoInnerJoin lateral lhs rhs res | lateral rhs lhs -> res where
|
||||
instance ( ToFrom a a'
|
||||
, ToFrom b b'
|
||||
, HasOnClause rhs (a' :& b')
|
||||
, rhs ~ (b, (a' :& b') -> SqlExpr Bool)
|
||||
, rhs ~ (b, (a' :& b') -> SqlExpr (Value Bool))
|
||||
) => DoInnerJoin NotLateral a rhs (a' :& b') where
|
||||
doInnerJoin _ = innerJoin
|
||||
|
||||
@ -343,7 +356,7 @@ instance ( ToFrom a a'
|
||||
, ToAlias b
|
||||
, ToAliasReference b
|
||||
, d ~ (a' :& b)
|
||||
) => DoInnerJoin Lateral a (a' -> SqlQuery b, d -> SqlExpr Bool) d where
|
||||
) => DoInnerJoin Lateral a (a' -> SqlQuery b, d -> SqlExpr (Value Bool)) d where
|
||||
doInnerJoin _ = innerJoinLateral
|
||||
|
||||
instance ( DoInnerJoin lateral lhs rhs r, lateral ~ IsLateral rhs )
|
||||
@ -358,7 +371,7 @@ instance ( ToFrom a a'
|
||||
, ToMaybe b'
|
||||
, ToMaybeT b' ~ mb
|
||||
, HasOnClause rhs (a' :& mb)
|
||||
, rhs ~ (b, (a' :& mb) -> SqlExpr Bool)
|
||||
, rhs ~ (b, (a' :& mb) -> SqlExpr (Value Bool))
|
||||
) => DoLeftJoin NotLateral a rhs (a' :& mb) where
|
||||
doLeftJoin _ = leftJoin
|
||||
|
||||
@ -368,7 +381,7 @@ instance ( ToFrom a a'
|
||||
, SqlSelect b r
|
||||
, ToAlias b
|
||||
, ToAliasReference b
|
||||
) => DoLeftJoin Lateral a (a' -> SqlQuery b, d -> SqlExpr Bool) d where
|
||||
) => DoLeftJoin Lateral a (a' -> SqlQuery b, d -> SqlExpr (Value Bool)) d where
|
||||
doLeftJoin _ = leftJoinLateral
|
||||
|
||||
instance ( DoLeftJoin lateral lhs rhs r, lateral ~ IsLateral rhs )
|
||||
@ -394,7 +407,7 @@ instance ( ToFrom a a'
|
||||
, ToMaybeT a' ~ ma
|
||||
, HasOnClause rhs (ma :& b')
|
||||
, ErrorOnLateral b
|
||||
, rhs ~ (b, (ma :& b') -> SqlExpr Bool)
|
||||
, rhs ~ (b, (ma :& b') -> SqlExpr (Value Bool))
|
||||
) => ToFrom (RightOuterJoin a rhs) (ma :& b') where
|
||||
toFrom (RightOuterJoin a b) = rightJoin a b
|
||||
|
||||
@ -406,7 +419,7 @@ instance ( ToFrom a a'
|
||||
, ToMaybeT b' ~ mb
|
||||
, HasOnClause rhs (ma :& mb)
|
||||
, ErrorOnLateral b
|
||||
, rhs ~ (b, (ma :& mb) -> SqlExpr Bool)
|
||||
, rhs ~ (b, (ma :& mb) -> SqlExpr (Value Bool))
|
||||
) => ToFrom (FullOuterJoin a rhs) (ma :& mb) where
|
||||
toFrom (FullOuterJoin a b) = fullOuterJoin a b
|
||||
|
||||
|
||||
@ -21,15 +21,14 @@ 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
|
||||
(DBName(..), Entity, PersistEntity, PersistValue)
|
||||
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/
|
||||
-- @since 3.5.0.0
|
||||
newtype SqlSetOperation a = SqlSetOperation
|
||||
{ unSqlSetOperation :: NeedParens -> SqlQuery (a, IdentInfo -> (TLB.Builder, [PersistValue]))}
|
||||
|
||||
@ -42,7 +41,7 @@ instance ToAliasReference a => ToFrom (SqlSetOperation a) a where
|
||||
|
||||
-- | Type class to support direct use of @SqlQuery@ in a set operation tree
|
||||
--
|
||||
-- /Since: 3.5.0.0/
|
||||
-- @since 3.5.0.0
|
||||
class ToSqlSetOperation a r | a -> r where
|
||||
toSqlSetOperation :: a -> SqlSetOperation r
|
||||
instance ToSqlSetOperation (SqlSetOperation a) a where
|
||||
@ -67,7 +66,7 @@ instance (SqlSelect a r, ToAlias a, ToAliasReference a) => ToSqlSetOperation (Sq
|
||||
pure (aliasedValue, \info -> first (parensM p') $ toRawSql SELECT info aliasedQuery)
|
||||
|
||||
-- | Helper function for defining set operations
|
||||
-- /Since: 3.5.0.0/
|
||||
-- @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
|
||||
@ -83,7 +82,7 @@ instance ToSqlSetOperation a a' => ToSqlSetOperation (Union a a) a' where
|
||||
-- | Overloaded @union_@ function to support use in both 'SqlSetOperation'
|
||||
-- and 'withRecursive'
|
||||
--
|
||||
-- /Since: 3.5.0.0/
|
||||
-- @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
|
||||
@ -95,7 +94,7 @@ instance (ToSqlSetOperation a c, ToSqlSetOperation b c, res ~ SqlSetOperation c)
|
||||
-- | Overloaded @unionAll_@ function to support use in both 'SqlSetOperation'
|
||||
-- and 'withRecursive'
|
||||
--
|
||||
-- /Since: 3.5.0.0/
|
||||
-- @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
|
||||
|
||||
@ -15,23 +15,27 @@ type ToAliasT a = a
|
||||
class ToAlias a where
|
||||
toAlias :: a -> SqlQuery a
|
||||
|
||||
instance {-# OVERLAPPABLE #-} ToAlias (SqlExpr a) where
|
||||
instance ToAlias (SqlExpr (Value a)) where
|
||||
toAlias e@(ERaw m f)
|
||||
| Just _ <- sqlExprMetaAlias m, not (sqlExprMetaIsReference m) = pure e
|
||||
| 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 (ERaw m f) = do
|
||||
ident <- newIdentFor (DBName "v")
|
||||
pure $ ERaw m{sqlExprMetaIsReference = False, sqlExprMetaAlias = Just ident} f
|
||||
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 (ERaw m f) = do
|
||||
ident <- newIdentFor (DBName "v")
|
||||
pure $ ERaw m{sqlExprMetaIsReference = False, sqlExprMetaAlias = Just ident} f
|
||||
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
|
||||
|
||||
@ -16,7 +16,7 @@ type ToAliasReferenceT a = a
|
||||
class ToAliasReference a where
|
||||
toAliasReference :: Ident -> a -> SqlQuery a
|
||||
|
||||
instance {-# OVERLAPPABLE #-} ToAliasReference (SqlExpr a) where
|
||||
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, [])
|
||||
@ -24,7 +24,7 @@ instance {-# OVERLAPPABLE #-} ToAliasReference (SqlExpr a) where
|
||||
|
||||
instance ToAliasReference (SqlExpr (Entity a)) where
|
||||
toAliasReference aliasSource (ERaw m _)
|
||||
| Just _ <- sqlExprMetaAlias m, False <- sqlExprMetaIsReference m =
|
||||
| Just _ <- sqlExprMetaAlias m =
|
||||
pure $ ERaw m{sqlExprMetaIsReference = True} $ \_ info ->
|
||||
(useIdent info aliasSource, [])
|
||||
toAliasReference _ e = pure e
|
||||
|
||||
@ -15,10 +15,18 @@ 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 (SqlExpr a) where
|
||||
type ToMaybeT (SqlExpr a) = SqlExpr (Maybe (Nullable a))
|
||||
toMaybe = veryUnsafeCoerceSqlExpr
|
||||
|
||||
instance (ToMaybe a, ToMaybe b) => ToMaybe (a,b) where
|
||||
type ToMaybeT (a, b) = (ToMaybeT a, ToMaybeT b)
|
||||
|
||||
@ -1,270 +0,0 @@
|
||||
{-# LANGUAGE DerivingVia #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE FunctionalDependencies #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
|
||||
module Database.Esqueleto.Experimental.WindowFunctions
|
||||
where
|
||||
|
||||
import Control.Arrow (first)
|
||||
import Data.Coerce (coerce)
|
||||
import Data.Int (Int64)
|
||||
import Data.Semigroup (First(..))
|
||||
import qualified Data.Text.Lazy.Builder as TLB
|
||||
import Database.Esqueleto.Experimental.Aggregates
|
||||
import Database.Esqueleto.Internal.Internal
|
||||
( IdentInfo
|
||||
, NeedParens(..)
|
||||
, OrderBy
|
||||
, SideData(..)
|
||||
, SomeValue(..)
|
||||
, SqlExpr(..)
|
||||
, SqlQuery(..)
|
||||
, SqlSelect(..)
|
||||
, ToSomeValues(..)
|
||||
, Value(..)
|
||||
, asc
|
||||
, noMeta
|
||||
, parens
|
||||
, parensM
|
||||
, select
|
||||
, uncommas'
|
||||
, unsafeSqlFunction
|
||||
, val
|
||||
, (?.)
|
||||
, (^.)
|
||||
)
|
||||
import Database.Esqueleto.Internal.PersistentImport
|
||||
( Entity
|
||||
, EntityField
|
||||
, PersistEntity
|
||||
, PersistField
|
||||
, PersistValue(..)
|
||||
, SqlReadT
|
||||
, fromPersistValue
|
||||
)
|
||||
|
||||
--( "LAG(?) OVER (PARTITION BY ?, ? ORDER BY ? ASC ROWS BETWEEN ? PRECEEDING AND UNBOUNDED FOLLOWING)"
|
||||
--, [PersistInt64 10,PersistInt64 10,PersistBool True,PersistInt64 10,PersistInt64 1]
|
||||
--)
|
||||
example =
|
||||
lag_ (val @Int64 10) Nothing Nothing `over_`
|
||||
( partitionBy_ (val @Int64 10, val True)
|
||||
<> frame_ (rows $ between (preceeding 1) unboundedFollowing)
|
||||
<> orderBy_ [asc (val @Int64 10)]
|
||||
)
|
||||
|
||||
example2 = countRows_ @Int64 `over_` ()
|
||||
|
||||
lag :: SqlExpr (Value a) -> WindowExpr a
|
||||
lag v = lag_ v Nothing Nothing
|
||||
|
||||
lag_ :: SqlExpr a -> Maybe (SqlExpr Int64) -> Maybe (SqlExpr a) -> WindowExpr a
|
||||
lag_ v mOffset mDefaultVal =
|
||||
coerce $
|
||||
case (mOffset, mDefaultVal) of
|
||||
(Just offset, Just defaultVal) ->
|
||||
unsafeSqlFunction "LAG" (v, offset, defaultVal)
|
||||
(Just offset, Nothing) ->
|
||||
unsafeSqlFunction "LAG" (v, offset)
|
||||
(Nothing, _) ->
|
||||
unsafeSqlFunction "LAG" v
|
||||
|
||||
-- Phantom helper type
|
||||
data PartitionBy
|
||||
data Window = Window
|
||||
{ windowPartitionBy :: Maybe (First (SqlExpr PartitionBy))
|
||||
, windowOrderBy :: Maybe [SqlExpr OrderBy]
|
||||
, windowFrame :: Maybe (First Frame)
|
||||
}
|
||||
|
||||
partitionBy_ :: ToSomeValues a => a -> Window
|
||||
partitionBy_ expr = mempty{windowPartitionBy = Just $ First $ ERaw noMeta $ \_ info ->
|
||||
let (b, v) = uncommas' $ fmap (\(SomeValue (ERaw _ f)) -> f Never info) $ toSomeValues expr
|
||||
in ("PARTITION BY " <> b, v)
|
||||
}
|
||||
|
||||
orderBy_ :: [SqlExpr OrderBy] -> Window
|
||||
orderBy_ [] = mempty
|
||||
orderBy_ exprs = mempty{windowOrderBy=Just exprs}
|
||||
|
||||
class RenderWindow a where
|
||||
renderWindow :: IdentInfo -> a -> (TLB.Builder, [PersistValue])
|
||||
instance RenderWindow () where
|
||||
renderWindow _ = mempty
|
||||
instance RenderWindow Window where
|
||||
renderWindow info window =
|
||||
let (partition, partitionVal) = maybe mempty ((\(ERaw _ f) -> f Never info) . getFirst) (windowPartitionBy window)
|
||||
(order, orderVal) = maybe mempty (first ((<>) " ORDER BY ") . uncommas' . fmap (\(ERaw _ f) -> f Never info)) (windowOrderBy window)
|
||||
(frame, frameVal) = maybe mempty (renderWindow info . getFirst) (windowFrame window)
|
||||
in (partition <> order <> frame, partitionVal <> orderVal <> frameVal)
|
||||
|
||||
instance Semigroup Window where
|
||||
(Window a b c) <> (Window a' b' c') = Window (a <> a') (b <> b') (c <> c')
|
||||
|
||||
instance Monoid Window where
|
||||
mempty = Window mempty mempty mempty
|
||||
mappend = (<>)
|
||||
|
||||
data Frame = Frame (Maybe FrameKind) FrameBody (Maybe FrameExclusion)
|
||||
|
||||
frame_ :: ToFrame frame => frame -> Window
|
||||
frame_ f = mempty{windowFrame = Just $ First $ toFrame f}
|
||||
|
||||
instance RenderWindow Frame where
|
||||
renderWindow info (Frame mKind frameBody mExclusion) =
|
||||
let (kind, kindVals) = maybe mempty (renderWindow info) mKind
|
||||
(exclusion, exclusionVals) = maybe mempty (renderWindow info) mExclusion
|
||||
(body, bodyVals) = renderWindow info frameBody
|
||||
in (" " <> kind <> body <> exclusion, kindVals <> bodyVals <> exclusionVals)
|
||||
|
||||
class ToFrame a where
|
||||
toFrame :: a -> Frame
|
||||
instance ToFrame Frame where
|
||||
toFrame = id
|
||||
|
||||
newtype FrameKind = FrameKind { unFrameKind :: (TLB.Builder, [PersistValue]) }
|
||||
|
||||
instance RenderWindow FrameKind where
|
||||
renderWindow _ = unFrameKind
|
||||
|
||||
frameKind :: ToFrame frame => TLB.Builder -> frame -> Frame
|
||||
frameKind tlb frame =
|
||||
let Frame _ b e = toFrame frame
|
||||
in Frame (Just (FrameKind (tlb <> " ", []))) b e
|
||||
|
||||
range :: ToFrame frame => frame -> Frame
|
||||
range = frameKind "RANGE"
|
||||
|
||||
rows :: ToFrame frame => frame -> Frame
|
||||
rows = frameKind "ROWS"
|
||||
|
||||
groups :: ToFrame frame => frame -> Frame
|
||||
groups = frameKind "GROUPS"
|
||||
|
||||
newtype FrameExclusion = FrameExclusion { unFrameExclusion :: (TLB.Builder, [PersistValue]) }
|
||||
|
||||
instance RenderWindow FrameExclusion where
|
||||
renderWindow _ = unFrameExclusion
|
||||
|
||||
frameExclusion :: ToFrame frame => TLB.Builder -> frame -> Frame
|
||||
frameExclusion tlb frame =
|
||||
let Frame k b _ = toFrame frame
|
||||
in Frame k b (Just $ FrameExclusion (" EXCLUDE " <> tlb, []))
|
||||
|
||||
excludeCurrentRow :: ToFrame frame => frame -> Frame
|
||||
excludeCurrentRow = frameExclusion "CURRENT ROW"
|
||||
|
||||
excludeGroup :: ToFrame frame => frame -> Frame
|
||||
excludeGroup = frameExclusion "GROUP"
|
||||
|
||||
excludeTies :: ToFrame frame => frame -> Frame
|
||||
excludeTies = frameExclusion "TIES"
|
||||
|
||||
excludeNoOthers :: ToFrame frame => frame -> Frame
|
||||
excludeNoOthers = frameExclusion "NO OTHERS"
|
||||
|
||||
-- In order to prevent runtime errors we do some magic rewriting of queries that wouldn't be valid SQL.
|
||||
-- In the case of an implicit frame end `following 10` would become BETWEEN 10 FOLLOWING AND CURRENT ROW
|
||||
-- This is illegal so `following 10` instead becomes `BETWEEN CURRENT_ROW AND 10 FOLLOWING`
|
||||
-- Additionally `BETWEEN` requires that the frame start be before the frame end.
|
||||
-- To prevent this error the frame will be flipped automatically.
|
||||
-- i.e. `between (following 10) (preceeding 10)` becomes `BETWEEEN 10 PRECEEDING AND 10 FOLLOWING`
|
||||
-- therefore `between (following 10) (preceeding 10) === between (preceeding 10) (following 10)
|
||||
data FrameBody
|
||||
= FrameStart FrameRange
|
||||
| FrameBetween FrameRange FrameRange
|
||||
|
||||
instance ToFrame FrameBody where
|
||||
toFrame b = Frame Nothing b Nothing
|
||||
|
||||
instance RenderWindow FrameBody where
|
||||
renderWindow info (FrameStart (FrameRangeFollowing b)) = renderWindow info (FrameBetween FrameRangeCurrentRow (FrameRangeFollowing b))
|
||||
renderWindow info (FrameStart f) = renderWindow info f
|
||||
renderWindow info (FrameBetween startRange endRange)
|
||||
| startRange > endRange = renderWindow info (FrameBetween endRange startRange)
|
||||
renderWindow info (FrameBetween r r') =
|
||||
let (b, v) = renderWindow info r
|
||||
(b', v') = renderWindow info r'
|
||||
in ("BETWEEN " <> b <> " AND " <> b', v <> v')
|
||||
|
||||
instance ToFrame FrameRange where
|
||||
toFrame r = Frame Nothing (FrameStart r) Nothing
|
||||
instance RenderWindow FrameRange where
|
||||
renderWindow _ (FrameRangeCurrentRow) = ("CURRENT ROW", [])
|
||||
renderWindow _ (FrameRangePreceeding bounds) = renderBounds bounds <> (" PRECEEDING", [])
|
||||
renderWindow _ (FrameRangeFollowing bounds) = renderBounds bounds <> (" FOLLOWING", [])
|
||||
|
||||
renderBounds :: FrameRangeBound -> (TLB.Builder, [PersistValue])
|
||||
renderBounds (FrameRangeUnbounded) = ("UNBOUNDED", [])
|
||||
renderBounds (FrameRangeBounded i) = ("?", [PersistInt64 i])
|
||||
|
||||
data FrameRange
|
||||
= FrameRangePreceeding FrameRangeBound
|
||||
| FrameRangeCurrentRow
|
||||
| FrameRangeFollowing FrameRangeBound
|
||||
deriving Eq
|
||||
|
||||
instance Ord FrameRange where
|
||||
FrameRangePreceeding b1 <= FrameRangePreceeding b2 = b1 <= b2
|
||||
FrameRangePreceeding _ <= FrameRangeCurrentRow = True
|
||||
FrameRangePreceeding _ <= FrameRangeFollowing _ = True
|
||||
FrameRangeCurrentRow <= FrameRangePreceeding _ = False
|
||||
FrameRangeCurrentRow <= FrameRangeCurrentRow = True
|
||||
FrameRangeCurrentRow <= FrameRangeFollowing _ = True
|
||||
FrameRangeFollowing _ <= FrameRangePreceeding _ = False
|
||||
FrameRangeFollowing _ <= FrameRangeCurrentRow = False
|
||||
FrameRangeFollowing b1 <= FrameRangeFollowing b2 = b1 <= b2
|
||||
|
||||
data FrameRangeBound
|
||||
= FrameRangeUnbounded
|
||||
| FrameRangeBounded Int64
|
||||
deriving Eq
|
||||
|
||||
instance Ord FrameRangeBound where
|
||||
FrameRangeUnbounded <= FrameRangeBounded _ = False
|
||||
FrameRangeUnbounded <= FrameRangeUnbounded = True
|
||||
FrameRangeBounded _ <= FrameRangeUnbounded = True
|
||||
FrameRangeBounded a <= FrameRangeBounded b = a <= b
|
||||
|
||||
between :: FrameRange -> FrameRange -> FrameBody
|
||||
between = FrameBetween
|
||||
|
||||
unboundedPreceeding :: FrameRange
|
||||
unboundedPreceeding = FrameRangePreceeding FrameRangeUnbounded
|
||||
|
||||
preceeding :: Int64 -> FrameRange
|
||||
preceeding offset = FrameRangePreceeding (FrameRangeBounded offset)
|
||||
|
||||
following :: Int64 -> FrameRange
|
||||
following offset = FrameRangeFollowing (FrameRangeBounded offset)
|
||||
|
||||
unboundedFollowing :: FrameRange
|
||||
unboundedFollowing = FrameRangeFollowing FrameRangeUnbounded
|
||||
|
||||
currentRow :: FrameRange
|
||||
currentRow = FrameRangeCurrentRow
|
||||
|
||||
data WindowAggregate
|
||||
class Over expr where
|
||||
over_ :: RenderWindow window => expr a -> window -> SqlAggregate WindowAggregate a
|
||||
|
||||
newtype WindowExpr a = WindowExpr { unsafeWindowExpr :: SqlExpr a }
|
||||
instance Over WindowExpr where
|
||||
(WindowExpr (ERaw _ f)) `over_` window = SqlAggregate $ ERaw noMeta $ \p info ->
|
||||
let (b, v) = f Never info
|
||||
(w, vw) = renderWindow info window
|
||||
in (parensM p $ b <> " OVER " <> parens w , v <> vw)
|
||||
|
||||
-- Only universally quantified SqlAggregate's can be used
|
||||
-- TODO Add nicer type error
|
||||
data NoWindow
|
||||
deriving via WindowExpr instance (s ~ NoWindow) => Over (SqlAggregate s)
|
||||
@ -16,6 +16,7 @@ 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:
|
||||
@ -43,7 +44,7 @@ parseOnExpr sqlBackend text = do
|
||||
-- with postgresql, mysql, and sqlite backends.
|
||||
mkEscapeChar :: SqlBackend -> Either String Char
|
||||
mkEscapeChar sqlBackend =
|
||||
case Text.uncons (connEscapeName sqlBackend (DBName "")) of
|
||||
case Text.uncons (getEscapedRawName "" sqlBackend) of
|
||||
Nothing ->
|
||||
Left "Failed to get an escape character from the SQL backend."
|
||||
Just (c, _) ->
|
||||
@ -63,9 +64,9 @@ skipToEscape escapeChar = void (takeWhile (/= escapeChar))
|
||||
|
||||
parseEscapedIdentifier :: ExprParser [Char]
|
||||
parseEscapedIdentifier escapeChar = do
|
||||
char escapeChar
|
||||
_ <- char escapeChar
|
||||
str <- parseEscapedChars escapeChar
|
||||
char escapeChar
|
||||
_ <- char escapeChar
|
||||
pure str
|
||||
|
||||
parseTableAccess :: ExprParser TableAccess
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,143 +0,0 @@
|
||||
{-# LANGUAGE DeriveDataTypeable #-}
|
||||
{-# LANGUAGE EmptyDataDecls #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE FunctionalDependencies #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
|
||||
-- | This is an internal module, anything exported by this module
|
||||
-- may change without a major version bump. Please use only
|
||||
-- "Database.Esqueleto" if possible.
|
||||
--
|
||||
-- This module is deprecated as of 3.4.0.1, and will be removed in 3.5.0.0.
|
||||
module Database.Esqueleto.Internal.Language
|
||||
{-# DEPRECATED "Use Database.Esqueleto.Internal.Internal instead. This module will be removed in 3.5.0.0 " #-}
|
||||
( -- * The pretty face
|
||||
from
|
||||
, Value
|
||||
, pattern Value
|
||||
, unValue
|
||||
, 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_
|
||||
, 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
|
||||
, subSelectList
|
||||
, subSelectForeign
|
||||
, subSelectUnsafe
|
||||
) where
|
||||
|
||||
import Database.Esqueleto.Internal.Internal
|
||||
import Database.Esqueleto.Internal.PersistentImport
|
||||
@ -3,142 +3,141 @@
|
||||
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(..)
|
||||
( 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
|
||||
|
||||
@ -1,78 +0,0 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE ConstraintKinds #-}
|
||||
{-# LANGUAGE DeriveDataTypeable #-}
|
||||
{-# LANGUAGE EmptyDataDecls #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE FunctionalDependencies #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE InstanceSigs #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE Rank2Types #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
|
||||
|
||||
-- | This is an internal module, anything exported by this module
|
||||
-- may change without a major version bump. Please use only
|
||||
-- "Database.Esqueleto" if possible.
|
||||
--
|
||||
-- This module is deprecated as of 3.4.0.1, and will be removed in 3.5.0.0.
|
||||
module Database.Esqueleto.Internal.Sql
|
||||
{-# DEPRECATED "Use Database.Esqueleto.Internal.Internal instead. This module will be removed in 3.5.0.0 " #-}
|
||||
( -- * The pretty face
|
||||
SqlQuery
|
||||
, SqlExpr(..)
|
||||
, SqlEntity
|
||||
, select
|
||||
, selectSource
|
||||
, delete
|
||||
, deleteCount
|
||||
, update
|
||||
, updateCount
|
||||
, insertSelect
|
||||
, insertSelectCount
|
||||
-- * The guts
|
||||
, unsafeSqlCase
|
||||
, unsafeSqlBinOp
|
||||
, unsafeSqlValue
|
||||
, unsafeSqlEntity
|
||||
, unsafeSqlCastAs
|
||||
, unsafeSqlFunction
|
||||
, unsafeSqlExtractSubField
|
||||
, UnsafeSqlFunctionArgument
|
||||
, OrderByClause
|
||||
, rawSelectSource
|
||||
, runSource
|
||||
, rawEsqueleto
|
||||
, toRawSql
|
||||
, Mode(..)
|
||||
, NeedParens(..)
|
||||
, IdentState
|
||||
, renderExpr
|
||||
, initialIdentState
|
||||
, IdentInfo
|
||||
, SqlSelect(..)
|
||||
, veryUnsafeCoerceSqlExprValue
|
||||
, veryUnsafeCoerceSqlExprValueList
|
||||
-- * Helper functions
|
||||
, renderQueryToText
|
||||
, renderQuerySelect
|
||||
, renderQueryUpdate
|
||||
, renderQueryDelete
|
||||
, renderQueryInsertInto
|
||||
, makeOrderByNoNewline
|
||||
, uncommas'
|
||||
, parens
|
||||
, toArgList
|
||||
, builderToText
|
||||
, Ident(..)
|
||||
, valkey
|
||||
, valJ
|
||||
, deleteKey
|
||||
, associateJoin
|
||||
) where
|
||||
|
||||
import Database.Esqueleto.Internal.Internal
|
||||
416
src/Database/Esqueleto/Legacy.hs
Normal file
416
src/Database/Esqueleto/Legacy.hs
Normal 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.
|
||||
@ -14,5 +14,5 @@ import Database.Esqueleto.Internal.PersistentImport
|
||||
-- because MySQL uses `rand()`.
|
||||
--
|
||||
-- /Since: 2.6.0/
|
||||
random_ :: (PersistField a, Num a) => SqlExpr a
|
||||
random_ :: (PersistField a, Num a) => SqlExpr (Value a)
|
||||
random_ = unsafeSqlValue "RAND()"
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE Rank2Types #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
@ -26,6 +28,7 @@ module Database.Esqueleto.PostgreSQL
|
||||
, insertSelectWithConflict
|
||||
, insertSelectWithConflictCount
|
||||
, filterWhere
|
||||
, values
|
||||
-- * Internal
|
||||
, unsafeSqlAggregateFunction
|
||||
) where
|
||||
@ -33,37 +36,42 @@ module Database.Esqueleto.PostgreSQL
|
||||
#if __GLASGOW_HASKELL__ < 804
|
||||
import Data.Semigroup
|
||||
#endif
|
||||
import Control.Arrow (first, (***))
|
||||
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 Data.List.NonEmpty (NonEmpty((:|)))
|
||||
import qualified Data.List.NonEmpty as NonEmpty
|
||||
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
|
||||
random_ :: (PersistField a, Num a) => SqlExpr a
|
||||
random_ :: (PersistField a, Num a) => SqlExpr (Value a)
|
||||
random_ = unsafeSqlValue "RANDOM()"
|
||||
|
||||
-- | Empty array literal. (@val []@) does unfortunately not work
|
||||
emptyArray :: SqlExpr [a]
|
||||
emptyArray :: SqlExpr (Value [a])
|
||||
emptyArray = unsafeSqlValue "'{}'"
|
||||
|
||||
-- | Coalesce an array with an empty default value
|
||||
maybeArray ::
|
||||
(PersistField a, PersistField [a])
|
||||
=> SqlExpr (Maybe [a])
|
||||
-> SqlExpr [a]
|
||||
=> SqlExpr (Value (Maybe [a]))
|
||||
-> SqlExpr (Value [a])
|
||||
maybeArray x = coalesceDefault [x] (emptyArray)
|
||||
|
||||
-- | Aggregate mode
|
||||
@ -82,7 +90,7 @@ unsafeSqlAggregateFunction
|
||||
-> AggMode
|
||||
-> a
|
||||
-> [OrderByClause]
|
||||
-> SqlExpr b
|
||||
-> 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
|
||||
@ -106,14 +114,14 @@ unsafeSqlAggregateFunction name mode args orderByClauses = ERaw noMeta $ \_ info
|
||||
--- into an array.
|
||||
arrayAggWith
|
||||
:: AggMode
|
||||
-> SqlExpr a
|
||||
-> SqlExpr (Value a)
|
||||
-> [OrderByClause]
|
||||
-> SqlExpr (Maybe [a])
|
||||
-> SqlExpr (Value (Maybe [a]))
|
||||
arrayAggWith = unsafeSqlAggregateFunction "array_agg"
|
||||
|
||||
--- | (@array_agg@) Concatenate input values, including @NULL@s,
|
||||
--- into an array.
|
||||
arrayAgg :: (PersistField a) => SqlExpr a -> SqlExpr (Maybe [a])
|
||||
arrayAgg :: (PersistField a) => SqlExpr (Value a) -> SqlExpr (Value (Maybe [a]))
|
||||
arrayAgg x = arrayAggWith AggModeAll x []
|
||||
|
||||
-- | (@array_agg@) Concatenate distinct input values, including @NULL@s, into
|
||||
@ -122,19 +130,19 @@ arrayAgg x = arrayAggWith AggModeAll x []
|
||||
-- @since 2.5.3
|
||||
arrayAggDistinct
|
||||
:: (PersistField a, PersistField [a])
|
||||
=> SqlExpr a
|
||||
-> SqlExpr (Maybe [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
|
||||
arrayRemove :: SqlExpr [a] -> SqlExpr a -> SqlExpr [a]
|
||||
arrayRemove :: SqlExpr (Value [a]) -> SqlExpr (Value a) -> SqlExpr (Value [a])
|
||||
arrayRemove arr elem' = unsafeSqlFunction "array_remove" (arr, elem')
|
||||
|
||||
-- | Remove @NULL@ values from an array
|
||||
arrayRemoveNull :: SqlExpr [Maybe a] -> SqlExpr [a]
|
||||
arrayRemoveNull :: SqlExpr (Value [Maybe a]) -> SqlExpr (Value [a])
|
||||
-- This can't be a call to arrayRemove because it changes the value type
|
||||
arrayRemoveNull x = unsafeSqlFunction "array_remove" (x, unsafeSqlValue "NULL")
|
||||
|
||||
@ -144,10 +152,10 @@ arrayRemoveNull x = unsafeSqlFunction "array_remove" (x, unsafeSqlValue "NULL")
|
||||
stringAggWith ::
|
||||
SqlString s
|
||||
=> AggMode -- ^ Aggregate mode (ALL or DISTINCT)
|
||||
-> SqlExpr s -- ^ Input values.
|
||||
-> SqlExpr s -- ^ Delimiter.
|
||||
-> SqlExpr (Value s) -- ^ Input values.
|
||||
-> SqlExpr (Value s) -- ^ Delimiter.
|
||||
-> [OrderByClause] -- ^ ORDER BY clauses
|
||||
-> SqlExpr (Maybe s) -- ^ Concatenation.
|
||||
-> SqlExpr (Value (Maybe s)) -- ^ Concatenation.
|
||||
stringAggWith mode expr delim os =
|
||||
unsafeSqlAggregateFunction "string_agg" mode (expr, delim) os
|
||||
|
||||
@ -157,19 +165,19 @@ stringAggWith mode expr delim os =
|
||||
-- @since 2.2.8
|
||||
stringAgg ::
|
||||
SqlString s
|
||||
=> SqlExpr s -- ^ Input values.
|
||||
-> SqlExpr s -- ^ Delimiter.
|
||||
-> SqlExpr (Maybe s) -- ^ Concatenation.
|
||||
=> SqlExpr (Value s) -- ^ Input values.
|
||||
-> SqlExpr (Value s) -- ^ Delimiter.
|
||||
-> SqlExpr (Value (Maybe s)) -- ^ Concatenation.
|
||||
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
|
||||
chr :: SqlString s => SqlExpr Int -> SqlExpr s
|
||||
chr :: SqlString s => SqlExpr (Value Int) -> SqlExpr (Value s)
|
||||
chr = unsafeSqlFunction "chr"
|
||||
|
||||
now_ :: SqlExpr UTCTime
|
||||
now_ :: SqlExpr (Value UTCTime)
|
||||
now_ = unsafeSqlFunction "NOW" ()
|
||||
|
||||
upsert
|
||||
@ -206,7 +214,7 @@ upsertBy
|
||||
-- ^ the record in the database after the operation
|
||||
upsertBy uniqueKey record updates = do
|
||||
sqlB <- R.ask
|
||||
case connUpsertSql sqlB of
|
||||
case getConnUpsertSql sqlB of
|
||||
Nothing ->
|
||||
-- Postgres backend should have connUpsertSql, if this error is
|
||||
-- thrown, check changes on persistent
|
||||
@ -218,7 +226,7 @@ upsertBy uniqueKey record updates = do
|
||||
entDef = entityDef (Just record)
|
||||
updatesText conn = first builderToText $ renderUpdates conn updates
|
||||
#if MIN_VERSION_persistent(2,11,0)
|
||||
uniqueFields = NonEmpty.fromList (persistUniqueToFieldNames uniqueKey)
|
||||
uniqueFields = persistUniqueToFieldNames uniqueKey
|
||||
handler sqlB upsertSql = do
|
||||
let (updateText, updateVals) =
|
||||
updatesText sqlB
|
||||
@ -281,8 +289,8 @@ insertSelectWithConflict
|
||||
-- violated. The expression takes the current and excluded value to produce
|
||||
-- the updates.
|
||||
-> SqlWriteT m ()
|
||||
insertSelectWithConflict unique query =
|
||||
void . insertSelectWithConflictCount unique query
|
||||
insertSelectWithConflict unique query a =
|
||||
void $ insertSelectWithConflictCount unique query a
|
||||
|
||||
-- | Same as 'insertSelectWithConflict' but returns the number of rows affected.
|
||||
--
|
||||
@ -306,10 +314,10 @@ insertSelectWithConflictCount unique query conflictQuery = do
|
||||
updates = conflictQuery entCurrent entExcluded
|
||||
combine (tlb1,vals1) (tlb2,vals2) = (builderToText (tlb1 `mappend` tlb2), vals1 ++ vals2)
|
||||
entExcluded = unsafeSqlEntity (I "excluded")
|
||||
tableName = unDBName . entityDB . entityDef
|
||||
tableName = unEntityNameDB . getEntityDBName . entityDef
|
||||
entCurrent = unsafeSqlEntity (I (tableName proxy))
|
||||
uniqueDef = toUniqueDef unique
|
||||
constraint = TLB.fromText . unDBName . uniqueDBName $ uniqueDef
|
||||
constraint = TLB.fromText . unConstraintNameDB . uniqueDBName $ uniqueDef
|
||||
renderedUpdates :: (BackendCompatible SqlBackend backend) => backend -> (TLB.Builder, [PersistValue])
|
||||
renderedUpdates conn = renderUpdates conn updates
|
||||
conflict conn = (mconcat ([
|
||||
@ -350,11 +358,11 @@ insertSelectWithConflictCount unique query conflictQuery = do
|
||||
--
|
||||
-- @since 3.3.3.3
|
||||
filterWhere
|
||||
:: SqlExpr a
|
||||
:: SqlExpr (Value a)
|
||||
-- ^ Aggregate function
|
||||
-> SqlExpr Bool
|
||||
-> SqlExpr (Value Bool)
|
||||
-- ^ Filter clause
|
||||
-> SqlExpr a
|
||||
-> SqlExpr (Value a)
|
||||
filterWhere aggExpr clauseExpr = ERaw noMeta $ \_ info ->
|
||||
let (aggBuilder, aggValues) = case aggExpr of
|
||||
ERaw _ aggF -> aggF Never info
|
||||
@ -363,3 +371,68 @@ filterWhere aggExpr clauseExpr = ERaw noMeta $ \_ 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
|
||||
)
|
||||
|
||||
@ -136,9 +136,8 @@ module Database.Esqueleto.PostgreSQL.JSON
|
||||
) 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 ->., ->>., #>., #>>.
|
||||
@ -189,7 +188,7 @@ infixl 6 ||., -., --., #-.
|
||||
-- @
|
||||
--
|
||||
-- @since 3.1.0
|
||||
(->>.) :: JSONBExpr a -> JSONAccessor -> SqlExpr (Maybe Text)
|
||||
(->>.) :: JSONBExpr a -> JSONAccessor -> SqlExpr (Value (Maybe Text))
|
||||
(->>.) value (JSONKey txt) = unsafeSqlBinOp " ->> " value $ val txt
|
||||
(->>.) value (JSONIndex i) = unsafeSqlBinOp " ->> " value $ val i
|
||||
|
||||
@ -253,7 +252,7 @@ infixl 6 ||., -., --., #-.
|
||||
-- @
|
||||
--
|
||||
-- @since 3.1.0
|
||||
(#>>.) :: JSONBExpr a -> [Text] -> SqlExpr (Maybe Text)
|
||||
(#>>.) :: JSONBExpr a -> [Text] -> SqlExpr (Value (Maybe Text))
|
||||
(#>>.) value = unsafeSqlBinOp " #>> " value . mkTextArray
|
||||
|
||||
-- | /Requires PostgreSQL version >= 9.4/
|
||||
@ -275,7 +274,7 @@ infixl 6 ||., -., --., #-.
|
||||
-- @
|
||||
--
|
||||
-- @since 3.1.0
|
||||
(@>.) :: JSONBExpr a -> JSONBExpr b -> SqlExpr Bool
|
||||
(@>.) :: JSONBExpr a -> JSONBExpr b -> SqlExpr (Value Bool)
|
||||
(@>.) = unsafeSqlBinOp " @> "
|
||||
|
||||
-- | /Requires PostgreSQL version >= 9.4/
|
||||
@ -297,7 +296,7 @@ infixl 6 ||., -., --., #-.
|
||||
-- @
|
||||
--
|
||||
-- @since 3.1.0
|
||||
(<@.) :: JSONBExpr a -> JSONBExpr b -> SqlExpr Bool
|
||||
(<@.) :: JSONBExpr a -> JSONBExpr b -> SqlExpr (Value Bool)
|
||||
(<@.) = unsafeSqlBinOp " <@ "
|
||||
|
||||
-- | /Requires PostgreSQL version >= 9.4/
|
||||
@ -320,7 +319,7 @@ infixl 6 ||., -., --., #-.
|
||||
-- @
|
||||
--
|
||||
-- @since 3.1.0
|
||||
(?.) :: JSONBExpr a -> Text -> SqlExpr Bool
|
||||
(?.) :: JSONBExpr a -> Text -> SqlExpr (Value Bool)
|
||||
(?.) value = unsafeSqlBinOp " ?? " value . val
|
||||
|
||||
-- | /Requires PostgreSQL version >= 9.4/
|
||||
@ -343,7 +342,7 @@ infixl 6 ||., -., --., #-.
|
||||
-- @
|
||||
--
|
||||
-- @since 3.1.0
|
||||
(?|.) :: JSONBExpr a -> [Text] -> SqlExpr Bool
|
||||
(?|.) :: JSONBExpr a -> [Text] -> SqlExpr (Value Bool)
|
||||
(?|.) value = unsafeSqlBinOp " ??| " value . mkTextArray
|
||||
|
||||
-- | /Requires PostgreSQL version >= 9.4/
|
||||
@ -366,7 +365,7 @@ infixl 6 ||., -., --., #-.
|
||||
-- @
|
||||
--
|
||||
-- @since 3.1.0
|
||||
(?&.) :: JSONBExpr a -> [Text] -> SqlExpr Bool
|
||||
(?&.) :: JSONBExpr a -> [Text] -> SqlExpr (Value Bool)
|
||||
(?&.) value = unsafeSqlBinOp " ??& " value . mkTextArray
|
||||
|
||||
-- | /Requires PostgreSQL version >= 9.5/
|
||||
@ -579,5 +578,5 @@ infixl 6 ||., -., --., #-.
|
||||
(#-.) :: JSONBExpr a -> [Text] -> JSONBExpr b
|
||||
(#-.) value = unsafeSqlBinOp " #- " value . mkTextArray
|
||||
|
||||
mkTextArray :: [Text] -> SqlExpr PersistValue
|
||||
mkTextArray :: [Text] -> SqlExpr (Value PersistValue)
|
||||
mkTextArray = val . PersistArray . fmap toPersistValue
|
||||
|
||||
@ -2,22 +2,21 @@
|
||||
{-# LANGUAGE DeriveFunctor #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DeriveTraversable #-}
|
||||
{-# LANGUAGE DerivingStrategies #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# language DerivingStrategies #-}
|
||||
|
||||
module Database.Esqueleto.PostgreSQL.JSON.Instances where
|
||||
|
||||
import Data.Aeson (FromJSON(..), ToJSON(..), eitherDecodeStrict, encode)
|
||||
import Data.Aeson (FromJSON(..), ToJSON(..), encode, eitherDecodeStrict)
|
||||
import Data.Bifunctor (first)
|
||||
import qualified Data.ByteString.Lazy as BSL (toStrict)
|
||||
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.
|
||||
@ -42,7 +41,7 @@ newtype JSONB a = JSONB { unJSONB :: a }
|
||||
-- | 'SqlExpr' of a NULL-able 'JSONB' value. Hence the 'Maybe'.
|
||||
--
|
||||
-- Note: NULL here is a PostgreSQL NULL, not a JSON 'null'
|
||||
type JSONBExpr a = SqlExpr (Maybe (JSONB a))
|
||||
type JSONBExpr a = SqlExpr (Value (Maybe (JSONB a)))
|
||||
|
||||
-- | Convenience function to lift a regular value into
|
||||
-- a 'JSONB' expression.
|
||||
@ -87,7 +86,7 @@ instance IsString JSONAccessor where
|
||||
|
||||
-- | @since 3.1.0
|
||||
instance (FromJSON a, ToJSON a) => PersistField (JSONB a) where
|
||||
toPersistValue = PersistDbSpecific . BSL.toStrict . encode . unJSONB
|
||||
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)
|
||||
|
||||
@ -14,5 +14,5 @@ import Database.Esqueleto.Internal.PersistentImport
|
||||
-- because MySQL uses `rand()`.
|
||||
--
|
||||
-- /Since: 2.6.0/
|
||||
random_ :: (PersistField a, Num a) => SqlExpr a
|
||||
random_ :: (PersistField a, Num a) => SqlExpr (Value a)
|
||||
random_ = unsafeSqlValue "RANDOM()"
|
||||
|
||||
12
stack-8.10.yaml
Normal file
12
stack-8.10.yaml
Normal 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
|
||||
@ -1,12 +1,12 @@
|
||||
resolver: lts-16.14
|
||||
resolver: lts-16.31
|
||||
|
||||
packages:
|
||||
- '.'
|
||||
- 'examples'
|
||||
|
||||
extra-deps:
|
||||
- persistent-2.11.0.0
|
||||
- persistent-template-2.9.1.0
|
||||
- persistent-mysql-2.10.3
|
||||
- persistent-postgresql-2.11.0.0
|
||||
- persistent-sqlite-2.11.0.0
|
||||
- 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
|
||||
|
||||
@ -1,4 +1,14 @@
|
||||
resolver: nightly-2020-09-20
|
||||
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
|
||||
|
||||
@ -3,10 +3,52 @@
|
||||
# For more information, please see the documentation at:
|
||||
# https://docs.haskellstack.org/en/stable/lock_files
|
||||
|
||||
packages: []
|
||||
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: 467884
|
||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/nightly/2020/1/24.yaml
|
||||
sha256: 55c1a4fc9222bc3b8cf91461f38e2641da675a7296f06528f47340c19d0c6e85
|
||||
original: nightly-2020-01-24
|
||||
size: 539378
|
||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/nightly/2022/3/29.yaml
|
||||
sha256: c959441a05f6fa4d45ae6e258290f04d399245b8436263b4abb525c7f73da6a5
|
||||
original: nightly-2022-03-29
|
||||
|
||||
@ -1 +1 @@
|
||||
stack-8.8.yaml
|
||||
stack-8.10.yaml
|
||||
2026
test/Common/Test.hs
2026
test/Common/Test.hs
File diff suppressed because it is too large
Load Diff
87
test/Common/Test/Import.hs
Normal file
87
test/Common/Test/Import.hs
Normal 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
189
test/Common/Test/Models.hs
Normal 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
|
||||
|
||||
22
test/Common/Test/Select.hs
Normal file
22
test/Common/Test/Select.hs
Normal 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) ]
|
||||
@ -4,12 +4,15 @@
|
||||
{-# 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 (runNoLoggingT, runStderrLoggingT)
|
||||
import Control.Monad.Trans.Reader (ReaderT)
|
||||
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)
|
||||
@ -22,28 +25,16 @@ import Database.Persist.MySQL
|
||||
, 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
|
||||
@ -53,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
|
||||
@ -70,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
|
||||
@ -91,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
|
||||
@ -104,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
|
||||
@ -130,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
|
||||
@ -155,22 +129,20 @@ 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 :: Spec
|
||||
testMysqlUnionWithLimits :: SpecDb
|
||||
testMysqlUnionWithLimits = do
|
||||
describe "MySQL Union" $ do
|
||||
it "supports limit/orderBy by parenthesizing" $ do
|
||||
run $ do
|
||||
itDb "supports limit/orderBy by parenthesizing" $ do
|
||||
mapM_ (insert . Foo) [1..6]
|
||||
|
||||
let q1 = do
|
||||
@ -191,59 +163,61 @@ testMysqlUnionWithLimits = do
|
||||
ret <- select $ Experimental.from $ q1 `union_` q2
|
||||
liftIO $ ret `shouldMatchList` [Value 1, Value 2, Value 4, Value 5]
|
||||
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
hspec $ do
|
||||
tests run
|
||||
|
||||
describe "Test MySQL locking" $ do
|
||||
testLocking withConn
|
||||
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
|
||||
testMysqlUnionWithLimits
|
||||
|
||||
|
||||
|
||||
|
||||
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 = "127.0.0.1"
|
||||
, connectUser = "travis"
|
||||
, connectPassword = "esqutest"
|
||||
, connectDatabase = "esqutest"
|
||||
, connectPort = 33306
|
||||
}
|
||||
|
||||
flip runSqlPool pool $ do
|
||||
migrateIt
|
||||
cleanDB
|
||||
|
||||
pure pool
|
||||
|
||||
@ -16,23 +16,21 @@
|
||||
|
||||
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
@ -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
22
test/Spec.hs
Normal 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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user