From 8891d6a373b6917b149ce401bfed9daa39012230 Mon Sep 17 00:00:00 2001 From: Eitan Chatav Date: Thu, 16 Dec 2021 15:15:59 -0500 Subject: [PATCH 01/18] stack ghc 9 --- stack.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack.yaml b/stack.yaml index 94846b4b..5c9e6631 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1,4 +1,4 @@ -resolver: lts-16.16 +resolver: nightly-2021-12-16 packages: - squeal-postgresql - squeal-postgresql-ltree From 2e111e63db8f37d0c1410dbdc25de1da4b742e08 Mon Sep 17 00:00:00 2001 From: Eitan Chatav Date: Thu, 16 Dec 2021 15:16:21 -0500 Subject: [PATCH 02/18] saturate --- .../src/Squeal/PostgreSQL/Expression.hs | 4 +- .../Squeal/PostgreSQL/Expression/Inline.hs | 92 ++++++++++++------- .../src/Squeal/PostgreSQL/Expression/Null.hs | 2 +- .../Squeal/PostgreSQL/Session/Transaction.hs | 14 +-- 4 files changed, 67 insertions(+), 45 deletions(-) diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression.hs index 8bcf8bfa..ff37e690 100644 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression.hs +++ b/squeal-postgresql/src/Squeal/PostgreSQL/Expression.hs @@ -342,7 +342,7 @@ function :: (Has sch db schema, Has fun schema ('Function ('[x] :=> 'Returns y))) => QualifiedAlias sch fun -- ^ function name -> Fun db x y -function = unsafeFunction . renderSQL +function f = unsafeFunction $ renderSQL f -- | >>> printSQL $ unsafeFunctionN "f" (currentTime :* localTimestamp :* false *: inline 'a') -- f(CURRENT_TIME, LOCALTIMESTAMP, FALSE, (E'a' :: char(1))) @@ -369,7 +369,7 @@ functionN , SListI xs ) => QualifiedAlias sch fun -- ^ function alias -> FunN db xs y -functionN = unsafeFunctionN . renderSQL +functionN f = unsafeFunctionN $ renderSQL f instance Num (Expression grp lat with db params from (null 'PGint2)) where diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Inline.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Inline.hs index 5fde4cbb..17749ac3 100644 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Inline.hs +++ b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Inline.hs @@ -101,29 +101,41 @@ instance Inline Bool where True -> true False -> false instance JSON.ToJSON x => Inline (Json x) where - inline = inferredtype . UnsafeExpression - . singleQuotedUtf8 . toStrict . JSON.encode . getJson + inline (Json x) + = inferredtype + . UnsafeExpression + . singleQuotedUtf8 + . toStrict + . JSON.encode + $ x instance JSON.ToJSON x => Inline (Jsonb x) where - inline = inferredtype . UnsafeExpression - . singleQuotedUtf8 . toStrict . JSON.encode . getJsonb + inline (Jsonb x) + = inferredtype + . UnsafeExpression + . singleQuotedUtf8 + . toStrict + . JSON.encode + $ x instance Inline Char where inline chr = inferredtype . UnsafeExpression $ "E\'" <> fromString (escape chr) <> "\'" -instance Inline String where inline = fromString +instance Inline String where inline x = fromString x instance Inline Int16 where - inline + inline x = inferredtype . UnsafeExpression . toStrict . toLazyByteString . int16Dec + $ x instance Inline Int32 where - inline + inline x = inferredtype . UnsafeExpression . toStrict . toLazyByteString . int32Dec + $ x instance Inline Int64 where inline x = if x == minBound @@ -151,29 +163,33 @@ instance Inline Double where where decimal = toStrict . toLazyByteString . doubleDec instance Inline Scientific where - inline + inline x = inferredtype . UnsafeExpression . toStrict . toLazyByteString . scientificBuilder -instance Inline Text where inline = fromString . Text.unpack -instance Inline Lazy.Text where inline = fromString . Lazy.Text.unpack + $ x +instance Inline Text where inline x = fromString . Text.unpack $ x +instance Inline Lazy.Text where inline x = fromString . Lazy.Text.unpack $ x instance (KnownNat n, 1 <= n) => Inline (VarChar n) where - inline + inline x = inferredtype . UnsafeExpression . escapeQuotedText . getVarChar + $ x instance (KnownNat n, 1 <= n) => Inline (FixChar n) where - inline + inline x = inferredtype . UnsafeExpression . escapeQuotedText . getFixChar -instance Inline x => Inline (Const x tag) where inline = inline @x . coerce -instance Inline x => Inline (SOP.K x tag) where inline = inline @x . coerce -instance Inline x => Inline (Constant x tag) where inline = inline @x . coerce + $ x +instance Inline x => Inline (Const x tag) where inline (Const x) = inline x +instance Inline x => Inline (SOP.K x tag) where inline (SOP.K x) = inline x +instance Inline x => Inline (Constant x tag) where + inline (Constant x) = inline x instance Inline DiffTime where inline dt = let @@ -185,58 +201,64 @@ instance Inline DiffTime where interval_ (fromIntegral secs) Seconds +! interval_ (fromIntegral microsecs) Microseconds instance Inline Day where - inline + inline x = inferredtype . UnsafeExpression . singleQuotedUtf8 . fromString . iso8601Show + $ x instance Inline UTCTime where - inline + inline x = inferredtype . UnsafeExpression . singleQuotedUtf8 . fromString . iso8601Show + $ x instance Inline (TimeOfDay, TimeZone) where - inline + inline x = inferredtype . UnsafeExpression . singleQuotedUtf8 . fromString . formatShow (timeOfDayAndOffsetFormat ExtendedFormat) + $ x instance Inline TimeOfDay where - inline + inline x = inferredtype . UnsafeExpression . singleQuotedUtf8 . fromString - . iso8601Show + . iso8601Show + $ x instance Inline LocalTime where - inline + inline x = inferredtype . UnsafeExpression . singleQuotedUtf8 . fromString . iso8601Show + $ x instance Inline (Range Int32) where - inline = range int4range . fmap inline + inline x = range int4range . fmap (\y -> inline y) $ x instance Inline (Range Int64) where - inline = range int8range . fmap inline + inline x = range int8range . fmap (\y -> inline y) $ x instance Inline (Range Scientific) where - inline = range numrange . fmap inline + inline x = range numrange . fmap (\y -> inline y) $ x instance Inline (Range LocalTime) where - inline = range tsrange . fmap inline + inline x = range tsrange . fmap (\y -> inline y) $ x instance Inline (Range UTCTime) where - inline = range tstzrange . fmap inline + inline x = range tstzrange . fmap (\y -> inline y) $ x instance Inline (Range Day) where - inline = range daterange . fmap inline + inline x = range daterange . fmap (\y -> inline y) $ x instance Inline UUID where - inline + inline x = inferredtype . UnsafeExpression . singleQuotedUtf8 . toASCIIBytes + $ x instance Inline Money where inline moolah = inferredtype . UnsafeExpression $ fromString (show dollars) @@ -245,17 +267,17 @@ instance Inline Money where (dollars,pennies) = cents moolah `divMod` 100 instance InlineParam x (NullPG x) => Inline (VarArray [x]) where - inline (VarArray xs) = array (inlineParam <$> xs) + inline (VarArray xs) = array ((\x -> inlineParam x) <$> xs) instance InlineParam x (NullPG x) => Inline (VarArray (Vector x)) where - inline (VarArray xs) = array (inlineParam <$> toList xs) + inline (VarArray xs) = array ((\x -> inlineParam x) <$> toList xs) instance Inline Oid where inline (Oid o) = inferredtype . UnsafeExpression . fromString $ show o instance ( SOP.IsEnumType x , SOP.HasDatatypeInfo x ) => Inline (Enumerated x) where - inline = + inline (Enumerated x) = let gshowConstructor :: NP SOP.ConstructorInfo xss @@ -273,22 +295,22 @@ instance . gshowConstructor (SOP.constructorInfo (SOP.datatypeInfo (SOP.Proxy @x))) . SOP.from - . getEnumerated + $ x instance ( SOP.IsRecord x xs , SOP.AllZip InlineField xs (RowPG x) ) => Inline (Composite x) where - inline + inline (Composite x) = row . SOP.htrans (SOP.Proxy @InlineField) inlineField . SOP.toRecord - . getComposite + $ x -- | Lifts `Inline` to `NullType`s. class InlineParam x ty where inlineParam :: x -> Expr ty instance (Inline x, pg ~ PG x) => InlineParam x ('NotNull pg) where inlineParam = inline instance (Inline x, pg ~ PG x) => InlineParam (Maybe x) ('Null pg) where - inlineParam = maybe null_ inline + inlineParam x = maybe null_ (\y -> inline y) x -- | Lifts `Inline` to fields. class InlineField diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Null.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Null.hs index 6f38dede..f4ee6e13 100644 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Null.hs +++ b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Null.hs @@ -68,7 +68,7 @@ monoNotNull :: (forall null. Expression grp lat with db params from (null ty)) -- ^ null polymorphic -> Expression grp lat with db params from ('NotNull ty) -monoNotNull = id +monoNotNull x = x -- | return the leftmost value which is not NULL -- diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Transaction.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Session/Transaction.hs index bcb64ddc..f1868563 100644 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Transaction.hs +++ b/squeal-postgresql/src/Squeal/PostgreSQL/Session/Transaction.hs @@ -90,14 +90,14 @@ transactionally => TransactionMode -> Transaction db x -- ^ run inside a transaction -> tx x -transactionally = Unsafe.transactionally +transactionally mode tx = Unsafe.transactionally mode tx -- | Run a computation `transactionally_`, in `defaultMode`. transactionally_ :: (MonadMask tx, MonadResult tx, MonadPQ db tx) => Transaction db x -- ^ run inside a transaction -> tx x -transactionally_ = Unsafe.transactionally_ +transactionally_ tx = Unsafe.transactionally_ tx {- | `transactionallyRetry` a computation; @@ -114,14 +114,14 @@ transactionallyRetry => TransactionMode -> Transaction db x -- ^ run inside a transaction -> tx x -transactionallyRetry = Unsafe.transactionallyRetry +transactionallyRetry mode tx = Unsafe.transactionallyRetry mode tx {- | `transactionallyRetry` in `retryMode`. -} transactionallyRetry_ :: (MonadMask tx, MonadResult tx, MonadPQ db tx) => Transaction db x -- ^ run inside a transaction -> tx x -transactionallyRetry_ = Unsafe.transactionallyRetry_ +transactionallyRetry_ tx = Unsafe.transactionallyRetry_ tx {- | Run a computation `ephemerally`; Like `transactionally` but always `Unsafe.rollback`, useful in testing. @@ -131,14 +131,14 @@ ephemerally => TransactionMode -> Transaction db x -- ^ run inside an ephemeral transaction -> tx x -ephemerally = Unsafe.ephemerally +ephemerally mode tx = Unsafe.ephemerally mode tx {- | Run a computation `ephemerally` in `defaultMode`. -} ephemerally_ :: (MonadMask tx, MonadResult tx, MonadPQ db tx) => Transaction db x -- ^ run inside an ephemeral transaction -> tx x -ephemerally_ = Unsafe.ephemerally_ +ephemerally_ tx = Unsafe.ephemerally_ tx {- | `withSavepoint`, used in a transaction block, allows a form of nested transactions, @@ -153,4 +153,4 @@ withSavepoint :: ByteString -- ^ savepoint name -> Transaction db (Either e x) -> Transaction db (Either e x) -withSavepoint = Unsafe.withSavepoint +withSavepoint sv tx = Unsafe.withSavepoint sv tx From 080eae14e23605eaba1961c3390673bb18cd276f Mon Sep 17 00:00:00 2001 From: Eitan Chatav Date: Fri, 17 Dec 2021 01:01:24 -0500 Subject: [PATCH 03/18] fix indexed monad transformer class Why does this work? Why didn't it work before? --- squeal-postgresql/src/Squeal/PostgreSQL/Session/Indexed.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Indexed.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Session/Indexed.hs index e414fad1..ec7eb66d 100644 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Indexed.hs +++ b/squeal-postgresql/src/Squeal/PostgreSQL/Session/Indexed.hs @@ -50,8 +50,8 @@ enabling use of standard @do@ notation for endo-index operations. -} class ( forall i j m. Monad m => Functor (t i j m) - , forall i j m. (i ~ j, Monad m) => Monad (t i j m) - , forall i j. i ~ j => MonadTrans (t i j) + , forall i m. Monad m => Monad (t i i m) + , forall i. MonadTrans (t i i) ) => IndexedMonadTrans t where {-# MINIMAL pqJoin | pqBind #-} From ff4efb06ac4997b3283f3ef2ca67a32d0ade78c4 Mon Sep 17 00:00:00 2001 From: Eitan Chatav Date: Fri, 17 Dec 2021 01:01:31 -0500 Subject: [PATCH 04/18] LTree --- .../src/Squeal/PostgreSQL/LTree.hs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/squeal-postgresql-ltree/src/Squeal/PostgreSQL/LTree.hs b/squeal-postgresql-ltree/src/Squeal/PostgreSQL/LTree.hs index f22e6af0..cb19376d 100644 --- a/squeal-postgresql-ltree/src/Squeal/PostgreSQL/LTree.hs +++ b/squeal-postgresql-ltree/src/Squeal/PostgreSQL/LTree.hs @@ -155,12 +155,12 @@ instance TypeError ('Text "LTree binary instances not yet implemented.") => ToPG db LTree where toPG = pure . Encoding.text_strict . getLTree instance Inline LTree where - inline + inline (UnsafeLTree x) = UnsafeExpression . parenthesized . (<> " :: ltree") . escapeQuotedText - . getLTree + $ x {- | lquery represents a regular-expression-like pattern for matching ltree values. @@ -228,12 +228,12 @@ instance TypeError ('Text "LQuery binary instances not yet implemented.") => ToPG db LQuery where toPG = pure . Encoding.text_strict . getLQuery instance Inline LQuery where - inline + inline (UnsafeLQuery x) = UnsafeExpression . parenthesized . (<> " :: lquery") . escapeQuotedText - . getLQuery + $ x {- | ltxtquery represents a full-text-search-like pattern for matching ltree values. @@ -269,12 +269,12 @@ instance TypeError ('Text "LTxtQuery binary instances not yet implemented.") => ToPG db LTxtQuery where toPG = pure . Encoding.text_strict . getLTxtQuery instance Inline LTxtQuery where - inline + inline (UnsafeLTxtQuery x) = UnsafeExpression . parenthesized . (<> " :: ltxtquery") . escapeQuotedText - . getLTxtQuery + $ x instance IsString (Expression grp lat with db params from (null PGltree)) where From ff0693eaef0ece7fc2872796345ffd172ffc24dd Mon Sep 17 00:00:00 2001 From: Eitan Chatav Date: Fri, 17 Dec 2021 10:29:53 -0500 Subject: [PATCH 05/18] fix test --- squeal-postgresql/test/Spec.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/squeal-postgresql/test/Spec.hs b/squeal-postgresql/test/Spec.hs index 112a662a..a9d52292 100644 --- a/squeal-postgresql/test/Spec.hs +++ b/squeal-postgresql/test/Spec.hs @@ -109,7 +109,7 @@ spec = before_ setupDB . after_ dropDB $ do let testUser = User "TestUser" newUser :: User -> Transaction DB () - newUser = manipulateParams_ insertUser + newUser usr = manipulateParams_ insertUser usr insertUserTwice :: Transaction DB () insertUserTwice = newUser testUser >> newUser testUser err23505 = UniqueViolation $ Char8.unlines From 17e613af7adbea185a5bf96ac04b458b575d6871 Mon Sep 17 00:00:00 2001 From: Eitan Chatav Date: Fri, 17 Dec 2021 11:51:27 -0500 Subject: [PATCH 06/18] redundant import --- squeal-postgresql/src/Squeal/PostgreSQL/Expression/Inline.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Inline.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Inline.hs index 17749ac3..0e6de289 100644 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Inline.hs +++ b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Inline.hs @@ -39,7 +39,6 @@ import Data.Binary.Builder (toLazyByteString) import Data.ByteString.Lazy (toStrict) import Data.ByteString.Builder (doubleDec, floatDec, int16Dec, int32Dec, int64Dec) import Data.ByteString.Builder.Scientific (scientificBuilder) -import Data.Coerce (coerce) import Data.Functor.Const (Const(Const)) import Data.Functor.Constant (Constant(Constant)) import Data.Int (Int16, Int32, Int64) From 17f68779b483f4ad9d34a0a70f710bc48f3a5f5e Mon Sep 17 00:00:00 2001 From: Eitan Chatav Date: Mon, 27 Dec 2021 10:21:17 -0800 Subject: [PATCH 07/18] remove unneeded dependency --- squeal-postgresql/squeal-postgresql.cabal | 1 - 1 file changed, 1 deletion(-) diff --git a/squeal-postgresql/squeal-postgresql.cabal b/squeal-postgresql/squeal-postgresql.cabal index 30eb09f9..85d0fc11 100644 --- a/squeal-postgresql/squeal-postgresql.cabal +++ b/squeal-postgresql/squeal-postgresql.cabal @@ -125,7 +125,6 @@ test-suite doctest build-depends: base >= 4.12.0.0 && < 5.0 , doctest >= 0.16.3 - , squeal-postgresql test-suite properties default-language: Haskell2010 From 10896ffab7d8ec2539bff38f00ca652c35f7b6a5 Mon Sep 17 00:00:00 2001 From: Eitan Chatav Date: Mon, 27 Dec 2021 10:33:52 -0800 Subject: [PATCH 08/18] update CI to correctly run old versions of GHC (hopefully) --- .github/workflows/ci.yml | 24 ++++++++++++------------ stack-ghc806.yaml | 5 +++++ stack-ghc808.yaml | 5 +++++ stack-ghc810.yaml | 5 +++++ 4 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 stack-ghc806.yaml create mode 100644 stack-ghc808.yaml create mode 100644 stack-ghc810.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc3fe582..daf09b3f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,10 +39,10 @@ jobs: stack-version: 'latest' - name: build - run: stack build --fast + run: stack build --fast --stack-yaml stack-ghc810.yaml - name: test - run: stack test --fast + run: stack test --fast --stack-yaml stack-ghc810.yaml env: PG_USER: postgres PG_HOST: localhost @@ -51,10 +51,10 @@ jobs: PG_PORT: ${{ job.services.postgres.ports['5432'] }} - name: benchmark - run: stack bench --fast + run: stack bench --fast --stack-yaml stack-ghc810.yaml - name: documentation - run: stack haddock --fast + run: stack haddock --fast --stack-yaml stack-ghc810.yaml - name: cache uses: actions/cache@v2 @@ -91,10 +91,10 @@ jobs: stack-version: 'latest' - name: build - run: stack build --fast + run: stack build --fast --stack-yaml stack-ghc808.yaml - name: test - run: stack test --fast + run: stack test --fast --stack-yaml stack-ghc808.yaml env: PG_USER: postgres PG_HOST: localhost @@ -103,10 +103,10 @@ jobs: PG_PORT: ${{ job.services.postgres.ports['5432'] }} - name: benchmark - run: stack bench --fast + run: stack bench --fast --stack-yaml stack-ghc808.yaml - name: documentation - run: stack haddock --fast + run: stack haddock --fast --stack-yaml stack-ghc808.yaml - name: cache uses: actions/cache@v2 @@ -143,10 +143,10 @@ jobs: stack-version: 'latest' - name: build - run: stack build --fast + run: stack build --fast --stack-yaml stack-ghc810.yaml - name: test - run: stack test --fast + run: stack test --fast --stack-yaml stack-ghc810.yaml env: PG_USER: postgres PG_HOST: localhost @@ -155,10 +155,10 @@ jobs: PG_PORT: ${{ job.services.postgres.ports['5432'] }} - name: benchmark - run: stack bench --fast + run: stack bench --fast --stack-yaml stack-ghc810.yaml - name: documentation - run: stack haddock --fast + run: stack haddock --fast --stack-yaml stack-ghc810.yaml - name: cache uses: actions/cache@v2 diff --git a/stack-ghc806.yaml b/stack-ghc806.yaml new file mode 100644 index 00000000..1eada539 --- /dev/null +++ b/stack-ghc806.yaml @@ -0,0 +1,5 @@ +resolver: lts-14.27 +packages: +- squeal-postgresql +- squeal-postgresql-ltree +- squeal-postgresql-uuid-ossp diff --git a/stack-ghc808.yaml b/stack-ghc808.yaml new file mode 100644 index 00000000..bdf488cb --- /dev/null +++ b/stack-ghc808.yaml @@ -0,0 +1,5 @@ +resolver: lts-18.20 +packages: +- squeal-postgresql +- squeal-postgresql-ltree +- squeal-postgresql-uuid-ossp diff --git a/stack-ghc810.yaml b/stack-ghc810.yaml new file mode 100644 index 00000000..bdf488cb --- /dev/null +++ b/stack-ghc810.yaml @@ -0,0 +1,5 @@ +resolver: lts-18.20 +packages: +- squeal-postgresql +- squeal-postgresql-ltree +- squeal-postgresql-uuid-ossp From 8ab188bcd8bbeb6fae7e470dd1b21d0ca5dfa99b Mon Sep 17 00:00:00 2001 From: Eitan Chatav Date: Mon, 27 Dec 2021 10:54:28 -0800 Subject: [PATCH 09/18] fix things --- .github/workflows/ci.yml | 24 ++++++++++++------------ stack-ghc808.yaml => stack-ghc8_10.yaml | 0 stack-ghc806.yaml => stack-ghc8_6.yaml | 0 stack-ghc810.yaml => stack-ghc8_8.yaml | 0 4 files changed, 12 insertions(+), 12 deletions(-) rename stack-ghc808.yaml => stack-ghc8_10.yaml (100%) rename stack-ghc806.yaml => stack-ghc8_6.yaml (100%) rename stack-ghc810.yaml => stack-ghc8_8.yaml (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index daf09b3f..7e110238 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,10 +39,10 @@ jobs: stack-version: 'latest' - name: build - run: stack build --fast --stack-yaml stack-ghc810.yaml + run: stack build --fast --stack-yaml stack-ghc8_10.yaml - name: test - run: stack test --fast --stack-yaml stack-ghc810.yaml + run: stack test --fast --stack-yaml stack-ghc8_10.yaml env: PG_USER: postgres PG_HOST: localhost @@ -51,10 +51,10 @@ jobs: PG_PORT: ${{ job.services.postgres.ports['5432'] }} - name: benchmark - run: stack bench --fast --stack-yaml stack-ghc810.yaml + run: stack bench --fast --stack-yaml stack-ghc8_10.yaml - name: documentation - run: stack haddock --fast --stack-yaml stack-ghc810.yaml + run: stack haddock --fast --stack-yaml stack-ghc8_10.yaml - name: cache uses: actions/cache@v2 @@ -91,10 +91,10 @@ jobs: stack-version: 'latest' - name: build - run: stack build --fast --stack-yaml stack-ghc808.yaml + run: stack build --fast --stack-yaml stack-ghc8_8.yaml - name: test - run: stack test --fast --stack-yaml stack-ghc808.yaml + run: stack test --fast --stack-yaml stack-ghc8_8.yaml env: PG_USER: postgres PG_HOST: localhost @@ -103,10 +103,10 @@ jobs: PG_PORT: ${{ job.services.postgres.ports['5432'] }} - name: benchmark - run: stack bench --fast --stack-yaml stack-ghc808.yaml + run: stack bench --fast --stack-yaml stack-ghc8_8.yaml - name: documentation - run: stack haddock --fast --stack-yaml stack-ghc808.yaml + run: stack haddock --fast --stack-yaml stack-ghc8_8.yaml - name: cache uses: actions/cache@v2 @@ -143,10 +143,10 @@ jobs: stack-version: 'latest' - name: build - run: stack build --fast --stack-yaml stack-ghc810.yaml + run: stack build --fast --stack-yaml stack-ghc8_6.yaml - name: test - run: stack test --fast --stack-yaml stack-ghc810.yaml + run: stack test --fast --stack-yaml stack-ghc8_6.yaml env: PG_USER: postgres PG_HOST: localhost @@ -155,10 +155,10 @@ jobs: PG_PORT: ${{ job.services.postgres.ports['5432'] }} - name: benchmark - run: stack bench --fast --stack-yaml stack-ghc810.yaml + run: stack bench --fast --stack-yaml stack-ghc8_6.yaml - name: documentation - run: stack haddock --fast --stack-yaml stack-ghc810.yaml + run: stack haddock --fast --stack-yaml stack-ghc8_6.yaml - name: cache uses: actions/cache@v2 diff --git a/stack-ghc808.yaml b/stack-ghc8_10.yaml similarity index 100% rename from stack-ghc808.yaml rename to stack-ghc8_10.yaml diff --git a/stack-ghc806.yaml b/stack-ghc8_6.yaml similarity index 100% rename from stack-ghc806.yaml rename to stack-ghc8_6.yaml diff --git a/stack-ghc810.yaml b/stack-ghc8_8.yaml similarity index 100% rename from stack-ghc810.yaml rename to stack-ghc8_8.yaml From 3cf9d22e00bad43e760a78fe41c22ab5872e25c8 Mon Sep 17 00:00:00 2001 From: Eitan Chatav Date: Mon, 27 Dec 2021 11:32:50 -0800 Subject: [PATCH 10/18] fix things --- .gitignore | 1 + .../src/Squeal/PostgreSQL/Expression/Json.hs | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index a1f56f08..0afc615e 100644 --- a/.gitignore +++ b/.gitignore @@ -19,5 +19,6 @@ cabal.sandbox.config cabal.project.local .DS_Store stack.yaml.lock +*.yaml.lock tags .*.swp diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Json.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Json.hs index b91ccc08..3a2c7283 100644 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Json.hs +++ b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Json.hs @@ -337,7 +337,7 @@ jsonbPretty = unsafeFunction "jsonb_pretty" {- | Expands the outermost JSON object into a set of key/value pairs. >>> printSQL (select Star (from (jsonEach (inline (Json (object ["a" .= "foo", "b" .= "bar"])))))) -SELECT * FROM json_each(('{"a":"foo","b":"bar"}' :: json)) +SELECT * FROM json_each(('{"b":"bar","a":"foo"}' :: json)) -} jsonEach :: null 'PGjson -|-> ("json_each" ::: '["key" ::: 'NotNull 'PGtext, "value" ::: 'NotNull 'PGjson]) @@ -346,7 +346,7 @@ jsonEach = unsafeSetFunction "json_each" {- | Expands the outermost binary JSON object into a set of key/value pairs. >>> printSQL (select Star (from (jsonbEach (inline (Jsonb (object ["a" .= "foo", "b" .= "bar"])))))) -SELECT * FROM jsonb_each(('{"a":"foo","b":"bar"}' :: jsonb)) +SELECT * FROM jsonb_each(('{"b":"bar","a":"foo"}' :: jsonb)) -} jsonbEach :: null 'PGjsonb -|-> @@ -356,7 +356,7 @@ jsonbEach = unsafeSetFunction "jsonb_each" {- | Expands the outermost JSON object into a set of key/value pairs. >>> printSQL (select Star (from (jsonEachText (inline (Json (object ["a" .= "foo", "b" .= "bar"])))))) -SELECT * FROM json_each_text(('{"a":"foo","b":"bar"}' :: json)) +SELECT * FROM json_each_text(('{"b":"bar","a":"foo"}' :: json)) -} jsonEachText :: null 'PGjson -|-> @@ -376,7 +376,7 @@ jsonArrayElementsText = unsafeSetFunction "json_array_elements_text" {- | Expands the outermost binary JSON object into a set of key/value pairs. >>> printSQL (select Star (from (jsonbEachText (inline (Jsonb (object ["a" .= "foo", "b" .= "bar"])))))) -SELECT * FROM jsonb_each_text(('{"a":"foo","b":"bar"}' :: jsonb)) +SELECT * FROM jsonb_each_text(('{"b":"bar","a":"foo"}' :: jsonb)) -} jsonbEachText :: null 'PGjsonb -|-> @@ -386,7 +386,7 @@ jsonbEachText = unsafeSetFunction "jsonb_each_text" {- | Returns set of keys in the outermost JSON object. >>> printSQL (jsonObjectKeys (inline (Json (object ["a" .= "foo", "b" .= "bar"])))) -json_object_keys(('{"a":"foo","b":"bar"}' :: json)) +json_object_keys(('{"b":"bar","a":"foo"}' :: json)) -} jsonObjectKeys :: null 'PGjson -|-> @@ -396,7 +396,7 @@ jsonObjectKeys = unsafeSetFunction "json_object_keys" {- | Returns set of keys in the outermost JSON object. >>> printSQL (jsonbObjectKeys (inline (Jsonb (object ["a" .= "foo", "b" .= "bar"])))) -jsonb_object_keys(('{"a":"foo","b":"bar"}' :: jsonb)) +jsonb_object_keys(('{"b":"bar","a":"foo"}' :: jsonb)) -} jsonbObjectKeys :: null 'PGjsonb -|-> From 9dfacf389c01ee50325b7de94bd592fb310c3752 Mon Sep 17 00:00:00 2001 From: Eitan Chatav Date: Mon, 27 Dec 2021 18:51:59 -0800 Subject: [PATCH 11/18] remove 8_6, add 9_0 support in CI --- .github/workflows/ci.yml | 42 ++++++++++++++++++++-------------------- stack-ghc8_6.yaml | 5 ----- 2 files changed, 21 insertions(+), 26 deletions(-) delete mode 100644 stack-ghc8_6.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e110238..ea674ba3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ on: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" - ghc8_10: + ghc9_0: # The type of runner that the job will run on runs-on: ubuntu-latest @@ -33,16 +33,16 @@ jobs: - uses: actions/checkout@v2 - uses: haskell/actions/setup@v1 with: - ghc-version: '8.10.5' # Exact version of ghc to use - # cabal-version: 'latest'. Omitted, but defalts to 'latest' enable-stack: true stack-version: 'latest' + stack-no-global: true + stack-setup-ghc: true - name: build - run: stack build --fast --stack-yaml stack-ghc8_10.yaml + run: stack build --fast - name: test - run: stack test --fast --stack-yaml stack-ghc8_10.yaml + run: stack test --fast env: PG_USER: postgres PG_HOST: localhost @@ -51,10 +51,10 @@ jobs: PG_PORT: ${{ job.services.postgres.ports['5432'] }} - name: benchmark - run: stack bench --fast --stack-yaml stack-ghc8_10.yaml + run: stack bench --fast - name: documentation - run: stack haddock --fast --stack-yaml stack-ghc8_10.yaml + run: stack haddock --fast - name: cache uses: actions/cache@v2 @@ -63,7 +63,7 @@ jobs: ".stack-work" "/root/.stack/" key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} - ghc8_8: + ghc8_10: # The type of runner that the job will run on runs-on: ubuntu-latest @@ -85,16 +85,16 @@ jobs: - uses: actions/checkout@v2 - uses: haskell/actions/setup@v1 with: - ghc-version: '8.8.4' # Exact version of ghc to use - # cabal-version: 'latest'. Omitted, but defalts to 'latest' enable-stack: true stack-version: 'latest' + stack-no-global: true + stack-setup-ghc: true - name: build - run: stack build --fast --stack-yaml stack-ghc8_8.yaml + run: stack build --fast --stack-yaml stack-ghc8_10.yaml - name: test - run: stack test --fast --stack-yaml stack-ghc8_8.yaml + run: stack test --fast --stack-yaml stack-ghc8_10.yaml env: PG_USER: postgres PG_HOST: localhost @@ -103,10 +103,10 @@ jobs: PG_PORT: ${{ job.services.postgres.ports['5432'] }} - name: benchmark - run: stack bench --fast --stack-yaml stack-ghc8_8.yaml + run: stack bench --fast --stack-yaml stack-ghc8_10.yaml - name: documentation - run: stack haddock --fast --stack-yaml stack-ghc8_8.yaml + run: stack haddock --fast --stack-yaml stack-ghc8_10.yaml - name: cache uses: actions/cache@v2 @@ -115,7 +115,7 @@ jobs: ".stack-work" "/root/.stack/" key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} - ghc8_6: + ghc8_8: # The type of runner that the job will run on runs-on: ubuntu-latest @@ -137,16 +137,16 @@ jobs: - uses: actions/checkout@v2 - uses: haskell/actions/setup@v1 with: - ghc-version: '8.6.5' # Exact version of ghc to use - # cabal-version: 'latest'. Omitted, but defalts to 'latest' enable-stack: true stack-version: 'latest' + stack-no-global: true + stack-setup-ghc: true - name: build - run: stack build --fast --stack-yaml stack-ghc8_6.yaml + run: stack build --fast --stack-yaml stack-ghc8_8.yaml - name: test - run: stack test --fast --stack-yaml stack-ghc8_6.yaml + run: stack test --fast --stack-yaml stack-ghc8_8.yaml env: PG_USER: postgres PG_HOST: localhost @@ -155,10 +155,10 @@ jobs: PG_PORT: ${{ job.services.postgres.ports['5432'] }} - name: benchmark - run: stack bench --fast --stack-yaml stack-ghc8_6.yaml + run: stack bench --fast --stack-yaml stack-ghc8_8.yaml - name: documentation - run: stack haddock --fast --stack-yaml stack-ghc8_6.yaml + run: stack haddock --fast --stack-yaml stack-ghc8_8.yaml - name: cache uses: actions/cache@v2 diff --git a/stack-ghc8_6.yaml b/stack-ghc8_6.yaml deleted file mode 100644 index 1eada539..00000000 --- a/stack-ghc8_6.yaml +++ /dev/null @@ -1,5 +0,0 @@ -resolver: lts-14.27 -packages: -- squeal-postgresql -- squeal-postgresql-ltree -- squeal-postgresql-uuid-ossp From dd46ecb88a9f833806d284514962287faa021547 Mon Sep 17 00:00:00 2001 From: Eitan Chatav Date: Wed, 26 Jan 2022 11:39:40 -0800 Subject: [PATCH 12/18] 1st pass at minimization --- README.md | 300 --- RELEASE NOTES.md | 1883 ----------------- scrap-your-nils.md | 60 - squeal-core-concepts-handbook.md | 980 --------- squeal-postgresql-ltree/LICENSE | 31 - squeal-postgresql-ltree/README.md | 1 - .../squeal-postgresql-ltree.cabal | 35 - .../src/Squeal/PostgreSQL/LTree.hs | 477 ----- squeal-postgresql-uuid-ossp/LICENSE | 31 - squeal-postgresql-uuid-ossp/README.md | 1 - .../squeal-postgresql-uuid-ossp.cabal | 29 - .../src/Squeal/PostgreSQL/UUID/OSSP.hs | 109 - squeal-postgresql/LICENSE | 31 - squeal-postgresql/README.md | 11 - squeal-postgresql/bench/Gauge.hs | 116 - squeal-postgresql/bench/Gauge/DBHelpers.hs | 67 - squeal-postgresql/bench/Gauge/DBSetup.hs | 134 -- squeal-postgresql/bench/Gauge/Queries.hs | 145 -- squeal-postgresql/bench/Gauge/Schema.hs | 93 - squeal-postgresql/bench/README.md | 9 - squeal-postgresql/docs-upload.sh | 18 - squeal-postgresql/exe/Example.hs | 170 -- squeal-postgresql/squeal-postgresql.cabal | 81 - squeal-postgresql/test/Property.hs | 232 -- squeal-postgresql/test/Spec.hs | 250 --- squeal-presentation-raveline.md | 794 ------- squeal-presentation.pdf | Bin 233684 -> 0 bytes squeal.gif | Bin 6399 -> 0 bytes stack-ghc8_10.yaml | 5 - stack-ghc8_8.yaml | 5 - stack.yaml | 2 - 31 files changed, 6100 deletions(-) delete mode 100644 README.md delete mode 100644 RELEASE NOTES.md delete mode 100644 scrap-your-nils.md delete mode 100644 squeal-core-concepts-handbook.md delete mode 100644 squeal-postgresql-ltree/LICENSE delete mode 100644 squeal-postgresql-ltree/README.md delete mode 100644 squeal-postgresql-ltree/squeal-postgresql-ltree.cabal delete mode 100644 squeal-postgresql-ltree/src/Squeal/PostgreSQL/LTree.hs delete mode 100644 squeal-postgresql-uuid-ossp/LICENSE delete mode 100644 squeal-postgresql-uuid-ossp/README.md delete mode 100644 squeal-postgresql-uuid-ossp/squeal-postgresql-uuid-ossp.cabal delete mode 100644 squeal-postgresql-uuid-ossp/src/Squeal/PostgreSQL/UUID/OSSP.hs delete mode 100644 squeal-postgresql/LICENSE delete mode 100644 squeal-postgresql/README.md delete mode 100644 squeal-postgresql/bench/Gauge.hs delete mode 100644 squeal-postgresql/bench/Gauge/DBHelpers.hs delete mode 100644 squeal-postgresql/bench/Gauge/DBSetup.hs delete mode 100644 squeal-postgresql/bench/Gauge/Queries.hs delete mode 100644 squeal-postgresql/bench/Gauge/Schema.hs delete mode 100644 squeal-postgresql/bench/README.md delete mode 100755 squeal-postgresql/docs-upload.sh delete mode 100644 squeal-postgresql/exe/Example.hs delete mode 100644 squeal-postgresql/test/Property.hs delete mode 100644 squeal-postgresql/test/Spec.hs delete mode 100644 squeal-presentation-raveline.md delete mode 100644 squeal-presentation.pdf delete mode 100644 squeal.gif delete mode 100644 stack-ghc8_10.yaml delete mode 100644 stack-ghc8_8.yaml diff --git a/README.md b/README.md deleted file mode 100644 index cace7055..00000000 --- a/README.md +++ /dev/null @@ -1,300 +0,0 @@ -# squeal - -![squeal-icon](https://raw.githubusercontent.com/morphismtech/squeal/dev/squeal.gif) - -[![GitHub CI](https://github.com/morphismtech/squeal/workflows/CI/badge.svg)](https://github.com/morphismtech/squeal/actions) - -[Github](https://github.com/morphismtech/squeal) - -[Hackage](https://hackage.haskell.org/package/squeal-postgresql) - -[Stackage](https://www.stackage.org/package/squeal-postgresql) - -[YouTube](https://www.youtube.com/watch?v=rWfEQfAaNc4) - -## introduction - -Squeal is a deep embedding of SQL into Haskell. By "deep embedding", I am abusing the -term somewhat. What I mean is that Squeal embeds both SQL terms and SQL types -into Haskell at the term and type levels respectively. This leads to a very high level -of type-safety in Squeal. - -Squeal embeds not just the structured query language of SQL but also the -data manipulation language and the data definition language; that's `SELECT`, -`INSERT`, `UPDATE`, `DELETE`, `WITH`, `CREATE`, `DROP`, and `ALTER` commands. - -Squeal expressions closely match their corresponding SQL expressions so that -the SQL they actually generate is completely predictable. They are also highly -composable and cover a large portion of SQL. - -## features - -* generic encoding of Haskell tuples and records into query parameters - and generic decoding of query results into Haskell records - using [`generics-sop`](https://hackage.haskell.org/package/generics-sop) -* access to SQL alias system using the `OverloadedLabels` extension -* type-safe `NULL` and `DEFAULT` -* type-safe SQL constraints `CHECK`, `UNIQUE`, `PRIMARY KEY` and `FOREIGN KEY` -* type-safe aggregation -* escape hatches for writing raw SQL -* [`mtl`](https://hackage.haskell.org/package/mtl) compatible monad transformer - for executing as well as preparing queries and manipulations - and [Atkey](https://bentnib.org/paramnotions-jfp.pdf) indexed monad transformer - for executing definitions. -* linear, pure or impure, one-way or rewindable migrations -* connection pools -* transactions -* views -* array, composite and enumerated types -* json functions and operations -* multischema support -* correlated subqueries -* window functions -* text search -* time functions -* ranges -* indexes -* inlining - -## installation - -`stack install squeal-postgresql` - -## testing - -Start postgres on localhost port `5432` and create a database named `exampledb`. - -`stack test` - -## contributing - -We welcome contributors. -Please make pull requests on the `dev` branch instead of `master`. -The `Issues` page is a good place to communicate. - -## usage - -Let's see an example! - -First, we need some language extensions because Squeal uses modern GHC -features. - -```Haskell ->>> :set -XDataKinds -XDeriveGeneric -XOverloadedLabels -XFlexibleContexts ->>> :set -XOverloadedStrings -XTypeApplications -XTypeOperators -XGADTs -``` - -We'll need some imports. - -```Haskell ->>> import Control.Monad.IO.Class (liftIO) ->>> import Data.Int (Int32) ->>> import Data.Text (Text) ->>> import Squeal.PostgreSQL -``` - -We'll use generics to easily convert between Haskell and PostgreSQL values. - -```Haskell ->>> import qualified Generics.SOP as SOP ->>> import qualified GHC.Generics as GHC -``` - -The first step is to define the schema of our database. This is where -we use `DataKinds` and `TypeOperators`. - -```Haskell ->>> :{ -type UsersColumns = - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "name" ::: 'NoDef :=> 'NotNull 'PGtext ] -type UsersConstraints = '[ "pk_users" ::: 'PrimaryKey '["id"] ] -type EmailsColumns = - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4 - , "email" ::: 'NoDef :=> 'Null 'PGtext ] -type EmailsConstraints = - '[ "pk_emails" ::: 'PrimaryKey '["id"] - , "fk_user_id" ::: 'ForeignKey '["user_id"] "public" "users" '["id"] ] -type Schema = - '[ "users" ::: 'Table (UsersConstraints :=> UsersColumns) - , "emails" ::: 'Table (EmailsConstraints :=> EmailsColumns) ] -type DB = Public Schema -:} -``` - -Notice the use of type operators. - -`:::` is used to pair an alias `Symbol` with a `SchemasType`, a `SchemumType`, -a `TableConstraint` or a `ColumnType`. It is intended to connote Haskell's `::` -operator. - -`:=>` is used to pair `TableConstraints` with a `ColumnsType`, -yielding a `TableType`, or to pair an `Optionality` with a `NullType`, -yielding a `ColumnType`. It is intended to connote Haskell's `=>` operator - -Next, we'll write `Definition`s to set up and tear down the schema. In -Squeal, a `Definition` like `createTable`, `alterTable` or `dropTable` -has two type parameters, corresponding to the schema -before being run and the schema after. We can compose definitions using `>>>`. -Here and in the rest of our commands we make use of overloaded -labels to refer to named tables and columns in our schema. - -```Haskell ->>> :{ -let - setup :: Definition (Public '[]) DB - setup = - createTable #users - ( serial `as` #id :* - (text & notNullable) `as` #name ) - ( primaryKey #id `as` #pk_users ) >>> - createTable #emails - ( serial `as` #id :* - (int & notNullable) `as` #user_id :* - (text & nullable) `as` #email ) - ( primaryKey #id `as` #pk_emails :* - foreignKey #user_id #users #id - (OnDelete Cascade) (OnUpdate Cascade) `as` #fk_user_id ) -:} -``` - -We can easily see the generated SQL is unsurprising looking. - -```Haskell ->>> printSQL setup -``` -```SQL -CREATE TABLE "users" ("id" serial, "name" text NOT NULL, CONSTRAINT "pk_users" PRIMARY KEY ("id")); -CREATE TABLE "emails" ("id" serial, "user_id" int NOT NULL, "email" text NULL, CONSTRAINT "pk_emails" PRIMARY KEY ("id"), CONSTRAINT "fk_user_id" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE); -``` - -Notice that `setup` starts with an empty public schema `(Public '[])` and produces `DB`. -In our `createTable` commands we included `TableConstraint`s to define -primary and foreign keys, making them somewhat complex. Our `teardown` -`Definition` is simpler. - -```Haskell ->>> :{ -let - teardown :: Definition DB (Public '[]) - teardown = dropTable #emails >>> dropTable #users -:} - ->>> printSQL teardown -``` -```SQL -DROP TABLE "emails"; -DROP TABLE "users"; -``` - -We'll need a Haskell type for `User`s. We give the type `Generics.SOP.Generic` and -`Generics.SOP.HasDatatypeInfo` instances so that we can encode and decode `User`s. - -```Haskell ->>> :set -XDerivingStrategies -XDeriveAnyClass ->>> :{ -data User = User { userName :: Text, userEmail :: Maybe Text } - deriving stock (Show, GHC.Generic) - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) -:} -``` - -Next, we'll write `Statement`s to insert `User`s into our two tables. -A `Statement` has three type parameters, the schemas it refers to, -input parameters and an output row. When -we insert into the users table, we will need a parameter for the `name` -field but not for the `id` field. Since it's serial, we can use a default -value. However, since the emails table refers to the users table, we will -need to retrieve the user id that the insert generates and insert it into -the emails table. We can do this in a single `Statement` by using a -`with` `manipulation`. - -```Haskell ->>> :{ -let - insertUser :: Statement DB User () - insertUser = manipulation $ with (u `as` #u) e - where - u = insertInto #users - (Values_ (Default `as` #id :* Set (param @1) `as` #name)) - OnConflictDoRaise (Returning_ (#id :* param @2 `as` #email)) - e = insertInto_ #emails $ Select - (Default `as` #id :* Set (#u ! #id) `as` #user_id :* Set (#u ! #email) `as` #email) - (from (common #u)) -:} -``` - -```Haskell ->>> printSQL insertUser -``` -```SQL -WITH "u" AS (INSERT INTO "users" ("id", "name") VALUES (DEFAULT, ($1 :: text)) RETURNING "id" AS "id", ($2 :: text) AS "email") INSERT INTO "emails" ("user_id", "email") SELECT "u"."id", "u"."email" FROM "u" AS "u" -``` - -Next we write a `Statement` to retrieve users from the database. We're not -interested in the ids here, just the usernames and email addresses. We -need to use an `innerJoin` to get the right result. - -```Haskell ->>> :{ -let - getUsers :: Statement DB () User - getUsers = query $ select_ - (#u ! #name `as` #userName :* #e ! #email `as` #userEmail) - ( from (table (#users `as` #u) - & innerJoin (table (#emails `as` #e)) - (#u ! #id .== #e ! #user_id)) ) -:} -``` - -```Haskell ->>> printSQL getUsers -``` -```SQL -SELECT "u"."name" AS "userName", "e"."email" AS "userEmail" FROM "users" AS "u" INNER JOIN "emails" AS "e" ON ("u"."id" = "e"."user_id") -``` - -Let's create some users to add to the database. - -```Haskell ->>> :{ -let - users :: [User] - users = - [ User "Alice" (Just "alice@gmail.com") - , User "Bob" Nothing - , User "Carole" (Just "carole@hotmail.com") - ] -:} -``` - -Now we can put together all the pieces into a program. The program -connects to the database, sets up the schema, inserts the user data -(using prepared statements as an optimization), queries the user -data and prints it out and finally closes the connection. We can thread -the changing schema information through by using the indexed `PQ` monad -transformer and when the schema doesn't change we can use `Monad` and -`MonadPQ` functionality. - -```Haskell ->>> :{ -let - session :: PQ DB DB IO () - session = do - executePrepared_ insertUser users - usersResult <- execute getUsers - usersRows <- getRows usersResult - liftIO $ print usersRows -in - withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $ - define setup - & pqThen session - & pqThen (define teardown) -:} -[User {userName = "Alice", userEmail = Just "alice@gmail.com"},User {userName = "Bob", userEmail = Nothing},User {userName = "Carole", userEmail = Just "carole@hotmail.com"}] -``` - -This should get you up and running with Squeal. Once you're writing more complicated -queries and need a deeper understanding of Squeal's types and how everything -fits together, check out the [Core Concepts Handbook](squeal-core-concepts-handbook.md). diff --git a/RELEASE NOTES.md b/RELEASE NOTES.md deleted file mode 100644 index b7e3c0da..00000000 --- a/RELEASE NOTES.md +++ /dev/null @@ -1,1883 +0,0 @@ -## RELEASE NOTES - -## Version 0.8.1.1 - -Fix a bug in how the new `Has` type mismatch errors -were implemented, which made it do the expensive pretty-printing -even in the non-error case, resulting in extreme memory usage -at compile time for non-trivial cases. - -## Version 0.8.1.0 - -Improvements to type errors for `Has`/`HasErr`, `HasParameter`, -and trying to aggregate without grouping. - -### `Has` -#### Lookup failed -Now tells you specifically that lookup failed, -the kind of thing we were trying to look up and in what, -and a pretty-printed (usually, alphabetized and names-only) -version of what we were looking in. -``` -exe/Example.hs:112:11-41: error: - • Could not find table, view, typedef, index, function, or procedure (SchemumType) named "sers" - in schema (SchemaType): - Tables: - '["emails", "users"] - - - *Raw schema (SchemaType)*: - '[ '("users", - 'Table - ('["pk_users" ::: 'PrimaryKey '["id"]] - :=> '["id" ::: ('Def :=> 'NotNull 'PGint4), - "name" ::: ('NoDef :=> 'NotNull 'PGtext), - "vec" ::: ('NoDef :=> 'NotNull ('PGvararray ('Null 'PGint2)))])), - "emails" - ::: 'Table - ('["pk_emails" ::: 'PrimaryKey '["id"], - "fk_user_id" ::: 'ForeignKey '["user_id"] "user" "users" '["id"]] - :=> '["id" ::: ('Def :=> 'NotNull 'PGint4), - "user_id" ::: ('NoDef :=> 'NotNull 'PGint4), - "email" ::: ('NoDef :=> 'Null 'PGtext)])] - - • In the first argument of ‘(&)’, namely - ‘table ((#user ! #sers) `as` #u)’ - In the first argument of ‘from’, namely - ‘(table ((#user ! #sers) `as` #u) - & innerJoin - (table ((#user ! #emails) `as` #e)) (#u ! #id .== #e ! #user_id))’ - In the second argument of ‘select_’, namely - ‘(from - (table ((#user ! #sers) `as` #u) - & innerJoin - (table ((#user ! #emails) `as` #e)) (#u ! #id .== #e ! #user_id)))’ - | -112 | ( from (table ((#user ! #sers) `as` #u) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -exe/Example.hs:(106,15)-(107,92): error: - • Could not find schema (SchemaType) named "use" - in database (SchemasType): - '["org", "public", "user"] - - *Raw database (SchemasType)*: - '[ '("public", PublicSchema), "user" ::: UserSchema, - "org" ::: OrgSchema] - - • In the expression: - insertInto_ - (#use ! #emails) - (Values_ - (Default `as` #id - :* Set (param @1) `as` #user_id :* Set (param @2) `as` #email)) - In an equation for ‘insertEmail’: - insertEmail - = insertInto_ - (#use ! #emails) - (Values_ - (Default `as` #id - :* Set (param @1) `as` #user_id :* Set (param @2) `as` #email)) - | -106 | insertEmail = insertInto_ (#use ! #emails) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^... -``` -#### Lookup succeeded, but types don't match -Now specifies the lookup that the type mismatch comes from with kind -information. Does not pretty-print because the value of the key is important. -``` -exe/Example.hs:111:4-12: error: - • Type mismatch when looking up column (NullType) named "vec" - in row (RowType): - '[ '("id", 'NotNull 'PGint4), "name" ::: 'NotNull 'PGtext, - "vec" ::: 'NotNull ('PGvararray ('Null 'PGint2))] - - Expected: 'NotNull 'PGtext - But found: 'NotNull ('PGvararray ('Null 'PGint2)) - - • In the first argument of ‘as’, namely ‘#u ! #vec’ - In the first argument of ‘(:*)’, namely ‘#u ! #vec `as` #userName’ - In the first argument of ‘select_’, namely - ‘(#u ! #vec `as` #userName - :* #e ! #email `as` #userEmail :* #u ! #vec `as` #userVec)’ - | -111 | (#u ! #vec `as` #userName :* #e ! #email `as` #userEmail :* #u ! #vec `as` #userVec) - | ^^^^^^^^^ -``` -#### Ambiguous types -Generally identical, except complains about being unable to satisfy `Has` instead of exposing `HasErr` -``` - -exe/Example.hs:103:16-37: error: - • Ambiguous type variables ‘constraints0’, - ‘constraint0’ arising from a use of ‘OnConstraint’ - prevents the constraint ‘(Has - "pk_users" constraints0 constraint0)’ from being solved. - Probable fix: use a type annotation to specify what ‘constraints0’, - ‘constraint0’ should be. - These potential instances exist: - three instances involving out-of-scope types - (use -fprint-potential-instances to see them all) - • In the first argument of ‘OnConflict’, namely - ‘(OnConstraint #pk_users)’ - In the third argument of ‘insertInto’, namely - ‘(OnConflict (OnConstraint #pk_users) DoNothing)’ - In the expression: - insertInto - (#user ! #users) - (Values_ - (Default `as` #id - :* Set (param @1) `as` #name :* Set (param @2) `as` #vec)) - (OnConflict (OnConstraint #pk_users) DoNothing) - (Returning_ (#id `as` #fromOnly)) - | -103 | (OnConflict (OnConstraint #pk_users) DoNothing) (Returning_ (#id `as` #fromOnly)) - | ^^^^^^^^^^^^^^^^^^^^^^ -``` -### `HasParameter` -#### Looking up index 0 -Now gives a special error about params being 1-indexed. -``` -exe/Example.hs:118:18-25: error: - • Tried to get the param at index 0, but params are 1-indexed - • In the first argument of ‘Set’, namely ‘(param @0)’ - In the first argument of ‘as’, namely ‘Set (param @0)’ - In the first argument of ‘(:*)’, namely ‘Set (param @0) `as` #id’ - | -118 | (Values_ (Set (param @0) `as` #id :* setUser)) - | ^^^^^^^^ -``` -#### Looking up an out-of-bounds parameter -Now gives a special error, returning the entire parameter list as well. -Does not pretty-print since order is important, and there's no separate -keys and values. -``` - -exe/Example.hs:118:18-25: error: - • Index 4 is out of bounds in 1-indexed parameter list: - '[ 'NotNull 'PGint4, 'NotNull 'PGtext, - 'NotNull ('PGvararray ('Null 'PGint2))] - • In the first argument of ‘Set’, namely ‘(param @4)’ - In the first argument of ‘as’, namely ‘Set (param @4)’ - In the first argument of ‘(:*)’, namely ‘Set (param @4) `as` #id’ - | -118 | (Values_ (Set (param @4) `as` #id :* setUser)) - | ^^^^^^^^ -``` -#### Type mismatch when doing lookup -Now gives a custom error similar to the one added for `Has`. -``` -exe/Example.hs:118:18: error: - • Type mismatch when looking up param at index 2 - in 1-indexed parameter list: - '[ 'NotNull 'PGint4, 'NotNull 'PGtext, - 'NotNull ('PGvararray ('Null 'PGint2))] - - Expected: 'NotNull 'PGtext - But found: 'NotNull 'PGint4 - - • In the first argument of ‘Set’, namely ‘(param @2)’ - In the first argument of ‘as’, namely ‘Set (param @2)’ - In the first argument of ‘(:*)’, namely ‘Set (param @2) `as` #id’ - | -118 | (Values_ (Set (param @2) `as` #id :* setUser)) - | ^^^^^^^^ -``` -### Using aggregates with an `'Ungrouped` `Expression` -Now gives a custom error with some guidance. -``` -exe/Example.hs:118:4: error: - • Cannot use aggregate functions to construct an Ungrouped Expression. Add a 'groupBy' to your TableExpression. If you want to aggregate across the entire result set, use 'groupBy Nil'. - • In the first argument of ‘as’, namely ‘countStar’ - In the first argument of ‘(:*)’, namely ‘countStar `as` #count’ - In the first argument of ‘select_’, namely - ‘(countStar `as` #count :* Nil)’ - | -118 | (countStar `as` #count :* Nil) - | ^^^^^^^^^ -``` - -### Version 0.8 - -Thanks to Adam Wespiser, Cullin Poresky, Scott Fleischman -and William Yao for lots of contributions. - -### Materialized CTEs - -Scott Fleischman contributed materialization support to Squeal's -WITH statements. - -### LTrees and UUID - -New packages `squeal-postgresql-ltree` and `squeal-postgresql-uuid-ossp` -were created to offer functionality from those Postgres extensions. - -### Safe Transactions - -Previously, Squeal transactions were "unsafe", allowing for arbitrary -`IO`. Now, Squeal provides a new type `Transaction` that is a RankNType. - -```Haskell -type Transaction db x = forall m. - ( MonadPQ db m - , MonadResult m - , MonadCatch m - ) => m x -``` - -A `Transaction` only permits database operations and error handling, -no arbitrary `IO`. The class `MonadResult` is new but all of its -methods are old and used to be constrained as `MonadIO`, -now as `MonadResult`. - -Additionally, a new function `withSavepoint` was added, allowing -for a kind of nested transactions. - -### Bug fixes - -Various bugs were fixed. Most importantly, poor asynchronous exception -handling was ameliorated. - -### Version 0.7 - -Thanks to Samuel Schlesinger, Adam Wespiser, Cullin Poresky, -Matthew Doty and Mark Wotton for tons of contributions. -Version 0.7 of Squeal makes many changes. - -**Inter-schema Foreign Key Bug** -Unfortunately, there was a bug in inter-schema foreign keys in previous -versions of Squeal. Essentially, it was erroneously assumed that -foreign keys always point to tables in the public schema. To remedy this -the `ForeignKey` type has changed kind from - -```Haskell ->>> :kind 'ForeignKey -'ForeignKey :: [Symbol] - -> Symbol -> [Symbol] -> TableConstraint -``` - -to - -```Haskell ->>> :kind 'ForeignKey -'ForeignKey :: [Symbol] - -> Symbol -> Symbol -> [Symbol] -> TableConstraint -``` - -To upgrade your database schemas type, you will have to change, e.g. - -```Haskell -'ForeignKey '["foo_id1", "foo_id2"] "foo" '["id1", "id2"] -``` - -to - -```Haskell -'ForeignKey '["foo_id1", "foo_id2"] "public" "foo" '["id1", "id2"] -``` - -**Locking Clauses** - -You can now add row level locking clauses to your `select` queries - -**Polymorphic Lateral Contexts** - -Previously, lateral contexts which are used for lateral joins -and subquery expressions had to have monomorphic lateral contexts, -which greatly reduced composability of queries involving lateral -joins. Squeal 0.7 fixes this limitation, making it possible to -have polymorphic lateral context! When looking up a column heretofore, -the relevant typeclasses would search through `Join lat from`. -This is the "correct" ordering as far as the structure from -left to right in the query, making lat consistently ordered as -one goes through nested lateral joins or nested subquery expressions. -However, it doesn't really matter how the lookup orders the columns. -And if the lookup searches through Join from lat instead then thanks -to good old Haskell lazy list appending, if a query only references -columns in from then it will work no matter the lat. -With a small proviso; if you leave lat polymorphic, -then you must qualify all columns since there could be more than -one table even if from has only one table in it. - -**Decoders** - -The `DecodeRow` `Monad` now has a `MonadFail` instance. - -New row decoder combinators have been added. The functions -`appendRows` and `consRow` let you build row decoders up -from pieces. - -Previously, Squeal made it easy to decode enum types to Haskell -enum types (sum types with nullary constructors) so long as -the Haskell type exactly matches the enum type. However, because -of limitations in Haskell - constructors must be capitalized, -name conflicts are often disambiguated with extra letters, etc - -it's often the case that their constructors won't exactly match the -Postgres enum type's labels. The new function `enumValue` allows -to define typesafe custom enum decoders, similar to how `rowValue` -allows to define typesafe custom composite decoders. - -```Haskell ->>> :{ -data Dir = North | East | South | West -instance IsPG Dir where - type PG Dir = 'PGenum '["north", "south", "east", "west"] -instance FromPG Dir where - fromPG = enumValue $ - label @"north" North :* - label @"south" South :* - label @"east" East :* - label @"west" West -:} -``` - -**Definitions** - -New DDL statements have been added allowing to rename and -reset the schema of different schemum objects. Also, new DDL statements -have been added for adding comments to schemum objects. - -**Procedures** - -Squeal now supports procedure definitions and calls. - -**cmdTuples and cmdStatus** - -The `cmdTuples` and `cmdStatus` functions from `LibPQ` are now -included. - -**PQ Monad Instances** - -the `PQ` `Monad` has been given instances for `MonadCatch`, -`MonadThrow`, `MonadMask`, `MonadBase`, `MonadBaseControl`, and -`MonadTransControl`. - -**Referential Actions** - -A new type `ReferentialAction` has been factored out of -`OnDeleteClause`s and `OnUpdateClause`s. And Missing actions, -`SetNotNull` and `SetDefault` are now included. - -To upgrade, change from e.g. `OnDeleteCascade` to `OnDelete Cascade`. - -**Array functions** - -Squeal now offers typesafe indexing for fixed length arrays and matrices, -with new functions `index1` and `index2`. And new functions `arrAny` -and `arrAll` have been added to enable comparisons to any or all elements -of a variable length array. - -**Manipulations** - -Tables being manipulated are now re-aliasable, and updates can reference -"from" clauses, actually called `UsingClause`s in Squeal, similar to deletes. - -**Other changes** -New tests and bugfixes have been added. More support for encoding and decoding -of different types has been added. Time values now use `iso8601` formatting -for inlining. Also, the GitHub repo has moved from using Circle CI to using -GitHub Actions for continuous integration testing. - -### Version 0.6 - -Version 0.6 makes a number of large changes and additions to Squeal. -I want to thank folks who contributed issues and pull requests; -ilyakooo0, tuomohopia, league, Raveline, Sciencei, mwotton, and more. - -I particularly would like to thank my employer SimSpace and colleagues. -We are actively using Squeal at SimSpace which has pushed its development. - -My colleague Mark Wotton has also created a project -[squealgen](https://github.com/mwotton/squealgen) to generate -a Squeal schema directly from the database which is awesome. - -**Module hierarchy** - -Squeal had been growing some rather large modules, whereas I prefer -sub-thousand line modules. Accordingly, I split up the module -hierarchy further. This means there's 60 modules which looks a little -overwhelming, but I think it makes it easier to locate functionality. -It also makes working in a single module less overwhelming. -All relevant functionality is still being exported by `Squeal.PostgreSQL`. - -**Statement Profunctors** - -Squeal's top level queries and manipulations left something to be desired. -Because `Query_` and `Manipulation_` were type families, they could be -a bit confusing to use. For instance, - -```Haskell ->>> :{ -selectUser :: Query_ DB UserId User -selectUser = select_ - (#id `as` #userId :* #name `as` #userName) - (from (table #users) & where_ (#id .== param @1)) -:} ->>> :t selectUser -selectUser - :: Query - '[] - '[] - '["public" ::: '["users" ::: 'Table ('[] :=> UsersColumns)]] - '[ 'NotNull 'PGint4] - '["userId" ::: 'NotNull 'PGint4, "userName" ::: 'NotNull 'PGtext] -``` - -So the `UserId` and `User` types are completely replaced by corresponding -Postgres types. This means that the query can be run, for instance, -with any parameter that is a generic singleton container of `Int32`. -We've lost apparent type safety. You could accidentally run `selectUser` -with a `WidgetId` parameter instead of a `UserId` and it could typecheck. - -That's because `Query` is a pure SQL construct, with no knowledge for -how to encode or decode Haskell values. - -Another annoyance of `Query_` and `Manipulation_` is that they _must_ -be applied to Haskell types which exactly match their corresponding -Postgres types. So, in practice, you often end up with one-off -data type definitions just to have a type that exactly matches, -having the same field names, and the same ordering, etc. as the -returned row. - -Both of these issues are solved with the new `Statement` type. Let's -see its definition. - -```Haskell -data Statement db x y where - Manipulation - :: (SOP.All (OidOfNull db) params, SOP.SListI row) - => EncodeParams db params x - -> DecodeRow row y - -> Manipulation '[] db params row - -> Statement db x y - Query - :: (SOP.All (OidOfNull db) params, SOP.SListI row) - => EncodeParams db params x - -> DecodeRow row y - -> Query '[] '[] db params row - -> Statement db x y -``` - -You can see that a `Statement` bundles either a `Query` or a `Manipulation` -together with a way to `EncodeParams` and a way to `DecodeRow`. This -ties the statement to actual Haskell types. Going back to the example, - -```Haskell ->>> :{ -selectUser :: Statement DB UserId User -selectUser = query $ select_ - (#id `as` #userId :* #name `as` #userName) - (from (table #users) & where_ (#id .== param @1)) -:} -``` - -Now we really do have the type safety of only being able to `executeParams` -`selectUser` with a `UserId` parameter. Here we've used the smart -constructor `query` which automatically uses the generic instances of -`UserId` and `User` to construct a way to `EncodeParams` and a way to -`DecodeRow`. We can use the `Query` constructor to do custom encodings -and decodings. - -```Haskell ->>> :{ -selectUser :: Statement DB UserId (UserId, Text) -selectUser = Query enc dec sql where - enc = contramap getUserId aParam - dec = do - uid <- #id - uname <- #name - return (uid, uname) - sql = select Star (from (table #users) & where_ (#id .== param @1)) -:} -``` - -`EncodeParams` and `DecodeRow` both have convenient APIs. `EncodeParams` -is `Contravariant` and can be composed with combinators. `DecodeRow` -is a `Monad` and has `IsLabel` instances. Since `Statement`s bundle -both together, they form `Profunctor`s, where you can `lmap` over -parameters and `rmap` over rows. - -The `Statement` `Profunctor` is heavily influenced by -the `Statement` `Profunctor` from Nikita Volkov's excellent `hasql` library, -building on the use of `postgresql-binary` for encoding and decoding. - -**Deriving** - -Many Haskell types have corresponding Postgres types like `Double` -corresponds to `float8`. Squeal makes this an open relationship with the -`PG` type family. Squeal 0.6 makes it easy to generate `PG` of your -Haskell types, though you might have to turn on `-XUndecidableInstances`, -by deriving an `IsPG` instance. -In addition to having a corresponding Postgres type, -to fully embed your Haskell type you want instances of `ToPG db` to -encode your type as an out-of-line parameter, `FromPG` to -decode your type from a result value, and `Inline` to inline -values of your type directly in SQL statements. - -```Haskell ->>> :{ -newtype CustomerId = CustomerId {getCustomerId :: Int32} - deriving newtype (IsPG, ToPG db, FromPG, Inline) -:} - ->>> :kind! PG CustomerId -PG CustomerId :: PGType -= 'PGint4 -``` - -You can even embed your Haskell records and enum types using -deriving via. - -```Haskell ->>> :{ -data Complex = Complex {real :: Double, imaginary :: Double} - deriving stock (GHC.Generic) - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) - deriving (IsPG, ToPG db, FromPG, Inline) via Composite Complex -:} - ->>> :kind! PG Complex -PG Complex :: PGType -= 'PGcomposite - '["real" ::: 'NotNull 'PGfloat8, - "imaginary" ::: 'NotNull 'PGfloat8] - ->>> printSQL (inline (Complex 0 1)) -ROW((0.0 :: float8), (1.0 :: float8)) - ->>> :{ -data Answer = Yes | No - deriving stock (GHC.Generic) - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) - deriving (IsPG, ToPG db, FromPG, Inline) via Enumerated Answer -:} - ->>> :kind! PG Answer -PG Answer :: PGType -= 'PGenum '["Yes", "No"] - ->>> printSQL (inline Yes) -'Yes' -``` - -You can also embed your types encoded as `Json` or `Jsonb`. - -```Haskell ->>> :{ -data Foo = Bar Int | Baz Char Text - deriving stock (GHC.Generic) - deriving anyclass (ToJSON, FromJSON) - deriving (IsPG, ToPG db, FromPG, Inline) via Jsonb Foo -:} - ->>> :kind! PG Foo -PG Foo :: PGType -= 'PGjsonb - ->>> printSQL (inline (Baz 'a' "aaa")) -('{"tag":"Baz","contents":["a","aaa"]}' :: jsonb) -``` - -One thing to notice about `ToParam db` is that it has an -extra parameter `db` that the other classes don't have. That's -because for some types, such as arrays and composites, you -need to know the OID of the element types in order to unambiguously -encode those types. And if the element types are user defined, -then they have to be looked up in the database. The extra parameter -lets us look through the schema for a matching type, and then -look up that type's OID. - -**Migrations** - -Previously Squeal migrations could be either pure, involving only -data definitions, or impure, allowing arbitrary `IO`. But, they -had to be rewindable; that is, every migration step had to have -an inverse. Squeal 0.6 generalizes to allow both invertible and -one-way migrations. The core datatype for migrations, the free -category `Path` has been moved to its own package `free-categories`. - -**Aggregation** - -Squeal 0.6 enables filtering and ordering for aggregate -arguments and filtering for window function arguments. - -```Haskell -arrayAgg (All #col & orderBy [AscNullsFirst #col] & filterWhere (#col .< 100)) -``` - -To upgrade existing code, if you have an aggregate with multiple arguments, -use `Alls` instead of `All` or `Distincts` instead of `Distinct` -and if you have a window function, apply either `Window` or `Windows` -to its argument(s). Additionally, convenient functions `allNotNull` and -`distinctNotNull` safely filter out `NULL`. - -**Ranges** - -Squeal 0.6 adds both Haskell and corresponding Postgres range types. - -```Haskell -data Bound x - = Infinite -- ^ unbounded - | Closed x -- ^ inclusive - | Open x -- ^ exclusive - -data Range x = Empty | NonEmpty (Bound x) (Bound x) - -(<=..<=), (<..<), (<=..<), (<..<=) :: x -> x -> Range x -moreThan, atLeast, lessThan, atMost :: x -> Range x -singleton :: x -> Range x -whole :: Range x -``` - -**Indexes and functions** - -Squeal 0.6 adds support for creating and dropping user defined -indexes and functions to your schema, which can then be used -in statements. - -**Lateral joins** - -Squeal 0.6 adds support for lateral joins, which may reference previous -items. - -**Null handling** - -Some null handling functions were added such as `monoNotNull` -and `unsafeNotNull`. Because Squeal is aggressively `NULL` polymorphic, -sometimes inference errors can occur. You can apply `monoNotNull` -to fix something to be not `NULL`. You can apply `unsafeNotNull` -when you know that something can't be `NULL`, for instance if you've -filtered `NULL` out of a column. - -**Other changes** - -Lots of other things changed. `Literal` and `literal` are now called -`Inline` and `inline`. `ColumnConstraint` is called `Optionality`. -`NullityType`s are called `NullTypes`. -Squeal 0.6 adds support for domain types. It more carefully types -`CREATE _ IF NOT EXISTS` and `DROP _ IF EXISTS` definitions. The -`Exception` type was refactored to remove `Maybe`s and new pattern -synonyms were defined to easily match on a few common SQL errors. -`VarChar` and `FixChar` types were added with smart constructors. -Many bugs were fixed. Also, many more tests were added and -a new benchmark suite. A lot more things were changed that I've -probably forgotten about. - -### Version 0.5.2 - -Fixes a bug in pool API and implementation. - -### Version 0.5 - -Version 0.5 makes a number of large changes and additions to Squeal. -I want to thank folks who contributed issues and pull requests; -Raveline, rimmington, ilyakooo0, chshersh, reygoch, and more. - -**Multi-schema support** - -Previous versions of Squeal only supported definitions in the "public" -schema. Squeal 0.5 adds support for multiple schemas with the new kind - -```Haskell -type SchemasType = [(Symbol,SchemaType)] -``` - -In order to upgrade your queries, manipulations and definitions from -Squeal 0.4, you will have to apply the `Public` type family, which indicates -that your only schema is the "public" schema. - -```Haskell --- Squeal 0.4 -setup :: Definition '[] Schema - --- Squeal 0.5 -setup :: Definition (Public '[]) (Public Schema) -``` - -You can create non-public schemas using `createSchema` and inversely -remove them using `dropSchema`. There is also an idempotent -`createSchemaIfNotExists`. - -In order to handle aliases which may refer to non-public schemas, Squeal 0.5 -introduces `QualifiedAlias`es. A `QualifiedAlias` can be referred to using -the `(!)` operator from the `IsQualified` typeclass; but if you want to refer -to an alias from the "public" schema, you can continue to use a single -overloaded label, no need to write `#public ! #tab`, just write `#tab`. - -**Top-level statements** - -As a consequence of multi-schema support, common table expressions that -a `Query` or `Manipulation` may refer to had to be broken into a new parameter. -Additionally, `Query` gained another new parameter for its outer scope which -will be discussed in the next section. - -To simplify type signatures that you have to write for top-level queries -and manipulations, Squeal 0.5 introduces the `Query_` and `Manipulation_` -type families. - -``` -type family Query_ - (schemas :: SchemasType) - (parameters :: Type) - (row :: Type) where - Query_ schemas params row = - Query '[] '[] schemas (TuplePG params) (RowPG row) -``` - -As you see, a top-level `Query_` has no outer scope and no common -table expressions in scope, they are empty `'[]`. Moreover, -its parameters and row parameters are now `Type`s, which are -converted to corresponding Postgres types using the `TuplePG` -and `RowPG` type families. This means you can write the type signatures -for your top-level statements using the Haskell types that you -intend to input and retrieve when running the statements. Similar -to `Query_`, there is a `Manipulation_` type, the only difference -being that a general `Manipulation` doesn't have an outer scope, -only a scope for common table expressions. - -**Subquery expressions** - -Previous versions of Squeal offered subquery expression support -but there were issues. The common use case of using the `IN` operator -from SQL was overly complex. There was a proliferation of subquery -functions to handle different comparison operators, each with two versions -for comparing `ALL` or `ANY` of the query rows. And, most confoundedly, -there was no way to support the `EXISTS` subquery because the "outer scope" -for a query was not available. - -Squeal adds an outer scope to the `Query` type. - -```Haskell -newtype Query - (outer :: FromType) - (commons :: FromType) - (schemas :: SchemasType) - (params :: [NullityType]) - (row :: RowType) - = UnsafeQuery { renderQuery :: ByteString } -``` - -This enables a well-typed `exists` function. - -```Haskell -exists - :: Query (Join outer from) commons schemas params row - -> Condition outer commons grp schemas params from -``` - -The `in_` and `notIn` functions were simplified to handle the most -common use case of comparing to a list of values. - -```Haskell -in_ - :: Expression outer commons grp schemas params from ty -- ^ expression - -> [Expression outer commons grp schemas params from ty] - -> Condition outer commons grp schemas params from -``` - -Finally, general subquery comparisons were abstracted to work with -any comparison operators, using the `Operator` type which will be discussed in -the next section. - -```Haskell -subAll - :: Expression outer commons grp schemas params from ty1 - -> Operator ty1 ty2 ('Null 'PGbool) - -> Query (Join outer from) commons schemas params '[col ::: ty2] - -> Condition outer commons grp schemas params from - -subAny - :: Expression outer commons grp schemas params from ty1 - -> Operator ty1 ty2 ('Null 'PGbool) - -> Query (Join outer from) commons schemas params '[col ::: ty2] - -> Condition outer commons grp schemas params from -``` - -**Expression RankNTypes** - -Squeal 0.5 introduces RankNType type synonyms for common expression patterns. -The simplest example is the `Expr` type, a type for "closed" expressions -that cannot reference any aliases or parameters. - -```Haskell -type Expr x - = forall outer commons grp schemas params from - . Expression outer commons grp schemas params from x -``` - -There is also a function type `(-->)`, which is a subtype of the usual Haskell function -type `(->)`. - -```Haskell -type (-->) x y - = forall outer commons grp schemas params from - . Expression outer commons grp schemas params from x - -> Expression outer commons grp schemas params from y -``` - -We saw in the subquery section that there is an `Operator` type. - -```Haskell -type Operator x1 x2 y - = forall outer commons grp schemas params from - . Expression outer commons grp schemas params from x1 - -> Expression outer commons grp schemas params from x2 - -> Expression outer commons grp schemas params from y -``` - -There are also types `FunctionN` and `FunctionVar` for n-ary functions -and variadic functions. - -``` -type FunctionN xs y - = forall outer commons grp schemas params from - . NP (Expression outer commons grp schemas params from) xs - -> Expression outer commons grp schemas params from y -``` - -An n-ary function takes an `NP` list of `Expression`s as its argument. -Squeal 0.5 adds a helpful operator `(*:)`, to help scrap your `Nil`s. -You can construct an `NP` list now by using the operator `(:*)`, -until your last element, using `(*:)` there. For instance, the function -`atan2_` takes two arguments, `atan2_ (pi *: 2)`. - -**Selections** - -Previously, Squeal provided a couple versions of `SELECT` depending -on whether you wanted to select all columns of a unique table -in the from clause, i.e. `*`, all columns of a particular table -in the from clause, i.e. `.*`, or a list of columns from the from clause. - -Squeal 0.5 refactors this pattern into a `Selection` GADT type, allowing -for abstraction and combinations that were not possible before. - -```Haskell -data Selection outer commons grp schemas params from row where - Star - :: HasUnique tab from row - => Selection outer commons 'Ungrouped schemas params from row - DotStar - :: Has tab from row - => Alias tab - -> Selection outer commons 'Ungrouped schemas params from row - List - :: SListI row - => NP (Aliased (Expression outer commons grp schemas params from)) row - -> Selection outer commons grp schemas params from row - Over - :: SListI row - => NP (Aliased (WindowFunction outer commons grp schemas params from)) row - -> WindowDefinition outer commons grp schemas params from - -> Selection outer commons grp schemas params from row - Also - :: Selection outer commons grp schemas params from right - -> Selection outer commons grp schemas params from left - -> Selection outer commons grp schemas params from (Join left right) -``` - -In addition to the `Star`, `DotStar` and `List` constructors, there is an -`Also` constructor combinator, which enables users to combine `Selection`s. -Additionally, there is an `Over` constructor that is used to enable -window functions, described in the next section. - -To upgrade from Squeal 0.4, you will replace `selectStar` with `select Star`, -replace `selectDotStar #tab` with `select (DotStar #tab)`. The `List` -selection is such a common use case that there is a function `select_` which -automatically applies it, so to upgrade from Squeal 0.4, replace `select` -with `select_`. There are also independent `selectDistinct` and `selectDistinct_` -functions to filter out duplicate rows. - -**Query clauses** - -Previously, Squeal provided a couple versions of `INSERT` depending -on whether you wanted to insert `VALUES` or a query. - -Squeal 0.5 refactors this pattern into a `QueryClause` GADT. - -``` --- Squeal 0.4 -insertRow_ #tab (Set 2 `as` #col1 :* Default `as` #col2) -insertQuery_ #tab (selectStar (from (table #other_tab))) - --- Squeal 0.5 -insertInto_ #tab (Values_ (Set 2 `as` #col1 :* Default `as` #col2)) -insertInto_ #tab (Subquery (select Star (from (table #other_tab)))) -``` - -**Window functions and aggregation** - -Previous versions of Squeal provided no support for window functions. -Squeal 0.5 adds support for window functions. We saw `Over` in the -previous section. - -```Haskell -Over - :: SListI row - => NP (Aliased (WindowFunction outer commons grp schemas params from)) row - -> WindowDefinition outer commons grp schemas params from - -> Selection outer commons grp schemas params from row -``` - -`Over` combines window functions with window definitions. A `WindowDefinition` -is constructed using the `partitionBy` function, optionally with an `orderBy` -clause. For example, - -```Haskell -query :: Query_ (Public Schema) () (Row Int32 Int32) -query = select - (#col1 & Also (rank `as` #col2 `Over` (partitionBy #col1 & orderBy [#col2 & Asc]))) - (from (table #tab)) -``` - -Here the `rank` function is a `WindowFunction` - -```Haskell -rank :: WinFun0 ('NotNull 'PGint8) -rank = UnsafeWindowFunction "rank()" -``` - -`WinFun0` is a RankNType, like `Expr` was, used for no argument window functions. -Similarly, there are also `WinFun1` and `WinFunN` types for -window functions and n-ary window functions. - -In order to use the same syntax for certain window functions and aggregate functions, -a new typeclass `Aggregate` was introduced. So for instance, `sum_` can be -either a `Distinction` `Expression` or a `WindowFunction`, used either with -a `groupBy` or with a `partitionBy` in the appropriate way. - -``` -data Distinction (expr :: kind -> Type) (ty :: kind) - = All (expr ty) - | Distinct (expr ty) -``` - -Aggregate functions can be run over all rows or only over distinct rows, -while window functions bear no such distinction. - -**Migrations** - -Thanks to [https://github.com/Raveline](Raveline) Squeal 0.5 introduces -a function `defaultMain` to easily create a command line program to -run or rewind your migrations. - -Previously, Squeal's migrations were impure, allowing in addition to -running `Definition`s to run arbitrary `IO` operations, such as inserting -data into the database or printing out status messages. In order to -provide compatibility with other migration systems, Squeal 0.5 introduces -pure migrations, that is migrations that are only `Definition`s. - -```Haskell -data Migration p schemas0 schemas1 = Migration - { name :: Text -- ^ The `name` of a `Migration`. - -- Each `name` in a `Migration` should be unique. - , up :: p schemas0 schemas1 -- ^ The `up` instruction of a `Migration`. - , down :: p schemas1 schemas0 -- ^ The `down` instruction of a `Migration`. - } deriving (GHC.Generic) -``` - -A pure migration is a `Migration Definition`, a pair of inverse -`Definition`s with a unique name. To recover impure migrations, Squeal 0.5 -introduces the `Terminally` type. - -```Haskell -newtype Terminally trans monad x0 x1 = Terminally - { runTerminally :: trans x0 x1 monad () } -``` - -`Terminally` applies the indexed monad transformer and the monad it transforms -to the unit type `()`, thereby turning an indexed monad into a `Category`. An -impure migration is a `Migration (Terminally PQ IO)`. You can always cast -a pure migration into an impure migration with the functor, `pureMigration`. - -```Haskell -pureMigration - :: Migration Definition schemas0 schemas1 - -> Migration (Terminally PQ IO) schemas0 schemas1 -``` - -To run either pure or impure migrations, Squeal 0.5 introduces -a typeclass, `Migratory`. - -``` -class Category p => Migratory p where - - migrateUp - :: AlignedList (Migration p) schemas0 schemas1 - -> PQ schemas0 schemas1 IO () - - migrateDown - :: AlignedList (Migration p) schemas0 schemas1 - -> PQ schemas1 schemas0 IO () -``` - -The signatures of `migrateUp` and `migrateDown` have been changed -to make them easier to compose with other `PQ` actions. - -**Transactions** - -You can now run transactions `ephemerally`, guaranteed to roll back, -but return the result or throw the exception that the transaction -would have generated. This is useful for testing. You can also -`transactionallyRetry` computations, retrying the transaction if -a [serialization failure](https://www.postgresql.org/docs/11/transaction-iso.html#XACT-REPEATABLE-READ) occurs. - -**Types** - -Squeal now supports money via a `Money` Haskell `Type` and a `'PGmoney` -`PGType`. Squeal 0.5 also adds support for creating domain types -with `createDomain`. - -**Time** - -Squeal 0.5 adds support for new functions; `now`, `makeDate`, `makeTime`, -, `makeTimestamp`, and `makeTimestamptz`, a function `interval_` for -constructing time intervals out multiples of `TimeUnit`s, and a new -`TimeOp` class, defining affine space operators for time types and their -differences. - -**Literals** - -Squeal allows you to include Haskell values in your statements using -out of line `parameter`s, but often you want to include them inline, -as a SQL `literal`. Squeal 0.5 enables this using the `Literal` class. - -**Set returning functions** - -Squeal 0.5 adds support for set returning functions such as `unnest`, -with the `RankNType` `SetOfFunction`, which can be used as as a `FromClause`. - -**Text search** - -Squeal 0.5 adds extensive support for text search types, functions and operators. - -**Much more** - -Lots more changes went into and under the hood of the new version. -The `Expression` module was split into coherent submodules since it -had grown to immense proportions. New modules `Alias`, `PG` and `List` -were added to relieve some of the burden that the `Schema` module -had been carrying. Rendering has been better unified with a new -`RenderSQL` typeclass. Type level list concatenation with the `Additional` -typeclass has been added. `Manipulation`s `update` and `delete` -were upgraded, so you can leave out fields you don't want to update and -use `USING` clauses in deletes. Upserts which were previously broken -now work. The `IO` typeclass hierarchy was changed from `MonadBase` -and `MonadBaseControl` to `MonadIO` and `MonadUnliftIO`. A new -`withRecursive` `Manipulation` was added. `SquealException`s were -refactored and a `trySqueal` function added. Arrays were refactored. -And there was probably more I've forgotten. - -### Version 0.4 - -Version 0.4 strengthens Squeal's type system, adds -support for multidimensional arrays, improves support -for container type, improves `with` statements, -improves runtime exceptions, accomodates SQL's three-valued logic, -adds subquery expressions, and adds table and view type expressions. - -**Types** - -Squeal 0.4 renames some kinds to aid intuition: - -```Haskell -type RowType = [(Symbol, NullityType)] -- previously RelationType -type FromType = [(Symbol, RowType)] -- previously RelationsType -``` - -Null safety for array and composite types is gained by having the base -type of an array be a `NullityType` and the base type of a composite -a `RowType`. - -```Haskell -data PGType - = .. - | PGvararray NullityType - | PGfixarray Nat NullityType - | PGcomposite RowType -``` - -Squeal embeds Postgres types into Haskell using data kinds and type-in-type: - -```Haskell -data PGType = PGbool | .. -data NullityType = Null PGType | NotNull PGType -type RowType = [(Symbol, NullityType)] -- previously RelationType -type FromType = [(Symbol, RowType)] -- previously RelationsType -``` - -In another sense, we can embed Haskell types -into Postgres types by providing type families: - -```Haskell -type family PG (hask :: Type) :: PGType -type family NullPG (hask :: Type) :: NullityType -type family TuplePG (hask :: Type) :: [NullityType] -type family RowPG (hask :: Type) :: RowType -``` - -Let's look at these one by one. - -`PG` was introduced in Squeal 0.3. It was a closed type family that -associates some Haskell types to their obvious corresponding Postgres -types like `PG Double = 'PGfloat8`. It only worked on base types, -no arrays or composites. Squeal 0.4 extends it to -such container types and makes it an open type family so that -users can make their own type instances. - -`NullPG` had a different name before but it does the obvious -thing for Haskell with `Maybe`s: - -```Haskell -type family NullPG hask where - NullPG (Maybe hask) = 'Null (PG hask) - NullPG hask = 'NotNull (PG hask) -``` - -`TuplePG` uses generics to turn tuple types (including records) -into lists of `NullityType`s in the logical way, e.g. -`TuplePG (Bool, Day) = '[ 'PGbool, 'PGdate]`. - -`RowPG` also uses generics to turn record types into a `RowType` in the logical way, e.g. - -```Haskell ->>> data Person = Person { name :: Text, age :: Int32 } deriving GHC.Generic ->>> instance SOP.Generic Person ->>> instance SOP.HasDatatypeInfo Person ->>> :kind! TuplePG Person -TuplePG Person :: [NullityType] -= '['NotNull 'PGtext, 'NotNull 'PGint4] ->>> :kind! RowPG Person -RowPG Person :: [(Symbol, NullityType)] -= '["name" ::: 'NotNull 'PGtext, "age" ::: 'NotNull 'PGint4] -``` - -We've already seen a hint of why these types are useful in one construction -from Squeal 0.3. Creating composite types in Postgres directly from a Haskell -record type essentially uses `RowPG`. Another important use is in simplifying -the type signatures for a `Query` or `Manipulation`. Very often, you will have -a tuple type corresponding to the parameters and a record type corresponding -to the returned columns of a `Query` or `Manipulation`. Instead of writing -boilerplate signature you can reuse these with the help of `TuplePG` and `RowPG` - -For instance: - -```Haskell ->>> :{ -let - query :: Query '["user" ::: 'View (RowPG Person)] (TuplePG (Only Int32)) (RowPG Person) - query = selectStar (from (view #user) & where_ (#age .> param @1)) -:} -``` - -**Arrays** - -In addition to being able to encode and decode basic Haskell types -like `Int16` and `Text`, Squeal 0.4 permits you to encode and decode Haskell types to -Postgres array types. The `Vector` type corresponds to to variable length arrays. -And thanks to an idea from [Mike Ledger](https://github.com/mikeplus64), -homogeneous tuples correspond to fixed length arrays. We can even -create multi-dimensional fixed length arrays. Let's see an example. - -```Haskell ->>> :{ -data Row = Row - { col1 :: Vector Int16 - , col2 :: (Maybe Int16,Maybe Int16) - , col3 :: ((Int16,Int16),(Int16,Int16),(Int16,Int16)) - } deriving (Eq, GHC.Generic) -:} - ->>> instance Generic Row ->>> instance HasDatatypeInfo Row -``` - -Define a simple round trip query. - -```Haskell ->>> :{ -let - roundTrip :: Query '[] (TuplePG Row) (RowPG Row) - roundTrip = values_ $ - parameter @1 (int2 & vararray) `as` #col1 :* - parameter @2 (int2 & fixarray @2) `as` #col2 :* - parameter @3 (int2 & fixarray @2 & fixarray @3) `as` #col3 -:} - ->>> :set -XOverloadedLists ->>> let input = Row [1,2] (Just 1,Nothing) ((1,2),(3,4),(5,6)) ->>> :{ -void . withConnection "host=localhost port=5432 dbname=exampledb" $ do - result <- runQueryParams roundTrip input - Just output <- firstRow result - liftBase . print $ input == output -:} -True -``` - -**Containers** - -Squeal aims to provide a correspondence between Haskell types and Postgres types. -In particular, Haskell ADTs with nullary constructors can correspond to -Postgres enum types and Haskell record types can correspond to Postgres -composite types. However, it's not always obvious that that's how a user -will choose to store values of those types. So Squeal 0.4 introduces newtypes -whose purpose is to specify how a user wants to store values of a type. - -```Haskell -newtype Json hask = Json {getJson :: hask} -newtype Jsonb hask = Jsonb {getJsonb :: hask} -newtype Composite record = Composite {getComposite :: record} -newtype Enumerated enum = Enumerated {getEnumerated :: enum} -``` - -Let's see an example: - -```Haskell ->>> data Schwarma = Beef | Lamb | Chicken deriving (Eq, Show, GHC.Generic) ->>> instance SOP.Generic Schwarma ->>> instance SOP.HasDatatypeInfo Schwarma ->>> ->>> data Person = Person {name :: Text, age :: Int32} deriving (Eq, Show, GHC.Generic) ->>> instance SOP.Generic Person ->>> instance SOP.HasDatatypeInfo Person ->>> instance Aeson.FromJSON Person ->>> instance Aeson.ToJSON Person -``` - -We can create the equivalent Postgres types directly from their Haskell types. - -```Haskell ->>> :{ -type Schema = - '[ "schwarma" ::: 'Typedef (PG (Enumerated Schwarma)) - , "person" ::: 'Typedef (PG (Composite Person)) - ] -:} - ->>> :{ -let - setup :: Definition '[] Schema - setup = - createTypeEnumFrom @Schwarma #schwarma >>> - createTypeCompositeFrom @Person #person -:} -``` - -Let's demonstrate how to associate our Haskell types `Schwarma` and `Person` -with enumerated, composite or json types in Postgres. First create a Haskell -`Row` type using the `Enumerated`, `Composite` and `Json` newtypes as fields. - -```Haskell ->>> :{ -data Row = Row - { schwarma :: Enumerated Schwarma - , person1 :: Composite Person - , person2 :: Json Person - } deriving (Eq, GHC.Generic) -:} - ->>> instance Generic Row ->>> instance HasDatatypeInfo Row ->>> :{ -let - input = Row - (Enumerated Chicken) - (Composite (Person "Faisal" 24)) - (Json (Person "Ahmad" 48)) -:} -``` - -Once again, define a round trip query. - -```Haskell ->>> :{ -let - roundTrip :: Query Schema (TuplePG Row) (RowPG Row) - roundTrip = values_ $ - parameter @1 (typedef #schwarma) `as` #schwarma :* - parameter @2 (typedef #person) `as` #person1 :* - parameter @3 json `as` #person2 -:} -``` - -Finally, we can drop our type definitions. - -```Haskell ->>> :{ -let - teardown :: Definition Schema '[] - teardown = dropType #schwarma >>> dropType #person -:} -``` - -Now let's run it. - -```Haskell ->>> :{ -let - session = do - result <- runQueryParams roundTrip input - Just output <- firstRow result - liftBase . print $ input == output -in - void . withConnection "host=localhost port=5432 dbname=exampledb" $ - define setup - & pqThen session - & pqThen (define teardown) -:} -True -``` - -**With** - -Squeal 0.3 supported WITH statements but there's a couple problems with them. -Here's the type signature for `with` in Squeal 0.3. - -```Haskell -with - :: SOP.SListI commons - => NP (Aliased (Manipulation schema params)) (common ': commons) - -- ^ common table expressions - -> Manipulation (With (common ': commons) schema) params results - -> Manipulation schema params results -``` - -The first problem is that `with` only works with `Manipulations`. -It can work on `Query`s by using `queryStatement` but it still will -return a `Manipulation`. We can fix this issue by making `with` a -method of a type class with instances for both `Query` and `Manipulation`. - -The second problem is that all the common table expressions -refer the base schema, whereas in SQL, each subsequent CTE can -refer to previous CTEs as well. But this can be fixed! First define a datatype: - -```Haskell -data CommonTableExpression statement params schema0 schema1 where - CommonTableExpression - :: Aliased (statement schema params) (alias ::: cte) - -> CommonTableExpression statement params schema (alias ::: 'View cte ': schema) -``` - -It's just a wrapper around an aliased statement, where the statement -could be either a `Query` or `Manipulation`, but it augments the schema -by adding a view to it. It almost looks like a morphism between schemas -but there is no way yet to compose them. Luckily, Squeal already has -a datatype for this, which is used for migrations, the `AlignedList` -type which is really the "free category". So we can then define a `With` type class: - -```Haskell -class With statement where - with - :: AlignedList (CommonTableExpression statement params) schema0 schema1 - -> statement schema1 params row - -> statement schema0 params row -``` - -By giving `Aliasable` instances to CTEs and aligned singleton lists of CTEs (i.e. scrap-your-nils), we get a nice syntax for WITH statements in Squeal. - -Here's an example of using `with` for a `Query` and a `Manipulation`: - -```Haskell ->>> :{ -let - query :: Query - '[ "t1" ::: 'View - '[ "c1" ::: 'NotNull 'PGtext - , "c2" ::: 'NotNull 'PGtext] ] - '[] - '[ "c1" ::: 'NotNull 'PGtext - , "c2" ::: 'NotNull 'PGtext ] - query = with ( - selectStar (from (view #t1)) `as` #t2 :>> - selectStar (from (view #t2)) `as` #t3 - ) (selectStar (from (view #t3))) -in printSQL query -:} -WITH "t2" AS (SELECT * FROM "t1" AS "t1"), "t3" AS (SELECT * FROM "t2" AS "t2") SELECT * FROM "t3" AS "t3" - ->>> type ProductsTable = '[] :=> '["product" ::: 'NoDef :=> 'NotNull 'PGtext, "date" ::: 'Def :=> 'NotNull 'PGdate] - ->>> :{ -let - manipulation :: Manipulation - '[ "products" ::: 'Table ProductsTable - , "products_deleted" ::: 'Table ProductsTable - ] '[ 'NotNull 'PGdate] '[] - manipulation = with - (deleteFrom #products (#date .< param @1) ReturningStar `as` #deleted_rows) - (insertQuery_ #products_deleted (selectStar (from (view (#deleted_rows `as` #t))))) -in printSQL manipulation -:} -WITH "deleted_rows" AS (DELETE FROM "products" WHERE ("date" < ($1 :: date)) RETURNING *) INSERT INTO "products_deleted" SELECT * FROM "deleted_rows" AS "t" -``` - -**Three Valued Logic** - -In previous versions of Squeal, conditions followed classical two valued logic -of `true` and `false`. - -```Haskell --- Squeal 0.3 -type Condition schema from grouping params = - Expression schema from grouping params ('NotNull 'PGbool) -``` - -I had thought that three valued logic, which is what SQL uses, was confusing. -However, multiple users reported being confused at being forced to do `NULL` -handling, particularly in their left joins. Since the original motivation -of being less confusing evaporated I decided to switch to three valued logic -of `true`, `false` and `null_`. - -```Haskell --- Squeal 0.4 -type Condition schema from grouping params = - Expression schema from grouping params ('Null 'PGbool) -``` - -**Subquery Expressions** - -Squeal 0.4 adds support for subquery expressions such as `IN` and `op ANY/ALL`. - -**Row Types** - -Squeal 0.4 adds functions to define type expressions from tables and views -and a type expression for user defined types, `typetable`, `typeview` and -`typedef`. - -**Runtime Exceptions** - -Squeal now has an exception type which gives details on the sort of error -encountered and handler functions. - -```Haskell -data SquealException - = PQException - { sqlExecStatus :: LibPQ.ExecStatus - , sqlStateCode :: Maybe ByteString - -- ^ https://www.postgresql.org/docs/current/static/errcodes-appendix.html - , sqlErrorMessage :: Maybe ByteString - } - | ResultException Text - | ParseException Text - deriving (Eq, Show) -instance Exception SquealException - -catchSqueal - :: MonadBaseControl IO io - => io a - -> (SquealException -> io a) -- ^ handler - -> io a - -handleSqueal - :: MonadBaseControl IO io - => (SquealException -> io a) -- ^ handler - -> io a -> io a -``` - -**Additional Changes** - -Squeal 0.4 adds `field` and `index` functions to get components of composite -and array expressions. - -Squeal 0.4 adds a dependency on `records-sop` to offload a lot of boilerplate -type family logic that was needed for `RowPG`. - -The above changes required major and minor changes to Squeal DSL functions. -Please consult the documentation. - -### Version 0.3.2 - August 4, 2018 - -Version 0.3.2 features extensive support for `JSON` functionality with -more than 50 new functions. -This work is entirely due to [Mike Ledger](https://github.com/mikeplus64) -who has been making terrific contributions to Squeal. Thanks! -We also got some examples in the documentation for pools submitted by -[Raveline](https://github.com/Raveline). I'm so pleased to be -getting pull requests and issue submissions from you all! - -### Version 0.3.1 - July 7, 2018 - -Version 0.3.1 of Squeal enables the "Scrap your Nils" trick for -heterogeneous lists of `Alias`es, `Aliased` expressions, `PGlabel`s and `By`s -with the typeclasses `IsLabel`, `IsQualified`, `IsPGlabel`, -and the new `Aliasable` typeclass, to eliminate all need of using `Nil` in a list. -There were a couple minor name changes, i.e. the function `group` was renamed to `groupBy`. -Please consult the documentation. - -### Version 0.3 - June 26, 2018 - -Version 0.3 of Squeal adds views as well as composite and enumerated types to Squeal. -To support these features, a new kind `SchemumType` was added. - -```Haskell -data SchemumType - = Table TableType - | View RelationType - | Typedef PGType -``` - -As a consequence, you will have to update your schema definitions like so: - -```Haskell --- Squeal 0.2 -type Schema = - '[ "users" ::: - '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=> - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "name" ::: 'NoDef :=> 'NotNull 'PGtext - ] - ] - --- Squeal 0.3 -type Schema = - '[ "users" ::: 'Table ( - '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=> - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "name" ::: 'NoDef :=> 'NotNull 'PGtext - ]) - ] -``` - -**Views** - -You can now create, drop, and query views. - -```Haskell ->>> :{ -type ABC = - ('[] :: TableConstraints) :=> - '[ "a" ::: 'NoDef :=> 'Null 'PGint4 - , "b" ::: 'NoDef :=> 'Null 'PGint4 - , "c" ::: 'NoDef :=> 'Null 'PGint4 - ] -type BC = - '[ "b" ::: 'Null 'PGint4 - , "c" ::: 'Null 'PGint4 - ] -:} - ->>> :{ -let - definition :: Definition '["abc" ::: 'Table ABC ] '["abc" ::: 'Table ABC, "bc" ::: 'View BC] - definition = createView #bc (select (#b :* #c :* Nil) (from (table #abc))) -in printSQL definition -:} -CREATE VIEW "bc" AS SELECT "b" AS "b", "c" AS "c" FROM "abc" AS "abc"; - ->>> :{ -let - definition :: Definition '["abc" ::: 'Table ABC, "bc" ::: 'View BC] '["abc" ::: 'Table ABC] - definition = dropView #bc -in printSQL definition -:} -DROP VIEW "bc"; - ->>> :{ -let - query :: Query '["abc" ::: 'Table ABC, "bc" ::: 'View BC] '[] BC - query = selectStar (from (view #bc)) -in printSQL query -:} -SELECT * FROM "bc" AS "bc" -``` - -**Enumerated Types** - -PostgreSQL has a powerful type system. It even allows for user defined types. -For instance, you can define enumerated types which are data types that comprise -a static, ordered set of values. They are equivalent to Haskell algebraic data -types whose constructors are nullary. An example of an enum type might be the days of the week, -or a set of status values for a piece of data. - -Enumerated types are created using the `createTypeEnum` command, for example: - -```Haskell ->>> :{ -let - definition :: Definition '[] '["mood" ::: 'Typedef ('PGenum '["sad", "ok", "happy"])] - definition = createTypeEnum #mood (label @"sad" :* label @"ok" :* label @"happy" :* Nil) -:} ->>> printSQL definition -CREATE TYPE "mood" AS ENUM ('sad', 'ok', 'happy'); -``` - -Enumerated types can also be generated from a Haskell algebraic data type with nullary constructors, for example: - -```Haskell ->>> data Schwarma = Beef | Lamb | Chicken deriving GHC.Generic ->>> instance SOP.Generic Schwarma ->>> instance SOP.HasDatatypeInfo Schwarma - ->>> :kind! EnumFrom Schwarma -EnumFrom Schwarma :: PGType -= 'PGenum '["Beef", "Lamb", "Chicken"] - ->>> :{ -let - definition :: Definition '[] '["schwarma" ::: 'Typedef (EnumFrom Schwarma)] - definition = createTypeEnumFrom @Schwarma #schwarma -:} ->>> printSQL definition -CREATE TYPE "schwarma" AS ENUM ('Beef', 'Lamb', 'Chicken'); -``` - -You can express values of an enum type using `label`, which is an overloaded method -of the `IsPGlabel` typeclass. - -```Haskell ->>> :{ -let - expression :: Expression sch rels grp params ('NotNull (EnumFrom Schwarma)) - expression = label @"Chicken" -in printSQL expression -:} -'Chicken' -``` - -**Composite Types** - -In addition to enum types, you can define composite types. -A composite type represents the structure of a row or record; -it is essentially just a list of field names and their data types. - - -`createTypeComposite` creates a composite type. The composite type is -specified by a list of attribute names and data types. - -```Haskell ->>> :{ -let - definition :: Definition '[] '["complex" ::: 'Typedef ('PGcomposite '["real" ::: 'PGfloat8, "imaginary" ::: 'PGfloat8])] - definition = createTypeComposite #complex (float8 `As` #real :* float8 `As` #imaginary :* Nil) -:} ->>> printSQL definition -CREATE TYPE "complex" AS ("real" float8, "imaginary" float8); -``` - -Composite types are almost equivalent to Haskell record types. -However, because of the potential presence of `NULL` -all the record fields must be `Maybe`s of basic types. -Composite types can be generated from a Haskell record type, for example: - -```Haskell ->>> data Complex = Complex {real :: Maybe Double, imaginary :: Maybe Double} deriving GHC.Generic ->>> instance SOP.Generic Complex ->>> instance SOP.HasDatatypeInfo Complex - ->>> :kind! CompositeFrom Complex -CompositeFrom Complex :: PGType -= 'PGcomposite '['("real", 'PGfloat8), '("imaginary", 'PGfloat8)] - ->>> :{ -let - definition :: Definition '[] '["complex" ::: 'Typedef (CompositeFrom Complex)] - definition = createTypeCompositeFrom @Complex #complex -in printSQL definition -:} -CREATE TYPE "complex" AS ("real" float8, "imaginary" float8); -``` - -A row constructor is an expression that builds a row value -(also called a composite value) using values for its member fields. - -```Haskell ->>> :{ -let - i :: Expression '[] '[] 'Ungrouped '[] ('NotNull (CompositeFrom Complex)) - i = row (0 `As` #real :* 1 `As` #imaginary :* Nil) -:} ->>> printSQL i -ROW(0, 1) -``` - -You can also use `(&)` to apply a field label to a composite value. - -```Haskell ->>> :{ -let - expr :: Expression '[] '[] 'Ungrouped '[] ('Null 'PGfloat8) - expr = i & #imaginary -in printSQL expr -:} -(ROW(0, 1)).imaginary -``` - -Both composite and enum types can be automatically encoded from and decoded to their equivalent Haskell types. -And they can be dropped. - -```Haskell ->>> :{ -let - definition :: Definition '["mood" ::: 'Typedef ('PGenum '["sad", "ok", "happy"])] '[] - definition = dropType #mood -:} ->>> printSQL definition -DROP TYPE "mood"; -``` - -**Additional Changes** - -Squeal 0.3 also introduces a typeclass `HasAll` similar to `Has` but for a list of aliases. -This makes it possible to clean up some unfortunately messy Squeal 0.2 definitions. - -```Haskell --- Squeal 0.2 ->>> unique (Column #a :* Column #b :* Nil) - --- Squeal 0.3 ->>> unique (#a :* #b :* Nil) -``` - -Squeal 0.3 also adds `IsLabel` instances for `Aliased` expressions and tables as well as -heterogeneous lists, allowing for some more economy of code. - -```Haskell --- Squeal 0.2 (or 0.3) ->>> select (#a `As` #a :* Nil) (from (table (#t `As` #t))) - --- Squeal 0.3 ->>> select #a (from (table #t)) -``` - -Squeal 0.3 also fixes a bug that prevented joined queries on self-referencing tables. - -The above changes required major and minor changes to Squeal DSL functions. -Please consult the documentation. - -### Version 0.2.1 - April 7, 2018 - -This minor update fixes an issue where alias identifiers could conflict with -reserved words in PostgreSQL. To fix the issue, alias identifiers are now -quoted. Thanks to Petter Rasmussen for the fix. - -### Version 0.2 - March 26, 2018 - -**Changes** -- **Constraints** - Type level table constraints like primary and foreign keys and column constraints like having `DEFAULT`. -- **Migrations** - Support for linear, invertible migrations tracked in an auxiliary table -- **Arrays** - Support for fixed- and variable-length arrays -- **Aliases** - Generalized `Has` constraint -- **Pools** - Support for pooled connections -- **Transactions** - Support for transaction control language -- **Queries, Manipulations, Definitions** - Small and large changes to Squeal's DSL - -Well, a lot of things changed! - -**Constraints** - -An important request was to bring constraints to the type level. -This means that more of the schema is statically known. In `0.1` column constraints - which boil down -to having `DEFAULT` or not - were at the type level, but they were confusingly named. - -```haskell -0.1: 'Optional ('NotNull 'PGInt4) -0.2: 'Def :=> 'NotNull 'PGInt4 -0.1: 'Required ('NotNull 'PGInt4) -0.2: 'NoDef :=> 'NotNull 'PGInt4 -``` - -The `:=>` type operator is intended to helpfully connote a constraint relation. -It's also used for table constraints which are new in `0.2`. - -```haskell -"emails" ::: - '[ "pk_emails" ::: 'PrimaryKey '["id"] - , "fk_user_id" ::: 'ForeignKey '["user_id"] "public" "users" '["id"] - ] :=> - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4 - , "email" ::: 'NoDef :=> 'Null 'PGtext - ] -``` - -Another change in the constraint system was the removal of column constraints from -query and manipulation results, as result columns don't support a notion of `DEFAULT`. -This necessitates a distinction between `TableType`s which have both column and table -constraints and `RelationType`s which have neither. - -**Migrations** - -Migrations are a hot topic and many people have requested this feature. Squeal `0.2` -adds support for linear, invertible migrations. That is, a migration is a named, -invertible, schema-tracking computation: - -```haskell -data Migration io schema0 schema1 = Migration - { name :: Text -- ^ The `name` of a `Migration`. - -- Each `name` in a `Migration` should be unique. - , up :: PQ schema0 schema1 io () -- ^ The `up` instruction of a `Migration`. - , down :: PQ schema1 schema0 io () -- ^ The `down` instruction of a `Migration`. - } -``` - -And, `Migration`s can be put together in an "aligned" list: - -```haskell -data AlignedList p x0 x1 where - Done :: AlignedList p x x - (:>>) :: p x0 x1 -> AlignedList p x1 x2 -> AlignedList p x0 x2 -``` - -These aligned lists are free categories and might look familiar from -the [reflections without remorse](http://okmij.org/ftp/Haskell/zseq.pdf) technique, -which uses their cousins, aligned sequences. - -In the context of migration, they allow one to chain new migrations as a -schema evolves over time. `Migration`s' execution is tracked in an auxiliary -migrations table. Migration lists can then be run or rewinded. - -```haskell -migrateUp - :: MonadBaseControl IO io - => AlignedList (Migration io) schema0 schema1 -- ^ migrations to run - -> PQ - ("schema_migrations" ::: MigrationsTable ': schema0) - ("schema_migrations" ::: MigrationsTable ': schema1) - io () - -migrateDown - :: MonadBaseControl IO io - => AlignedList (Migration io) schema0 schema1 -- ^ migrations to rewind - -> PQ - ("schema_migrations" ::: MigrationsTable ': schema1) - ("schema_migrations" ::: MigrationsTable ': schema0) - io () -``` - -**Aliases** - -In Squeal `0.1`, we had different typeclasses `HasColumn` and `HasTable` to indicate -that a table has a column or that a schema has a table. In Squeal `0.2` this has been -unified to a single typeclass, - -```haskell -class KnownSymbol alias => - Has (alias :: Symbol) (fields :: [(Symbol,kind)]) (field :: kind) - | alias fields -> field where -``` - -**Arrays** - -Support for array types has been added to Squeal `0.2` through -the `'PGfixarray` and `'PGvararray` `PGType`s. Array values can be -constructed using the `array` function and can be encoded from and decoded to -Haskell `Vector`s. - -**Pools** - -Squeal `0.2` provides a monad transformer `PoolPQ` that's an instance of `MonadPQ`. -The `resource-pool` library is leveraged to provide striped pools of `Connection`s. -`PoolPQ` should be a drop in replacement for running `Manipulation`s and `Query`s with -`PQ`. - -**Transactions** - -Squeal `0.2` supports a simple transaction control language. A computation in -`MonadPQ` can be called `transactionally` with different levels of isolation. -Additionally, a schema changing computation, a data definition, can be run in a -transaction. Running a computation in a transaction means that all SQL statements -will be rolled back if an exception is encountered. - -**Queries, Manipulations and Definitions** - -The above changes required major and minor changes to Squeal DSL functions. -Please consult the documentation. diff --git a/scrap-your-nils.md b/scrap-your-nils.md deleted file mode 100644 index 04a0188d..00000000 --- a/scrap-your-nils.md +++ /dev/null @@ -1,60 +0,0 @@ -## Scrap your Nils - -One of the most useful types I've come across in Haskell is the type of -"heterogeneous lists". This is the same as the [Rec](http://hackage.haskell.org/package/vinyl-0.8.1.1/docs/Data-Vinyl-Core.html) -datatype from the [vinyl](http://hackage.haskell.org/package/vinyl) library. -It's also the same as the [NP](http://hackage.haskell.org/package/generics-sop-0.3.2.0/docs/Generics-SOP-NP.html) -datatype from the [generics-sop](http://hackage.haskell.org/package/generics-sop) library. -Squeal makes heavy use of this type. - -```Haskell ->>> import Generics.SOP (NP(..)) ->>> :i NP -type role NP representational nominal -data NP (a :: k -> *) (b :: [k]) where - Nil :: forall k (a :: k -> *). NP a '[] - (:*) :: forall k (a :: k -> *) (x :: k) (xs :: [k]). - (a x) -> (NP a xs) -> NP a (x : xs) - -- Defined in ‘Generics.SOP.NP’ -``` - -This type allows us to construct "product" types, where the types of the individual -"terms" are hosted at the type level. - -```Haskell ->>> :set -XDataKinds ->>> import Generics.SOP (I(..)) ->>> let example = I "foo" :* I pi :* Nil :: NP I '[String, Double] ->>> example -I "foo" :* I 3.141592653589793 :* Nil -``` - -One thing Squeal uses `NP` for is to form lists of aliases, -using GHC's `OverloadedLabels` extension, hosting the names -of the aliases themselves at the type level. - -```Haskell ->>> :set -XKindSignatures -XOverloadedLabels -XFlexibleInstances -XMultiParamTypeClasses ->>> import GHC.TypeLits ->>> import GHC.OverloadedLabels ->>> data Alias (alias :: Symbol) = Alias deriving (Eq,Ord,Show) ->>> instance IsLabel label (Alias label) where fromLabel = Alias ->>> let aliases = #foo :* #bar :* Nil :: NP Alias '["foo", "bar"] -``` - -However, it's very ugly to have to end every list with `:* Nil`. -When I announced the release of Squeal, people rightly [complained](https://www.reddit.com/r/haskell/comments/6yr5v6/announcing_squeal_a_deep_embedding_of_sql_in/dmq8vvn) -about the syntactic noise. Luckily, there's a neat trick we can use to get rid of it. -Making an `IsLabel` instance not only for elements of our list -but also for lists of length 1, we can ask GHC to interpret -the last label as a list. - -```Haskell ->>> instance IsLabel label (NP Alias '[label]) where fromLabel = Alias :* Nil ->>> let aliases' = #foo :* #bar :: NP Alias '["foo", "bar"] -``` - -Version 0.3.1 of Squeal enables the "Scrap your Nils" trick for -heterogeneous lists of `Alias`es, `Aliased` expressions, `PGlabel`s and `By`s -with the typeclasses `IsLabel`, `IsQualified`, `IsPGlabel`, -and the new `Aliasable` typeclass, to eliminate all need of using `Nil` in a list. diff --git a/squeal-core-concepts-handbook.md b/squeal-core-concepts-handbook.md deleted file mode 100644 index feeee5b3..00000000 --- a/squeal-core-concepts-handbook.md +++ /dev/null @@ -1,980 +0,0 @@ -# Squeal Core Concepts Handbook - -This handbook isn't intended as a comprehensive reference to Squeal; that's what -the Haddock is for. Instead, this is meant to give you an understanding of the -fundamental parts of Squeal: its core types and typeclasses, as well as its -heavy use of phantom types. Once you understand these, which are the most -complicated part of learning to use Squeal, you should have a solid base to -figure out everything else. - -At its core, you can view Squeal as a small group of easy-to-understand types -(`Query`, `Manipulation`, `Statement`, `Expression`, `FromClause`, and -`TableExpression`) that have hard-to-understand type parameters (`Expression -grouping lat with db params from ty`). The former map to your existing -understanding of SQL in a fairly obvious way; the latter make sure that your -queries are actually valid. - -We can get our first, most basic understanding of Squeal by ignoring the type -parameters entirely and looking specifically at those core types. - -## Squeal's Core Types - -Imagine, if you would, a world where we only had those six types above, with no -type parameters attached to them. - -That would be possible, right? And you can see how they could fit together to -form larger queries. That gives us the composability we wanted. - -[`Query`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Query.html#t:Query) and [`Manipulation`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Manipulation.html#t:Manipulation) -line up cleanly with what we expect a SQL query to look like, so let's start by -looking at those two types. - -`Query`s represent mainly SQL **SELECT**. `Manipulation`s represent **UPDATE**, -**INSERT INTO**, and **DELETE FROM**. - -```haskell -query = select - (List $ #u ! #name `as` #userName :* #e ! #email `as` #userEmail) - ( from (table (#users `as` #u) - & innerJoin (table (#emails `as` #e)) - (#u ! #id .== #e ! #user_id)) ) -``` - -`Query`s have two parts, the description of the columns selected, and the -`TableExpression` that describes where to select those columns from. - -```text -query = select - - (List $ #u ! #name `as` #userName } the column selection - :* #e ! #email `as` #userEmail) } - - ( from (table (#users `as` #u) } - & innerJoin (table (#emails `as` #e)) } the TableExpression - (#u ! #id .== #e ! #user_id)) ) } -``` - -A [`TableExpression`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Query-Table.html#t:TableExpression) -is generated from a [`FromClause`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Query-From.html#t:FromClause), -which only describes the joins. You convert a `FromClause` to a `TableExpression` using `from`. Once you -have a TableExpression, that's where functions like [`where_`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Query-Table.html#v:where_) -and [`having`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Query-Table.html#v:having) -let you restrict the rows you get back. - -```text -( from } - ( table (#users `as` #u) } } - & innerJoin } a FromClause } the whole thing is - (table (#emails `as` #e)) } } a TableExpression - (#u ! #id .== #e ! #user_id)) } } -& where_ ... } -) } -``` - -Manipulations aren't particularly different from Querys. The one thing that -should be pointed out is that all of them specify a table using an `Aliased -(QualifiedAlias sch) (tab ::: tab0)`, and that UPDATE and DELETE have a -[`UsingClause`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Manipulation.html#t:UsingClause) -instead of a `TableExpression` to specify additional joins. `UsingClause` specifically -takes a `FromClause`, to prevent you from doing things like putting `WHERE` or `HAVING` on them. - -```haskell -manip = deleteFrom - #users - (Using (table #emails)) - (#users ! #id .== #emails ! #user_id) - Returning_ Nil -``` - -Once you get into specifying the actual bits of data you care about, you're -primarily concerned with generating values of type [`Expression`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Expression.html#t:Expression). -We're still ignoring most of the type parameters in Squeal's types, but in the case of -Expression the very last one, `ty`, is of interest to us. - -```haskell --- imagine a stripped-down version of Expression -data Expr ty - --- this is for teaching purposes; will not typecheck -foo :: Expr ('NotNull 'PGint4) -foo = #table ! #field1 -``` - -You'll construct Expressions (and `Condition`, which is just an alias for -`Expression (null 'PGbool)`) everywhere. - -The one last piece of the puzzle that we haven't explained yet: how do you -specify which columns in a Query that you're returning? How do you specify -which columns you're updating in an UPDATE Manipulation? The key here is the -[`NP`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Type-List.html#t:NP) -type, which Squeal reexports from `generics-sop`. You'll see this type in a -lot of different places. It will also confuse the hell out of you the first time -you see it in documentation. - -```haskell -selectedColumns :: NP '[ "userName" ::: 'NotNull 'PGtext, "userEmail" ::: 'Null 'PGtext ] -selectedColumns = - #users ! #name `as` #userName - :* #emails ! #email `as` #userEmail -``` - -It's a way for Squeal to have heterogeneous lists that also typecheck against the -DB schema. We'll talk about how that typechecking works later, once we -understand Squeal's type parameters. From a practical perspective, mainly -you need to know that you construct them using `(:*)`. - -All of the types we've talked about so far live in "Postgres-land;" they only -know about Postgres types, and have no knowledge of how to translate those types -into actual in-memory Haskell data. That translation lives inside the -[`Statement`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Type-List.html#t:NP) -type, which lives one level above Query and Manipulation. Both -Query and Manipulation can be converted to Statement by providing an encoder to -convert the query's params from Haskell types to Postgres types, and a decoder -to do the opposite for the query's results. We'll look at encoding and decoding -to/from PG later; just remember that conceptually the Statement type is there to -combine a raw query with an associated Haskell <=> PG codec. - -Let's put all this together in a concrete example. Say we had the following -typical query: - -```haskell -getUsers :: Statement DB () User -getUsers = query $ select - (List $ #u ! #name `as` #userName :* #e ! #email `as` #userEmail) - ( from (table (#users `as` #u) - & innerJoin (table (#emails `as` #e)) - (#u ! #id .== #e ! #user_id)) ) -``` - -Since all this is is an expression built out of the smaller types we've looked -at above, we can expand this out into its constituent parts and see just -how everything fits together. Ignore the amount of noise in the type -parameters; focus on the type heads like Query, Expression etc. that we -talked about. - -```haskell -getUsersQuery :: Query with lat DB params '[ "userName" ::: 'NotNull 'PGtext, "userEmail" ::: 'Null 'PGtext ] -getUsersQuery = - select - (List getUsersSelection) - ( from (table (#users `as` #u) - & innerJoin (table (#emails `as` #e)) - (#u ! #id .== #e ! #user_id)) ) - -getUsersSelection :: NP (Aliased (Expression 'Ungrouped with lat DB params - '[ "u" ::: '[ "id" ::: 'NotNull 'PGint4 - , "name" ::: 'NotNull 'PGtext - ] - , "e" ::: '[ "id" ::: 'NotNull 'PGint4 - , "user_id" ::: 'NotNull 'PGint4 - , "email" ::: 'Null 'PGtext - ] - ])) - '[ "userName" ::: 'NotNull 'PGtext, "userEmail" ::: 'Null 'PGtext ] -getUsersSelection = - #u ! #name `as` #userName :* #e ! #email `as` #userEmail - -getUsersFrom :: TableExpression 'Ungrouped lat with DB params - '[ "u" ::: '[ "id" ::: 'NotNull 'PGint4 - , "name" ::: 'NotNull 'PGtext - ] - , "e" ::: '[ "id" ::: 'NotNull 'PGint4 - , "user_id" ::: 'NotNull 'PGint4 - , "email" ::: 'Null 'PGtext - ] - ] -getUsersFrom = - ( from (table (#users `as` #u) - & innerJoin (table (#emails `as` #e)) - getUsersJoinExpr)) - -getUsersJoinExpr :: Expression 'Ungrouped lat with DB params - (Join - '[ "u" ::: '[ "id" ::: 'NotNull 'PGint4 - , "name" ::: 'NotNull 'PGtext - ] - ] - '[ "e" ::: '[ "id" ::: 'NotNull 'PGint4 - , "user_id" ::: 'NotNull 'PGint4 - , "email" ::: 'Null 'PGtext - ] - ]) - ('Null 'PGbool) -getUsersJoinExpr = - #u ! #id .== #e ! #user_id -``` - -These definitions can be loaded into the REPL. You can try playing around with -them if you'd like to get a feel for how the types work. You'll need the -following DB schema type in scope: - -```haskell -type UsersColumns = - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "name" ::: 'NoDef :=> 'NotNull 'PGtext - ] - -type UsersConstraints = '[ "pk_users" ::: 'PrimaryKey '["id"] ] - -type EmailsColumns = - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4 - , "email" ::: 'NoDef :=> 'Null 'PGtext ] - -type EmailsConstraints = - '[ "pk_emails" ::: 'PrimaryKey '["id"] - , "fk_user_id" ::: 'ForeignKey '["user_id"] "public" "users" '["id"] - ] - -type Schema = - '[ "users" ::: 'Table (UsersConstraints :=> UsersColumns) - , "emails" ::: 'Table (EmailsConstraints :=> EmailsColumns) - ] - -type DB = Public Schema -``` - -## Where's the polymorphism? Actually constructing Expressions - -So far we've been using `(!)` and `as` without really understanding them; we -just use them as if they're equivalent to SQL `.` and `AS`, and assuming that -they'll do the right thing. - -But what's the actual type of this fragment? - -```haskell -#some_table ! #yay -``` - -When we were writing simple queries, we didn't particularly have to care about -what type this is. But as we write more complicated queries and start to think -about making our queries more modular, we need to be able to express the types -of values like this. - -For instance, depending on where you use it, it might be an Expression: - -```haskell -foo :: Expression 'Ungrouped lat with db '[] '[ "some_table" ::: '[ "yay" ::: 'NotNull 'PGint4 ] ] ('NotNull 'PGint4) -foo = #some_table ! #yay -``` - -But it can also be an NP: - -```haskell -bar :: NP (Expression 'Ungrouped lat with db '[] '[ "some_table" ::: '[ "yay" ::: 'NotNull 'PGint4 ] ]) '[ 'NotNull 'PGint4 ] -bar = #some_table ! #yay -``` - -It can even become a table name: - -```haskell -baz :: Aliased (QualifiedAlias "some_table") ("yay" ::: "yay") -baz = #some_table ! #yay - -fromClause :: FromClause lat with - '[ "some_table" ::: - '[ "yay" ::: 'Table - ('[] :=> '[ "a" ::: 'NoDef :=> 'NotNull 'PGint4 ]) - ] - ] - '[] - '[ "yay" ::: '[ "a" ::: 'NotNull 'PGint4 ] ] -fromClause = table baz -``` - -The key here is that both `(!)` and `as` are polymorphic in their return -type. This polymorphism is key to making Squeal convenient to write, but it can -be very confusing when first starting to use the library, as it makes it hard to -understand how to construct a certain Squeal type. This misunderstanding seems -like a consequence of how we usually use polymorphic returns. Usually the -polymorphic return is on a function that does some kind of conversion or -processing; canonical examples are things like `read` from Base, or `fromJSON` -from Aeson. In Squeal, that type conversion happens when you *name* things. It -accomplishes the goal of embedded SQL in Haskell in a relatively easy-to-read -way, but it takes some getting used to. - -What `(!)` can return is defined by the typeclass [`IsQualified`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Type-Alias.html#t:IsQualified); -similarly, `as` uses a typeclass called [`Aliasable`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Type-Alias.html#t:Aliasable). -If you squint through the mess of type parameter noise, you can see that this -is how certain types are constructed (for instance, our NP lists of expressions -in select lists, and expressions themselves): - -![IsQualified/Aliasable allows us to construct many different types with labels](squeal/isqualified-intro.png) - -and thus, how you can use `(!)` and `as` in so many places. - -This is especially important around uses of NP and update lists; a common -mistake is writing something like this while doing SELECTs: - -```haskell -select - (List $ - #table ! #field1 - :* #table ! #field2 - :* Nil) - (from ...) -``` - -But this final `Nil` is actually unnecessary! The NP cons operator `:*` takes -another NP as its second argument, which is why you might think you need Nil to -terminate the NP list. But if you look at the instances of `IsQualified`, it can -polymorphically return an NP already! - -![IsQualified allows us to directly create a value of type NP](squeal/isqualified-can-be-np.png) - -So we can simplify our definition a bit: - -```haskell --- this typechecks -select - (List $ - -- note that these two lines have different types - (#table ! #field1 :: Aliased (Expression _ _ _ _ _) _) - :* (#table ! #field2 :: NP (Aliased (Expression _ _ _ _ _) _))) - (from ...) -``` - -We'll see later that this is also Squeal's preferred way to handle things like -CTEs. - -What does all this mean for us? It gives us the type signatures we'd need when -we want to, say, have a common query that's parameterized by a table name, or a -query that needs to be parameterized by which column to run a WHERE on. For -instance, for a table name as a parameter, we'd want an `Aliased (QualifiedAlias -sch) tab`, since that's the parameter for `table`. We can then be assured that -callers can construct values of this type, since we can see an instance for -`IsQualified` for it. - -![Table names can be constructed through IsQualified as well](squeal/isqualified-alias.png) - -Following the typeclasses, we see that we can do something similar for -Expression, since there are instances for `IsQualified` and `Aliasable` for -generating Expressions as well. - -If you wanted to be completely general and allow a Squeal identifier to be used -in, say, both an expression and table name position, you could use the -IsQualified and Aliasable constraints directly. In general this doesn't seem to -be very useful though. - -One last thing is that while the return type of `(!)` is polymorphic, its -arguments are not; it takes two [`Alias`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Type-Alias.html#t:Alias)es, which are essentially renamed -`Proxy`'s. Since you can also generate Aliases from labels like `#table`, this -provides another avenue you can use to make your queries more general and -modular; take in Aliases as parameters and use `(!)` to convert to the type you -need on-demand. - -## Squeal's type parameters - -It's finally time to talk about the heart and soul of Squeal, what makes it -tick: the plethora of type parameters on all of its core types. - -This is where Squeal handles the heavy lifting of ensuring that all of your -queries are well-formed, that they reference columns that actually exist, that -your SQL comparisons are well-typed, that the columns that you insert as DEFAULT -actually have default values, and so on. - -To refresh your memory, here's the full type signature of `Expression`, in all of -its phantomly-typed glory: - -```haskell -newtype Expression grp lat with db params from ty -``` - -One quick note before we dive into these. Throughout Squeal you'll see the -type operator `(:::)`. For instance, Squeal uses type-level specifications of -table columns that look like so: - -```haskell -type Columns = - '[ "col1" ::: 'NotNull 'PGbool - , "col2" ::: 'NotNull 'PGint4 - ] -``` - -In the Squeal ecosystem, this essentially means "type of." Note that it's *3* -colons, not 2 like for normal type signatures. Underneath the hood, `(:::)` is -just an alias for type-level tuples, so the above type is equivalent to the -following: - -```haskell -type Columns = - '[ '("col1", 'NotNull 'PGbool) - , '("col2", 'NotNull 'PGint4) - ] -``` - -With that out of the way, let's go through Squeal's type parameters one-by-one, -shall we? - -### `grp` - -In SQL, an expression is either a "normal" value or an aggregate value. You probably -already understand this intuitively, even if you never put a name on it. For -instance, let's say you wrote a query like the following: - -```SQL -CREATE TABLE foo - ( user_id INT4 NOT NULL - , value INT4 NOT NULL - ); - -SELECT user_id, SUM(value) - FROM foo - GROUP BY user_id; -``` - -In the select list of the above query, `user_id` and `SUM(value)` are both -"aggregate" values; **user_id** because it's included in the GROUP BY, and -**SUM(value)** since it's directly calling an aggregate function. - -If we don't call any aggregating functions or do a GROUP BY, we have "normal" -values instead: - -```SQL -SELECT user_id, value - FROM foo; -``` - -It's important to understand the distinction, because you can't mix normal and -aggregate values in the same query: - -```SQL -SELECT user_id, SUM(value) - FROM foo; - --- ERROR: column "foo.user_id" must appear in the GROUP BY clause or be used in an aggregate function --- LINE 1: SELECT user_id, SUM(value) -``` - -Postgres barfs, because SUM constrains it to only produce one row, but then what -should the `user_id` of that row be? The complicated part is that an identifier -like `user_id` could be either a normal or an aggregate value, depending on the -surrounding context. So Squeal is obligated to keep track of whether an -expression was generated from an aggregate function or is part of the GROUP BY, -which it does using the `grp` type parameter on an Expression. - -It can either be `'Ungrouped`, meaning a normal value, or `'Grouped bys`, which -indicates that Expression is valid in any context where the columns `bys` are -grouped. - -For instance, selecting `#table ! #field1` in an aggregated context -would want a type like `Expression ('Grouped '[ "table" ::: "field1" ]) ...`, -indicating that this selection isn't valid without that field grouped. Calling -a function like Squeal's [`count`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Expression-Aggregate.html#v:count) -would return a type like `Expression ('Grouped bys) ...`, where the type variable -being abstract means that it doesn't care *what* columns the query is grouped by, -just that it's grouped in *some* way. - -However, if you use an aggregate function, the query *does* have to be grouped, -which can cause a confusing error the first time you see it. If you're used -to writing things like `SELECT COUNT(id) FROM ...` in SQL, you might try to -naively translate this into Squeal like so: - -```haskell -foo :: Query lat with - '[ "public" ::: - '[ "table" ::: 'Table - ('[] :=> '[ "id" ::: 'NoDef :=> 'NotNull 'PGint4 ]) - ] - ] - '[] - '[ "a" ::: 'NotNull 'PGint8 ] -foo = select - (count (All $ #table ! #id) `as` #a) - (from (table #table)) -``` - -Which will promptly fail with a type error: - -```haskell - • No instance for (Aggregate AggregateArg (Expression 'Ungrouped)) - arising from a use of ‘count’ - • In the first argument of ‘as’, namely - ‘count (All $ #table ! #id)’ - In the first argument of ‘select’, namely - ‘(count (All $ #table ! #id) `as` #a)’ - In the expression: - select (count (All $ #table ! #id) `as` #a) (from (table #table)) -``` - -What this is *essentially* telling you is that you forgot to group your query, -which you need to do **even if you're running your aggregate on all rows**. We -do this by explicitly telling Squeal to group on no columns, which is one of the -few legitimate uses of `Nil`: - -```haskell -foo :: Query lat with - '[ "public" ::: - '[ "table" ::: 'Table - ('[] :=> '[ "id" ::: 'NoDef :=> 'NotNull 'PGint4 ]) - ] - ] - '[] - '[ "a" ::: 'NotNull 'PGint8 ] -foo = select - (count (All $ #table ! #id) `as` #a) - ( from (table #table) - & groupBy Nil -- group by nothing! - ) -``` - -You will mess this up at least 2 times. - -### `from` - -When a column is specified in the selection list, how do you know when it's -valid? This may seem like a silly question, but consider the following -(stupid) query: - -```SQL -SELECT bar.id FROM foo; -``` - -No one would write a query like this. Syntactically it seems fine. But it's -nonsense, because we haven't pulled in the `bar` table to our query! And -Postgres agrees with us. - -``` -ERROR: missing FROM-clause entry for table "bar" -LINE 1: SELECT bar.id -``` - -Every query creates a *scope* for identifiers, separate from the database scope -of table names and schemas, and each join adds a new name to this scope. This is -plain to see if we use AS to rename a table that we pull in. - -```haskell -SELECT f.user_id FROM foo AS f; --- ok - -SELECT foo.user_id FROM foo AS f; --- ERROR: invalid reference to FROM-clause entry for table "foo" --- LINE 1: SELECT foo.user_id FROM foo AS f; -``` - -The name `f` is only in scope for this query. Even when you *don't* specify -an alias in the SQL, it works as if you had specified the alias as the table's -name, which we can see since the latter query gets rejected by Postgres. - -In order to figure out whether identifiers like `#table ! #field` are valid, -Squeal needs to track this scope as well. It does so using the `from` type -variable. - -In fact, this type variable is all you need to write standalone expressions. - -```haskell --- doesn't typecheck -badExpr :: Expression 'Ungrouped '[] with db params from ('NotNull 'PGint4) -badExpr = #table ! #field - --- does typecheck -goodExpr :: Expression 'Ungrouped '[] with db params - '[ "table" ::: '[ "field" ::: 'NotNull 'PGint4 ] ] - ('NotNull 'PGint4) -goodExpr = #table ! #field -``` - -Whenever you create an expression by referring to a column by name, it's this -scope inside the `from` type variable that Squeal checks to ensure that the -reference is valid. If you're getting `HasErr` constraint violations, it's -likely that the contents of this variable aren't in the right form. - -The only way to add things to `from` is by using joins. If you look at the type -signature of the various join functions that Squeal provides, they take an -existing `FromClause` and add an extra table to it. - -```haskell -table - :: (Has sch db schema, Has tab schema (Table table)) - => Aliased (QualifiedAlias sch) (alias ::: tab) - -> FromClause lat with db params '[alias ::: TableToRow table] - -- `table` (and `view`, `common`, `subquery`) produce a FromClause - -- containing a single set of columns... - -innerJoin - :: FromClause lat with db params right - -> Condition Ungrouped lat with db params (Join left right) - -> FromClause lat with db params left - -> FromClause lat with db params (Join left right) - -- ...which the join functions append to the tables already in `from` -``` - -For a more complicated query, here's what the value of `from` may end up -looking like: - -```haskell -type QueryFrom = - '[ "table1" ::: - '[ "id" ::: 'NotNull 'PGuuid - , "field1" ::: 'Null 'PGint4 - , "field2" ::: 'NotNull 'PGtext - ] - , "table2" ::: - '[ "id" ::: 'NotNull 'PGuuid - , "field1" ::: 'NotNull 'PGbool - ] - , "table3" ::: - '[ "id" ::: 'NotNull 'PGuuid - , "table2_id" ::: 'NotNull 'PGuuid - , "created_at" ::: 'NotNull 'PGtimestamptz - ] - ] - -type DB = -- ... some schema with the appropriate tables ... - -froms :: FromClause lat with DB params QueryFrom -froms = - ( table (#table1 `as` #table1) - & innerJoin (table (#table2 `as` #table2)) - (#table2 ! #id .== #table1 ! #id) - & innerJoin (table #table3) - (#table3 ! #table2_id .== #table2 ! #id) - ) -``` - -Note that the order of tables in `from` needs to be the same order in which they -were joined. - -Explicitly providing a type alias for which tables a query has access to -like this is often useful, especially when you want to allow callers of your -query to pass in custom expressions. For instance, if you wanted to allow -callers to pass in a filtering condition, you could do that by ensuring that -the `from` in the passed-in Expression is valid for the tables that the query -joins on. - -```haskell -parameterized :: - Expression 'Ungrouped lat with db params QueryFrom ('NotNull 'PGbool) - -> Query lat with db params '[ "a" ::: 'NotNull 'PGint4 ] -parameterized cond = - select - (#table1 ! #field1) - ( from froms - & where_ cond - ) -``` - -Then, as you'd expect, any callsite could use any columns specified in that -query scope, while rejecting any references that aren't in scope. - -```haskell --- these typecheck -qry1 = parameterized (#table1 ! #field2 .== "mobile") -qry2 = parameterized (#table2 ! #field1) - --- these don't -qry3 = parameterized (#table4 ! #id .== "SimSpace") -qry4 = parameterized (#table1 ! #nonfield) -``` - -Note the "flow" of type-level data going on here: we start off with existing -table definitions in `db`, which get pulled into the value of `from` by using -`innerJoin`, `leftOuterJoin`, etc. Once `from` has the right type-level value, -now expressions like `#table1 ! #field2` will satisfy their `IsQualified` -constraint and typecheck. - -### `lat` - -Remember when I said that `from` was the only place that Squeal looked when -it was checking whether column references were valid? That was a slight lie. -Squeal actually checks one other place, the `lat` type param. - -You can see this in the various IsQualified instances for Expression, Aliased Expression, -etc. - -![IsQualified looks in both lat and from for scoping expressions](squeal/isqualified-join-constraint.png) - -Other than joins, there's one other place where identifiers can come into scope -for a query: a Postgres-specific feature called lateral joins. If you've used -Microsoft SQL Server before, you might know these as CROSS APPLYs, but if not, -it's totally understandable if you've never seen a lateral join before. - -To understand what a lateral join does, it's helpful to compare it to a normal -subquery join. When you do a normal subquery join, the subquery is a completely -different query from the one it's being joined into, which means that the subquery -can't access anything that's in scope from the parent query. - -```SQL -SELECT * - FROM some_table st - JOIN (SELECT * FROM some_other_table sot WHERE st.id = sot.table_id) - -- error: st is not in scope in the subquery -``` - -But sometimes having those values in scope would be extremely useful. Say you -have a table for your users, and another table containing login events. - -```SQL -CREATE TABLE "user" - ( id SERIAL PRIMARY KEY NOT NULL - , username TEXT NOT NULL - , email TEXT NOT NULL - , created_at TIMESTAMPTZ NOT NULL - ); - -CREATE TABLE "login_event" - ( user_id INT4 NOT NULL REFERENCES "user" (id) - , source TEXT NOT NULL -- web|mobile|embed - , timestamp TIMESTAMPTZ NOT NULL - ); -``` - -How would you write a query to get, for each user, the latest login, including -all the username/email information, and the source information? It's possible -using normal subqueries, albeit somewhat painful to write. If you don't believe -me, stop reading and try writing a raw SQL query to get this data. - -Lateral joins give you a more convenient way to write "dependent" queries like -this. They act more like a "foreach" loop: Postgres will run the lateral -subquery for each row being joined to, and calculate separate sets of rows to -join with. - -```SQL -SELECT u.id, u.username, u.email, u.created_at, - le.source, le.timestamp - FROM public.user AS u - INNER JOIN LATERAL - (SELECT le.source, le.timestamp - FROM public.login_event AS le - WHERE le.user_id = u.id - ORDER BY le.timestamp DESC - LIMIT 1) AS le - ON TRUE; -``` - -Helpful, right? But since it's another way in which identifiers can come into -scope for a query, Squeal needs a way to represent it. - -Structurally, `lat` looks exactly like `from`; it's a mapping of table -names to lists of columns and their types. You'll never have to worry about it -unless you're using lateral joins; Squeal provides explicit lateral versions -of all the normal join functions which are there if you need them. Since most -of the time you won't be using these, well... - -### `db` - -We saw in the discussion of `from` how that type parameter gets populated by -usages of functions like `innerJoin`, `leftOuterJoin`, and so on. But the -table types that get joined on have to *come* from `db`, which we can see -happening in the type signatures for `table` and `view`: - -```haskell -table - :: (Has sch db schema, Has tab schema (Table table)) - => Aliased (QualifiedAlias sch) (alias ::: tab) - -> FromClause lat with db params '[alias ::: TableToRow table] - -view - :: (Has sch db schema, Has vw schema (View view)) - => Aliased (QualifiedAlias sch) (alias ::: vw) - -> FromClause lat with db params '[alias ::: view] -``` - -It can be a little easier to see what's going on here if we have an explicit -DB type. Let's define one. - -```haskell -type DB = '[ "public" ::: Schema ] - -type Schema = '[ "users" ::: Table UsersTable ] - -type UsersTable = - '[] :=> '[ "id" ::: 'NotNull 'PGuuid ] - --- using our new type -table (#public ! #users) -``` - -Walk through the constraints being discharged: the first `Has sch db schema` -becomes `Has "public" DB schema`, checking whether the "public" schema exists -within our database type. The second `Has` becomes `Has "users" schema (Table table)`, -checking whether there's a correctly named table within the schema found by -the first constraint. Once those constraints are discharged, the compiler -is happy to give us a `FromClause` which we can use in our joins and queries, -thus bringing columns into scope. - -That's all `db` is: a type-level tree structure representing your entire -database schema, such that downstream code can figure out what tables and -columns exists, and thus, whether your queries are reasonable. You can think -of it as the source of truth about column types that everything starts from. - -In practice most of the compilation errors you encounter won't be caused by your -`db` type. It's not usually a go-to type parameter to put constraints on for -abstraction, either. Squeal does not provide any functionality for keeping -your top-level DB type definition in sync with your actual Postgres schema, -however. Ensuring that your DB type reflects reality is your responsibility. - -### `with` - -You've probably seen SQL queries of the form `WITH AS SELECT ...`, -like a let-binding but for a subquery. Squeal has support for these sorts of -queries as well, using the [`with`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Query-With.html#v:with) function. - -```haskell -qry :: Query lat with DB params '[ "a" ::: 'NotNull 'PGbool ] -qry = with - (select (true `as` #a) (from (table #users)) `as` #cte) - (select Star - (from (common #cte))) -- note the use of `common` to bring the CTE into scope - -- for this query -``` - -Since these subqueries are scoped to each individual query, Squeal needs some -way of keeping track of them so that it can check that usages of `common` are -valid. As you might guess, that storage is happening in the `with` type -parameter. - -```haskell -with - ( select (true `as` #a) (from (table #users)) `as` #subq1 - :>> select (false `as`#b) (from (table #users)) `as` #subq2 - ) - - --- :: Query ... with ... --- where `with` = '[ "subq1" ::: '[ "a" ::: 'NotNull 'PGbool ], "subq2" ::: '[ "b" ::: 'NotNull 'PGbool ] ] -``` - -Note that when passing the CTEs to `with`, we use `(:>>)` to construct the list. -When constructing select lists and such, we were using `(:*)`, but here the -argument has a different type. Instead of a value of type `NP`, `with` takes in -a value of type [`Path`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Type-List.html#t:Path). -The practical difference is that subqueries further in the list can use subqueries -earlier in the list. Just remember that you need to use this pointier list -constructor when using CTEs. - -In theory `with` can be a useful point of abstraction, by specifying that some -query needs a subquery of some other type in scope. In practice you can -usually just pass that subquery as a Haskell parameter. - -One small trick about using CTEs in Squeal: `with` requires its subqueries to -all be the same type, either all `Query`'s or all `Manipulation`s. That's -a little annoying, since it seems like you couldn't, say, run an update and -then do a query on the updated rows; you'd have a `Manipulation` and a `Query` -in the same CTE expression. But Squeal provides a convenient function -[`queryStatement`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Manipulation.html#v:queryStatement) -to convert from a `Query` to a `Manipulation` (since any query is just a manipulation -that touches no rows). That'll allow you to implement cases like these. - -A caution about the queries in a CTE block: Squeal represents them as a -sequential path, but [Postgres actually runs them concurrently, so their order is -unpredictable.](https://www.postgresql.org/docs/12/queries-with.html#QUERIES-WITH-MODIFYING) -So you may not be able to rely on them to, say, run an update statement and a query in the order -they're written. You can resolve this by having one statement in the query -depend on a value returned by another one. - -### `params` - -When generating queries, it's possible to write normal Haskell functions that -take in values and inline those values into your queries. However, Squeal also -provides a built-in hole for parameters, which you use through the `param` -function instead of inlining them. - -```haskell -qry - :: params ~ '[ 'NotNull 'PGint4, 'NotNull 'PGtext ] - => Query lat with DB params '[ "email" ::: 'Null 'PGtext ] -qry = select - (#e ! #email) - ( from (table (#users `as` #u) - & innerJoin (table (#emails `as` #e)) - (#e ! #user_id .== #u ! #id)) - & where_ (#u ! #id .== param @1) -- specify which param with TypeApplications - & where_ (#u ! #name .== param @2) -- note that the params are 1-indexed - ) -``` - -The main reason to specify the `params` variable and pass values in this way -is to make some common value available to all the components of a query, without -needing to explicitly pass it around. For instance, when writing a query with -CTEs, you'll probably have some common ID that all parts of the query need. - -```haskell -qry - :: params ~ '[ 'NotNull 'PGuuid ] - => Query lat with DB params ... -qry = with - ( ... -- all CTE definitions here can make use of the UUID param - ) - ... -- as can the main query -``` - -Another distinction between inlining and parameters is that parameters are -supplied to the DB using Postgres' binary format, and thus aren't susceptible -to SQL injection attacks. Inlined values are, as the name implies, directly -inlined in the SQL string that gets sent to the database, and thus quite a bit -more dangerous. - -Because of this, you should prefer passing data into a query using parameters -whenever possible. - -There isn't much else to say about the `params` type variable; this is all it's -used for. - -### `ty` - -Nothing special here, it's just the Postgres type and nullity of the expression. - -You can see the possible nullities on the constructors of [`NullType`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Type-Schema.html#t:NullType), -and the possible types on the constructors of [`PGType`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Type-Schema.html#t:PGType). - -## Encoding and decoding - -Despite everything we've looked at, we still don't know how to bridge the gap -between Haskell-land and Postgres-land. How do we get the data from the database -into a form that our program can understand? - -As mentioned earlier, the type that bridges this gap is `Statement`, as it -combines some query with encoders for parameters, and decoders for PG rows. - -Squeal provides the [`query`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Session-Statement.html#v:query) -and [`manipulation`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Session-Statement.html#v:manipulation) -functions for converting Query's and Manipulations into Statement, but note -that these rely on Squeal's generic encoding/decoding, which takes away control -of the translation. Sometimes it works, but most of the time you're not doing -1-to-1 translation between a record type and rows with *exactly* the same number -of columns, where the column names are *exactly* the same as the record fields. -For those cases, it's better to write your codecs manually. - -My recommendation is to use the constructors of [`Statement`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Session-Statement.html#t:Statement) -directly: `Query` and `Manipulation`. You can see that these take in -arguments of type [`EncodeParams`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Session-Encode.html#t:EncodeParams) -and [`DecodeRow`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Session-Decode.html#t:DecodeRow). - -For `EncodeParams`, you need to turn the parameter type into a list of functions -for pulling the individual values using `(.*)` and `(*.)`. `(*.)` is for the -very last element of the list, `(.*)` is for everything else. - -```haskell -data SomeType = SomeType { a :: Bool, b :: Int32, c :: Int64 } - --- Notice how we don't actually take in a value of SomeType, --- we just create a list of accessor functions -enc :: EncodeParams db '[ 'NotNull 'PGbool, 'NotNull 'PGint4, 'NotNull 'PGint8 ] SomeType -enc = a .* b *. c -``` - -You will mix up these operators and get a confusing type error at least once. - -`DecodeRow` implements both `Monad` and `IsLabel`, which lets you construct -decoders like the following: - -```haskell --- Note how we can decode from a row with completely different --- column names from our record type -dec :: DecodeRow - '[ "a_bool" ::: 'NotNull 'PGbool - , "a_4byteint" ::: 'NotNull 'PGint4 - , "an_8byteint" ::: 'NotNull 'PGint8 - ] - SomeType -dec = do - a <- #a_bool - b <- #a_4byteint - c <- #an_8byteint - pure $ SomeType { a = a, b = b, c = c } -``` - -Since it implements `Monad`, you can implement complicated conditional parsers, -key off of certain columns being null or non-null to parse further columns, -construct intermediate data structures from groups of fields, etc. diff --git a/squeal-postgresql-ltree/LICENSE b/squeal-postgresql-ltree/LICENSE deleted file mode 100644 index d71d6fe7..00000000 --- a/squeal-postgresql-ltree/LICENSE +++ /dev/null @@ -1,31 +0,0 @@ -Copyright (c) 2020 Morphism, LLC - -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - * Neither the names of the copyright holders nor the names of the - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/squeal-postgresql-ltree/README.md b/squeal-postgresql-ltree/README.md deleted file mode 100644 index ad88be59..00000000 --- a/squeal-postgresql-ltree/README.md +++ /dev/null @@ -1 +0,0 @@ -# squeal-postgresql-ltree diff --git a/squeal-postgresql-ltree/squeal-postgresql-ltree.cabal b/squeal-postgresql-ltree/squeal-postgresql-ltree.cabal deleted file mode 100644 index 019f1ca7..00000000 --- a/squeal-postgresql-ltree/squeal-postgresql-ltree.cabal +++ /dev/null @@ -1,35 +0,0 @@ -name: squeal-postgresql-ltree -version: 0.1.0.0 -synopsis: LTree extension for Squeal -description: LTree extension for Squeal -homepage: https://github.com/morphismtech/squeal/ltree -bug-reports: https://github.com/morphismtech/squeal/issues -license: BSD3 -license-file: LICENSE -author: Eitan Chatav -maintainer: eitan.chatav@gmail.com -copyright: Copyright (c) 2021 Morphism, LLC -category: Database -build-type: Simple -cabal-version: >=1.18 -extra-doc-files: README.md - -source-repository head - type: git - location: https://github.com/morphismtech/squeal.git - -library - hs-source-dirs: src - exposed-modules: - Squeal.PostgreSQL.LTree - default-language: Haskell2010 - ghc-options: -Wall - build-depends: - base >= 4.12.0.0 && < 5.0 - , bytestring >= 0.10.10.0 - , generics-sop >= 0.5.1.0 - , mtl >= 2.2.2 - , postgresql-binary >= 0.12.2 - , postgresql-libpq >= 0.9.4.2 - , squeal-postgresql >= 0.7.0.1 - , text >= 1.2.3.2 diff --git a/squeal-postgresql-ltree/src/Squeal/PostgreSQL/LTree.hs b/squeal-postgresql-ltree/src/Squeal/PostgreSQL/LTree.hs deleted file mode 100644 index cb19376d..00000000 --- a/squeal-postgresql-ltree/src/Squeal/PostgreSQL/LTree.hs +++ /dev/null @@ -1,477 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.LTree -Description: ltree -Copyright: (c) Eitan Chatav, 2020 -Maintainer: eitan@morphism.tech -Stability: experimental - -This module implements a data type ltree for representing -labels of data stored in a hierarchical tree-like structure. --} - -{-# LANGUAGE - DataKinds - , DeriveGeneric - , DerivingStrategies - , FlexibleInstances - , GeneralizedNewtypeDeriving - , MultiParamTypeClasses - , OverloadedStrings - , PolyKinds - , TypeFamilies - , TypeOperators - , TypeSynonymInstances - , UndecidableInstances -#-} - -{-# OPTIONS_GHC -fno-warn-orphans #-} - -module Squeal.PostgreSQL.LTree - ( -- * Definition - createLTree - -- * Types - , LTree(..), LQuery(..), LTxtQuery(..) - , PGltree, PGlquery, PGltxtquery - , ltree, lquery, ltxtquery - -- * Functions - , subltree, subpath, subpathEnd - , nlevel, indexLTree, indexOffset - , text2ltree, ltree2text, lca - -- * Operators - , (%~), (~%), (%?), (?%), (%@), (@%) - , (@>%), (%<@), (<@%), (%@>) - , (&~), (~&), (&?), (?&), (&@), (@&) - , (?@>), (?<@), (?~), (?@) - ) where - -import Control.Exception hiding (TypeError) -import Control.Monad.Reader -import Data.String -import Data.Text -import GHC.Generics -import GHC.TypeLits (ErrorMessage(Text), TypeError) -import Squeal.PostgreSQL -import Squeal.PostgreSQL.Render - -import qualified Database.PostgreSQL.LibPQ as LibPQ -import qualified Generics.SOP as SOP -import qualified PostgreSQL.Binary.Decoding as Decoding -import qualified PostgreSQL.Binary.Encoding as Encoding - --- | Postgres ltree type -type PGltree = 'UnsafePGType "ltree" --- | Postgres lquery type -type PGlquery = 'UnsafePGType "lquery" --- | Postgres ltxtquery type -type PGltxtquery = 'UnsafePGType "ltxtquery" - --- | Loads ltree extension into the current database. -createLTree :: Definition db db -createLTree = UnsafeDefinition "CREATE EXTENSION \"ltree\";" - --- | Postgres ltree type expression -ltree :: TypeExpression db (null PGltree) -ltree = UnsafeTypeExpression "ltree" - --- | Postgres lquery type expression -lquery :: TypeExpression db (null PGlquery) -lquery = UnsafeTypeExpression "lquery" - --- | Postgres ltxtquery type expression -ltxtquery :: TypeExpression db (null PGltxtquery) -ltxtquery = UnsafeTypeExpression "ltxtquery" - -instance PGTyped db PGltree where pgtype = ltree -instance PGTyped db PGlquery where pgtype = lquery -instance PGTyped db PGltxtquery where pgtype = ltxtquery - -instance OidOf db PGltree where - oidOf = oidLtreeLookup "oid" "ltree" -instance OidOf db PGlquery where - oidOf = oidLtreeLookup "oid" "lquery" -instance OidOf db PGltxtquery where - oidOf = oidLtreeLookup "oid" "ltxtquery" -instance OidOfArray db PGltree where - oidOfArray = oidLtreeLookup "typarray" "ltree" -instance OidOfArray db PGlquery where - oidOfArray = oidLtreeLookup "typarray" "lquery" -instance OidOfArray db PGltxtquery where - oidOfArray = oidLtreeLookup "typarray" "ltxtquery" - -oidLtreeLookup - :: String - -> String - -> ReaderT (SOP.K LibPQ.Connection db) IO LibPQ.Oid -oidLtreeLookup tyOrArr name = ReaderT $ \(SOP.K conn) -> do - resultMaybe <- LibPQ.execParams conn q [] LibPQ.Binary - case resultMaybe of - Nothing -> throwIO $ ConnectionException oidErr - Just result -> do - numRows <- LibPQ.ntuples result - when (numRows /= 1) $ throwIO $ RowsException oidErr 1 numRows - valueMaybe <- LibPQ.getvalue result 0 0 - case valueMaybe of - Nothing -> throwIO $ ConnectionException oidErr - Just value -> case Decoding.valueParser Decoding.int value of - Left err -> throwIO $ DecodingException oidErr err - Right oid' -> return $ LibPQ.Oid oid' - where - oidErr = "oidOf " <> fromString (name <> tyOrArr) - q = "SELECT " <> fromString tyOrArr - <> " FROM pg_type WHERE typname = \'" - <> fromString name <> "\';" - -{- | -A label is a sequence of alphanumeric characters and underscores -(for example, in C locale the characters A-Za-z0-9_ are allowed). -Labels must be less than 256 bytes long. - -@ -Examples: 42, Personal_Services -@ - -A label path is a sequence of zero or more labels separated by dots, -for example L1.L2.L3, representing a path from the root of a -hierarchical tree to a particular node. The length of a label path -must be less than 65Kb, but keeping it under 2Kb is preferable. -In practice this is not a major limitation; for example, -the longest label path in the DMOZ catalogue -(http://www.dmoz.org) is about 240 bytes. - -@ -Example: Top.Countries.Europe.Russia -@ - -ltree stores a label path. --} -newtype LTree = UnsafeLTree {getLTree :: Text} - deriving stock (Eq,Ord,Show,Read,Generic) --- | `PGltree` -instance IsPG LTree where type PG LTree = PGltree -instance TypeError ('Text "LTree binary instances not yet implemented.") - => FromPG LTree where - fromPG = UnsafeLTree <$> devalue Decoding.text_strict -instance TypeError ('Text "LTree binary instances not yet implemented.") - => ToPG db LTree where - toPG = pure . Encoding.text_strict . getLTree -instance Inline LTree where - inline (UnsafeLTree x) - = UnsafeExpression - . parenthesized - . (<> " :: ltree") - . escapeQuotedText - $ x - -{- | -lquery represents a regular-expression-like pattern for matching ltree values. -A simple word matches that label within a path. -A star symbol (*) matches zero or more labels. For example: - -@ -foo Match the exact label path foo -*.foo.* Match any label path containing the label foo -*.foo Match any label path whose last label is foo -@ - -Star symbols can also be quantified to restrict how many labels they can match: - -@ -*{n} Match exactly n labels -*{n,} Match at least n labels -*{n,m} Match at least n but not more than m labels -*{,m} Match at most m labels — same as *{0,m} -@ - -There are several modifiers that can be put at the end of a non-star label -in lquery to make it match more than just the exact match: - -@ -\@ Match case-insensitively, for example a@ matches A -* Match any label with this prefix, for example foo* matches foobar -% Match initial underscore-separated words -@ - -The behavior of % is a bit complicated. -It tries to match words rather than the entire label. -For example foo_bar% matches foo_bar_baz but not foo_barbaz. -If combined with *, prefix matching applies to each word separately, -for example foo_bar%* matches foo1_bar2_baz but not foo1_br2_baz. - -Also, you can write several possibly-modified labels separated with -| (OR) to match any of those labels, -and you can put ! (NOT) at the start to match any label -that doesn't match any of the alternatives. - -Here's an annotated example of lquery: - -@ -Top.*{0,2}.sport*@.!football|tennis.Russ*|Spain -1. 2. 3. 4. 5. -@ - -This query will match any label path that: - -1. begins with the label Top -2. and next has zero to two labels before -3. a label beginning with the case-insensitive prefix sport -4. then a label not matching football nor tennis -5. and then ends with a label beginning with Russ or exactly matching Spain. --} -newtype LQuery = UnsafeLQuery {getLQuery :: Text} - deriving stock (Eq,Ord,Show,Read,Generic) --- | `PGlquery` -instance IsPG LQuery where type PG LQuery = PGlquery -instance TypeError ('Text "LQuery binary instances not yet implemented.") - => FromPG LQuery where - fromPG = UnsafeLQuery <$> devalue Decoding.text_strict -instance TypeError ('Text "LQuery binary instances not yet implemented.") - => ToPG db LQuery where - toPG = pure . Encoding.text_strict . getLQuery -instance Inline LQuery where - inline (UnsafeLQuery x) - = UnsafeExpression - . parenthesized - . (<> " :: lquery") - . escapeQuotedText - $ x - -{- | -ltxtquery represents a full-text-search-like pattern for matching ltree values. -An ltxtquery value contains words, -possibly with the modifiers @, *, % at the end; -the modifiers have the same meanings as in lquery. -Words can be combined with & (AND), | (OR), ! (NOT), and parentheses. -The key difference from lquery is that ltxtquery matches words -without regard to their position in the label path. - -Here's an example ltxtquery: - -@ -Europe & Russia*@ & !Transportation -@ - -This will match paths that contain the label Europe and any label -beginning with Russia (case-insensitive), but not paths containing -the label Transportation. The location of these words within the -path is not important. Also, when % is used, the word can be matched -to any underscore-separated word within a label, regardless of position. - -Note: ltxtquery allows whitespace between symbols, but ltree and lquery do not. --} -newtype LTxtQuery = UnsafeLTxtQuery {getLTxtQuery :: Text} - deriving stock (Eq,Ord,Show,Read,Generic) --- | `PGltxtquery` -instance IsPG LTxtQuery where type PG LTxtQuery = PGltxtquery -instance TypeError ('Text "LTxtQuery binary instances not yet implemented.") - => FromPG LTxtQuery where - fromPG = UnsafeLTxtQuery <$> devalue Decoding.text_strict -instance TypeError ('Text "LTxtQuery binary instances not yet implemented.") - => ToPG db LTxtQuery where - toPG = pure . Encoding.text_strict . getLTxtQuery -instance Inline LTxtQuery where - inline (UnsafeLTxtQuery x) - = UnsafeExpression - . parenthesized - . (<> " :: ltxtquery") - . escapeQuotedText - $ x - -instance IsString - (Expression grp lat with db params from (null PGltree)) where - fromString - = UnsafeExpression - . parenthesized - . (<> " :: ltree") - . escapeQuotedString -instance IsString - (Expression grp lat with db params from (null PGlquery)) where - fromString - = UnsafeExpression - . parenthesized - . (<> " :: lquery") - . escapeQuotedString -instance IsString - (Expression grp lat with db params from (null PGltxtquery)) where - fromString - = UnsafeExpression - . parenthesized - . (<> " :: ltxtquery") - . escapeQuotedString - --- | Returns subpath of ltree from position start to position end-1 (counting from 0). -subltree :: '[null PGltree, null 'PGint4, null 'PGint4] ---> null PGltree -subltree = unsafeFunctionN "subltree" - --- | Returns subpath of ltree starting at position offset, with length len. --- If offset is negative, subpath starts that far from the end of the path. --- If len is negative, leaves that many labels off the end of the path. -subpath :: '[null PGltree, null 'PGint4, null 'PGint4] ---> null PGltree -subpath = unsafeFunctionN "subpath" - --- | Returns subpath of ltree starting at position offset, --- extending to end of path. If offset is negative, --- subpath starts that far from the end of the path. -subpathEnd :: '[null PGltree, null 'PGint4] ---> null PGltree -subpathEnd = unsafeFunctionN "subpath" - --- | Returns number of labels in path. -nlevel :: null PGltree --> null 'PGint4 -nlevel = unsafeFunction "nlevel" - --- | Returns position of first occurrence of b in a, or -1 if not found. -indexLTree :: '[null PGltree, null PGltree] ---> null 'PGint4 -indexLTree = unsafeFunctionN "index" - --- | Returns position of first occurrence of b in a, or -1 if not found. --- The search starts at position offset; --- negative offset means start -offset labels from the end of the path. -indexOffset :: '[null PGltree, null PGltree, null 'PGint4] ---> null 'PGint4 -indexOffset = unsafeFunctionN "index" - --- | Casts text to ltree. -text2ltree :: null 'PGtext --> null PGltree -text2ltree = unsafeFunction "text2ltree" - --- | Casts ltree to text. -ltree2text :: null PGltree --> null 'PGtext -ltree2text = unsafeFunction "ltree2text" - --- | Computes longest common ancestor of paths in array. -lca :: null ('PGvararray ('NotNull PGltree)) --> null PGltree -lca = unsafeFunction "lca" - -{- | -`(@>)` Is left argument an ancestor of right (or equal)? - -`(<@)` Is left argument a descendant of right (or equal)? --} -instance PGSubset PGltree - --- | Does ltree match lquery? -(%~) :: Operator (null0 PGltree) (null1 PGlquery) ('Null 'PGbool) -(%~) = unsafeBinaryOp "~" -infix 4 %~ - --- | Does ltree match lquery? -(~%) :: Operator (null1 PGlquery) (null0 PGltree) ('Null 'PGbool) -(~%) = unsafeBinaryOp "~" -infix 4 ~% - --- | Does ltree match any lquery in array? -(%?) :: Operator - (null0 PGltree) (null1 ('PGvararray ('NotNull PGlquery))) ('Null 'PGbool) -(%?) = unsafeBinaryOp "?" -infix 4 %? - --- | Does ltree match any lquery in array? -(?%) :: Operator - (null0 ('PGvararray ('NotNull PGlquery))) (null1 PGltree) ('Null 'PGbool) -(?%) = unsafeBinaryOp "?" -infix 4 ?% - --- | Does ltree match ltxtquery? -(%@) :: Operator (null0 PGltree) (null1 PGltxtquery) ('Null 'PGbool) -(%@) = unsafeBinaryOp "@" -infix 4 %@ - --- | Does ltree match ltxtquery? -(@%) :: Operator (null0 PGltxtquery) (null1 PGltree) ('Null 'PGbool) -(@%) = unsafeBinaryOp "@" -infix 4 @% - --- | `(<>)` Concatenates ltree paths. -instance Semigroup - (Expression grp lat with db params from (null PGltree)) where - (<>) = unsafeBinaryOp "||" -instance Monoid - (Expression grp lat with db params from (null PGltree)) where - mempty = fromString "" - mappend = (<>) - --- | Does array contain an ancestor of ltree? -(@>%) :: Operator - (null0 ('PGvararray ('NotNull PGltree))) (null1 PGltree) ('Null 'PGbool) -(@>%) = unsafeBinaryOp "@>" -infix 4 @>% - --- | Does array contain an ancestor of ltree? -(%<@) :: Operator - (null0 PGltree) (null1 ('PGvararray ('NotNull PGltree))) ('Null 'PGbool) -(%<@) = unsafeBinaryOp "<@" -infix 4 %<@ - --- | Does array contain a descendant of ltree? -(<@%) :: Operator - (null0 ('PGvararray ('NotNull PGltree))) (null1 PGltree) ('Null 'PGbool) -(<@%) = unsafeBinaryOp "<@" -infix 4 <@% - --- | Does array contain a descendant of ltree? -(%@>) :: Operator - (null0 PGltree) (null1 ('PGvararray ('NotNull PGltree))) ('Null 'PGbool) -(%@>) = unsafeBinaryOp "@>" -infix 4 %@> - --- | Does array contain any path matching lquery? -(&~) :: Operator - (null0 ('PGvararray ('NotNull PGltree))) (null1 PGlquery) ('Null 'PGbool) -(&~) = unsafeBinaryOp "~" -infix 4 &~ - --- | Does array contain any path matching lquery? -(~&) :: Operator - (null0 PGlquery) (null1 ('PGvararray ('NotNull PGltree))) ('Null 'PGbool) -(~&) = unsafeBinaryOp "~" -infix 4 ~& - --- | Does ltree array contain any path matching any lquery? -(&?) :: Operator - (null0 ('PGvararray ('NotNull PGltree))) - (null1 ('PGvararray ('NotNull PGlquery))) - ('Null 'PGbool) -(&?) = unsafeBinaryOp "?" -infix 4 &? - --- | Does ltree array contain any path matching any lquery? -(?&) :: Operator - (null0 ('PGvararray ('NotNull PGlquery))) - (null1 ('PGvararray ('NotNull PGltree))) - ('Null 'PGbool) -(?&) = unsafeBinaryOp "?" -infix 4 ?& - --- | Does array contain any path matching ltxtquery? -(&@) :: Operator - (null0 ('PGvararray ('NotNull PGltree))) (null1 PGltxtquery) ('Null 'PGbool) -(&@) = unsafeBinaryOp "@" -infix 4 &@ - --- | Does array contain any path matching ltxtquery? -(@&) :: Operator - (null0 PGltxtquery) (null1 ('PGvararray ('NotNull PGltree))) ('Null 'PGbool) -(@&) = unsafeBinaryOp "@" -infix 4 @& - --- | Returns first array entry that is an ancestor of ltree, or NULL if none. -(?@>) :: Operator - (null0 ('PGvararray ('NotNull PGltree))) (null1 PGltree) ('Null PGltree) -(?@>) = unsafeBinaryOp "?@>" -infix 4 ?@> - --- | Returns first array entry that is a descendant of ltree, or NULL if none. -(?<@) :: Operator - (null0 ('PGvararray ('NotNull PGltree))) (null1 PGltree) ('Null PGltree) -(?<@) = unsafeBinaryOp "?<@" -infix 4 ?<@ - --- | Returns first array entry that matches lquery, or NULL if none. -(?~) :: Operator - (null0 ('PGvararray ('NotNull PGltree))) (null1 PGlquery) ('Null PGltree) -(?~) = unsafeBinaryOp "?~" -infix 4 ?~ - --- | Returns first array entry that matches ltxtquery, or NULL if none. -(?@) :: Operator - (null0 ('PGvararray ('NotNull PGltree))) (null1 PGltxtquery) ('Null PGltree) -(?@) = unsafeBinaryOp "?@" -infix 4 ?@ diff --git a/squeal-postgresql-uuid-ossp/LICENSE b/squeal-postgresql-uuid-ossp/LICENSE deleted file mode 100644 index d71d6fe7..00000000 --- a/squeal-postgresql-uuid-ossp/LICENSE +++ /dev/null @@ -1,31 +0,0 @@ -Copyright (c) 2020 Morphism, LLC - -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - * Neither the names of the copyright holders nor the names of the - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/squeal-postgresql-uuid-ossp/README.md b/squeal-postgresql-uuid-ossp/README.md deleted file mode 100644 index 32a05d5a..00000000 --- a/squeal-postgresql-uuid-ossp/README.md +++ /dev/null @@ -1 +0,0 @@ -# squeal-postgresql-uuid-ossp diff --git a/squeal-postgresql-uuid-ossp/squeal-postgresql-uuid-ossp.cabal b/squeal-postgresql-uuid-ossp/squeal-postgresql-uuid-ossp.cabal deleted file mode 100644 index 48b167a4..00000000 --- a/squeal-postgresql-uuid-ossp/squeal-postgresql-uuid-ossp.cabal +++ /dev/null @@ -1,29 +0,0 @@ -name: squeal-postgresql-uuid-ossp -version: 0.1.0.0 -synopsis: UUID OSSP extension for Squeal -description: UUID OSSP extension for Squeal -homepage: https://github.com/morphismtech/squeal/uuid-ossp -bug-reports: https://github.com/morphismtech/squeal/issues -license: BSD3 -license-file: LICENSE -author: Eitan Chatav -maintainer: eitan.chatav@gmail.com -copyright: Copyright (c) 2021 Morphism, LLC -category: Database -build-type: Simple -cabal-version: >=1.18 -extra-doc-files: README.md - -source-repository head - type: git - location: https://github.com/morphismtech/squeal.git - -library - hs-source-dirs: src - exposed-modules: - Squeal.PostgreSQL.UUID.OSSP - default-language: Haskell2010 - ghc-options: -Wall - build-depends: - base >= 4.12.0.0 && < 5.0 - , squeal-postgresql >= 0.7.0.1 diff --git a/squeal-postgresql-uuid-ossp/src/Squeal/PostgreSQL/UUID/OSSP.hs b/squeal-postgresql-uuid-ossp/src/Squeal/PostgreSQL/UUID/OSSP.hs deleted file mode 100644 index 039c09b9..00000000 --- a/squeal-postgresql-uuid-ossp/src/Squeal/PostgreSQL/UUID/OSSP.hs +++ /dev/null @@ -1,109 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.UUID.OSSP -Description: uuid-ossp -Copyright: (c) Eitan Chatav, 2020 -Maintainer: eitan@morphism.tech -Stability: experimental - -This module provides functions to generate universally -unique identifiers (UUIDs) using one of several standard algorithms. -There are also functions to produce certain special UUID constants. --} - -{-# LANGUAGE - DataKinds - , OverloadedStrings - , TypeOperators -#-} - -module Squeal.PostgreSQL.UUID.OSSP - ( -- * Definition - createUuidOssp - -- * Generation - , uuidGenerateV1 - , uuidGenerateV1mc - , uuidGenerateV3 - , uuidGenerateV4 - , uuidGenerateV5 - -- * Constants - , uuidNil - , uuidNSUrl - , uuidNSDns - , uuidNSOid - , uuidNSX500 - ) where - -import Squeal.PostgreSQL - --- | Loads ltree extension into the current database. -createUuidOssp :: Definition db db -createUuidOssp = UnsafeDefinition "CREATE EXTENSION \"uuid-ossp\";" - --- | This function generates a version 1 UUID. --- This involves the MAC address of the computer and a time stamp. --- Note that UUIDs of this kind reveal the identity of the computer --- that created the identifier and the time at which it did so, --- which might make it unsuitable for certain security-sensitive applications. -uuidGenerateV1 :: Expr (null 'PGuuid) -uuidGenerateV1 = UnsafeExpression "uuid_generate_v1()" - --- | This function generates a version 1 UUID but uses a random multicast --- MAC address instead of the real MAC address of the computer. -uuidGenerateV1mc :: Expr (null 'PGuuid) -uuidGenerateV1mc = UnsafeExpression "uuid_generate_v1mc()" - -{- | This function generates a version 3 UUID in the given namespace -using the specified input name. The namespace should be one of the -special constants produced by the uuidNS* functions. -(It could be any UUID in theory.) -The name is an identifier in the selected namespace. -For example: -@ -uuidGenerateV3 (uuidNSUrl *: "http://www.postgresql.org") -@ - -The name parameter will be MD5-hashed, -so the cleartext cannot be derived from the generated UUID. -The generation of UUIDs by this method has no random or -environment-dependent element and is therefore reproducible. --} -uuidGenerateV3 :: '[null 'PGuuid, null 'PGtext] ---> null 'PGuuid -uuidGenerateV3 = unsafeFunctionN "uuid_generate_v3" - -{- | This function generates a version 4 UUID, -which is derived entirely from random numbers. --} -uuidGenerateV4 :: Expr (null 'PGuuid) -uuidGenerateV4 = UnsafeExpression "uuid_generate_v4()" - -{- | This function generates a version 5 UUID, -which works like a version 3 UUID except that -SHA-1 is used as a hashing method. -Version 5 should be preferred over version 3 because -SHA-1 is thought to be more secure than MD5. --} -uuidGenerateV5 :: '[null 'PGuuid, null 'PGtext] ---> null 'PGuuid -uuidGenerateV5 = unsafeFunctionN "uuid_generate_v5" - --- | A "nil" UUID constant, which does not occur as a real UUID. -uuidNil :: Expr (null 'PGuuid) -uuidNil = UnsafeExpression "uuid_nil()" - --- | Constant designating the DNS namespace for UUIDs. -uuidNSDns :: Expr (null 'PGuuid) -uuidNSDns = UnsafeExpression "uuid_ns_dns()" - --- | Constant designating the URL namespace for UUIDs. -uuidNSUrl :: Expr (null 'PGuuid) -uuidNSUrl = UnsafeExpression "uuid_ns_url()" - --- | Constant designating the ISO object identifier (OID) namespace for UUIDs. --- (This pertains to ASN.1 OIDs, --- which are unrelated to the OIDs used in PostgreSQL.) -uuidNSOid :: Expr (null 'PGuuid) -uuidNSOid = UnsafeExpression "uuid_ns_oid()" - --- | Constant designating the X.500 distinguished --- name (DN) namespace for UUIDs. -uuidNSX500 :: Expr (null 'PGuuid) -uuidNSX500 = UnsafeExpression "uuid_ns_x500()" diff --git a/squeal-postgresql/LICENSE b/squeal-postgresql/LICENSE deleted file mode 100644 index a905e6f1..00000000 --- a/squeal-postgresql/LICENSE +++ /dev/null @@ -1,31 +0,0 @@ -Copyright (c) 2017 Morphism, LLC - -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - * Neither the names of the copyright holders nor the names of the - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/squeal-postgresql/README.md b/squeal-postgresql/README.md deleted file mode 100644 index 1cd89548..00000000 --- a/squeal-postgresql/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# squeal-postgresql - -![squeal-icon](https://raw.githubusercontent.com/morphismtech/squeal/dev/squeal.gif) - -[![CircleCI](https://circleci.com/gh/echatav/squeal.svg?style=svg&circle-token=a699a654ef50db2c3744fb039cf2087c484d1226)](https://circleci.com/gh/morphismtech/squeal) - -[Github](https://github.com/morphismtech/squeal) - -[Hackage](https://hackage.haskell.org/package/squeal-postgresql) - -[Stackage](https://www.stackage.org/package/squeal-postgresql) diff --git a/squeal-postgresql/bench/Gauge.hs b/squeal-postgresql/bench/Gauge.hs deleted file mode 100644 index 17243723..00000000 --- a/squeal-postgresql/bench/Gauge.hs +++ /dev/null @@ -1,116 +0,0 @@ -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE OverloadedLabels #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeApplications #-} -{-# LANGUAGE TypeOperators #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE ScopedTypeVariables #-} - -module Main where - -import Squeal.PostgreSQL hiding ( defaultMain ) -import Gauge.Main -import Gauge.Main.Options ( defaultConfig - , Config(..) - , Verbosity(..) - , DisplayMode(..) - , Mode(..) - ) -import GHC.Generics -import qualified Generics.SOP as SOP -import Test.QuickCheck --- For CI -import Main.Utf8 ( withUtf8 ) --- For keeping a track of which question ID to query -import Data.Int ( Int64 ) -import Data.IORef --- Project imports -import Gauge.Schema -import Gauge.Queries -import Gauge.DBSetup ( teardownDB ) -import Gauge.DBHelpers ( initDBWithPool - , getRandomUser - , runDbWithPool - , SquealPool - ) - -main :: IO () -main = do - -- A mutable hack here to keep track of - -- pulling a new user by ID from the db instead of the same id - currentId <- newIORef (1 :: UserId) - - -- Define benchmarks - let - queryRenderGroup :: Benchmark - queryRenderGroup = bgroup - "Render Queries" - [ bench "createUser: weak head normal form" $ whnf renderSQL createUser - , bench "createUser: normal form" $ nf renderSQL createUser - , bench "userDetails: weak head normal form" $ whnf renderSQL userDetails - , bench "userDetails: normal form" $ nf renderSQL userDetails - , bench "insertDeviceDetails: weak head normal form" - $ whnf renderSQL insertDeviceDetails - , bench "insertDeviceDetails: normal form" - $ nf renderSQL insertDeviceDetails - ] - - -- Queries against an actual DB - - -- 1. Initialize Schema to DB - -- 2. Make connection pool and pass it to tests - -- 3. Generate users on the fly and add them to DB - -- 4. Tear the Schema down from the DB - - dbInsertsGroup :: Benchmark - dbInsertsGroup = - envWithCleanup initDBWithPool (const teardownDB) $ \pool -> bgroup - "Run individual INSERTs against DB using a connection pool" - [ bgroup - "INSERT: add users to the table users" - [ bench "Run individual INSERT statement" $ makeRunOnce $ perRunEnv - getRandomUser - -- The actual action to benchmark - (\(user :: InsertUser) -> - runDbWithPool pool $ createUserSession user - ) - ] - ] - - dbSelectsGroup :: Benchmark - dbSelectsGroup = - envWithCleanup initDBWithPool (const teardownDB) $ \pool -> bgroup - "Run individual SELECTs against DB using a connection pool" - [ bgroup - "SELECT: fetch users from the table users individually" - [ bench "Fetch a single user" $ makeRunOnce $ perRunEnv - (insertAndIncrement pool currentId) - (\(id_ :: UserId) -> runDbWithPool pool $ userDetailsSession id_ - ) - ] - ] - - withUtf8 $ defaultMain [queryRenderGroup, dbInsertsGroup, dbSelectsGroup] - - --- | Configure the benchmark to run only once (per IO action) -makeRunOnce :: Benchmarkable -> Benchmarkable -makeRunOnce current = current { perRun = True } - -getAndIncrementId :: (IORef UserId) -> IO UserId -getAndIncrementId currentId = do - current <- readIORef currentId - writeIORef currentId (current + 1) - return current - --- | This INSERTs a row in the db so that there's always a row to query. --- Otherwise 'getRow 0' throws an exception. --- NOTE: will make benchmark time slower but does not affect results. -insertAndIncrement :: SquealPool -> (IORef UserId) -> IO UserId -insertAndIncrement pool currentId = do - user <- getRandomUser - _ <- runDbWithPool pool $ createUserSession user - getAndIncrementId currentId diff --git a/squeal-postgresql/bench/Gauge/DBHelpers.hs b/squeal-postgresql/bench/Gauge/DBHelpers.hs deleted file mode 100644 index 5c591386..00000000 --- a/squeal-postgresql/bench/Gauge/DBHelpers.hs +++ /dev/null @@ -1,67 +0,0 @@ -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE OverloadedLabels #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeApplications #-} -{-# LANGUAGE TypeOperators #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE StandaloneDeriving #-} - -module Gauge.DBHelpers where - -import qualified Data.ByteString.Char8 as C -import qualified Data.Text as T -import Control.Monad ( void ) -import Control.Monad.IO.Class ( liftIO ) -import Control.Monad.Loops ( iterateWhile ) -import GHC.Generics ( Generic ) -import Test.QuickCheck -import Squeal.PostgreSQL -import qualified Squeal.PostgreSQL.Session.Transaction.Unsafe as Unsafe -import Control.DeepSeq --- Project imports -import Gauge.Schema ( Schemas ) -import Gauge.Queries ( InsertUser(..) ) -import Gauge.DBSetup - -newtype SquealPool = SquealPool {getSquealPool :: Pool (K Connection Schemas)} deriving (Generic) --- Below may be wrong - it may screw up the whole connection pool using in tests -instance NFData SquealPool where - rnf = rwhnf - -runDbErr - :: SquealPool -> PQ Schemas Schemas IO b -> IO (Either SquealException b) -runDbErr pool session = do - liftIO . runUsingConnPool pool $ trySqueal (Unsafe.transactionally_ session) - -runDbWithPool :: SquealPool -> PQ Schemas Schemas IO b -> IO b -runDbWithPool pool session = do - errOrResult <- runDbErr pool session - case errOrResult of - Left err -> throwSqueal err - Right result -> return result - --- | Helper -runUsingConnPool :: SquealPool -> PQ Schemas Schemas IO x -> IO x -runUsingConnPool (SquealPool pool) = usingConnectionPool pool - -makePool :: C.ByteString -> IO SquealPool -makePool connStr = do - pool <- createConnectionPool connStr 1 0.5 10 - return $ SquealPool pool - -initDBWithPool :: IO SquealPool -initDBWithPool = do - void initDB - pool <- makePool connectionString - return pool - -getRandomUser :: IO InsertUser -getRandomUser = iterateWhile noEmptyEmail $ generate arbitrary - where - noEmptyEmail InsertUser { userEmail = userEmail } = T.length userEmail < 5 diff --git a/squeal-postgresql/bench/Gauge/DBSetup.hs b/squeal-postgresql/bench/Gauge/DBSetup.hs deleted file mode 100644 index b7a5529d..00000000 --- a/squeal-postgresql/bench/Gauge/DBSetup.hs +++ /dev/null @@ -1,134 +0,0 @@ -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE OverloadedLabels #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeApplications #-} -{-# LANGUAGE TypeOperators #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE ScopedTypeVariables #-} - -module Gauge.DBSetup where - -import Data.ByteString ( ByteString ) -import qualified Data.ByteString.Char8 as C -import Control.Monad ( void ) -import GHC.Generics -import Squeal.PostgreSQL --- Project imports -import Gauge.Schema ( Schemas - , DeviceOS - , IPLocation - ) - - --- First create enums as they're needed in the Schema -setup :: Definition (Public '[]) Schemas -setup = - createTypeEnumFrom @DeviceOS #device_os - >>> createTypeCompositeFrom @IPLocation #ip_location - >>> createTable - #users - ( serial8 - `as` #id - :* (text & notNullable) - `as` #email - :* (text & notNullable) - `as` #password - :* (text & nullable) - `as` #first_name - :* (int2 & nullable) - `as` #birthyear - ) - (primaryKey #id `as` #pk_users :* unique #email `as` #email) - >>> createTable - #user_devices - ( serial8 - `as` #id - :* notNullable int8 - `as` #user_id - :* (text & notNullable) - `as` #token - :* (typedef #device_os & notNullable) - `as` #os - ) - ( primaryKey #id - `as` #pk_user_devices - :* foreignKey #user_id #users #id (OnDelete Cascade) (OnUpdate Cascade) - `as` #fk_user_id - :* unique #token - `as` #token - ) - --- Drop types last because tables depend on them -teardown :: Definition Schemas (Public '[]) -teardown = - dropTableCascade #user_devices - >>> dropTableCascade #users - >>> dropType #ip_location - >>> dropType #device_os - --- With env vars, we could use the commented keys -data PGConfig = PGConfig - { pgHost :: String -- "PG_HOST" - , pgPort :: Int -- "PG_PORT" - , pgDbname :: String -- "PG_DBNAME" - , pgUser :: String -- "PG_USER" - , pgPassword :: String -- "PG_PASSWORD" - } - deriving (Generic, Show) - --- | Helper: unused now, but primarily for testing locally -makeConnStr :: PGConfig -> ByteString -makeConnStr PGConfig { pgHost = host, pgPort = portNumber, pgDbname = dbName, pgUser = user, pgPassword = pw } - = C.pack - $ "host=" - <> host - <> " dbname=" - <> dbName - <> " user=" - <> user - <> " password=" - <> pw - <> " port=" - <> show portNumber - -connectionString :: ByteString -connectionString = "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" - -performDBAction :: Definition a b -> String -> IO () -performDBAction action message = do - void - $ withConnection connectionString - $ manipulate_ (UnsafeManipulation "SET client_min_messages TO WARNING;") - & pqThen (define action) - putStrLn message - -initDB :: IO () -initDB = - performDBAction setup "Initialized Schema & corresponding tables for Database" - -teardownDB :: IO () -teardownDB = performDBAction teardown "Dropped all database tables" - -dbSchema :: Definition '["public" ::: '[]] (Drop "public" '["public" ::: '[]]) -dbSchema = dropSchemaCascade #public - -dropDBSchema :: IO () -dropDBSchema = performDBAction dbSchema "Dropped Public schema from database" - --- | Concatenate two `ByteString`s with a space between. -(<+>) :: ByteString -> ByteString -> ByteString -infixr 7 <+> -str1 <+> str2 = str1 <> " " <> str2 - --- | Drop table custom SQL statement with 'cascade' -dropTableCascade - :: (Has sch schemas schema, Has tab schema ( 'Table table)) - => QualifiedAlias sch tab -- ^ table to remove - -> Definition schemas (Alter sch (Drop tab schema) schemas) -dropTableCascade tab = - UnsafeDefinition $ "DROP TABLE" <+> renderSQL tab <> " cascade;" diff --git a/squeal-postgresql/bench/Gauge/Queries.hs b/squeal-postgresql/bench/Gauge/Queries.hs deleted file mode 100644 index 0f38e690..00000000 --- a/squeal-postgresql/bench/Gauge/Queries.hs +++ /dev/null @@ -1,145 +0,0 @@ -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE OverloadedLabels #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeApplications #-} -{-# LANGUAGE TypeOperators #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DerivingStrategies #-} - -module Gauge.Queries where - -import Squeal.PostgreSQL -import GHC.Generics ( Generic ) -import qualified Generics.SOP as SOP --- Need below for deriving instances -import Control.DeepSeq -import Data.Text ( Text ) -import Data.Int ( Int16 - , Int64 - ) -import Test.QuickCheck ( Arbitrary(..) ) -import Generic.Random ( genericArbitrarySingle ) --- Import Orphan instances -import Test.QuickCheck.Instances ( ) --- Project imports -import Gauge.Schema - --- Types - -type UserId = Int64 --- Insert user -data InsertUser = InsertUser - { userEmail :: Text - , userPassword :: Text - , userFirstName :: Maybe Text - , userBirthyear :: Maybe Int16 - } - deriving (Show, Eq, Generic, NFData) -instance SOP.Generic InsertUser -instance SOP.HasDatatypeInfo InsertUser --- Arbitrary instances for producing values with quickcheck -instance Arbitrary InsertUser where - arbitrary = genericArbitrarySingle - -sampleInsertUser :: InsertUser -sampleInsertUser = InsertUser { userEmail = "mark@gmail.com" - , userPassword = "MySecretPassword" - , userFirstName = Just "Mark" - , userBirthyear = Just 1980 - } - -data APIDBUser_ = APIDBUser_ - { userId :: UserId - , email :: Text - , first_name :: Maybe Text - , birthyear :: Maybe Int16 - } - deriving (Show, Eq, Generic, NFData) -instance SOP.Generic APIDBUser_ -instance SOP.HasDatatypeInfo APIDBUser_ --- Arbitrary instances for producing values with quickcheck -instance Arbitrary APIDBUser_ where - arbitrary = genericArbitrarySingle - -data Row3 a b c = Row4 - { col1 :: a - , col2 :: b - , col3 :: c - } - deriving stock Generic - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) - --- (UserId, Token, OS) -type DeviceDetailsRow = Row3 UserId Text (Enumerated DeviceOS) - --- -- Queries - -createUserSession :: InsertUser -> PQ Schemas Schemas IO APIDBUser_ -createUserSession insertUser = - getRow 0 =<< manipulateParams createUser insertUser - -createUser :: Manipulation_ Schemas InsertUser APIDBUser_ -createUser = insertInto - #users - (Values_ - ( Default - `as` #id - :* Set (param @1) - `as` #email - :* Set (param @2) - `as` #password - :* Set (param @3) - `as` #first_name - :* Set (param @4 & cast int2) - `as` #birthyear - ) - ) - OnConflictDoRaise - (Returning_ - ( #id - `as` #userId - :* #email - `as` #email - :* #first_name - `as` #first_name - :* #birthyear - `as` #birthyear - ) - ) - -userDetailsSession :: UserId -> PQ Schemas Schemas IO APIDBUser_ -userDetailsSession uID = getRow 0 =<< runQueryParams userDetails (Only uID) - -userDetails :: Query_ Schemas (Only UserId) APIDBUser_ -userDetails = select_ - ( #id - `as` #userId - :* #email - `as` #email - :* #first_name - `as` #first_name - :* #birthyear - `as` #birthyear - ) - (from (table #users) & where_ (#id .== (param @1 & cast int8))) - -insertDeviceDetails :: Manipulation_ Schemas DeviceDetailsRow () -insertDeviceDetails = insertInto - #user_devices - (Values_ - ( Default - `as` #id - :* Set (param @1) - `as` #user_id - :* Set (param @2) - `as` #token - :* Set (parameter @3 (typedef #device_os)) - `as` #os - ) - ) - OnConflictDoRaise - (Returning_ Nil) diff --git a/squeal-postgresql/bench/Gauge/Schema.hs b/squeal-postgresql/bench/Gauge/Schema.hs deleted file mode 100644 index e0fd6aa4..00000000 --- a/squeal-postgresql/bench/Gauge/Schema.hs +++ /dev/null @@ -1,93 +0,0 @@ -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE OverloadedLabels #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeApplications #-} -{-# LANGUAGE TypeOperators #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE DeriveGeneric #-} - -module Gauge.Schema where - -import Squeal.PostgreSQL -import GHC.Generics -import qualified Generics.SOP as SOP - - --- Type - -data DeviceOS = Android | IOS - deriving (Show, Read, Eq, Generic) --- DeviceOS is converted to PG Enum type -instance SOP.Generic DeviceOS -instance SOP.HasDatatypeInfo DeviceOS - --- Defined extra types for the database --- Operating system enum -type PGDeviceOS = PG (Enumerated DeviceOS) -type DeviceOSType = 'Typedef PGDeviceOS - --- For composite type -data IPLocation = IPLocation - { countryShort :: String - , region :: String - , city :: String - } - deriving (Show, Read, Eq, Generic) - -instance SOP.Generic IPLocation -instance SOP.HasDatatypeInfo IPLocation - --- IPLocation Composite type -type PGIPLocation = PG (Composite IPLocation) -type IPLocationType = 'Typedef PGIPLocation - --- SCHEMA - --- Users - -type UsersColumns = '[ - "id" ::: 'Def :=> 'NotNull 'PGint8 - , "email" ::: 'NoDef :=> 'NotNull 'PGtext - , "password" ::: 'NoDef :=> 'NotNull 'PGtext - , "first_name" ::: 'NoDef :=> 'Null 'PGtext - , "birthyear" ::: 'NoDef :=> 'Null 'PGint2 - ] - -type UsersConstraints = '[ - "pk_users" ::: 'PrimaryKey '["id"] - , "email" ::: 'Unique '["email"] - ] - -type UsersTable = 'Table (UsersConstraints :=> UsersColumns) - --- User devices -type UserDevicesColumns = '[ - "id" ::: 'Def :=> 'NotNull 'PGint8 -- ID as PK because user might have many same OS devices - , "user_id" ::: 'NoDef :=> 'NotNull 'PGint8 - , "token" ::: 'NoDef :=> 'NotNull 'PGtext - , "os" ::: 'NoDef :=> 'NotNull PGDeviceOS - ] - -type UserDevicesConstraints = '[ - "pk_user_devices" ::: 'PrimaryKey '["id"] - , "fk_user_id" ::: 'ForeignKey '["user_id"] "public" "users" '["id"] - , "token" ::: 'Unique '["token"] - ] - -type UserDevicesTable = 'Table (UserDevicesConstraints :=> UserDevicesColumns) - --- Schema --- Make sure to put types before tables, otherwise won't compile -type Schema = '[ - -- Enum types: - "device_os" ::: DeviceOSType - -- Composite types: - , "ip_location" ::: IPLocationType - -- Tables: - , "users" ::: UsersTable - , "user_devices" ::: UserDevicesTable - ] - -type Schemas = '["public" ::: Schema] diff --git a/squeal-postgresql/bench/README.md b/squeal-postgresql/bench/README.md deleted file mode 100644 index 4ad3847c..00000000 --- a/squeal-postgresql/bench/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Microbenchark suite for Squeal -> Benchmarking & profiling query rendering performance - -## Running - -Run benchmark suite with: -``` -stack bench -``` diff --git a/squeal-postgresql/docs-upload.sh b/squeal-postgresql/docs-upload.sh deleted file mode 100755 index ca4acf79..00000000 --- a/squeal-postgresql/docs-upload.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -# Run this script in the top-level of your package directory -# (where the .cabal file is) to compile documentation and -# upload it to hackage. - -# Requirements: -# cabal-install-1.24 (for --for-hackage) -# haddock 2.17 (for the hyperlinked source) - -set -e - -dir=$(mktemp -d dist-docs.XXXXXX) -trap 'rm -r "$dir"' EXIT - -cabal configure --builddir="$dir" -cabal haddock --builddir="$dir" --for-hackage --haddock-option=--hyperlinked-source -cabal upload -d $dir/*-docs.tar.gz diff --git a/squeal-postgresql/exe/Example.hs b/squeal-postgresql/exe/Example.hs deleted file mode 100644 index 6fe3cd9d..00000000 --- a/squeal-postgresql/exe/Example.hs +++ /dev/null @@ -1,170 +0,0 @@ -{-# LANGUAGE - DataKinds - , DeriveGeneric - , FlexibleContexts - , OverloadedLabels - , OverloadedStrings - , OverloadedLists - , TypeApplications - , TypeOperators -#-} - -module Main (main, main2, upsertUser) where - -import Control.Monad.IO.Class (MonadIO (..)) -import Data.Int (Int16, Int32) -import Data.Text (Text) -import Data.Vector (Vector) - -import Squeal.PostgreSQL - -import qualified Data.ByteString.Char8 as Char8 -import qualified Generics.SOP as SOP -import qualified GHC.Generics as GHC - -type UserSchema = - '[ "users" ::: 'Table ( - '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=> - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "name" ::: 'NoDef :=> 'NotNull 'PGtext - , "vec" ::: 'NoDef :=> 'NotNull ('PGvararray ('Null 'PGint2)) - ]) - , "emails" ::: 'Table ( - '[ "pk_emails" ::: 'PrimaryKey '["id"] - , "fk_user_id" ::: 'ForeignKey '["user_id"] "user" "users" '["id"] - ] :=> - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4 - , "email" ::: 'NoDef :=> 'Null 'PGtext - ]) - ] - -type PublicSchema = '[ "positive" ::: 'Typedef 'PGfloat4 ] - -type OrgSchema = - '[ "organizations" ::: 'Table ( - '[ "pk_organizations" ::: 'PrimaryKey '["id"] ] :=> - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "name" ::: 'NoDef :=> 'NotNull 'PGtext - ]) - , "members" ::: 'Table ( - '[ "fk_member" ::: 'ForeignKey '["member"] "user" "users" '["id"] - , "fk_organization" ::: 'ForeignKey '["organization"] "org" "organizations" '["id"] ] :=> - '[ "member" ::: 'NoDef :=> 'NotNull 'PGint4 - , "organization" ::: 'NoDef :=> 'NotNull 'PGint4 ]) - ] - -type Schemas - = '[ "public" ::: PublicSchema, "user" ::: UserSchema, "org" ::: OrgSchema ] - -setup :: Definition (Public '[]) Schemas -setup = - createDomain #positive real (#value .> 0 .&& (#value & isNotNull)) - >>> - createSchema #user - >>> - createSchema #org - >>> - createTable (#user ! #jokers) - ( serial `as` #id :* - (text & notNullable) `as` #name :* - (vararray int2 & notNullable) `as` #vec ) - ( primaryKey #id `as` #pk_users ) - >>> - alterTableRename (#user ! #jokers) #users - >>> - createTable (#user ! #emails) - ( serial `as` #id :* - columntypeFrom @Int32 `as` #user_id :* - columntypeFrom @(Maybe Text) `as` #email ) - ( primaryKey #id `as` #pk_emails :* - foreignKey #user_id (#user ! #users) #id - (OnDelete Cascade) (OnUpdate Cascade) `as` #fk_user_id ) - >>> - createTable (#org ! #organizations) - ( serial `as` #id :* - (text & notNullable) `as` #name ) - ( primaryKey #id `as` #pk_organizations ) - >>> - createTable (#org ! #members) - ( notNullable int4 `as` #member :* - notNullable int4 `as` #organization ) - ( foreignKey #member (#user ! #users) #id - (OnDelete Cascade) (OnUpdate Cascade) `as` #fk_member :* - foreignKey #organization (#org ! #organizations) #id - (OnDelete Cascade) (OnUpdate Cascade) `as` #fk_organization ) - -teardown :: Definition Schemas (Public '[]) -teardown = dropType #positive >>> dropSchemaCascade #user >>> dropSchemaCascade #org - -insertUser :: Manipulation_ Schemas (Text, VarArray (Vector (Maybe Int16))) (Only Int32) -insertUser = insertInto (#user ! #users) - (Values_ (Default `as` #id :* Set (param @1) `as` #name :* Set (param @2) `as` #vec)) - (OnConflict (OnConstraint #pk_users) DoNothing) (Returning_ (#id `as` #fromOnly)) - -insertEmail :: Manipulation_ Schemas (Int32, Maybe Text) () -insertEmail = insertInto_ (#user ! #emails) - (Values_ (Default `as` #id :* Set (param @1) `as` #user_id :* Set (param @2) `as` #email)) - -getUsers :: Query_ Schemas () User -getUsers = select_ - (#u ! #name `as` #userName :* #e ! #email `as` #userEmail :* #u ! #vec `as` #userVec) - ( from (table ((#user ! #users) `as` #u) - & innerJoin (table ((#user ! #emails) `as` #e)) - (#u ! #id .== #e ! #user_id)) ) - -upsertUser :: Manipulation_ Schemas (Int32, String, VarArray [Maybe Int16]) () -upsertUser = insertInto (#user ! #users `as` #u) - (Values_ (Set (param @1) `as` #id :* setUser)) - (OnConflict (OnConstraint #pk_users) (DoUpdate setUser [#u ! #id .== param @1])) - (Returning_ Nil) - where - setUser = Set (param @2) `as` #name :* Set (param @3) `as` #vec :* Nil - -data User - = User - { userName :: Text - , userEmail :: Maybe Text - , userVec :: VarArray (Vector (Maybe Int16)) - } deriving (Show, GHC.Generic) -instance SOP.Generic User -instance SOP.HasDatatypeInfo User - -users :: [User] -users = - [ User "Alice" (Just "alice@gmail.com") (VarArray [Just 1,Just 2,Nothing]) - , User "Bob" Nothing (VarArray [Nothing,Just (-3)]) - , User "Carole" (Just "carole@hotmail.com") (VarArray [Just 3,Nothing, Just 4]) - ] - -session :: (MonadIO pq, MonadPQ Schemas pq) => pq () -session = do - liftIO $ Char8.putStrLn "manipulating" - idResults <- traversePrepared insertUser ([(userName user, userVec user) | user <- users]) - ids <- traverse (fmap fromOnly . getRow 0) (idResults :: [Result (Only Int32)]) - traversePrepared_ insertEmail (zip (ids :: [Int32]) (userEmail <$> users)) - liftIO $ Char8.putStrLn "querying" - usersResult <- runQuery getUsers - usersRows <- getRows usersResult - liftIO $ print (usersRows :: [User]) - -main :: IO () -main = do - Char8.putStrLn "squeal" - connectionString <- pure - "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" - Char8.putStrLn $ "connecting to " <> connectionString - connection0 <- connectdb connectionString - Char8.putStrLn "setting up schema" - connection1 <- execPQ (define setup) connection0 - connection2 <- execPQ session connection1 - Char8.putStrLn "tearing down schema" - connection3 <- execPQ (define teardown) connection2 - finish connection3 - -main2 :: IO () -main2 = - withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $ - define setup - & pqThen session - & pqThen (define teardown) diff --git a/squeal-postgresql/squeal-postgresql.cabal b/squeal-postgresql/squeal-postgresql.cabal index 85d0fc11..5b386e3e 100644 --- a/squeal-postgresql/squeal-postgresql.cabal +++ b/squeal-postgresql/squeal-postgresql.cabal @@ -125,84 +125,3 @@ test-suite doctest build-depends: base >= 4.12.0.0 && < 5.0 , doctest >= 0.16.3 - -test-suite properties - default-language: Haskell2010 - type: exitcode-stdio-1.0 - hs-source-dirs: test - ghc-options: -Wall - main-is: Property.hs - build-depends: - base >= 4.12.0.0 && < 5.0 - , bytestring >= 0.10.10.0 - , hedgehog >= 1.0.2 - , generics-sop >= 0.5.1.0 - , mtl >= 2.2.2 - , scientific >= 0.3.6.2 - , squeal-postgresql - , time >= 1.9.3 - , with-utf8 >= 1.0 - -test-suite spec - default-language: Haskell2010 - type: exitcode-stdio-1.0 - hs-source-dirs: test - ghc-options: -Wall - main-is: Spec.hs - build-depends: - async >= 2.2.2 - , base >= 4.12.0.0 && < 5.0 - , bytestring >= 0.10.10.0 - , generics-sop >= 0.5.1.0 - , hspec >= 2.7.1 - , mtl >= 2.2.2 - , squeal-postgresql - , text >= 1.2.3.2 - , vector >= 0.12.1.2 - -benchmark gauge - type: exitcode-stdio-1.0 - hs-source-dirs: bench - main-is: Gauge.hs - other-modules: - Gauge.DBHelpers - , Gauge.DBSetup - , Gauge.Queries - , Gauge.Schema - default-language: Haskell2010 - ghc-options: - -O2 - -threaded - "-with-rtsopts=-N" - -rtsopts - -funbox-strict-fields - build-depends: - base >= 4.12.0.0 && < 5.0 - , bytestring >= 0.10.10.0 - , deepseq >= 1.4.4.0 - , gauge >= 0.2.5 - , generic-random >= 1.3.0.1 - , generics-sop >= 0.5.1.0 - , monad-loops >= 0.4.3 - , mtl >= 2.2.2 - , QuickCheck >= 2.13.2 - , quickcheck-instances >= 0.3.22 - , scientific >= 0.3.6.2 - , squeal-postgresql - , text >= 1.2.3.2 - , with-utf8 >= 1.0 - -executable example - default-language: Haskell2010 - hs-source-dirs: exe - ghc-options: -Wall - main-is: Example.hs - build-depends: - base >= 4.10.0.0 && < 5.0 - , bytestring >= 0.10.10.0 - , generics-sop >= 0.5.1.0 - , mtl >= 2.2.2 - , squeal-postgresql - , text >= 1.2.3.2 - , transformers >= 0.5.6.2 - , vector >= 0.12.1.2 diff --git a/squeal-postgresql/test/Property.hs b/squeal-postgresql/test/Property.hs deleted file mode 100644 index 12067c14..00000000 --- a/squeal-postgresql/test/Property.hs +++ /dev/null @@ -1,232 +0,0 @@ -{-# LANGUAGE - DataKinds - , DeriveAnyClass - , DeriveGeneric - , DerivingStrategies - , DerivingVia - , FlexibleContexts - , GADTs - , LambdaCase - , OverloadedLabels - , OverloadedStrings - , ScopedTypeVariables - , TypeApplications - , TypeOperators - , UndecidableInstances -#-} - -module Main (main) where - -import Control.Monad.Trans -import Data.ByteString (ByteString) -import Data.ByteString.Char8 (unpack) -import Data.Scientific (fromFloatDigits) -import Data.Fixed (Fixed(MkFixed), Micro, Pico) -import Data.String (IsString(fromString)) -import Data.Time -import Hedgehog hiding (Range) -import Main.Utf8 -import Squeal.PostgreSQL hiding (check) -import qualified Generics.SOP as SOP -import qualified GHC.Generics as GHC -import qualified Hedgehog.Gen as Gen -import qualified Hedgehog.Main as Main -import qualified Hedgehog.Range as Range - -main :: IO () -main = withUtf8 $ do - withConnection connectionString $ define createSchwarma - Main.defaultMain [checkSequential roundtrips] - withConnection connectionString $ define dropSchwarma - -roundtrips :: Group -roundtrips = Group "roundtrips" - [ roundtrip int2 genInt16 - , roundtrip int4 genInt32 - , roundtrip int8 genInt64 - , roundtrip bool Gen.bool - , roundtrip numeric genScientific - , roundtrip float4 genFloat - , roundtrip float8 genDouble - , roundtripOn normalizeAscii text genStringAscii - , roundtripOn normalizeUtf8 text genStringUnicode - -- , roundtripOn normalizeUtf8 text genStringAll - , roundtripOn normalizeTimeOfDay time genTimeOfDay - -- , roundtrip timetz genTimeWithZone - , roundtripOn normalizeLocalTime timestamp genLocalTime - , roundtrip timestamptz genUTCTime - , roundtrip date genDay - , roundtrip interval genDiffTime - , roundtripOn normalizeIntRange int4range (genRange genInt32) - , roundtripOn normalizeIntRange int8range (genRange genInt64) - , roundtrip numrange (genRange genScientific) - , roundtripOn (fmap normalizeLocalTime) tsrange (genRange genLocalTime) - , roundtrip tstzrange (genRange genUTCTime) - , roundtripOn normalizeIntRange daterange (genRange genDay) - , roundtrip (typedef #schwarma) genSchwarma - , roundtrip (vararray (typedef #schwarma)) genSchwarmaArray - ] - where - genInt16 = Gen.int16 Range.exponentialBounded - genInt32 = Gen.int32 Range.exponentialBounded - genInt64 = Gen.int64 Range.exponentialBounded - genScientific = fromFloatDigits <$> genFloat - genPosFloat = Gen.float - (Range.exponentialFloatFrom 1 minPosFloat maxPosFloat) - genFloat = Gen.prune $ Gen.choice - [ genPosFloat - , negate <$> genPosFloat - , Gen.element [0,1/0,-1/0] - ] - genPosDouble = Gen.double - (Range.exponentialFloatFrom 1 minPosFloat maxPosFloat) - genDouble = Gen.prune $ Gen.choice - [ genPosDouble - , negate <$> genPosDouble - , Gen.element [0,1/0,-1/0] - ] - genStringAscii = Gen.string (Range.linear 0 100) Gen.ascii - -- genStringLatin1 = Gen.string (Range.linear 0 100) Gen.latin1 - genStringUnicode = Gen.string (Range.linear 0 100) Gen.unicode - -- genStringAll = Gen.string (Range.linear 0 100) Gen.unicodeAll - genRange gen = do - lb <- gen - ub <- Gen.filter (lb <) gen - Gen.element - [ Empty, singleton lb, whole - , lb <=..<= ub , lb <=..< ub, lb <..<= ub, lb <..< ub - , atLeast lb, moreThan lb, atMost ub, lessThan ub ] - genDay = do - y <- toInteger <$> Gen.int (Range.constant 2000 2019) - m <- Gen.int (Range.constant 1 12) - d <- Gen.int (Range.constant 1 28) - return $ fromGregorian y m d - genDiffTime = do - secs <- secondsToDiffTime . toInteger <$> - Gen.int (Range.constant 0 86401) - picos <- picosecondsToDiffTime . (* 1000000) . toInteger <$> - Gen.int (Range.constant 0 (1000000 - 1)) - return $ secs + picos - genUTCTime = UTCTime <$> genDay <*> genDiffTime - genTimeOfDay = do - h <- Gen.int (Range.constant 0 23) - m <- Gen.int (Range.constant 0 59) - s <- MkFixed . toInteger <$> Gen.int (Range.constant 0 59) - return $ TimeOfDay h m s - genLocalTime = LocalTime <$> genDay <*> genTimeOfDay - -- genTimeZone = Gen.element $ map (read @TimeZone) - -- [ "UTC", "UT", "GMT", "EST", "EDT", "CST" - -- , "CDT", "MST", "MDT", "PST", "PDT" ] - genSchwarma = Gen.enumBounded @_ @Schwarma - genSchwarmaArray = VarArray <$> Gen.list (Range.constant 1 10) genSchwarma - -roundtrip - :: forall x - . ( ToPG DB x, FromPG x, Inline x - , OidOf DB (PG x), PGTyped DB (PG x) - , Show x, Eq x, NullPG x ~ 'NotNull (PG x) ) - => TypeExpression DB ('NotNull (PG x)) - -> Gen x - -> (PropertyName, Property) -roundtrip = roundtripOn id - -roundtripOn - :: forall x - . ( ToPG DB x, FromPG x, Inline x - , OidOf DB (PG x), PGTyped DB (PG x) - , Show x, Eq x, NullPG x ~ 'NotNull (PG x) ) - => (x -> x) - -> TypeExpression DB ('NotNull (PG x)) - -> Gen x - -> (PropertyName, Property) -roundtripOn norm ty gen = propertyWithName $ do - x <- forAll gen - Just (Only y) <- lift . withConnection connectionString $ - firstRow =<< runQueryParams - (values_ (parameter @1 ty `as` #fromOnly)) (Only x) - Just (Only z) <- lift . withConnection connectionString $ - firstRow =<< runQuery - (values_ (inline @x @'NotNull x `as` #fromOnly)) - y === z - norm x === y - where - propertyWithName prop = - (fromString (unpack (renderSQL ty)), property prop) - -maxPosFloat :: RealFloat a => a -maxPosFloat = x - where - n = floatDigits x - b = floatRadix x - (_, u) = floatRange x - x = encodeFloat (b^n - 1) (u - n) - -minPosFloat :: RealFloat a => a -minPosFloat = x - where - n = floatDigits x - b = floatRadix x - (l, _) = floatRange x - x = encodeFloat (b^n - 1) (l - n - 1) - -connectionString :: ByteString -connectionString = "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" - -normalizeIntRange :: (Enum int, Ord int) => Range int -> Range int -normalizeIntRange = \case - Empty -> Empty - NonEmpty l u -> - let - l' = normalizeL l - u' = normalizeU u - in if emptyNormalized l' u' then Empty else NonEmpty l' u' - where - normalizeL = \case - Open l -> Closed (succ l) - normalized -> normalized - normalizeU = \case - Closed u -> Open (succ u) - normalized -> normalized - emptyNormalized (Closed l) (Open u) = l >= u - emptyNormalized _ _ = False - -normalizeTimeOfDay :: TimeOfDay -> TimeOfDay -normalizeTimeOfDay (TimeOfDay h m s) = TimeOfDay h m - . fromRational @Pico - . toRational @Micro - . fromRational @Micro - . toRational @Pico - $ s - -normalizeLocalTime :: LocalTime -> LocalTime -normalizeLocalTime (LocalTime d t) = LocalTime d (normalizeTimeOfDay t) - --- normalizeTimeWithZone :: (TimeOfDay, TimeZone) -> (TimeOfDay, TimeZone) --- normalizeTimeWithZone (t, z) = (normalizeTimeOfDay t, z) - -normalizeAscii :: String -> String -normalizeAscii = (stripped =<<) - where - stripped = \case - '\NUL' -> "" - ch -> [ch] - -normalizeUtf8 :: String -> String -normalizeUtf8 = (stripped =<<) - where - stripped = \case - '\NUL' -> "" - ch -> [ch] - -data Schwarma = Chicken | Lamb | Beef - deriving stock (Eq, Show, Bounded, Enum, GHC.Generic) - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) - deriving (IsPG, FromPG, ToPG db, Inline) via Enumerated Schwarma - -type DB = '["public" ::: '["schwarma" ::: 'Typedef (PG Schwarma)]] - -createSchwarma :: Definition '["public" ::: '[]] DB -createSchwarma = createTypeEnumFrom @Schwarma #schwarma - -dropSchwarma :: Definition DB '["public" ::: '[]] -dropSchwarma = dropType #schwarma diff --git a/squeal-postgresql/test/Spec.hs b/squeal-postgresql/test/Spec.hs deleted file mode 100644 index a9d52292..00000000 --- a/squeal-postgresql/test/Spec.hs +++ /dev/null @@ -1,250 +0,0 @@ -{-# LANGUAGE - DataKinds - , DeriveAnyClass - , DeriveGeneric - , DerivingStrategies - , DerivingVia - , DuplicateRecordFields - , FlexibleContexts - , FlexibleInstances - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , RankNTypes - , StandaloneDeriving - , TypeApplications - , TypeFamilies - , TypeSynonymInstances - , TypeInType - , TypeOperators - , UndecidableInstances -#-} - -module Main (main) where - -import Control.Concurrent.Async (replicateConcurrently) -import Data.ByteString (ByteString) -import Data.Int (Int32) -import Data.Text (Text) -import Test.Hspec - -import qualified Data.ByteString.Char8 as Char8 (unlines) -import qualified Generics.SOP as SOP -import qualified GHC.Generics as GHC - -import Squeal.PostgreSQL - -main :: IO () -main = hspec spec - -type UsersConstraints = - '[ "pk_users" ::: 'PrimaryKey '["id"] - , "unique_names" ::: 'Unique '["name"] ] - -type UsersColumns = - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "name" ::: 'NoDef :=> 'NotNull 'PGtext ] - -type Schema = - '[ "users" ::: 'Table (UsersConstraints :=> UsersColumns) - , "person" ::: 'Typedef (PG Person) ] - -type DB = '[ "public" ::: Schema ] - -data User = User - { userName :: Text - } deriving stock (Eq, Show, GHC.Generic) - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) - -insertUser :: Manipulation_ DB User () -insertUser = insertInto_ #users - (Values_ (Default `as` #id :* Set (param @1) `as` #name)) - -insertUsers :: Text -> [Text] -> Statement DB () () -insertUsers name1 names = manipulation $ insertInto_ #users $ Values - (Default `as` #id :* Set (inline name1) `as` #name) - [Default `as` #id :* Set (inline namei) `as` #name | namei <- names] - -deleteUser :: Text -> Statement DB () () -deleteUser name1 = manipulation $ deleteFrom_ #users (#name .== inline name1) - -setup :: Definition (Public '[]) DB -setup = - createTable #users - ( serial `as` #id :* - notNullable text `as` #name ) - ( primaryKey #id `as` #pk_users :* - unique #name `as` #unique_names ) >>> - createTypeCompositeFrom @Person #person - -teardown :: Definition DB (Public '[]) -teardown = dropType #person >>> dropTable #users - -silent :: Statement db () () -silent = manipulation $ UnsafeManipulation "Set client_min_messages TO WARNING" - -silence :: MonadPQ db pq => pq () -silence = execute_ silent - -setupDB :: IO () -setupDB = withConnection connectionString $ - silence & pqThen (define setup) - -dropDB :: IO () -dropDB = withConnection connectionString $ - silence & pqThen (define teardown) - -connectionString :: ByteString -connectionString = "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" - -data Person = Person { name :: Maybe String, age :: Maybe Int32 } - deriving (Eq, Show, GHC.Generic, SOP.Generic, SOP.HasDatatypeInfo) - deriving (IsPG, FromPG, ToPG db, Inline) via (Composite Person) - -spec :: Spec -spec = before_ setupDB . after_ dropDB $ do - - describe "Exceptions" $ do - - let - testUser = User "TestUser" - newUser :: User -> Transaction DB () - newUser usr = manipulateParams_ insertUser usr - insertUserTwice :: Transaction DB () - insertUserTwice = newUser testUser >> newUser testUser - err23505 = UniqueViolation $ Char8.unlines - [ "ERROR: duplicate key value violates unique constraint \"unique_names\"" - , "DETAIL: Key (name)=(TestUser) already exists." ] - - it "should be thrown for constraint violation" $ - withConnection connectionString insertUserTwice - `shouldThrow` (== err23505) - - it "should be rethrown for constraint violation in a transaction" $ - withConnection connectionString (transactionally_ insertUserTwice) - `shouldThrow` (== err23505) - - describe "Pools" $ - - it "should manage concurrent transactions" $ do - pool <- createConnectionPool - "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" 1 0.5 10 - let - qry :: Query_ (Public '[]) () (Only Char) - qry = values_ (inline 'a' `as` #fromOnly) - session = usingConnectionPool pool $ transactionally_ $ - firstRow =<< runQuery qry - chrs <- replicateConcurrently 10 session - chrs `shouldSatisfy` all (== Just (Only 'a')) - - describe "Ranges" $ - - it "should correctly decode ranges" $ do - - rangesOut <- withConnection connectionString $ do - let - qry :: Query_ (Public '[]) () (Only (Range Int32)) - qry = values - ( range int4range (atLeast 3) `as` #fromOnly ) - [ range int4range (3 <=..< 5) `as` #fromOnly - , range int4range Empty `as` #fromOnly - , range int4range whole `as` #fromOnly ] - getRows =<< runQuery qry - (fromOnly <$> rangesOut :: [Range Int32]) `shouldBe` - [ atLeast 3, 3 <=..< 5, Empty, whole ] - - describe "Parameters" $ do - - it "should run queries that don't reference all their parameters" $ do - - out <- withConnection connectionString $ do - let - qry :: Query_ (Public '[]) (Char,Int32) (Only Int32) - qry = values_ (param @2 `as` #fromOnly) - firstRow =<< runQueryParams qry ('a', 3 :: Int32) - (fromOnly <$> out :: Maybe Int32) `shouldBe` Just 3 - - describe "Composite types" $ do - - it "should be embeddible" $ do - - let - - roundtrip :: Query_ DB (Only Person) (Only Person) - roundtrip = values_ (param @1 `as` #fromOnly) - - roundtrip_inline :: Person -> Query_ DB () (Only Person) - roundtrip_inline person = values_ (inline person `as` #fromOnly) - - roundtrip_array :: Query_ DB - (Only (VarArray [Person])) (Only (VarArray [Person])) - roundtrip_array = values_ (param @1 `as` #fromOnly) - - oneway :: Query_ DB () (Only Person) - oneway = values_ (row ("Adam" `as` #name :* 6000 `as` #age) `as` #fromOnly) - - oneway_array :: Query_ DB () (Only (VarArray [Person])) - oneway_array = values_ $ array - [ row ("Adam" `as` #name :* 6000 `as` #age) - , row ("Lucy" `as` #name :* 2420000 `as` #age) - ] `as` #fromOnly - - unsafeQ :: Query_ DB () (Only (VarArray [Composite Person])) - unsafeQ = UnsafeQuery "select array[row(\'Adam\', 6000)]" - - nothingQ :: Query_ DB () (Only Person) - nothingQ = values_ (row (null_ `as` #name :* null_ `as` #age) `as` #fromOnly) - - adam = Person (Just "Adam") (Just 6000) - lucy = Person (Just "Lucy") (Just 2420000) - people = VarArray [adam, lucy] - - out <- withConnection connectionString $ - firstRow =<< runQueryParams roundtrip (Only adam) - out_inline <- withConnection connectionString $ - firstRow =<< runQuery (roundtrip_inline adam) - out_array <- withConnection connectionString $ - firstRow =<< runQueryParams roundtrip_array (Only people) - out2 <- withConnection connectionString $ - firstRow =<< runQuery oneway - out2_array <- withConnection connectionString $ - firstRow =<< runQuery oneway_array - unsafe_array <- withConnection connectionString $ - firstRow =<< runQuery unsafeQ - nothings <- withConnection connectionString $ - firstRow =<< runQuery nothingQ - - out `shouldBe` Just (Only adam) - out_inline `shouldBe` Just (Only adam) - out_array `shouldBe` Just (Only people) - out2 `shouldBe` Just (Only adam) - out2_array `shouldBe` Just (Only people) - unsafe_array `shouldBe` Just (Only (VarArray [Composite adam])) - nothings `shouldBe` Just (Only (Person Nothing Nothing)) - - describe "cmdStatus and cmdTuples" $ do - - let - statusAndTuples stmnt = withConnection connectionString $ do - result <- execute stmnt - status <- cmdStatus result - tuples <- cmdTuples result - return (status, tuples) - - it "should tell you about the command and the number of rows effected" $ do - - (status1, tuples1) <- statusAndTuples (insertUsers "Jonah" ["Isaiah"]) - status1 `shouldBe` "INSERT 0 2" - tuples1 `shouldBe` Just 2 - - (status2, tuples2) <- statusAndTuples (deleteUser "Noah") - status2 `shouldBe` "DELETE 0" - tuples2 `shouldBe` Just 0 - - (status3, tuples3) <- statusAndTuples (deleteUser "Jonah") - status3 `shouldBe` "DELETE 1" - tuples3 `shouldBe` Just 1 - - (status4, tuples4) <- statusAndTuples silent - status4 `shouldBe` "SET" - tuples4 `shouldBe` Nothing diff --git a/squeal-presentation-raveline.md b/squeal-presentation-raveline.md deleted file mode 100644 index 034d9ded..00000000 --- a/squeal-presentation-raveline.md +++ /dev/null @@ -1,794 +0,0 @@ -# Squeal - -_A bridge between SQL and Haskell_ - -by [@Raveline](https://github.com/raveline) - ---- - -# Rationale - -Using `postgresql-simple` you have a typical "Trial, error, despair" workflow: - -- Write query manually or semi-automatically - -- Write haskell code using the query - -- Pray that the query syntax is correct - -- Pray that it returns what you want - -- Realize the inefficiency of prayer when it comes to SQL - -- Iterate till it works - ---- - -![Can't take this anymore](http://gif.eraveline.eu/static/img/0x16e.gif) - ---- - -# Use your best friend: GHC - -Squeal provides several eDSL to make your SQL typesafe: - -- Type level eDSL to express schema; - -- Value level eDSL to manipulate schema (plus migration, yeehaw !); - -- Type level eDSL to express queries; - -- Value level eDSL to perform queries - -However, it's not an ORM. There's no caching, lazy loading - you retain control over your memory. Also, joins - and mostly aggregation after joins - have to be handled manually. - ---- - -# Part I. Schema & migration - ---- - -# The Schema - -- The schema will be used to validate _everything_: migration, queries, etc. - -- A schema is mostly a collection of tables. - -- In this presentation, we will create a very basic database modeling a parliament. We will store _members of parliament_ and _parliamentary groups_. - -- A simple Schema to represent a Parliament could be: - -```haskell -type Schema = '[ "mp" ::: 'Table MemberOfParliament - , "groupp" ::: 'Table ParliamentGroup ] -``` - ---- - -> _note_ We need the `DataKinds` extension to be able to express heterogenous lists containing -> specific types like this one. - -> You can perfectly call your table "group" and not "groupp" even though it is a -> keyword in SQL - Squeal queries will be properly escaped. - ---- - -# A small example - -```haskell -type ParliamentaryGroup = - '[ "pk_groupp" ::: 'PrimaryKey '["groupp_id"]] - :=> - '[ "groupp_id" ::: 'NoDef :=> 'NotNull 'PGuuid - , "name" ::: 'NoDef :=> 'NotNull 'PGtext - ] -``` - -- That's a whole simple table defined in one go. - -- `:::` lets us define a column or a constraint. - -- `:=>` associates the constraints to the column. - -- Let's split constraints and column to study the syntax a bit more. - ---- - -> _note_ We are using `:::` and `:=>` to quickly express associations when writing -> our schema. We need the `TypeOperators` extension.] - ---- - -![What does this means ?!](http://gif.eraveline.eu/static/img/0x27e.gif) - ---- - -# Defining a table - -```haskell -type MemberOfParliament = - '[ "pk_mp" ::: 'PrimaryKey '["mp_id"] - , "fk_mp_groupp" ::: 'ForeignKey '["mp_group"] "public" "groupp" '["groupp_id"] - ] :=> MpCols -``` - -- A table is a collection of constraints associated to a collection of columns. - -- We defined a table named "mp". - -- It has a constraint named "pk_mp", defining its primary key on column "mp_id". - -- It has a constraint named "fk_mp_groupp" defining a foreign key on column "mg_group", connected to the column "groupp_id" of table "groupp". - -- These constraints will be associated to the columns defined in `MpCols`. - ---- - -# Defining columns - -```haskell -type MpCols = - '[ "mp_id" ::: 'NoDef :=> 'NotNull 'PGuuid - , "first_name" ::: 'NoDef :=> 'NotNull 'PGtext - , "last_name" ::: 'NoDef :=> 'NotNull 'PGtext - ] -``` - -- A listing of column associate, for each element: - - * a name; - - * the mention of an eventual default value; - - * the nullability; - - * the type (obviously). - ---- - -> _note_ GHC is already helping. If I named the "mp_id" column differently, -> GHC would yell because I promised a primary key constraint on a column named `mp_id`, so there must be one.] - ---- - -# Implementating the schema - -- We will carry this schema type pretty much everywhere. - -- But before we play with this schema, we need to implement it. - ---- - -```haskell -setup :: Definition '[] Schema -setup = - createTable #groupp - ( notNullable uuid `as` #groupp_id - :* notNullable text `as` #name) - ( primaryKey #groupp_id `as` #pk_groupp ) - >>> createTable #mp - ( notNullable uuid `as` #mp_id - :* notNullable text `as` #first_name - :* notNullable text `as` #last_name - :* notNullable text `as` #mp_group) - ( primaryKey #mp_id `as` #pk_mp - :* foreignKey #mp_group #groupp #groupp_id - OnDeleteCascade OnUpdateCascade - `as` #fk_mp_groupp) -``` - ---- - -# Damn, that's verbose - -- Yes. But you need verbosity to get type safety. - -- On the plus side, it's fairly straightforward. - -- You use `:*` to compose the element of the heterogenous list of columns. - -- You use `>>>` to compose table creation. - -> _note_ The compiler will catch any mistype between Schema and -> definition; wrong nullability, wrong type, wrong name, etc. - ---- - -> _note_ You'll also need `OverloadedLabels`, for naming stuff. -> This is mostly to avoid having to write manual proxies all -> the time and for convenience. - ---- - -# Setting up the schema - -- Squeal comes with a very good migration manager, handling _upgrades_ AND -_downgrades_. - -- A migration is a simple type: - -```haskell -Migration io schema0 schema1 -``` - -- The first parameter is a BaseMonad (typically, IO). - -- The second parameter is "the current schema of your DB". - -- The last one is "what you will migrate to". - -- The `Definition` type used to define upgrade and downgrade functions use the - same logic (from one schema to the other). - ---- - -# Defining our first migration - -- We have the setup bit, we need the teardown: - -```haskell -tearDown :: Definition Schema '[] -tearDown = dropTable #mp >>> dropTable #groupp -``` - -> _note_ GHC will also detect the _proper_ order of what you typed in -> downgrade and upgrade should there be any conflict (with foreign keys). - -```haskell -initDB :: Migration IO '[] Schema -initDB = - Migration { name = "Schema creation" - , up = void $ define setup - , down = void $ define tearDown } -``` - ---- - -# Simple migrator example - -```haskell -main :: IO () -main = do - printSQL setup - void $ withConnection connectionString $ - migrateUp $ single initDB -``` - -- We print the migration query; you can do this for all queries generated through Squeal. - -- We use `migrateUp` to perform the migration. - -- You can run several migration at the same time, to run only one use `single`. - ---- - -![That was easy](http://gif.eraveline.eu/static/img/0x47f.gif) - ---- - -# Part II. Insertions - ---- - -# Manipulations & Queries - -Besides the specific case of migration, you will mostly perform: - -- `Manipulation` : inserting, updating and deleting data. - -- `Query` : fetching data. - -- Both types are parametric over the same things: - - * A schema - - * Input parameters - - * Output - -- Simplified, they look like this: - -```haskell -Manipulation schema params columns - -Query schema params columns -``` - - ---- - -# Our Haskell model - -```haskell -type Parliament = [Group] - -data Group = - Group { name :: Text - , members :: [MemberOfParliament] } - -data MemberOfParliament = - MemberOfParliament { firstName :: Text - , lastName :: Text } -``` - -> _note_ We didn't use anything from Squeal. -> The model can be entirely separated from the persistence layer. - ---- - -# Inserting a Parliamentary Group - -- A group is very simple: it's a uuid and a name. Let's define our params: - -```haskell -type GroupInsertionParams = '[ 'NotNull 'PGuuid - , 'NotNull 'PGtext ] -``` - -- Params are not named, but they are indexed. You just need nullability and type. - ---- - -```haskell -groupInsertion :: Manipulation Schema GroupInsertionParams '[] -groupInsertion = - insertRow_ #groupp ( Set (param @1) `as` #groupp_id - :* Set (param @2) `as` #name ) -``` - ---- - -- `TypeApplication` lets us use the index of parameters (counting from 1). - -- Once again: all this is checked by the compiler. Wrong name, wrong type... you'll get a compile error. - -- `insertRow_` is a simplified version of `insertRow`. `insertRow` lets you express a `RETURNING` clause and the expected behaviour in case of conflict. - ---- - -_Inserting a Member of Parliament_ -# Doing an INSERT INTO ... SELECT - -- We could create a naive query that takes MP uuid, first name, last name and group uuid... - ---- - -- But that's boring. So let's use the `INSERT INTO ... SELECT`. - ---- - -- We will build a query that will return as constants our MP's uuid, first name and last name... - ---- - -- ... and fetch the uuid of a group given the name of the group. - ---- - -- There's a `insertQuery` utility function for that. All we have to do is write the `select` ! - -```haskell -mpInsertion :: Manipulation Schema MpParams '[] -mpInsertion = - insertQuery_ #mp selectGroup -- we just have to write selectGroup ! -``` - ---- - -_Inserting a Member of Parliament_ -# The query type - -- A query looks just like a `Manipulation`: - -```haskell -Query schema params returns -``` - -- Returns use a special syntax, that demands names, nullability and types: - -```haskell -type ExampleReturnType = '[ "some_column" ::: 'NotNull 'PGuuid - , "other_column" ::: 'Null 'PGtext ] -``` - -- It is not the same as the column definition we used in the schema. That one also expects that you specify, for each column, an eventual default: - -```haskell -type ExampleSchemaCols = - '[ "some_column" ::: 'NoDef :=> 'NotNull 'PGuuid - , "other_column" ::: 'NoDef :=> 'Null 'PGtext ] -``` - ---- - -_Inserting a Member of Parliament_ -# Don't rewrite column definitions - -- We want our return type to be "all columns from table MP expressed as return type". - -- But we cannot reuse our neat `MpCols` alias, since the type do not match as we've just seen. - -- Hopefully, there's a neat Type Family that will let you convert any table you defined to the type of the equivalent row: `TableToRow`. - -- We finally have the signature for our SELECT query. - -```haskell -selectGroup :: Query Schema MpParams (TableToRow MemberOfParliament) -``` - ---- - -_Inserting a Member of Parliament_ -# Our final Select query - -- Our intermediate query looks like this: - -```haskell -selectGroup = - select - ( param @1 `as` #mp_id - :* param @2 `as` #first_name - :* param @3 `as` #last_name - :* #groupp ! #groupp_id `as` #mp_group - ) - ( from (table #groupp) - & where_ ( #groupp ! #name .== param @4 ) - ) -``` - -- The first three columns are constant defined through our params; - -- ... we alias the column name using `as`... - -- ... and when we need the result from the table we use the `#table ! #column` syntax. - -- We'll go back to the "from" block later. - ---- - -# Dealing with the connection context - -- Queries are runned in a connection context: - - * When dealing with a single-connection context, use the type `PQ`. - - * When dealing with a pool of connection, use the type `PoolPQ`. - -- Or get rid of the context and use `mtl` style, with the typeclass `MonadPQ`. - -- We'll use that to demonstrate how to actually run our queries. - ---- - -# Inserting a whole group - -```haskell -insertGroup :: (MonadPQ Schema m, MonadBaseControl IO m) => Group -> m () -insertGroup g@(Group name _) = do - uuid' <- liftBase nextRandom - void $ manipulateParams groupInsertion (uuid', name) - insertMps g -``` - -- Remember that we defined a group as a name and a list of MPs. - -- We are in `MonadBaseControl`, so we cannot use `liftIO`, we need `liftBase`. - -- To perform a simple insertion, use `manipulateParams`. It takes into parameter -instances of `ToParam`, but you will typically use tuples or Generic-SOP. - ---- - -# Inserting a bunch of MPs - -```haskell -insertMps :: (MonadPQ Schema m, MonadBaseControl IO m) => Group -> m () -insertMps (Group groupName mps) = - let tuplify Mp{..} = (, firstName, lastName, groupName) <$> nextRandom - params = traverse tuplify mps - in void $ liftBase params >>= traversePrepared mpInsertion -``` - -- This time, we want to do a `preparedStatement`. We'll use `traversePrepare` -which behaves like `manipulateParams`, but expect a list of `ToParam` -instances. - -- We build our tuple manually again, mostly because we want to generate UUID on -the fly and we don't want to have them in our model. - ---- - -# Inserting a whole parliament - -```haskell -insertParliament :: - (MonadPQ Schema m, MonadBaseControl IO m) => Parliament -> m () -insertParliament = traverse_ insertGroup -``` - -- That's simple enough ! - -- If we want to be a bit safer, though, we can wrap this call in a -`transactionally_` function, which will put all that in a transaction. - ---- - -![Very simple](https://media1.tenor.com/images/0188c63209aced59f1583e1ca94e509e/tenor.gif?itemid=3550689) - - ---- - -# Part III. Selects - ---- - -# Composable queries - -- Building basic queries is easy, and is well documented. - -- However, the real interest of a tool like Squeal is in DRYness, and the documentation is still lacking in "how-to" related to composability. - -- I'll build an example showing how column selections and from clause can be factorized. - -- We want to write two queries: - - * One to get all members of a specific group. - - * One to get all the parliament. - ---- - -# Decomposing a `Query` - -- A query typically stars with `select` (variants are available). - -- It then takes: - - * A heterogenous list of fields with a scary signature; - - * A virtual table (the from clause and filter clauses), called a `TableExpression`. - -- And return fields. Ours will look like this: - -```haskell -type GroupRowResult = - '[ "groupName" ::: 'NotNull 'PGtext - , "firstName" ::: 'NotNull 'PGtext - , "lastName" ::: 'NotNull 'PGtext ] -``` - ---- - -# The Table Expression - -- It necessarily contains a `fromClause` (table, view or subquery, plus optional joins). - -- You can add where, groups, "HAVING" clause, order, etc. - -- `from` creates a basic `TableExpression` that you extend through various function to add clauses. - -- For our queries, we will share a common `FromClause`. - ---- - -# The From Clause - -``` -FromClause schema params from -``` - -- We need Schema, input parameters and a `FromType` giving the available fields of the expression. - -- In our case, we'll join the table `mp` and the table `groupp`, meaning the `from` will be all fields of these tables. - -- We can use `TableToRow` to be more DRY: - -```haskell -type BaseParliamentSelection = - '[ "g" ::: TableToRow GroupCols - , "m" ::: TableToRow MpCols ] -``` - -> _note_ We've also put everything with table aliases: "g" and "m". - ---- - -# Writing our from clause - -```haskell -baseParliamentTables :: - FromClause Schema (param :: [NullType]) BaseParliamentSelection -baseParliamentTables = - table - (#groupp `as` #g) - & innerJoin - (table (#mp `as` #m)) - (#m ! #mp_group .== #g ! #groupp_id) -``` - -- When picking "from" something, you need to specify the type with a function: `table` for a table, `view` for a view, etc. - -- All joins are available. `innerJoin` is the most basic one. It takes the joined table and the joining condition. - -- We have to specifiy the type of `param` (current limitation of the lib), even though _any_ params will be compatible with this, so we can plug `where` clauses depending on params should we need to ! - ---- - -# Typing the common selection - -- Both our querie will need the same fields. This has to be factorized too. - -- The scary signature of selection fields: - -```haskell -NP (Aliased (Expression schema from grouping params)) cols -``` - -- `NP` is for heterogenous lists ("n-ary product"). - -- `from` is the virtual table type, so our `BaseParliamentSelection`. - -- `grouping` is there to make a distinction between aggregated / unaggregated queries. - -- And finally, cols is the return type. - ---- - -# Our common selection - -```haskell -groupSelection :: - NP (Aliased (Expression Schema BaseParliamentSelection 'Ungrouped param)) - GroupRowResult -groupSelection = - #g ! #name `as` #groupName - :* #m ! #first_name `as` #firstName - :* #m ! #last_name `as` #lastName -``` - -- I hope you like big signatures. - -- But it's what let GHC checks that all alias and columns are available. - -- And that you're returning what you really intend to return. - ---- - -# Putting it all together - -```haskell -selectParliament :: Query Schema '[] GroupRowResult -selectParliament = - select groupSelection (from baseParliamentTables) -``` - -```haskell -selectGroupMembers :: Query Schema '[ 'NotNull 'PGtext] GroupRowResult -selectGroupMembers = - select groupSelection - (from baseParliamentTables - & where_ (#g ! #name .== param @1)) -``` - -- That is fairly DRY. And _entirely typesafe_. - ---- - -# Actually fetching the data - -- We'll build an intermediary datatype representing our rows. - -- We'll make it match our row. - -- And we'll add generic-SOP so that we can build them from query results. - -```haskell -type GroupRowResult = - '[ "groupName" ::: 'NotNull 'PGtext - , "firstName" ::: 'NotNull 'PGtext - , "lastName" ::: 'NotNull 'PGtext ] -``` - -```haskell -data GroupRow = - GroupRow { groupName :: Text - , firstName :: Text - , lastName :: Text } - deriving (Generic) - -instance SOP.Generic GroupRow -instance SOP.HasDatatypeInfo GroupRow -``` - ---- - -# Aggregate result logic - -- We will get tabular, SQL data. - -- Our results will be `[GroupRow]`. - -- We'll build `Groups` from this: - -```haskell -buildGroup :: NE.NonEmpty GroupRow -> Group -buildGroup grs = - let buildMP (GroupRow _ f l) = Mp f l - name = groupName . NE.head $ grs - members = NE.toList (buildMP <$> grs) - in Group{..} - -rowToGroups :: [GroupRow] -> [Group] -rowToGroups = - let grouped = fmap NE.fromList . L.groupBy ((==) `on` groupName) - in fmap buildGroup . grouped -``` - ---- - -# Finally calling our queries - -- Query with params will use `runQueryParams`. - -- Query without params will use `runQuery`. - -- Result is inside a `MonadPQ` and of type `K` from generic-sop. - -- You get your actual result using `getRows`. Exemple: - -```haskell -getParliament :: (MonadPQ Schema m, MonadBaseControl IO m) => m [Group] -getParliament = do - res <- runQuery selectParliament - rows <- getRows res - pure $ rowToGroups rows -``` - -- In real life of course you'll write: - -```haskell -getParliament = - rowToGroups <$> (runQuery selectParliament >>= getRows) -``` - ---- - -# Our query with params - -```haskell -getGroupMembers :: - (MonadPQ Schema m, MonadBaseControl IO m) => Text -> m (Maybe Group) -getGroupMembers = - listToMaybe . rowToGroups <$> - (runQueryParams selectGroupMembers (Only t) >>= getRows) -``` - -- And that's it. - ---- - -![Eazy](https://media.tenor.com/images/8fc7c4077efe11b4a3a3b9ae4e643e87/tenor.gif) - ---- - -# Conclusion - ---- - -# Pros & Cons - -- Not everything is included: some cool stuff like array_agg and window functions are not available yet. - -- But development is _very_ active. `IN` clauses were missing but were added in the 4.0 released recently. - -- It's bleeding edge. You need latest LTS to be comfortable and psql >= 9.5. - -- It's Postgres-only but I would say that's a feature. Multi-DB tools are even more complex. - -- The author & maintainer is <3. - -- Typesafe. 'nuff said. - ---- - -# Thank you ! diff --git a/squeal-presentation.pdf b/squeal-presentation.pdf deleted file mode 100644 index 523904e39c1d252da11ab3e94a4695beccf7049b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 233684 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

%rMxi8R|J-a1T_jxBzJ(*&W@yPqx$&uie# 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 diff --git a/squeal.gif b/squeal.gif deleted file mode 100644 index e2e62d7e797b9904285f24d39c2d263e6d0104fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6399 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 Date: Wed, 26 Jan 2022 11:48:15 -0800 Subject: [PATCH 13/18] second pass minimization --- squeal-postgresql/squeal-postgresql.cabal | 64 -- squeal-postgresql/src/Squeal/PostgreSQL.hs | 266 ------- .../src/Squeal/PostgreSQL/Definition.hs | 87 --- .../Squeal/PostgreSQL/Definition/Comment.hs | 165 ---- .../PostgreSQL/Definition/Constraint.hs | 351 --------- .../Squeal/PostgreSQL/Definition/Function.hs | 249 ------ .../src/Squeal/PostgreSQL/Definition/Index.hs | 186 ----- .../Squeal/PostgreSQL/Definition/Procedure.hs | 171 ----- .../Squeal/PostgreSQL/Definition/Schema.hs | 113 --- .../src/Squeal/PostgreSQL/Definition/Table.hs | 536 ------------- .../src/Squeal/PostgreSQL/Definition/Type.hs | 291 ------- .../src/Squeal/PostgreSQL/Definition/View.hs | 179 ----- .../src/Squeal/PostgreSQL/Expression.hs | 644 ---------------- .../Squeal/PostgreSQL/Expression/Aggregate.hs | 644 ---------------- .../src/Squeal/PostgreSQL/Expression/Array.hs | 242 ------ .../PostgreSQL/Expression/Comparison.hs | 210 ----- .../Squeal/PostgreSQL/Expression/Composite.hs | 93 --- .../Squeal/PostgreSQL/Expression/Default.hs | 60 -- .../Squeal/PostgreSQL/Expression/Inline.hs | 363 --------- .../src/Squeal/PostgreSQL/Expression/Json.hs | 484 ------------ .../src/Squeal/PostgreSQL/Expression/Logic.hs | 108 --- .../src/Squeal/PostgreSQL/Expression/Math.hs | 101 --- .../src/Squeal/PostgreSQL/Expression/Null.hs | 134 ---- .../Squeal/PostgreSQL/Expression/Parameter.hs | 130 ---- .../src/Squeal/PostgreSQL/Expression/Range.hs | 227 ------ .../src/Squeal/PostgreSQL/Expression/Sort.hs | 101 --- .../Squeal/PostgreSQL/Expression/Subquery.hs | 124 --- .../src/Squeal/PostgreSQL/Expression/Text.hs | 88 --- .../PostgreSQL/Expression/TextSearch.hs | 159 ---- .../src/Squeal/PostgreSQL/Expression/Time.hs | 247 ------ .../src/Squeal/PostgreSQL/Expression/Type.hs | 506 ------------ .../Squeal/PostgreSQL/Expression/Window.hs | 348 --------- .../src/Squeal/PostgreSQL/Manipulation.hs | 343 --------- .../Squeal/PostgreSQL/Manipulation/Call.hs | 117 --- .../Squeal/PostgreSQL/Manipulation/Delete.hs | 105 --- .../Squeal/PostgreSQL/Manipulation/Insert.hs | 288 ------- .../Squeal/PostgreSQL/Manipulation/Update.hs | 134 ---- .../src/Squeal/PostgreSQL/Query.hs | 417 ---------- .../src/Squeal/PostgreSQL/Query/From.hs | 114 --- .../src/Squeal/PostgreSQL/Query/From/Join.hs | 294 ------- .../src/Squeal/PostgreSQL/Query/From/Set.hs | 204 ----- .../src/Squeal/PostgreSQL/Query/Select.hs | 252 ------ .../src/Squeal/PostgreSQL/Query/Table.hs | 409 ---------- .../src/Squeal/PostgreSQL/Query/Values.hs | 88 --- .../src/Squeal/PostgreSQL/Query/With.hs | 245 ------ .../src/Squeal/PostgreSQL/Render.hs | 155 ---- .../src/Squeal/PostgreSQL/Session.hs | 362 --------- .../Squeal/PostgreSQL/Session/Connection.hs | 78 -- .../src/Squeal/PostgreSQL/Session/Decode.hs | 603 --------------- .../src/Squeal/PostgreSQL/Session/Encode.hs | 535 ------------- .../Squeal/PostgreSQL/Session/Exception.hs | 100 --- .../src/Squeal/PostgreSQL/Session/Indexed.hs | 126 --- .../Squeal/PostgreSQL/Session/Migration.hs | 456 ----------- .../src/Squeal/PostgreSQL/Session/Monad.hs | 654 ---------------- .../src/Squeal/PostgreSQL/Session/Oid.hs | 231 ------ .../src/Squeal/PostgreSQL/Session/Pool.hs | 130 ---- .../src/Squeal/PostgreSQL/Session/Result.hs | 223 ------ .../Squeal/PostgreSQL/Session/Statement.hs | 105 --- .../Squeal/PostgreSQL/Session/Transaction.hs | 156 ---- .../PostgreSQL/Session/Transaction/Unsafe.hs | 299 -------- .../src/Squeal/PostgreSQL/Type.hs | 201 ----- .../src/Squeal/PostgreSQL/Type/Alias.hs | 345 --------- .../src/Squeal/PostgreSQL/Type/List.hs | 199 ----- .../src/Squeal/PostgreSQL/Type/PG.hs | 336 -------- .../src/Squeal/PostgreSQL/Type/Schema.hs | 724 +----------------- 65 files changed, 1 insertion(+), 16698 deletions(-) delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Definition.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Definition/Comment.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Definition/Constraint.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Definition/Function.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Definition/Index.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Definition/Procedure.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Definition/Schema.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Definition/Table.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Definition/Type.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Definition/View.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Expression.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Expression/Aggregate.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Expression/Array.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Expression/Comparison.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Expression/Composite.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Expression/Default.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Expression/Inline.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Expression/Json.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Expression/Logic.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Expression/Math.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Expression/Null.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Expression/Parameter.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Expression/Range.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Expression/Sort.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Expression/Subquery.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Expression/Text.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Expression/TextSearch.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Expression/Time.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Expression/Type.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Expression/Window.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Manipulation.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Manipulation/Call.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Manipulation/Delete.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Manipulation/Insert.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Manipulation/Update.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Query.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Query/From.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Query/From/Join.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Query/From/Set.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Query/Select.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Query/Table.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Query/Values.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Query/With.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Render.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Session.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Session/Connection.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Session/Decode.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Session/Encode.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Session/Exception.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Session/Indexed.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Session/Migration.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Session/Monad.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Session/Oid.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Session/Pool.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Session/Result.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Session/Statement.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Session/Transaction.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Session/Transaction/Unsafe.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Type.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Type/Alias.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Type/List.hs delete mode 100644 squeal-postgresql/src/Squeal/PostgreSQL/Type/PG.hs diff --git a/squeal-postgresql/squeal-postgresql.cabal b/squeal-postgresql/squeal-postgresql.cabal index 5b386e3e..f0235bbb 100644 --- a/squeal-postgresql/squeal-postgresql.cabal +++ b/squeal-postgresql/squeal-postgresql.cabal @@ -5,7 +5,6 @@ description: Squeal is a type-safe embedding of PostgreSQL in Haskell homepage: https://github.com/morphismtech/squeal bug-reports: https://github.com/morphismtech/squeal/issues license: BSD3 -license-file: LICENSE author: Eitan Chatav maintainer: eitan.chatav@gmail.com copyright: Copyright (c) 2021 Morphism, LLC @@ -21,69 +20,6 @@ source-repository head library hs-source-dirs: src exposed-modules: - Squeal.PostgreSQL - Squeal.PostgreSQL.Definition - Squeal.PostgreSQL.Definition.Comment - Squeal.PostgreSQL.Definition.Constraint - Squeal.PostgreSQL.Definition.Function - Squeal.PostgreSQL.Definition.Index - Squeal.PostgreSQL.Definition.Table - Squeal.PostgreSQL.Definition.Type - Squeal.PostgreSQL.Definition.Procedure - Squeal.PostgreSQL.Definition.Schema - Squeal.PostgreSQL.Definition.View - Squeal.PostgreSQL.Expression - Squeal.PostgreSQL.Expression.Aggregate - Squeal.PostgreSQL.Expression.Array - Squeal.PostgreSQL.Expression.Comparison - Squeal.PostgreSQL.Expression.Composite - Squeal.PostgreSQL.Expression.Default - Squeal.PostgreSQL.Expression.Json - Squeal.PostgreSQL.Expression.Inline - Squeal.PostgreSQL.Expression.Logic - Squeal.PostgreSQL.Expression.Math - Squeal.PostgreSQL.Expression.Null - Squeal.PostgreSQL.Expression.Parameter - Squeal.PostgreSQL.Expression.Range - Squeal.PostgreSQL.Expression.Sort - Squeal.PostgreSQL.Expression.Subquery - Squeal.PostgreSQL.Expression.Text - Squeal.PostgreSQL.Expression.TextSearch - Squeal.PostgreSQL.Expression.Time - Squeal.PostgreSQL.Expression.Type - Squeal.PostgreSQL.Expression.Window - Squeal.PostgreSQL.Manipulation - Squeal.PostgreSQL.Manipulation.Call - Squeal.PostgreSQL.Manipulation.Delete - Squeal.PostgreSQL.Manipulation.Insert - Squeal.PostgreSQL.Manipulation.Update - Squeal.PostgreSQL.Render - Squeal.PostgreSQL.Query - Squeal.PostgreSQL.Query.From - Squeal.PostgreSQL.Query.From.Join - Squeal.PostgreSQL.Query.From.Set - Squeal.PostgreSQL.Query.Select - Squeal.PostgreSQL.Query.Table - Squeal.PostgreSQL.Query.Values - Squeal.PostgreSQL.Query.With - Squeal.PostgreSQL.Session - Squeal.PostgreSQL.Session.Connection - Squeal.PostgreSQL.Session.Decode - Squeal.PostgreSQL.Session.Encode - Squeal.PostgreSQL.Session.Exception - Squeal.PostgreSQL.Session.Indexed - Squeal.PostgreSQL.Session.Migration - Squeal.PostgreSQL.Session.Monad - Squeal.PostgreSQL.Session.Oid - Squeal.PostgreSQL.Session.Pool - Squeal.PostgreSQL.Session.Result - Squeal.PostgreSQL.Session.Statement - Squeal.PostgreSQL.Session.Transaction - Squeal.PostgreSQL.Session.Transaction.Unsafe - Squeal.PostgreSQL.Type - Squeal.PostgreSQL.Type.Alias - Squeal.PostgreSQL.Type.List - Squeal.PostgreSQL.Type.PG Squeal.PostgreSQL.Type.Schema default-language: Haskell2010 ghc-options: -Wall diff --git a/squeal-postgresql/src/Squeal/PostgreSQL.hs b/squeal-postgresql/src/Squeal/PostgreSQL.hs deleted file mode 100644 index 8deaa911..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL.hs +++ /dev/null @@ -1,266 +0,0 @@ -{-| -Module: Squeal.PostgreSQL -Description: export module -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -Squeal is a deep embedding of [PostgreSQL](https://www.postgresql.org) in Haskell. -Let's see an example! - -First, we need some language extensions because Squeal uses modern GHC -features. - ->>> :set -XDataKinds -XDeriveGeneric -XOverloadedLabels -XFlexibleContexts ->>> :set -XOverloadedStrings -XTypeApplications -XTypeOperators -XGADTs - -We'll need some imports. - ->>> import Control.Monad.IO.Class (liftIO) ->>> import Data.Int (Int32) ->>> import Data.Text (Text) ->>> import Squeal.PostgreSQL - -We'll use generics to easily convert between Haskell and PostgreSQL values. - ->>> import qualified Generics.SOP as SOP ->>> import qualified GHC.Generics as GHC - -The first step is to define the schema of our database. This is where -we use @DataKinds@ and @TypeOperators@. - ->>> :{ -type UsersColumns = - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "name" ::: 'NoDef :=> 'NotNull 'PGtext ] -type UsersConstraints = '[ "pk_users" ::: 'PrimaryKey '["id"] ] -type EmailsColumns = - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4 - , "email" ::: 'NoDef :=> 'Null 'PGtext ] -type EmailsConstraints = - '[ "pk_emails" ::: 'PrimaryKey '["id"] - , "fk_user_id" ::: 'ForeignKey '["user_id"] "public" "users" '["id"] ] -type Schema = - '[ "users" ::: 'Table (UsersConstraints :=> UsersColumns) - , "emails" ::: 'Table (EmailsConstraints :=> EmailsColumns) ] -type DB = Public Schema -:} - -Notice the use of type operators. - -`:::` is used to pair an alias `GHC.TypeLits.Symbol` with a `SchemasType`, a `SchemumType`, -a `TableConstraint` or a `ColumnType`. It is intended to connote Haskell's @::@ -operator. - -`:=>` is used to pair `TableConstraints` with a `ColumnsType`, -yielding a `TableType`, or to pair an `Optionality` with a `NullType`, -yielding a `ColumnType`. It is intended to connote Haskell's @=>@ operator - -Next, we'll write `Definition`s to set up and tear down the schema. In -Squeal, a `Definition` like `createTable`, `alterTable` or `dropTable` -has two type parameters, corresponding to the schema -before being run and the schema after. We can compose definitions using `>>>`. -Here and in the rest of our commands we make use of overloaded -labels to refer to named tables and columns in our schema. - ->>> :{ -let - setup :: Definition (Public '[]) DB - setup = - createTable #users - ( serial `as` #id :* - (text & notNullable) `as` #name ) - ( primaryKey #id `as` #pk_users ) >>> - createTable #emails - ( serial `as` #id :* - (int & notNullable) `as` #user_id :* - (text & nullable) `as` #email ) - ( primaryKey #id `as` #pk_emails :* - foreignKey #user_id #users #id - (OnDelete Cascade) (OnUpdate Cascade) `as` #fk_user_id ) -:} - -We can easily see the generated SQL is unsurprising looking. - ->>> printSQL setup -CREATE TABLE "users" ("id" serial, "name" text NOT NULL, CONSTRAINT "pk_users" PRIMARY KEY ("id")); -CREATE TABLE "emails" ("id" serial, "user_id" int NOT NULL, "email" text NULL, CONSTRAINT "pk_emails" PRIMARY KEY ("id"), CONSTRAINT "fk_user_id" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE); - -Notice that @setup@ starts with an empty public schema @(Public '[])@ and produces @DB@. -In our `createTable` commands we included `TableConstraint`s to define -primary and foreign keys, making them somewhat complex. Our @teardown@ -`Definition` is simpler. - ->>> :{ -let - teardown :: Definition DB (Public '[]) - teardown = dropTable #emails >>> dropTable #users -:} - ->>> printSQL teardown -DROP TABLE "emails"; -DROP TABLE "users"; - -We'll need a Haskell type for @User@s. We give the type `Generics.SOP.Generic` and -`Generics.SOP.HasDatatypeInfo` instances so that we can encode and decode @User@s. - ->>> :set -XDerivingStrategies -XDeriveAnyClass ->>> :{ -data User = User { userName :: Text, userEmail :: Maybe Text } - deriving stock (Show, GHC.Generic) - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) -:} - -Next, we'll write `Statement`s to insert @User@s into our two tables. -A `Statement` has three type parameters, the schemas it refers to, -input parameters and an output row. When -we insert into the users table, we will need a parameter for the @name@ -field but not for the @id@ field. Since it's serial, we can use a default -value. However, since the emails table refers to the users table, we will -need to retrieve the user id that the insert generates and insert it into -the emails table. We can do this in a single `Statement` by using a -`with` `manipulation`. - ->>> :{ -let - insertUser :: Statement DB User () - insertUser = manipulation $ with (u `as` #u) e - where - u = insertInto #users - (Values_ (Default `as` #id :* Set (param @1) `as` #name)) - OnConflictDoRaise (Returning_ (#id :* param @2 `as` #email)) - e = insertInto_ #emails $ Select - (Default `as` #id :* Set (#u ! #id) `as` #user_id :* Set (#u ! #email) `as` #email) - (from (common #u)) -:} - ->>> printSQL insertUser -WITH "u" AS (INSERT INTO "users" AS "users" ("id", "name") VALUES (DEFAULT, ($1 :: text)) RETURNING "id" AS "id", ($2 :: text) AS "email") INSERT INTO "emails" AS "emails" ("user_id", "email") SELECT "u"."id", "u"."email" FROM "u" AS "u" - -Next we write a `Statement` to retrieve users from the database. We're not -interested in the ids here, just the usernames and email addresses. We -need to use an `innerJoin` to get the right result. - ->>> :{ -let - getUsers :: Statement DB () User - getUsers = query $ select_ - (#u ! #name `as` #userName :* #e ! #email `as` #userEmail) - ( from (table (#users `as` #u) - & innerJoin (table (#emails `as` #e)) - (#u ! #id .== #e ! #user_id)) ) -:} - ->>> printSQL getUsers -SELECT "u"."name" AS "userName", "e"."email" AS "userEmail" FROM "users" AS "u" INNER JOIN "emails" AS "e" ON ("u"."id" = "e"."user_id") - -Let's create some users to add to the database. - ->>> :{ -let - users :: [User] - users = - [ User "Alice" (Just "alice@gmail.com") - , User "Bob" Nothing - , User "Carole" (Just "carole@hotmail.com") - ] -:} - -Now we can put together all the pieces into a program. The program -connects to the database, sets up the schema, inserts the user data -(using prepared statements as an optimization), queries the user -data and prints it out and finally closes the connection. We can thread -the changing schema information through by using the indexed `PQ` monad -transformer and when the schema doesn't change we can use `Monad` and -`MonadPQ` functionality. - ->>> :{ -let - session :: PQ DB DB IO () - session = do - executePrepared_ insertUser users - usersResult <- execute getUsers - usersRows <- getRows usersResult - liftIO $ print usersRows -in - withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $ - define setup - & pqThen session - & pqThen (define teardown) -:} -[User {userName = "Alice", userEmail = Just "alice@gmail.com"},User {userName = "Bob", userEmail = Nothing},User {userName = "Carole", userEmail = Just "carole@hotmail.com"}] - -This should get you up and running with Squeal. Once you're writing more complicated -queries and need a deeper understanding of Squeal's types and how everything -fits together, check out the -in the toplevel of Squeal's Git repo. --} -module Squeal.PostgreSQL - ( module X - , RenderSQL (..) - , printSQL - ) where - -import Squeal.PostgreSQL.Definition as X -import Squeal.PostgreSQL.Definition.Comment as X -import Squeal.PostgreSQL.Definition.Constraint as X -import Squeal.PostgreSQL.Definition.Function as X -import Squeal.PostgreSQL.Definition.Index as X -import Squeal.PostgreSQL.Definition.Procedure as X -import Squeal.PostgreSQL.Definition.Schema as X -import Squeal.PostgreSQL.Definition.Table as X -import Squeal.PostgreSQL.Definition.Type as X -import Squeal.PostgreSQL.Definition.View as X -import Squeal.PostgreSQL.Expression as X -import Squeal.PostgreSQL.Expression.Aggregate as X -import Squeal.PostgreSQL.Expression.Array as X -import Squeal.PostgreSQL.Expression.Comparison as X -import Squeal.PostgreSQL.Expression.Composite as X -import Squeal.PostgreSQL.Expression.Default as X -import Squeal.PostgreSQL.Expression.Json as X -import Squeal.PostgreSQL.Expression.Inline as X -import Squeal.PostgreSQL.Expression.Logic as X -import Squeal.PostgreSQL.Expression.Math as X -import Squeal.PostgreSQL.Expression.Null as X -import Squeal.PostgreSQL.Expression.Parameter as X -import Squeal.PostgreSQL.Expression.Range as X -import Squeal.PostgreSQL.Expression.Sort as X -import Squeal.PostgreSQL.Expression.Subquery as X -import Squeal.PostgreSQL.Expression.Text as X -import Squeal.PostgreSQL.Expression.TextSearch as X -import Squeal.PostgreSQL.Expression.Time as X -import Squeal.PostgreSQL.Expression.Type as X -import Squeal.PostgreSQL.Expression.Window as X -import Squeal.PostgreSQL.Manipulation as X -import Squeal.PostgreSQL.Manipulation.Call as X -import Squeal.PostgreSQL.Manipulation.Delete as X -import Squeal.PostgreSQL.Manipulation.Insert as X -import Squeal.PostgreSQL.Manipulation.Update as X -import Squeal.PostgreSQL.Query as X -import Squeal.PostgreSQL.Query.From as X -import Squeal.PostgreSQL.Query.From.Join as X -import Squeal.PostgreSQL.Query.From.Set as X -import Squeal.PostgreSQL.Query.Select as X -import Squeal.PostgreSQL.Query.Table as X -import Squeal.PostgreSQL.Query.Values as X -import Squeal.PostgreSQL.Query.With as X -import Squeal.PostgreSQL.Render (RenderSQL(..), printSQL) -import Squeal.PostgreSQL.Session as X -import Squeal.PostgreSQL.Session.Connection as X -import Squeal.PostgreSQL.Session.Decode as X -import Squeal.PostgreSQL.Session.Encode as X -import Squeal.PostgreSQL.Session.Exception as X -import Squeal.PostgreSQL.Session.Indexed as X -import Squeal.PostgreSQL.Session.Migration as X -import Squeal.PostgreSQL.Session.Monad as X -import Squeal.PostgreSQL.Session.Oid as X -import Squeal.PostgreSQL.Session.Pool as X -import Squeal.PostgreSQL.Session.Result as X -import Squeal.PostgreSQL.Session.Statement as X -import Squeal.PostgreSQL.Session.Transaction as X -import Squeal.PostgreSQL.Type as X -import Squeal.PostgreSQL.Type.Alias as X -import Squeal.PostgreSQL.Type.List as X -import Squeal.PostgreSQL.Type.PG as X -import Squeal.PostgreSQL.Type.Schema as X diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Definition.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Definition.hs deleted file mode 100644 index 8bf94edd..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Definition.hs +++ /dev/null @@ -1,87 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Definition -Description: data definition language -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -data definition language --} - -{-# LANGUAGE - AllowAmbiguousTypes - , ConstraintKinds - , DeriveAnyClass - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , LambdaCase - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeInType - , TypeOperators - , UndecidableSuperClasses - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Definition - ( -- * Definition - Definition (..) - , (>>>) - , manipulation_ - ) where - -import Control.Category -import Control.DeepSeq -import Data.ByteString -import Data.Monoid -import Prelude hiding ((.), id) - -import qualified GHC.Generics as GHC - -import Squeal.PostgreSQL.Manipulation -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - -{----------------------------------------- -statements ------------------------------------------} - --- | A `Definition` is a statement that changes the schemas of the --- database, like a `Squeal.PostgreSQL.Definition.Table.createTable`, --- `Squeal.PostgreSQL.Definition.Table.dropTable`, --- or `Squeal.PostgreSQL.Definition.Table.alterTable` command. --- `Definition`s may be composed using the `>>>` operator. -newtype Definition - (db0 :: SchemasType) - (db1 :: SchemasType) - = UnsafeDefinition { renderDefinition :: ByteString } - deriving (GHC.Generic,Show,Eq,Ord,NFData) - -instance RenderSQL (Definition db0 db1) where - renderSQL = renderDefinition - -instance Category Definition where - id = UnsafeDefinition ";" - ddl1 . ddl0 = UnsafeDefinition $ - renderSQL ddl0 <> "\n" <> renderSQL ddl1 - -instance db0 ~ db1 => Semigroup (Definition db0 db1) where (<>) = (>>>) -instance db0 ~ db1 => Monoid (Definition db0 db1) where mempty = id - --- | A `Manipulation` without input or output can be run as a statement --- along with other `Definition`s, by embedding it using `manipulation_`. -manipulation_ - :: Manipulation '[] db '[] '[] - -- ^ no input or output - -> Definition db db -manipulation_ = UnsafeDefinition . (<> ";") . renderSQL diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Comment.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Comment.hs deleted file mode 100644 index 18a616bb..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Comment.hs +++ /dev/null @@ -1,165 +0,0 @@ -{- | -Module: Squeal.PostgreSQL.Definition.Constraint -Description: comments -Copyright: (c) Eitan Chatav, 2020 -Maintainer: eitan@morphism.tech -Stability: experimental - -comments --} - -{-# LANGUAGE - AllowAmbiguousTypes - , ConstraintKinds - , DeriveAnyClass - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , LambdaCase - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeInType - , TypeOperators - , UndecidableSuperClasses - #-} - -module Squeal.PostgreSQL.Definition.Comment - ( commentOnTable - , commentOnType - , commentOnView - , commentOnFunction - , commentOnIndex - , commentOnColumn - , commentOnSchema - ) where - -import Squeal.PostgreSQL.Definition -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema -import GHC.TypeLits (KnownSymbol) -import Data.Text (Text) - -{----------------------------------------- -COMMENT statements ------------------------------------------} - -{- | -When a user views a table in the database (i.e. with \d+ ), it is useful -to be able to read a description of the table. --} -commentOnTable - :: ( KnownSymbol sch - , KnownSymbol tab - , Has sch db schema - , Has tab schema ('Table table) - ) - => QualifiedAlias sch tab -- ^ table - -> Text -- ^ comment - -> Definition db db -commentOnTable alias comm = UnsafeDefinition $ - "COMMENT ON TABLE" <+> renderSQL alias <+> "IS" <+> singleQuotedText comm <> ";" - -{- | -When a user views a type in the database (i.e with \dT ), it is useful to -be able to read a description of the type. --} -commentOnType - :: ( KnownSymbol sch - , KnownSymbol typ - , Has sch db schema - , Has typ schema ('Typedef type_) - ) - => QualifiedAlias sch typ -- ^ type - -> Text -- ^ comment - -> Definition db db -commentOnType alias comm = UnsafeDefinition $ - "COMMENT ON TYPE" <+> renderSQL alias <+> "IS" <+> singleQuotedText comm <> ";" - -{- | -When a user views a view in the database (i.e. with \dv ), it is useful -to be able to read a description of the view. --} -commentOnView - :: ( KnownSymbol sch - , KnownSymbol vie - , Has sch db schema - , Has vie schema ('View view) - ) - => QualifiedAlias sch vie -- ^ view - -> Text -- ^ comment - -> Definition db db -commentOnView alias comm = UnsafeDefinition $ - "COMMENT ON VIEW" <+> renderSQL alias <+> "IS" <+> singleQuotedText comm <> ";" - -{- | -When a user views an index in the database (i.e. with \di+ ), it is -useful to be able to read a description of the index. --} -commentOnIndex - :: ( KnownSymbol sch - , KnownSymbol ind - , Has sch db schema - , Has ind schema ('Index index) - ) - => QualifiedAlias sch ind -- ^ index - -> Text -- ^ comment - -> Definition db db -commentOnIndex alias comm = UnsafeDefinition $ - "COMMENT ON INDEX" <+> renderSQL alias <+> "IS" <+> singleQuotedText comm <> ";" - -{- | -When a user views a function in the database (i.e. with \df+ ), it is -useful to be able to read a description of the function. --} -commentOnFunction - :: ( KnownSymbol sch - , KnownSymbol fun - , Has sch db schema - , Has fun schema ('Function function) - ) - => QualifiedAlias sch fun -- ^ function - -> Text -- ^ comment - -> Definition db db -commentOnFunction alias comm = UnsafeDefinition $ - "COMMENT ON FUNCTION" <+> renderSQL alias <+> "IS" <+> singleQuotedText comm <> ";" - -{- | -When a user views a table in the database (i.e. with \d+
), it is useful -to be able to view descriptions of the columns in that table. --} -commentOnColumn - :: ( KnownSymbol sch - , KnownSymbol tab - , KnownSymbol col - , Has sch db schema - , Has tab schema ('Table '(cons, cols)) - , Has col cols '(def, nulltyp) - ) - => QualifiedAlias sch tab -- ^ table - -> Alias col -- ^ column - -> Text -- ^ comment - -> Definition db db -commentOnColumn table col comm = UnsafeDefinition $ - "COMMENT ON COLUMN" <+> renderSQL table <> "." <> renderSQL col <+> "IS" - <+> singleQuotedText comm <> ";" - -{- | -When a user views a schema in the database (i.e. with \dn+ ), it is -useful to be able to read a description. --} -commentOnSchema - :: ( KnownSymbol sch - , Has sch db schema - ) - => Alias sch -- ^ schema - -> Text -- ^ comment - -> Definition db db -commentOnSchema schema comm = UnsafeDefinition $ - "COMMENT ON SCHEMA" <+> renderSQL schema <> "IS" <+> singleQuotedText comm <> ";" diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Constraint.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Constraint.hs deleted file mode 100644 index 173cd5ca..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Constraint.hs +++ /dev/null @@ -1,351 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Definition.Constraint -Description: constraint expressions -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -constraint expressions --} - -{-# LANGUAGE - AllowAmbiguousTypes - , ConstraintKinds - , DeriveAnyClass - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , LambdaCase - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeInType - , TypeOperators - , UndecidableSuperClasses -#-} - -module Squeal.PostgreSQL.Definition.Constraint - ( -- * Table Constraints - TableConstraintExpression (..) - , check - , unique - , primaryKey - -- ** Foreign Keys - , foreignKey - , ForeignKeyed - , OnDeleteClause (..) - , OnUpdateClause (..) - , ReferentialAction (..) - ) where - -import Control.DeepSeq -import Data.ByteString -import GHC.TypeLits - -import qualified Generics.SOP as SOP -import qualified GHC.Generics as GHC - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Expression.Logic -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - --- | Data types are a way to limit the kind of data that can be stored in a --- table. For many applications, however, the constraint they provide is --- too coarse. For example, a column containing a product price should --- probably only accept positive values. But there is no standard data type --- that accepts only positive numbers. Another issue is that you might want --- to constrain column data with respect to other columns or rows. --- For example, in a table containing product information, --- there should be only one row for each product number. --- `TableConstraint`s give you as much control over the data in your tables --- as you wish. If a user attempts to store data in a column that would --- violate a constraint, an error is raised. This applies --- even if the value came from the default value definition. -newtype TableConstraintExpression - (sch :: Symbol) - (tab :: Symbol) - (db :: SchemasType) - (constraint :: TableConstraint) - = UnsafeTableConstraintExpression - { renderTableConstraintExpression :: ByteString } - deriving (GHC.Generic,Show,Eq,Ord,NFData) -instance RenderSQL - (TableConstraintExpression sch tab db constraint) where - renderSQL = renderTableConstraintExpression - -{-| A `check` constraint is the most generic `TableConstraint` type. -It allows you to specify that the value in a certain column must satisfy -a Boolean (truth-value) expression. - ->>> :{ -type Schema = '[ - "tab" ::: 'Table ('[ "inequality" ::: 'Check '["a","b"]] :=> '[ - "a" ::: 'NoDef :=> 'NotNull 'PGint4, - "b" ::: 'NoDef :=> 'NotNull 'PGint4 - ])] -:} - ->>> :{ -let - definition :: Definition (Public '[]) (Public Schema) - definition = createTable #tab - ( (int & notNullable) `as` #a :* - (int & notNullable) `as` #b ) - ( check (#a :* #b) (#a .> #b) `as` #inequality ) -:} - ->>> printSQL definition -CREATE TABLE "tab" ("a" int NOT NULL, "b" int NOT NULL, CONSTRAINT "inequality" CHECK (("a" > "b"))); --} -check - :: ( Has sch db schema - , Has tab schema ('Table table) - , HasAll aliases (TableToRow table) subcolumns ) - => NP Alias aliases - -- ^ specify the subcolumns which are getting checked - -> (forall t. Condition 'Ungrouped '[] '[] db '[] '[t ::: subcolumns]) - -- ^ a closed `Condition` on those subcolumns - -> TableConstraintExpression sch tab db ('Check aliases) -check _cols condition = UnsafeTableConstraintExpression $ - "CHECK" <+> parenthesized (renderSQL condition) - -{-| A `unique` constraint ensure that the data contained in a column, -or a group of columns, is unique among all the rows in the table. - ->>> :{ -type Schema = '[ - "tab" ::: 'Table( '[ "uq_a_b" ::: 'Unique '["a","b"]] :=> '[ - "a" ::: 'NoDef :=> 'Null 'PGint4, - "b" ::: 'NoDef :=> 'Null 'PGint4 - ])] -:} - ->>> :{ -let - definition :: Definition (Public '[]) (Public Schema) - definition = createTable #tab - ( (int & nullable) `as` #a :* - (int & nullable) `as` #b ) - ( unique (#a :* #b) `as` #uq_a_b ) -:} - ->>> printSQL definition -CREATE TABLE "tab" ("a" int NULL, "b" int NULL, CONSTRAINT "uq_a_b" UNIQUE ("a", "b")); --} -unique - :: ( Has sch db schema - , Has tab schema ('Table table) - , HasAll aliases (TableToRow table) subcolumns ) - => NP Alias aliases - -- ^ specify subcolumns which together are unique for each row - -> TableConstraintExpression sch tab db ('Unique aliases) -unique columns = UnsafeTableConstraintExpression $ - "UNIQUE" <+> parenthesized (renderSQL columns) - -{-| A `primaryKey` constraint indicates that a column, or group of columns, -can be used as a unique identifier for rows in the table. -This requires that the values be both unique and not null. - ->>> :{ -type Schema = '[ - "tab" ::: 'Table ('[ "pk_id" ::: 'PrimaryKey '["id"]] :=> '[ - "id" ::: 'Def :=> 'NotNull 'PGint4, - "name" ::: 'NoDef :=> 'NotNull 'PGtext - ])] -:} - ->>> :{ -let - definition :: Definition (Public '[]) (Public Schema) - definition = createTable #tab - ( serial `as` #id :* - (text & notNullable) `as` #name ) - ( primaryKey #id `as` #pk_id ) -:} - ->>> printSQL definition -CREATE TABLE "tab" ("id" serial, "name" text NOT NULL, CONSTRAINT "pk_id" PRIMARY KEY ("id")); --} -primaryKey - :: ( Has sch db schema - , Has tab schema ('Table table) - , HasAll aliases (TableToColumns table) subcolumns - , AllNotNull subcolumns ) - => NP Alias aliases - -- ^ specify the subcolumns which together form a primary key. - -> TableConstraintExpression sch tab db ('PrimaryKey aliases) -primaryKey columns = UnsafeTableConstraintExpression $ - "PRIMARY KEY" <+> parenthesized (renderSQL columns) - -{-| A `foreignKey` specifies that the values in a column -(or a group of columns) must match the values appearing in some row of -another table. We say this maintains the referential integrity -between two related tables. - ->>> :{ -type Schema = - '[ "users" ::: 'Table ( - '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=> - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "name" ::: 'NoDef :=> 'NotNull 'PGtext - ]) - , "emails" ::: 'Table ( - '[ "pk_emails" ::: 'PrimaryKey '["id"] - , "fk_user_id" ::: 'ForeignKey '["user_id"] "public" "users" '["id"] - ] :=> - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4 - , "email" ::: 'NoDef :=> 'Null 'PGtext - ]) - ] -:} - ->>> :{ -let - setup :: Definition (Public '[]) (Public Schema) - setup = - createTable #users - ( serial `as` #id :* - (text & notNullable) `as` #name ) - ( primaryKey #id `as` #pk_users ) >>> - createTable #emails - ( serial `as` #id :* - (int & notNullable) `as` #user_id :* - (text & nullable) `as` #email ) - ( primaryKey #id `as` #pk_emails :* - foreignKey #user_id #users #id - (OnDelete Cascade) (OnUpdate Cascade) `as` #fk_user_id ) -in printSQL setup -:} -CREATE TABLE "users" ("id" serial, "name" text NOT NULL, CONSTRAINT "pk_users" PRIMARY KEY ("id")); -CREATE TABLE "emails" ("id" serial, "user_id" int NOT NULL, "email" text NULL, CONSTRAINT "pk_emails" PRIMARY KEY ("id"), CONSTRAINT "fk_user_id" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE); - -A `foreignKey` can even be a table self-reference. - ->>> :{ -type Schema = - '[ "employees" ::: 'Table ( - '[ "employees_pk" ::: 'PrimaryKey '["id"] - , "employees_employer_fk" ::: 'ForeignKey '["employer_id"] "public" "employees" '["id"] - ] :=> - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "name" ::: 'NoDef :=> 'NotNull 'PGtext - , "employer_id" ::: 'NoDef :=> 'Null 'PGint4 - ]) - ] -:} - ->>> :{ -let - setup :: Definition (Public '[]) (Public Schema) - setup = - createTable #employees - ( serial `as` #id :* - (text & notNullable) `as` #name :* - (integer & nullable) `as` #employer_id ) - ( primaryKey #id `as` #employees_pk :* - foreignKey #employer_id #employees #id - (OnDelete Cascade) (OnUpdate Cascade) `as` #employees_employer_fk ) -in printSQL setup -:} -CREATE TABLE "employees" ("id" serial, "name" text NOT NULL, "employer_id" integer NULL, CONSTRAINT "employees_pk" PRIMARY KEY ("id"), CONSTRAINT "employees_employer_fk" FOREIGN KEY ("employer_id") REFERENCES "employees" ("id") ON DELETE CASCADE ON UPDATE CASCADE); --} -foreignKey - :: (ForeignKeyed db - sch0 sch1 - schema0 schema1 - child parent - table reftable - columns refcolumns - constraints cols - reftys tys ) - => NP Alias columns - -- ^ column or columns in the table - -> QualifiedAlias sch0 parent - -- ^ reference table - -> NP Alias refcolumns - -- ^ reference column or columns in the reference table - -> OnDeleteClause - -- ^ what to do when reference is deleted - -> OnUpdateClause - -- ^ what to do when reference is updated - -> TableConstraintExpression sch1 child db - ('ForeignKey columns sch0 parent refcolumns) -foreignKey keys parent refs ondel onupd = UnsafeTableConstraintExpression $ - "FOREIGN KEY" <+> parenthesized (renderSQL keys) - <+> "REFERENCES" <+> renderSQL parent - <+> parenthesized (renderSQL refs) - <+> renderSQL ondel - <+> renderSQL onupd - --- | A constraint synonym between types involved in a foreign key constraint. -type ForeignKeyed db - sch0 sch1 - schema0 schema1 - child parent - table reftable - columns refcolumns - constraints cols - reftys tys = - ( Has sch0 db schema0 - , Has sch1 db schema1 - , Has parent schema0 ('Table reftable) - , Has child schema1 ('Table table) - , HasAll columns (TableToColumns table) tys - , reftable ~ (constraints :=> cols) - , HasAll refcolumns cols reftys - , SOP.AllZip SamePGType tys reftys - , Uniquely refcolumns constraints ) - --- | `OnDeleteClause` indicates what to do with rows that reference a deleted row. -newtype OnDeleteClause = OnDelete ReferentialAction - deriving (GHC.Generic,Show,Eq,Ord) -instance NFData OnDeleteClause -instance RenderSQL OnDeleteClause where - renderSQL (OnDelete action) = "ON DELETE" <+> renderSQL action - --- | Analagous to `OnDeleteClause` there is also `OnUpdateClause` which is invoked --- when a referenced column is changed (updated). -newtype OnUpdateClause = OnUpdate ReferentialAction - deriving (GHC.Generic,Show,Eq,Ord) -instance NFData OnUpdateClause -instance RenderSQL OnUpdateClause where - renderSQL (OnUpdate action) = "ON UPDATE" <+> renderSQL action - -{- | When the data in the referenced columns is changed, -certain actions are performed on the data in this table's columns.-} -data ReferentialAction - = NoAction - {- ^ Produce an error indicating that the deletion or update - would create a foreign key constraint violation. - If the constraint is deferred, this error will be produced - at constraint check time if there still exist any referencing rows.-} - | Restrict - {- ^ Produce an error indicating that the deletion or update - would create a foreign key constraint violation. - This is the same as `NoAction` except that the check is not deferrable.-} - | Cascade - {- ^ Delete any rows referencing the deleted row, - or update the value of the referencing column - to the new value of the referenced column, respectively.-} - | SetNull {- ^ Set the referencing column(s) to null.-} - | SetDefault {- ^ Set the referencing column(s) to their default values.-} - deriving (GHC.Generic,Show,Eq,Ord) -instance NFData ReferentialAction -instance RenderSQL ReferentialAction where - renderSQL = \case - NoAction -> "NO ACTION" - Restrict -> "RESTRICT" - Cascade -> "CASCADE" - SetNull -> "SET NULL" - SetDefault -> "SET DEFAULT" diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Function.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Function.hs deleted file mode 100644 index 90b4f80f..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Function.hs +++ /dev/null @@ -1,249 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Definition.Function -Description: create and drop functions -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -create and drop functions --} - -{-# LANGUAGE - AllowAmbiguousTypes - , ConstraintKinds - , DeriveAnyClass - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , LambdaCase - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeInType - , TypeOperators - , UndecidableSuperClasses -#-} - -module Squeal.PostgreSQL.Definition.Function - ( -- * Create - createFunction - , createOrReplaceFunction - , createSetFunction - , createOrReplaceSetFunction - -- * Drop - , dropFunction - , dropFunctionIfExists - -- * Function Definition - , FunctionDefinition(..) - , languageSqlExpr - , languageSqlQuery - ) where - -import Control.DeepSeq -import Data.ByteString -import GHC.TypeLits - -import qualified Generics.SOP as SOP -import qualified GHC.Generics as GHC - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Definition -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Expression.Type -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Query -import Squeal.PostgreSQL.Query.Values -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - -{- | Create a function. - ->>> type Fn = 'Function ( '[ 'Null 'PGint4, 'Null 'PGint4] :=> 'Returns ( 'Null 'PGint4)) ->>> :{ -let - definition :: Definition (Public '[]) (Public '["fn" ::: Fn]) - definition = createFunction #fn (int4 *: int4) int4 $ - languageSqlExpr (param @1 * param @2 + 1) -in printSQL definition -:} -CREATE FUNCTION "fn" (int4, int4) RETURNS int4 language sql as $$ SELECT * FROM (VALUES (((($1 :: int4) * ($2 :: int4)) + (1 :: int4)))) AS t ("ret") $$; --} -createFunction - :: ( Has sch db schema - , KnownSymbol fun - , SOP.SListI args ) - => QualifiedAlias sch fun -- ^ function alias - -> NP (TypeExpression db) args -- ^ arguments - -> TypeExpression db ret -- ^ return type - -> FunctionDefinition db args ('Returns ret) -- ^ function definition - -> Definition db (Alter sch (Create fun ('Function (args :=> 'Returns ret)) schema) db) -createFunction fun args ret fundef = UnsafeDefinition $ - "CREATE" <+> "FUNCTION" <+> renderSQL fun - <+> parenthesized (renderCommaSeparated renderSQL args) - <+> "RETURNS" <+> renderSQL ret <+> renderSQL fundef <> ";" - -{- | Create or replace a function. -It is not possible to change the name or argument types -or return type of a function this way. - ->>> type Fn = 'Function ( '[ 'Null 'PGint4, 'Null 'PGint4] :=> 'Returns ( 'Null 'PGint4)) ->>> :{ -let - definition :: Definition (Public '["fn" ::: Fn]) (Public '["fn" ::: Fn]) - definition = - createOrReplaceFunction #fn - (int4 *: int4) int4 $ - languageSqlExpr (param @1 @('Null 'PGint4) * param @2 @('Null 'PGint4) + 1) -in printSQL definition -:} -CREATE OR REPLACE FUNCTION "fn" (int4, int4) RETURNS int4 language sql as $$ SELECT * FROM (VALUES (((($1 :: int4) * ($2 :: int4)) + (1 :: int4)))) AS t ("ret") $$; --} -createOrReplaceFunction - :: ( Has sch db schema - , KnownSymbol fun - , SOP.SListI args ) - => QualifiedAlias sch fun -- ^ function alias - -> NP (TypeExpression db) args -- ^ arguments - -> TypeExpression db ret -- ^ return type - -> FunctionDefinition db args ('Returns ret) -- ^ function definition - -> Definition db (Alter sch (CreateOrReplace fun ('Function (args :=> 'Returns ret)) schema) db) -createOrReplaceFunction fun args ret fundef = UnsafeDefinition $ - "CREATE" <+> "OR" <+> "REPLACE" <+> "FUNCTION" <+> renderSQL fun - <+> parenthesized (renderCommaSeparated renderSQL args) - <+> "RETURNS" <+> renderSQL ret <+> renderSQL fundef <> ";" - --- | Use a parameterized `Expression` as a function body -languageSqlExpr - :: Expression 'Ungrouped '[] '[] db args '[] ret - -- ^ function body - -> FunctionDefinition db args ('Returns ret) -languageSqlExpr expr = UnsafeFunctionDefinition $ - "language sql as" - <+> "$$" <+> renderSQL (values_ (expr `as` #ret)) <+> "$$" - --- | Use a parametrized `Query` as a function body -languageSqlQuery - :: Query '[] '[] db args rets - -- ^ function body - -> FunctionDefinition db args ('ReturnsTable rets) -languageSqlQuery qry = UnsafeFunctionDefinition $ - "language sql as" <+> "$$" <+> renderSQL qry <+> "$$" - -{- | Create a set function. - ->>> type Tab = 'Table ('[] :=> '["col" ::: 'NoDef :=> 'Null 'PGint4]) ->>> type Fn = 'Function ('[ 'Null 'PGint4, 'Null 'PGint4] :=> 'ReturnsTable '["ret" ::: 'Null 'PGint4]) ->>> :{ -let - definition :: Definition (Public '["tab" ::: Tab]) (Public '["tab" ::: Tab, "fn" ::: Fn]) - definition = createSetFunction #fn (int4 *: int4) (int4 `as` #ret) $ - languageSqlQuery (select_ ((param @1 * param @2 + #col) `as` #ret) (from (table #tab))) -in printSQL definition -:} -CREATE FUNCTION "fn" (int4, int4) RETURNS TABLE ("ret" int4) language sql as $$ SELECT ((($1 :: int4) * ($2 :: int4)) + "col") AS "ret" FROM "tab" AS "tab" $$; --} -createSetFunction - :: ( Has sch db schema - , KnownSymbol fun - , SOP.SListI args - , SOP.SListI rets ) - => QualifiedAlias sch fun -- ^ function alias - -> NP (TypeExpression db) args -- ^ arguments - -> NP (Aliased (TypeExpression db)) rets -- ^ return type - -> FunctionDefinition db args ('ReturnsTable rets) -- ^ function definition - -> Definition db (Alter sch (Create fun ('Function (args :=> 'ReturnsTable rets)) schema) db) -createSetFunction fun args rets fundef = UnsafeDefinition $ - "CREATE" <+> "FUNCTION" <+> renderSQL fun - <+> parenthesized (renderCommaSeparated renderSQL args) - <+> "RETURNS" <+> "TABLE" - <+> parenthesized (renderCommaSeparated renderRet rets) - <+> renderSQL fundef <> ";" - where - renderRet :: Aliased (TypeExpression s) r -> ByteString - renderRet (ty `As` col) = renderSQL col <+> renderSQL ty - -{- | Create or replace a set function. - ->>> type Tab = 'Table ('[] :=> '["col" ::: 'NoDef :=> 'Null 'PGint4]) ->>> type Fn = 'Function ('[ 'Null 'PGint4, 'Null 'PGint4] :=> 'ReturnsTable '["ret" ::: 'Null 'PGint4]) ->>> :{ -let - definition :: Definition (Public '["tab" ::: Tab, "fn" ::: Fn]) (Public '["tab" ::: Tab, "fn" ::: Fn]) - definition = createOrReplaceSetFunction #fn (int4 *: int4) (int4 `as` #ret) $ - languageSqlQuery (select_ ((param @1 * param @2 + #col) `as` #ret) (from (table #tab))) -in printSQL definition -:} -CREATE OR REPLACE FUNCTION "fn" (int4, int4) RETURNS TABLE ("ret" int4) language sql as $$ SELECT ((($1 :: int4) * ($2 :: int4)) + "col") AS "ret" FROM "tab" AS "tab" $$; --} -createOrReplaceSetFunction - :: ( Has sch db schema - , KnownSymbol fun - , SOP.SListI args - , SOP.SListI rets ) - => QualifiedAlias sch fun -- ^ function alias - -> NP (TypeExpression db) args -- ^ arguments - -> NP (Aliased (TypeExpression db)) rets -- ^ return type - -> FunctionDefinition db args ('ReturnsTable rets) -- ^ function definition - -> Definition db (Alter sch (CreateOrReplace fun ('Function (args :=> 'ReturnsTable rets)) schema) db) -createOrReplaceSetFunction fun args rets fundef = UnsafeDefinition $ - "CREATE" <+> "OR" <+> "REPLACE" <+> "FUNCTION" <+> renderSQL fun - <+> parenthesized (renderCommaSeparated renderSQL args) - <+> "RETURNS" <+> "TABLE" - <+> parenthesized (renderCommaSeparated renderRet rets) - <+> renderSQL fundef <> ";" - where - renderRet :: Aliased (TypeExpression s) r -> ByteString - renderRet (ty `As` col) = renderSQL col <+> renderSQL ty - -{- | Drop a function. - ->>> type Fn = 'Function ( '[ 'Null 'PGint4, 'Null 'PGint4] :=> 'Returns ( 'Null 'PGint4)) ->>> :{ -let - definition :: Definition (Public '["fn" ::: Fn]) (Public '[]) - definition = dropFunction #fn -in printSQL definition -:} -DROP FUNCTION "fn"; --} -dropFunction - :: (Has sch db schema, KnownSymbol fun) - => QualifiedAlias sch fun - -- ^ function alias - -> Definition db (Alter sch (DropSchemum fun 'Function schema) db) -dropFunction fun = UnsafeDefinition $ - "DROP FUNCTION" <+> renderSQL fun <> ";" - -{- | Drop a function. - ->>> type Fn = 'Function ( '[ 'Null 'PGint4, 'Null 'PGint4] :=> 'Returns ( 'Null 'PGint4)) ->>> :{ -let - definition :: Definition (Public '[]) (Public '[]) - definition = dropFunctionIfExists #fn -in printSQL definition -:} -DROP FUNCTION IF EXISTS "fn"; --} -dropFunctionIfExists - :: (Has sch db schema, KnownSymbol fun) - => QualifiedAlias sch fun - -- ^ function alias - -> Definition db (Alter sch (DropSchemumIfExists fun 'Function schema) db) -dropFunctionIfExists fun = UnsafeDefinition $ - "DROP FUNCTION IF EXISTS" <+> renderSQL fun <> ";" - -{- | Body of a user defined function-} -newtype FunctionDefinition db args ret = UnsafeFunctionDefinition - { renderFunctionDefinition :: ByteString } - deriving (Eq,Show,GHC.Generic,NFData) -instance RenderSQL (FunctionDefinition db args ret) where - renderSQL = renderFunctionDefinition diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Index.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Index.hs deleted file mode 100644 index 68d80545..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Index.hs +++ /dev/null @@ -1,186 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Definition.Index -Description: create and drop indexes -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -create and drop indexes --} - -{-# LANGUAGE - AllowAmbiguousTypes - , ConstraintKinds - , DeriveAnyClass - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , LambdaCase - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeInType - , TypeOperators - , UndecidableSuperClasses -#-} - -module Squeal.PostgreSQL.Definition.Index - ( -- * Create - createIndex - , createIndexIfNotExists - -- * Drop - , dropIndex - , dropIndexIfExists - -- * Index Method - , IndexMethod (..) - , btree - , hash - , gist - , spgist - , gin - , brin - ) where - -import Data.ByteString -import GHC.TypeLits - -import qualified GHC.Generics as GHC - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Definition -import Squeal.PostgreSQL.Expression.Sort -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL --- >>> :set -XPolyKinds - -{- | Create an index. - ->>> :{ -type Table = '[] :=> - '[ "a" ::: 'NoDef :=> 'Null 'PGint4 - , "b" ::: 'NoDef :=> 'Null 'PGfloat4 ] -:} - ->>> :{ -let - setup :: Definition (Public '[]) (Public '["tab" ::: 'Table Table, "ix" ::: 'Index 'Btree]) - setup = - createTable #tab (nullable int `as` #a :* nullable real `as` #b) Nil >>> - createIndex #ix #tab btree [#a & AscNullsFirst, #b & AscNullsLast] -in printSQL setup -:} -CREATE TABLE "tab" ("a" int NULL, "b" real NULL); -CREATE INDEX "ix" ON "tab" USING btree (("a") ASC NULLS FIRST, ("b") ASC NULLS LAST); --} -createIndex - :: (Has sch db schema, Has tab schema ('Table table), KnownSymbol ix) - => Alias ix -- ^ index alias - -> QualifiedAlias sch tab -- ^ table alias - -> IndexMethod method -- ^ index method - -> [SortExpression 'Ungrouped '[] '[] db '[] '[tab ::: TableToRow table]] - -- ^ sorted columns - -> Definition db (Alter sch (Create ix ('Index method) schema) db) -createIndex ix tab method cols = UnsafeDefinition $ - "CREATE" <+> "INDEX" <+> renderSQL ix <+> "ON" <+> renderSQL tab - <+> "USING" <+> renderSQL method - <+> parenthesized (commaSeparated (renderIndex <$> cols)) - <> ";" - where - renderIndex = \case - Asc expression -> parenthesized (renderSQL expression) <+> "ASC" - Desc expression -> parenthesized (renderSQL expression) <+> "DESC" - AscNullsFirst expression -> parenthesized (renderSQL expression) - <+> "ASC NULLS FIRST" - DescNullsFirst expression -> parenthesized (renderSQL expression) - <+> "DESC NULLS FIRST" - AscNullsLast expression -> parenthesized (renderSQL expression) - <+> "ASC NULLS LAST" - DescNullsLast expression -> parenthesized (renderSQL expression) - <+> "DESC NULLS LAST" - --- | Create an index if it doesn't exist. -createIndexIfNotExists - :: (Has sch db schema, Has tab schema ('Table table), KnownSymbol ix) - => Alias ix -- ^ index alias - -> QualifiedAlias sch tab -- ^ table alias - -> IndexMethod method -- ^ index method - -> [SortExpression 'Ungrouped '[] '[] db '[] '[tab ::: TableToRow table]] - -- ^ sorted columns - -> Definition db (Alter sch (CreateIfNotExists ix ('Index method) schema) db) -createIndexIfNotExists ix tab method cols = UnsafeDefinition $ - "CREATE INDEX IF NOT EXISTS" <+> renderSQL ix <+> "ON" <+> renderSQL tab - <+> "USING" <+> renderSQL method - <+> parenthesized (commaSeparated (renderIndex <$> cols)) - <> ";" - where - renderIndex = \case - Asc expression -> parenthesized (renderSQL expression) <+> "ASC" - Desc expression -> parenthesized (renderSQL expression) <+> "DESC" - AscNullsFirst expression -> parenthesized (renderSQL expression) - <+> "ASC NULLS FIRST" - DescNullsFirst expression -> parenthesized (renderSQL expression) - <+> "DESC NULLS FIRST" - AscNullsLast expression -> parenthesized (renderSQL expression) - <+> "ASC NULLS LAST" - DescNullsLast expression -> parenthesized (renderSQL expression) - <+> "DESC NULLS LAST" - -{- | -PostgreSQL provides several index types: -B-tree, Hash, GiST, SP-GiST, GIN and BRIN. -Each index type uses a different algorithm -that is best suited to different types of queries. --} -newtype IndexMethod ty = UnsafeIndexMethod {renderIndexMethod :: ByteString} - deriving stock (Eq, Ord, Show, GHC.Generic) -instance RenderSQL (IndexMethod ty) where renderSQL = renderIndexMethod --- | B-trees can handle equality and range queries on data --- that can be sorted into some ordering. -btree :: IndexMethod 'Btree -btree = UnsafeIndexMethod "btree" --- | Hash indexes can only handle simple equality comparisons. -hash :: IndexMethod 'Hash -hash = UnsafeIndexMethod "hash" --- | GiST indexes are not a single kind of index, --- but rather an infrastructure within which many different --- indexing strategies can be implemented. -gist :: IndexMethod 'Gist -gist = UnsafeIndexMethod "gist" --- | SP-GiST indexes, like GiST indexes, --- offer an infrastructure that supports various kinds of searches. -spgist :: IndexMethod 'Spgist -spgist = UnsafeIndexMethod "spgist" --- | GIN indexes are “inverted indexes” which are appropriate for --- data values that contain multiple component values, such as arrays. -gin :: IndexMethod 'Gin -gin = UnsafeIndexMethod "gin" --- | BRIN indexes (a shorthand for Block Range INdexes) store summaries --- about the values stored in consecutive physical block ranges of a table. -brin :: IndexMethod 'Brin -brin = UnsafeIndexMethod "brin" - --- | Drop an index. --- --- >>> printSQL (dropIndex #ix :: Definition (Public '["ix" ::: 'Index 'Btree]) (Public '[])) --- DROP INDEX "ix"; -dropIndex - :: (Has sch db schema, KnownSymbol ix) - => QualifiedAlias sch ix -- ^ index alias - -> Definition db (Alter sch (DropSchemum ix 'Index schema) db) -dropIndex ix = UnsafeDefinition $ "DROP INDEX" <+> renderSQL ix <> ";" - --- | Drop an index if it exists. -dropIndexIfExists - :: (Has sch db schema, KnownSymbol ix) - => QualifiedAlias sch ix -- ^ index alias - -> Definition db (Alter sch (DropSchemumIfExists ix 'Index schema) db) -dropIndexIfExists ix = UnsafeDefinition $ - "DROP INDEX IF EXISTS" <+> renderSQL ix <> ";" diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Procedure.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Procedure.hs deleted file mode 100644 index a1d12bb0..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Procedure.hs +++ /dev/null @@ -1,171 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Definition.Procedure -Description: create and drop procedures -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -create and drop procedures --} - -{-# LANGUAGE - AllowAmbiguousTypes - , ConstraintKinds - , DeriveAnyClass - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , LambdaCase - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeInType - , TypeOperators - , UndecidableSuperClasses -#-} - -module Squeal.PostgreSQL.Definition.Procedure - ( -- * Create - createProcedure - , createOrReplaceProcedure - -- * Drop - , dropProcedure - , dropProcedureIfExists - -- * Procedure Definition - , ProcedureDefinition(..) - , languageSqlManipulation - ) where - -import Control.DeepSeq -import Data.ByteString -import GHC.TypeLits - -import qualified Generics.SOP as SOP -import qualified GHC.Generics as GHC - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Definition -import Squeal.PostgreSQL.Expression.Type -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Manipulation -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - -{- | Create a procedure. - ->>> type Proc = 'Procedure '[ 'NotNull 'PGint4 ] ->>> type Thing = 'Table ('[] :=> '[ "id" ::: 'NoDef :=> 'NotNull 'PGint4 ]) ->>> :{ -let - definition :: Definition (Public '["things" ::: Thing ]) (Public '["things" ::: Thing, "proc" ::: Proc]) - definition = createProcedure #proc (one int4) - . languageSqlManipulation - $ [deleteFrom_ #things (#id .== param @1)] -in printSQL definition -:} -CREATE PROCEDURE "proc" (int4) language sql as $$ DELETE FROM "things" AS "things" WHERE ("id" = ($1 :: int4)); $$; --} -createProcedure - :: ( Has sch db schema - , KnownSymbol pro - , SOP.SListI args ) - => QualifiedAlias sch pro -- ^ procedure alias - -> NP (TypeExpression db) args -- ^ arguments - -> ProcedureDefinition db args -- ^ procedure definition - -> Definition db (Alter sch (Create pro ('Procedure args) schema) db) -createProcedure pro args prodef = UnsafeDefinition $ - "CREATE" <+> "PROCEDURE" <+> renderSQL pro - <+> parenthesized (renderCommaSeparated renderSQL args) - <+> renderSQL prodef <> ";" - -{- | Create or replace a procedure. -It is not possible to change the name or argument types -of a procedure this way. - ->>> type Proc = 'Procedure '[ 'NotNull 'PGint4 ] ->>> type Thing = 'Table ('[] :=> '[ "id" ::: 'NoDef :=> 'NotNull 'PGint4 ]) ->>> :{ -let - definition :: Definition (Public '["things" ::: Thing ]) (Public '["things" ::: Thing, "proc" ::: Proc]) - definition = createOrReplaceProcedure #proc (one int4) - . languageSqlManipulation - $ [deleteFrom_ #things (#id .== param @1)] -in printSQL definition -:} -CREATE OR REPLACE PROCEDURE "proc" (int4) language sql as $$ DELETE FROM "things" AS "things" WHERE ("id" = ($1 :: int4)); $$; --} -createOrReplaceProcedure - :: ( Has sch db schema - , KnownSymbol pro - , SOP.SListI args ) - => QualifiedAlias sch pro -- ^ procedure alias - -> NP (TypeExpression db) args -- ^ arguments - -> ProcedureDefinition db args -- ^ procedure definition - -> Definition db (Alter sch (CreateOrReplace pro ('Procedure args) schema) db) -createOrReplaceProcedure pro args prodef = UnsafeDefinition $ - "CREATE" <+> "OR" <+> "REPLACE" <+> "PROCEDURE" <+> renderSQL pro - <+> parenthesized (renderCommaSeparated renderSQL args) - <+> renderSQL prodef <> ";" - --- | Use a parameterized `Manipulation` as a procedure body -languageSqlManipulation - :: [Manipulation '[] db args '[]] - -- ^ procedure body - -> ProcedureDefinition db args -languageSqlManipulation mnps = UnsafeProcedureDefinition $ - "language sql as" <+> "$$" <+> Prelude.foldr (<+>) "" (Prelude.map ((<> ";") . renderSQL) mnps) <> "$$" - --- | - -{- | Drop a procedure. - ->>> type Proc = 'Procedure '[ 'Null 'PGint4, 'Null 'PGint4] ->>> :{ -let - definition :: Definition (Public '["proc" ::: Proc]) (Public '[]) - definition = dropProcedure #proc -in printSQL definition -:} -DROP PROCEDURE "proc"; --} -dropProcedure - :: (Has sch db schema, KnownSymbol pro) - => QualifiedAlias sch pro - -- ^ procedure alias - -> Definition db (Alter sch (DropSchemum pro 'Procedure schema) db) -dropProcedure pro = UnsafeDefinition $ - "DROP PROCEDURE" <+> renderSQL pro <> ";" - -{- | Drop a procedure. - ->>> type Proc = 'Procedure '[ 'Null 'PGint4, 'Null 'PGint4 ] ->>> :{ -let - definition :: Definition (Public '[]) (Public '[]) - definition = dropProcedureIfExists #proc -in printSQL definition -:} -DROP PROCEDURE IF EXISTS "proc"; --} -dropProcedureIfExists - :: (Has sch db schema, KnownSymbol pro) - => QualifiedAlias sch pro - -- ^ procedure alias - -> Definition db (Alter sch (DropSchemumIfExists pro 'Procedure schema) db) -dropProcedureIfExists pro = UnsafeDefinition $ - "DROP PROCEDURE IF EXISTS" <+> renderSQL pro <> ";" - -{- | Body of a user defined procedure-} -newtype ProcedureDefinition db args = UnsafeProcedureDefinition - { renderProcedureDefinition :: ByteString } - deriving (Eq,Show,GHC.Generic,NFData) -instance RenderSQL (ProcedureDefinition db args) where - renderSQL = renderProcedureDefinition diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Schema.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Schema.hs deleted file mode 100644 index b04ff422..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Schema.hs +++ /dev/null @@ -1,113 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Definition.Schema -Description: create and drop schemas -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -create and drop schemas --} - -{-# LANGUAGE - AllowAmbiguousTypes - , ConstraintKinds - , DeriveAnyClass - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , LambdaCase - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeInType - , TypeOperators - , UndecidableSuperClasses -#-} - -module Squeal.PostgreSQL.Definition.Schema - ( -- * Create - createSchema - , createSchemaIfNotExists - -- * Drop - , dropSchemaCascade - , dropSchemaCascadeIfExists - ) where - -import GHC.TypeLits - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Definition -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - -{- | -`createSchema` enters a new schema into the current database. -The schema name must be distinct from the name of any existing schema -in the current database. - -A schema is essentially a namespace: it contains named objects -(tables, data types, functions, and operators) whose names -can duplicate those of other objects existing in other schemas. -Named objects are accessed by `QualifiedAlias`es with the schema -name as a prefix. - ->>> :{ -let - definition :: Definition '["public" ::: '[]] '["public" ::: '[], "my_schema" ::: '[]] - definition = createSchema #my_schema -in printSQL definition -:} -CREATE SCHEMA "my_schema"; --} -createSchema - :: KnownSymbol sch - => Alias sch -- ^ schema alias - -> Definition db (Create sch '[] db) -createSchema sch = UnsafeDefinition $ - "CREATE" <+> "SCHEMA" <+> renderSQL sch <> ";" - -{- | Create a schema if it does not yet exist.-} -createSchemaIfNotExists - :: (KnownSymbol sch, Has sch db schema) - => Alias sch -- ^ schema alias - -> Definition db (CreateIfNotExists sch '[] db) -createSchemaIfNotExists sch = UnsafeDefinition $ - "CREATE" <+> "SCHEMA" <+> "IF" <+> "NOT" <+> "EXISTS" - <+> renderSQL sch <> ";" - --- | Drop a schema. --- Automatically drop objects (tables, functions, etc.) --- that are contained in the schema. --- --- >>> :{ --- let --- definition :: Definition '["muh_schema" ::: schema, "public" ::: public] '["public" ::: public] --- definition = dropSchemaCascade #muh_schema --- :} --- --- >>> printSQL definition --- DROP SCHEMA "muh_schema" CASCADE; -dropSchemaCascade - :: KnownSymbol sch - => Alias sch -- ^ schema alias - -> Definition db (Drop sch db) -dropSchemaCascade sch = UnsafeDefinition $ - "DROP SCHEMA" <+> renderSQL sch <+> "CASCADE;" - --- | Drop a schema if it exists. --- Automatically drop objects (tables, functions, etc.) --- that are contained in the schema. -dropSchemaCascadeIfExists - :: KnownSymbol sch - => Alias sch -- ^ schema alias - -> Definition db (DropIfExists sch db) -dropSchemaCascadeIfExists sch = UnsafeDefinition $ - "DROP SCHEMA IF EXISTS" <+> renderSQL sch <+> "CASCADE;" diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Table.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Table.hs deleted file mode 100644 index f18fd1a9..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Table.hs +++ /dev/null @@ -1,536 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Definition.Table -Description: create, drop and alter tables -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -create, drop and alter tables --} - -{-# LANGUAGE - AllowAmbiguousTypes - , ConstraintKinds - , DeriveAnyClass - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , LambdaCase - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeInType - , TypeOperators - , UndecidableSuperClasses -#-} - -module Squeal.PostgreSQL.Definition.Table - ( -- * Create - createTable - , createTableIfNotExists - -- * Drop - , dropTable - , dropTableIfExists - -- * Alter - , alterTable - , alterTableIfExists - , alterTableRename - , alterTableIfExistsRename - , alterTableSetSchema - , AlterTable (..) - -- ** Constraints - , addConstraint - , dropConstraint - -- ** Columns - , AddColumn (..) - , dropColumn - , renameColumn - , alterColumn - , AlterColumn (..) - , setDefault - , dropDefault - , setNotNull - , dropNotNull - , alterType - ) where - -import Control.DeepSeq -import Data.ByteString -import GHC.TypeLits - -import qualified Generics.SOP as SOP -import qualified GHC.Generics as GHC - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Definition -import Squeal.PostgreSQL.Definition.Constraint -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Expression.Type -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - -{- | `createTable` adds a table to the schema. - ->>> :set -XOverloadedLabels ->>> :{ -type Table = '[] :=> - '[ "a" ::: 'NoDef :=> 'Null 'PGint4 - , "b" ::: 'NoDef :=> 'Null 'PGfloat4 ] -:} - ->>> :{ -let - setup :: Definition (Public '[]) (Public '["tab" ::: 'Table Table]) - setup = createTable #tab - (nullable int `as` #a :* nullable real `as` #b) Nil -in printSQL setup -:} -CREATE TABLE "tab" ("a" int NULL, "b" real NULL); --} -createTable - :: ( KnownSymbol sch - , KnownSymbol tab - , columns ~ (col ': cols) - , SOP.SListI columns - , SOP.SListI constraints - , Has sch db0 schema0 - , db1 ~ Alter sch (Create tab ('Table (constraints :=> columns)) schema0) db0 ) - => QualifiedAlias sch tab -- ^ the name of the table to add - -> NP (Aliased (ColumnTypeExpression db0)) columns - -- ^ the names and datatype of each column - -> NP (Aliased (TableConstraintExpression sch tab db1)) constraints - -- ^ constraints that must hold for the table - -> Definition db0 db1 -createTable tab columns constraints = UnsafeDefinition $ - "CREATE TABLE" <+> renderCreation tab columns constraints - -{-| `createTableIfNotExists` creates a table if it doesn't exist, but does not add it to the schema. -Instead, the schema already has the table so if the table did not yet exist, the schema was wrong. -`createTableIfNotExists` fixes this. Interestingly, this property makes it an idempotent in -the `Control.Category.Category` of `Definition`s. - ->>> :set -XOverloadedLabels -XTypeApplications ->>> :{ -type Table = '[] :=> - '[ "a" ::: 'NoDef :=> 'Null 'PGint4 - , "b" ::: 'NoDef :=> 'Null 'PGfloat4 ] -:} - ->>> type Schemas = Public '["tab" ::: 'Table Table] - ->>> :{ -let - setup :: Definition Schemas Schemas - setup = createTableIfNotExists #tab - (nullable int `as` #a :* nullable real `as` #b) Nil -in printSQL setup -:} -CREATE TABLE IF NOT EXISTS "tab" ("a" int NULL, "b" real NULL); --} -createTableIfNotExists - :: ( KnownSymbol sch - , KnownSymbol tab - , columns ~ (col ': cols) - , SOP.SListI columns - , SOP.SListI constraints - , Has sch db0 schema0 - , db1 ~ Alter sch (CreateIfNotExists tab ('Table (constraints :=> columns)) schema0) db0 ) - => QualifiedAlias sch tab -- ^ the name of the table to add - -> NP (Aliased (ColumnTypeExpression db0)) columns - -- ^ the names and datatype of each column - -> NP (Aliased (TableConstraintExpression sch tab db1)) constraints - -- ^ constraints that must hold for the table - -> Definition db0 db1 -createTableIfNotExists tab columns constraints = UnsafeDefinition $ - "CREATE TABLE IF NOT EXISTS" - <+> renderCreation tab columns constraints - --- helper function for `createTable` and `createTableIfNotExists` -renderCreation - :: ( KnownSymbol sch - , KnownSymbol tab - , SOP.SListI columns - , SOP.SListI constraints ) - => QualifiedAlias sch tab -- ^ the name of the table to add - -> NP (Aliased (ColumnTypeExpression db0)) columns - -- ^ the names and datatype of each column - -> NP (Aliased (TableConstraintExpression sch tab db1)) constraints - -- ^ constraints that must hold for the table - -> ByteString -renderCreation tab columns constraints = renderSQL tab - <+> parenthesized - ( renderCommaSeparated renderColumnDef columns - <> ( case constraints of - Nil -> "" - _ -> ", " <> - renderCommaSeparated renderConstraint constraints ) ) - <> ";" - where - renderColumnDef :: Aliased (ColumnTypeExpression db) x -> ByteString - renderColumnDef (ty `As` column) = - renderSQL column <+> renderColumnTypeExpression ty - renderConstraint - :: Aliased (TableConstraintExpression sch tab db) constraint - -> ByteString - renderConstraint (constraint `As` alias) = - "CONSTRAINT" <+> renderSQL alias <+> renderSQL constraint - --- | `dropTable` removes a table from the schema. --- --- >>> :{ --- let --- definition :: Definition '["public" ::: '["muh_table" ::: 'Table t]] (Public '[]) --- definition = dropTable #muh_table --- :} --- --- >>> printSQL definition --- DROP TABLE "muh_table"; -dropTable - :: ( Has sch db schema - , KnownSymbol tab ) - => QualifiedAlias sch tab -- ^ table to remove - -> Definition db (Alter sch (DropSchemum tab 'Table schema) db) -dropTable tab = UnsafeDefinition $ "DROP TABLE" <+> renderSQL tab <> ";" - --- | Drop a table if it exists. -dropTableIfExists - :: ( Has sch db schema - , KnownSymbol tab) - => QualifiedAlias sch tab -- ^ table to remove - -> Definition db (Alter sch (DropSchemumIfExists tab 'Table schema) db) -dropTableIfExists tab = UnsafeDefinition $ - "DROP TABLE IF EXISTS" <+> renderSQL tab <> ";" - --- | `alterTable` changes the definition of a table from the schema. -alterTable - :: (Has sch db schema, KnownSymbol tab) - => QualifiedAlias sch tab -- ^ table to alter - -> AlterTable sch tab db table -- ^ alteration to perform - -> Definition db (Alter sch (Alter tab ('Table table) schema) db) -alterTable tab alteration = UnsafeDefinition $ - "ALTER TABLE" - <+> renderSQL tab - <+> renderAlterTable alteration - <> ";" - --- | `alterTable` changes the definition of a table from the schema. -alterTableIfExists - :: (Has sch db schema, KnownSymbol tab) - => QualifiedAlias sch tab -- ^ table to alter - -> AlterTable sch tab db table -- ^ alteration to perform - -> Definition db (Alter sch (AlterIfExists tab ('Table table) schema) db) -alterTableIfExists tab alteration = UnsafeDefinition $ - "ALTER TABLE IF EXISTS" - <+> renderSQL tab - <+> renderAlterTable alteration - <> ";" - --- | `alterTableRename` changes the name of a table from the schema. --- --- >>> type Schemas = '[ "public" ::: '[ "foo" ::: 'Table ('[] :=> '[]) ] ] --- >>> :{ --- let migration :: Definition Schemas '["public" ::: '["bar" ::: 'Table ('[] :=> '[]) ] ] --- migration = alterTableRename #foo #bar --- in printSQL migration --- :} --- ALTER TABLE "foo" RENAME TO "bar"; -alterTableRename - :: ( Has sch db schema - , KnownSymbol tab1 - , Has tab0 schema ('Table table)) - => QualifiedAlias sch tab0 -- ^ table to rename - -> Alias tab1 -- ^ what to rename it - -> Definition db (Alter sch (Rename tab0 tab1 schema) db ) -alterTableRename tab0 tab1 = UnsafeDefinition $ - "ALTER TABLE" <+> renderSQL tab0 - <+> "RENAME TO" <+> renderSQL tab1 <> ";" - --- | `alterTableIfExistsRename` changes the name of a table from the schema if it exists. --- --- >>> type Schemas = '[ "public" ::: '[ "foo" ::: 'Table ('[] :=> '[]) ] ] --- >>> :{ --- let migration :: Definition Schemas Schemas --- migration = alterTableIfExistsRename #goo #gar --- in printSQL migration --- :} --- ALTER TABLE IF EXISTS "goo" RENAME TO "gar"; -alterTableIfExistsRename - :: ( Has sch db schema - , KnownSymbol tab0 - , KnownSymbol tab1 ) - => QualifiedAlias sch tab0 -- ^ table to rename - -> Alias tab1 -- ^ what to rename it - -> Definition db (Alter sch (RenameIfExists tab0 tab1 schema) db ) -alterTableIfExistsRename tab0 tab1 = UnsafeDefinition $ - "ALTER TABLE IF EXISTS" <+> renderSQL tab0 - <+> "RENAME TO" <+> renderSQL tab1 <> ";" - -{- | This form moves the table into another schema. - ->>> type DB0 = '[ "sch0" ::: '[ "tab" ::: 'Table ('[] :=> '[]) ], "sch1" ::: '[] ] ->>> type DB1 = '[ "sch0" ::: '[], "sch1" ::: '[ "tab" ::: 'Table ('[] :=> '[]) ] ] ->>> :{ -let def :: Definition DB0 DB1 - def = alterTableSetSchema (#sch0 ! #tab) #sch1 -in printSQL def -:} -ALTER TABLE "sch0"."tab" SET SCHEMA "sch1"; --} -alterTableSetSchema - :: ( Has sch0 db schema0 - , Has tab schema0 ('Table table) - , Has sch1 db schema1 ) - => QualifiedAlias sch0 tab -- ^ table to move - -> Alias sch1 -- ^ where to move it - -> Definition db (SetSchema sch0 sch1 schema0 schema1 tab 'Table table db) -alterTableSetSchema tab sch = UnsafeDefinition $ - "ALTER TABLE" <+> renderSQL tab <+> "SET SCHEMA" <+> renderSQL sch <> ";" - --- | An `AlterTable` describes the alteration to perform on the columns --- of a table. -newtype AlterTable - (sch :: Symbol) - (tab :: Symbol) - (db :: SchemasType) - (table :: TableType) = - UnsafeAlterTable {renderAlterTable :: ByteString} - deriving (GHC.Generic,Show,Eq,Ord,NFData) - --- | An `addConstraint` adds a table constraint. --- --- >>> :{ --- let --- definition :: Definition --- '["public" ::: '["tab" ::: 'Table ('[] :=> '["col" ::: 'NoDef :=> 'NotNull 'PGint4])]] --- '["public" ::: '["tab" ::: 'Table ('["positive" ::: 'Check '["col"]] :=> '["col" ::: 'NoDef :=> 'NotNull 'PGint4])]] --- definition = alterTable #tab (addConstraint #positive (check #col (#col .> 0))) --- in printSQL definition --- :} --- ALTER TABLE "tab" ADD CONSTRAINT "positive" CHECK (("col" > (0 :: int4))); -addConstraint - :: ( KnownSymbol alias - , Has sch db schema - , Has tab schema ('Table (constraints :=> columns)) ) - => Alias alias - -> TableConstraintExpression sch tab db constraint - -- ^ constraint to add - -> AlterTable sch tab db (Create alias constraint constraints :=> columns) -addConstraint alias constraint = UnsafeAlterTable $ - "ADD" <+> "CONSTRAINT" <+> renderSQL alias - <+> renderSQL constraint - --- | A `dropConstraint` drops a table constraint. --- --- >>> :{ --- let --- definition :: Definition --- '["public" ::: '["tab" ::: 'Table ('["positive" ::: Check '["col"]] :=> '["col" ::: 'NoDef :=> 'NotNull 'PGint4])]] --- '["public" ::: '["tab" ::: 'Table ('[] :=> '["col" ::: 'NoDef :=> 'NotNull 'PGint4])]] --- definition = alterTable #tab (dropConstraint #positive) --- in printSQL definition --- :} --- ALTER TABLE "tab" DROP CONSTRAINT "positive"; -dropConstraint - :: ( KnownSymbol constraint - , Has sch db schema - , Has tab schema ('Table (constraints :=> columns)) ) - => Alias constraint - -- ^ constraint to drop - -> AlterTable sch tab db (Drop constraint constraints :=> columns) -dropConstraint constraint = UnsafeAlterTable $ - "DROP" <+> "CONSTRAINT" <+> renderSQL constraint - --- | An `AddColumn` is either @NULL@ or has @DEFAULT@. -class AddColumn ty where - -- | `addColumn` adds a new column, initially filled with whatever - -- default value is given or with @NULL@. - -- - -- >>> :{ - -- let - -- definition :: Definition - -- '["public" ::: '["tab" ::: 'Table ('[] :=> '["col1" ::: 'NoDef :=> 'Null 'PGint4])]] - -- '["public" ::: '["tab" ::: 'Table ('[] :=> - -- '[ "col1" ::: 'NoDef :=> 'Null 'PGint4 - -- , "col2" ::: 'Def :=> 'Null 'PGtext ])]] - -- definition = alterTable #tab (addColumn #col2 (text & nullable & default_ "foo")) - -- in printSQL definition - -- :} - -- ALTER TABLE "tab" ADD COLUMN "col2" text NULL DEFAULT (E'foo' :: text); - -- - -- >>> :{ - -- let - -- definition :: Definition - -- '["public" ::: '["tab" ::: 'Table ('[] :=> '["col1" ::: 'NoDef :=> 'Null 'PGint4])]] - -- '["public" ::: '["tab" ::: 'Table ('[] :=> - -- '[ "col1" ::: 'NoDef :=> 'Null 'PGint4 - -- , "col2" ::: 'NoDef :=> 'Null 'PGtext ])]] - -- definition = alterTable #tab (addColumn #col2 (text & nullable)) - -- in printSQL definition - -- :} - -- ALTER TABLE "tab" ADD COLUMN "col2" text NULL; - addColumn - :: ( KnownSymbol column - , Has sch db schema - , Has tab schema ('Table (constraints :=> columns)) ) - => Alias column -- ^ column to add - -> ColumnTypeExpression db ty -- ^ type of the new column - -> AlterTable sch tab db (constraints :=> Create column ty columns) - addColumn column ty = UnsafeAlterTable $ - "ADD COLUMN" <+> renderSQL column <+> renderColumnTypeExpression ty -instance {-# OVERLAPPING #-} AddColumn ('Def :=> ty) -instance {-# OVERLAPPABLE #-} AddColumn ('NoDef :=> 'Null ty) - --- | A `dropColumn` removes a column. Whatever data was in the column --- disappears. Table constraints involving the column are dropped, too. --- However, if the column is referenced by a foreign key constraint of --- another table, PostgreSQL will not silently drop that constraint. --- --- >>> :{ --- let --- definition :: Definition --- '["public" ::: '["tab" ::: 'Table ('[] :=> --- '[ "col1" ::: 'NoDef :=> 'Null 'PGint4 --- , "col2" ::: 'NoDef :=> 'Null 'PGtext ])]] --- '["public" ::: '["tab" ::: 'Table ('[] :=> '["col1" ::: 'NoDef :=> 'Null 'PGint4])]] --- definition = alterTable #tab (dropColumn #col2) --- in printSQL definition --- :} --- ALTER TABLE "tab" DROP COLUMN "col2"; -dropColumn - :: ( KnownSymbol column - , Has sch db schema - , Has tab schema ('Table (constraints :=> columns)) ) - => Alias column -- ^ column to remove - -> AlterTable sch tab db (constraints :=> Drop column columns) -dropColumn column = UnsafeAlterTable $ - "DROP COLUMN" <+> renderSQL column - --- | A `renameColumn` renames a column. --- --- >>> :{ --- let --- definition :: Definition --- '["public" ::: '["tab" ::: 'Table ('[] :=> '["foo" ::: 'NoDef :=> 'Null 'PGint4])]] --- '["public" ::: '["tab" ::: 'Table ('[] :=> '["bar" ::: 'NoDef :=> 'Null 'PGint4])]] --- definition = alterTable #tab (renameColumn #foo #bar) --- in printSQL definition --- :} --- ALTER TABLE "tab" RENAME COLUMN "foo" TO "bar"; -renameColumn - :: ( KnownSymbol column0 - , KnownSymbol column1 - , Has sch db schema - , Has tab schema ('Table (constraints :=> columns)) ) - => Alias column0 -- ^ column to rename - -> Alias column1 -- ^ what to rename the column - -> AlterTable sch tab db (constraints :=> Rename column0 column1 columns) -renameColumn column0 column1 = UnsafeAlterTable $ - "RENAME COLUMN" <+> renderSQL column0 <+> "TO" <+> renderSQL column1 - --- | An `alterColumn` alters a single column. -alterColumn - :: ( KnownSymbol column - , Has sch db schema - , Has tab schema ('Table (constraints :=> columns)) - , Has column columns ty0 ) - => Alias column -- ^ column to alter - -> AlterColumn db ty0 ty1 -- ^ alteration to perform - -> AlterTable sch tab db (constraints :=> Alter column ty1 columns) -alterColumn column alteration = UnsafeAlterTable $ - "ALTER COLUMN" <+> renderSQL column <+> renderAlterColumn alteration - --- | An `AlterColumn` describes the alteration to perform on a single column. -newtype AlterColumn (db :: SchemasType) (ty0 :: ColumnType) (ty1 :: ColumnType) = - UnsafeAlterColumn {renderAlterColumn :: ByteString} - deriving (GHC.Generic,Show,Eq,Ord,NFData) - --- | A `setDefault` sets a new default for a column. Note that this doesn't --- affect any existing rows in the table, it just changes the default for --- future insert and update commands. --- --- >>> :{ --- let --- definition :: Definition --- '["public" ::: '["tab" ::: 'Table ('[] :=> '["col" ::: 'NoDef :=> 'Null 'PGint4])]] --- '["public" ::: '["tab" ::: 'Table ('[] :=> '["col" ::: 'Def :=> 'Null 'PGint4])]] --- definition = alterTable #tab (alterColumn #col (setDefault 5)) --- in printSQL definition --- :} --- ALTER TABLE "tab" ALTER COLUMN "col" SET DEFAULT (5 :: int4); -setDefault - :: Expression 'Ungrouped '[] '[] db '[] '[] ty -- ^ default value to set - -> AlterColumn db (constraint :=> ty) ('Def :=> ty) -setDefault expression = UnsafeAlterColumn $ - "SET DEFAULT" <+> renderExpression expression - --- | A `dropDefault` removes any default value for a column. --- --- >>> :{ --- let --- definition :: Definition --- '["public" ::: '["tab" ::: 'Table ('[] :=> '["col" ::: 'Def :=> 'Null 'PGint4])]] --- '["public" ::: '["tab" ::: 'Table ('[] :=> '["col" ::: 'NoDef :=> 'Null 'PGint4])]] --- definition = alterTable #tab (alterColumn #col dropDefault) --- in printSQL definition --- :} --- ALTER TABLE "tab" ALTER COLUMN "col" DROP DEFAULT; -dropDefault :: AlterColumn db ('Def :=> ty) ('NoDef :=> ty) -dropDefault = UnsafeAlterColumn $ "DROP DEFAULT" - --- | A `setNotNull` adds a @NOT NULL@ constraint to a column. --- The constraint will be checked immediately, so the table data must satisfy --- the constraint before it can be added. --- --- >>> :{ --- let --- definition :: Definition --- '["public" ::: '["tab" ::: 'Table ('[] :=> '["col" ::: 'NoDef :=> 'Null 'PGint4])]] --- '["public" ::: '["tab" ::: 'Table ('[] :=> '["col" ::: 'NoDef :=> 'NotNull 'PGint4])]] --- definition = alterTable #tab (alterColumn #col setNotNull) --- in printSQL definition --- :} --- ALTER TABLE "tab" ALTER COLUMN "col" SET NOT NULL; -setNotNull - :: AlterColumn db (constraint :=> 'Null ty) (constraint :=> 'NotNull ty) -setNotNull = UnsafeAlterColumn $ "SET NOT NULL" - --- | A `dropNotNull` drops a @NOT NULL@ constraint from a column. --- --- >>> :{ --- let --- definition :: Definition --- '["public" ::: '["tab" ::: 'Table ('[] :=> '["col" ::: 'NoDef :=> 'NotNull 'PGint4])]] --- '["public" ::: '["tab" ::: 'Table ('[] :=> '["col" ::: 'NoDef :=> 'Null 'PGint4])]] --- definition = alterTable #tab (alterColumn #col dropNotNull) --- in printSQL definition --- :} --- ALTER TABLE "tab" ALTER COLUMN "col" DROP NOT NULL; -dropNotNull - :: AlterColumn db (constraint :=> 'NotNull ty) (constraint :=> 'Null ty) -dropNotNull = UnsafeAlterColumn $ "DROP NOT NULL" - --- | An `alterType` converts a column to a different data type. --- This will succeed only if each existing entry in the column can be --- converted to the new type by an implicit cast. --- --- >>> :{ --- let --- definition :: Definition --- '["public" ::: '["tab" ::: 'Table ('[] :=> '["col" ::: 'NoDef :=> 'NotNull 'PGint4])]] --- '["public" ::: '["tab" ::: 'Table ('[] :=> '["col" ::: 'NoDef :=> 'NotNull 'PGnumeric])]] --- definition = --- alterTable #tab (alterColumn #col (alterType (numeric & notNullable))) --- in printSQL definition --- :} --- ALTER TABLE "tab" ALTER COLUMN "col" TYPE numeric NOT NULL; -alterType :: ColumnTypeExpression db ty -> AlterColumn db ty0 ty -alterType ty = UnsafeAlterColumn $ "TYPE" <+> renderColumnTypeExpression ty diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Type.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Type.hs deleted file mode 100644 index ee29254d..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Definition/Type.hs +++ /dev/null @@ -1,291 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Definition.Type -Description: create and drop types -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -create and drop types --} - -{-# LANGUAGE - AllowAmbiguousTypes - , ConstraintKinds - , DeriveAnyClass - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , LambdaCase - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeInType - , TypeOperators - , UndecidableSuperClasses -#-} - -module Squeal.PostgreSQL.Definition.Type - ( -- * Create - createTypeEnum - , createTypeEnumFrom - , createTypeComposite - , createTypeCompositeFrom - , createTypeRange - , createDomain - -- * Drop - , dropType - , dropTypeIfExists - -- * Alter - , alterTypeRename - , alterTypeSetSchema - ) where - -import Data.ByteString -import Data.Monoid -import GHC.TypeLits -import Prelude hiding ((.), id) - -import qualified Generics.SOP as SOP - -import Squeal.PostgreSQL.Expression.Logic -import Squeal.PostgreSQL.Expression.Type -import Squeal.PostgreSQL.Definition -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Type.PG -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL --- >>> import qualified GHC.Generics as GHC --- >>> import qualified Generics.SOP as SOP - --- | Enumerated types are created using the `createTypeEnum` command, for example --- --- >>> printSQL $ (createTypeEnum #mood (label @"sad" :* label @"ok" :* label @"happy") :: Definition (Public '[]) '["public" ::: '["mood" ::: 'Typedef ('PGenum '["sad","ok","happy"])]]) --- CREATE TYPE "mood" AS ENUM ('sad', 'ok', 'happy'); -createTypeEnum - :: (KnownSymbol enum, Has sch db schema, SOP.All KnownSymbol labels) - => QualifiedAlias sch enum - -- ^ name of the user defined enumerated type - -> NP PGlabel labels - -- ^ labels of the enumerated type - -> Definition db (Alter sch (Create enum ('Typedef ('PGenum labels)) schema) db) -createTypeEnum enum labels = UnsafeDefinition $ - "CREATE" <+> "TYPE" <+> renderSQL enum <+> "AS" <+> "ENUM" <+> - parenthesized (renderSQL labels) <> ";" - --- | Enumerated types can also be generated from a Haskell type, for example --- --- >>> data Schwarma = Beef | Lamb | Chicken deriving GHC.Generic --- >>> instance SOP.Generic Schwarma --- >>> instance SOP.HasDatatypeInfo Schwarma --- >>> :{ --- let --- createSchwarma :: Definition (Public '[]) '["public" ::: '["schwarma" ::: 'Typedef (PG (Enumerated Schwarma))]] --- createSchwarma = createTypeEnumFrom @Schwarma #schwarma --- in --- printSQL createSchwarma --- :} --- CREATE TYPE "schwarma" AS ENUM ('Beef', 'Lamb', 'Chicken'); -createTypeEnumFrom - :: forall hask sch enum db schema. - ( SOP.Generic hask - , SOP.All KnownSymbol (LabelsPG hask) - , KnownSymbol enum - , Has sch db schema ) - => QualifiedAlias sch enum - -- ^ name of the user defined enumerated type - -> Definition db (Alter sch (Create enum ('Typedef (PG (Enumerated hask))) schema) db) -createTypeEnumFrom enum = createTypeEnum enum - (SOP.hpure label :: NP PGlabel (LabelsPG hask)) - -{- | `createTypeComposite` creates a composite type. The composite type is -specified by a list of attribute names and data types. - ->>> :{ -type PGcomplex = 'PGcomposite - '[ "real" ::: 'NotNull 'PGfloat8 - , "imaginary" ::: 'NotNull 'PGfloat8 ] -:} - ->>> :{ -let - setup :: Definition (Public '[]) '["public" ::: '["complex" ::: 'Typedef PGcomplex]] - setup = createTypeComposite #complex - (float8 `as` #real :* float8 `as` #imaginary) -in printSQL setup -:} -CREATE TYPE "complex" AS ("real" float8, "imaginary" float8); --} -createTypeComposite - :: (KnownSymbol ty, Has sch db schema, SOP.SListI fields) - => QualifiedAlias sch ty - -- ^ name of the user defined composite type - -> NP (Aliased (TypeExpression db)) fields - -- ^ list of attribute names and data types - -> Definition db (Alter sch (Create ty ('Typedef ('PGcomposite fields)) schema) db) -createTypeComposite ty fields = UnsafeDefinition $ - "CREATE" <+> "TYPE" <+> renderSQL ty <+> "AS" <+> parenthesized - (renderCommaSeparated renderField fields) <> ";" - where - renderField :: Aliased (TypeExpression db) x -> ByteString - renderField (typ `As` alias) = - renderSQL alias <+> renderSQL typ - --- | Composite types can also be generated from a Haskell type, for example --- --- >>> data Complex = Complex {real :: Double, imaginary :: Double} deriving GHC.Generic --- >>> instance SOP.Generic Complex --- >>> instance SOP.HasDatatypeInfo Complex --- >>> type Schema = '["complex" ::: 'Typedef (PG (Composite Complex))] --- >>> :{ --- let --- createComplex :: Definition (Public '[]) (Public Schema) --- createComplex = createTypeCompositeFrom @Complex #complex --- in --- printSQL createComplex --- :} --- CREATE TYPE "complex" AS ("real" float8, "imaginary" float8); -createTypeCompositeFrom - :: forall hask sch ty db schema. - ( SOP.All (FieldTyped db) (RowPG hask) - , KnownSymbol ty - , Has sch db schema ) - => QualifiedAlias sch ty - -- ^ name of the user defined composite type - -> Definition db (Alter sch (Create ty ( 'Typedef (PG (Composite hask))) schema) db) -createTypeCompositeFrom ty = createTypeComposite ty - (SOP.hcpure (SOP.Proxy :: SOP.Proxy (FieldTyped db)) fieldtype - :: NP (Aliased (TypeExpression db)) (RowPG hask)) - -{-| -`createDomain` creates a new domain. A domain is essentially a data type -with constraints (restrictions on the allowed set of values). - -Domains are useful for abstracting common constraints on fields -into a single location for maintenance. For example, several tables might -contain email address columns, all requiring the same -`Squeal.PostgreSQL.Definition.Table.Constraint.check` constraint -to verify the address syntax. Define a domain rather than setting up -each table's constraint individually. - ->>> :{ -let - createPositive :: Definition (Public '[]) (Public '["positive" ::: 'Typedef 'PGfloat4]) - createPositive = createDomain #positive real (#value .> 0) -in printSQL createPositive -:} -CREATE DOMAIN "positive" AS real CHECK (("value" > (0.0 :: float4))); --} -createDomain - :: (Has sch db schema, KnownSymbol dom) - => QualifiedAlias sch dom -- ^ domain alias - -> (forall null. TypeExpression db (null ty)) -- ^ underlying type - -> (forall tab. Condition 'Ungrouped '[] '[] db '[] '[tab ::: '["value" ::: 'Null ty]]) - -- ^ constraint on type - -> Definition db (Alter sch (Create dom ('Typedef ty) schema) db) -createDomain dom ty condition = - UnsafeDefinition $ "CREATE DOMAIN" <+> renderSQL dom - <+> "AS" <+> renderTypeExpression ty - <+> "CHECK" <+> parenthesized (renderSQL condition) <> ";" - -{- | Range types are data types representing a range of values -of some element type (called the range's subtype). -The subtype must have a total order so that it is well-defined -whether element values are within, before, or after a range of values. - -Range types are useful because they represent many element values -in a single range value, and because concepts such as overlapping ranges -can be expressed clearly. -The use of time and date ranges for scheduling purposes -is the clearest example; but price ranges, -measurement ranges from an instrument, and so forth can also be useful. - ->>> :{ -let - createSmallIntRange :: Definition (Public '[]) (Public '["int2range" ::: 'Typedef ('PGrange 'PGint2)]) - createSmallIntRange = createTypeRange #int2range int2 -in printSQL createSmallIntRange -:} -CREATE TYPE "int2range" AS RANGE (subtype = int2); --} -createTypeRange - :: (Has sch db schema, KnownSymbol range) - => QualifiedAlias sch range -- ^ range alias - -> (forall null. TypeExpression db (null ty)) -- ^ underlying type - -> Definition db (Alter sch (Create range ('Typedef ('PGrange ty)) schema) db) -createTypeRange range ty = UnsafeDefinition $ - "CREATE" <+> "TYPE" <+> renderSQL range <+> "AS" <+> "RANGE" <+> - parenthesized ("subtype" <+> "=" <+> renderTypeExpression ty) <> ";" - --- | Drop a type. --- --- >>> data Schwarma = Beef | Lamb | Chicken deriving GHC.Generic --- >>> instance SOP.Generic Schwarma --- >>> instance SOP.HasDatatypeInfo Schwarma --- >>> printSQL (dropType #schwarma :: Definition '["public" ::: '["schwarma" ::: 'Typedef (PG (Enumerated Schwarma))]] (Public '[])) --- DROP TYPE "schwarma"; -dropType - :: (Has sch db schema, KnownSymbol td) - => QualifiedAlias sch td - -- ^ name of the user defined type - -> Definition db (Alter sch (DropSchemum td 'Typedef schema) db) -dropType tydef = UnsafeDefinition $ "DROP TYPE" <+> renderSQL tydef <> ";" - --- | Drop a type if it exists. -dropTypeIfExists - :: (Has sch db schema, KnownSymbol td) - => QualifiedAlias sch td - -- ^ name of the user defined type - -> Definition db (Alter sch (DropSchemumIfExists td 'Typedef schema) db) -dropTypeIfExists tydef = UnsafeDefinition $ - "DROP TYPE IF EXISTS" <+> renderSQL tydef <> ";" - --- | `alterTypeRename` changes the name of a type from the schema. --- --- >>> type DB = '[ "public" ::: '[ "foo" ::: 'Typedef 'PGbool ] ] --- >>> :{ --- let def :: Definition DB '["public" ::: '["bar" ::: 'Typedef 'PGbool ] ] --- def = alterTypeRename #foo #bar --- in printSQL def --- :} --- ALTER TYPE "foo" RENAME TO "bar"; -alterTypeRename - :: ( Has sch db schema - , KnownSymbol ty1 - , Has ty0 schema ('Typedef ty)) - => QualifiedAlias sch ty0 -- ^ type to rename - -> Alias ty1 -- ^ what to rename it - -> Definition db (Alter sch (Rename ty0 ty1 schema) db ) -alterTypeRename ty0 ty1 = UnsafeDefinition $ - "ALTER TYPE" <+> renderSQL ty0 - <+> "RENAME TO" <+> renderSQL ty1 <> ";" - -{- | This form moves the type into another schema. - ->>> type DB0 = '[ "sch0" ::: '[ "ty" ::: 'Typedef 'PGfloat8 ], "sch1" ::: '[] ] ->>> type DB1 = '[ "sch0" ::: '[], "sch1" ::: '[ "ty" ::: 'Typedef 'PGfloat8 ] ] ->>> :{ -let def :: Definition DB0 DB1 - def = alterTypeSetSchema (#sch0 ! #ty) #sch1 -in printSQL def -:} -ALTER TYPE "sch0"."ty" SET SCHEMA "sch1"; --} -alterTypeSetSchema - :: ( Has sch0 db schema0 - , Has ty schema0 ('Typedef td) - , Has sch1 db schema1 ) - => QualifiedAlias sch0 ty -- ^ type to move - -> Alias sch1 -- ^ where to move it - -> Definition db (SetSchema sch0 sch1 schema0 schema1 ty 'Typedef td db) -alterTypeSetSchema ty sch = UnsafeDefinition $ - "ALTER TYPE" <+> renderSQL ty <+> "SET SCHEMA" <+> renderSQL sch <> ";" diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Definition/View.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Definition/View.hs deleted file mode 100644 index 668b27cd..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Definition/View.hs +++ /dev/null @@ -1,179 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Definition.View -Description: create and drop views -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -create and drop views --} - -{-# LANGUAGE - AllowAmbiguousTypes - , ConstraintKinds - , DeriveAnyClass - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , LambdaCase - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeInType - , TypeOperators - , UndecidableSuperClasses -#-} - -module Squeal.PostgreSQL.Definition.View - ( -- * Create - createView - , createOrReplaceView - -- * Drop - , dropView - , dropViewIfExists - -- * Alter - , alterViewRename - , alterViewSetSchema - ) where - -import GHC.TypeLits - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Definition -import Squeal.PostgreSQL.Query -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - -{- | Create a view. - ->>> type ABC = '["a" ::: 'NoDef :=> 'Null 'PGint4, "b" ::: 'NoDef :=> 'Null 'PGint4, "c" ::: 'NoDef :=> 'Null 'PGint4] ->>> type BC = '["b" ::: 'Null 'PGint4, "c" ::: 'Null 'PGint4] ->>> :{ -let - definition :: Definition - '[ "public" ::: '["abc" ::: 'Table ('[] :=> ABC)]] - '[ "public" ::: '["abc" ::: 'Table ('[] :=> ABC), "bc" ::: 'View BC]] - definition = - createView #bc (select_ (#b :* #c) (from (table #abc))) -in printSQL definition -:} -CREATE VIEW "bc" AS SELECT "b" AS "b", "c" AS "c" FROM "abc" AS "abc"; --} -createView - :: (Has sch db schema, KnownSymbol vw) - => QualifiedAlias sch vw -- ^ the name of the view to add - -> Query '[] '[] db '[] view -- ^ query - -> Definition db (Alter sch (Create vw ('View view) schema) db) -createView alias query = UnsafeDefinition $ - "CREATE" <+> "VIEW" <+> renderSQL alias <+> "AS" - <+> renderQuery query <> ";" - -{- | Create or replace a view. - ->>> type ABC = '["a" ::: 'NoDef :=> 'Null 'PGint4, "b" ::: 'NoDef :=> 'Null 'PGint4, "c" ::: 'NoDef :=> 'Null 'PGint4] ->>> type BC = '["b" ::: 'Null 'PGint4, "c" ::: 'Null 'PGint4] ->>> :{ -let - definition :: Definition - '[ "public" ::: '["abc" ::: 'Table ('[] :=> ABC)]] - '[ "public" ::: '["abc" ::: 'Table ('[] :=> ABC), "bc" ::: 'View BC]] - definition = - createOrReplaceView #bc (select_ (#b :* #c) (from (table #abc))) -in printSQL definition -:} -CREATE OR REPLACE VIEW "bc" AS SELECT "b" AS "b", "c" AS "c" FROM "abc" AS "abc"; --} -createOrReplaceView - :: (Has sch db schema, KnownSymbol vw) - => QualifiedAlias sch vw -- ^ the name of the view to add - -> Query '[] '[] db '[] view -- ^ query - -> Definition db (Alter sch (CreateOrReplace vw ('View view) schema) db) -createOrReplaceView alias query = UnsafeDefinition $ - "CREATE OR REPLACE VIEW" <+> renderSQL alias <+> "AS" - <+> renderQuery query <> ";" - --- | Drop a view. --- --- >>> :{ --- let --- definition :: Definition --- '[ "public" ::: '["abc" ::: 'Table ('[] :=> '["a" ::: 'NoDef :=> 'Null 'PGint4, "b" ::: 'NoDef :=> 'Null 'PGint4, "c" ::: 'NoDef :=> 'Null 'PGint4]) --- , "bc" ::: 'View ('["b" ::: 'Null 'PGint4, "c" ::: 'Null 'PGint4])]] --- '[ "public" ::: '["abc" ::: 'Table ('[] :=> '["a" ::: 'NoDef :=> 'Null 'PGint4, "b" ::: 'NoDef :=> 'Null 'PGint4, "c" ::: 'NoDef :=> 'Null 'PGint4])]] --- definition = dropView #bc --- in printSQL definition --- :} --- DROP VIEW "bc"; -dropView - :: (Has sch db schema, KnownSymbol vw) - => QualifiedAlias sch vw -- ^ view to remove - -> Definition db (Alter sch (DropSchemum vw 'View schema) db) -dropView vw = UnsafeDefinition $ "DROP VIEW" <+> renderSQL vw <> ";" - --- | Drop a view if it exists. --- --- >>> :{ --- let --- definition :: Definition --- '[ "public" ::: '["abc" ::: 'Table ('[] :=> '["a" ::: 'NoDef :=> 'Null 'PGint4, "b" ::: 'NoDef :=> 'Null 'PGint4, "c" ::: 'NoDef :=> 'Null 'PGint4]) --- , "bc" ::: 'View ('["b" ::: 'Null 'PGint4, "c" ::: 'Null 'PGint4])]] --- '[ "public" ::: '["abc" ::: 'Table ('[] :=> '["a" ::: 'NoDef :=> 'Null 'PGint4, "b" ::: 'NoDef :=> 'Null 'PGint4, "c" ::: 'NoDef :=> 'Null 'PGint4])]] --- definition = dropViewIfExists #bc --- in printSQL definition --- :} --- DROP VIEW IF EXISTS "bc"; -dropViewIfExists - :: (Has sch db schema, KnownSymbol vw) - => QualifiedAlias sch vw -- ^ view to remove - -> Definition db (Alter sch (DropIfExists vw schema) db) -dropViewIfExists vw = UnsafeDefinition $ - "DROP VIEW IF EXISTS" <+> renderSQL vw <> ";" - --- | `alterViewRename` changes the name of a view from the schema. --- --- >>> type DB = '[ "public" ::: '[ "foo" ::: 'View '[] ] ] --- >>> :{ --- let def :: Definition DB '["public" ::: '["bar" ::: 'View '[] ] ] --- def = alterViewRename #foo #bar --- in printSQL def --- :} --- ALTER VIEW "foo" RENAME TO "bar"; -alterViewRename - :: ( Has sch db schema - , KnownSymbol ty1 - , Has ty0 schema ('View vw)) - => QualifiedAlias sch ty0 -- ^ view to rename - -> Alias ty1 -- ^ what to rename it - -> Definition db (Alter sch (Rename ty0 ty1 schema) db ) -alterViewRename vw0 vw1 = UnsafeDefinition $ - "ALTER VIEW" <+> renderSQL vw0 - <+> "RENAME TO" <+> renderSQL vw1 <> ";" - -{- | This form moves the view into another schema. - ->>> type DB0 = '[ "sch0" ::: '[ "vw" ::: 'View '[] ], "sch1" ::: '[] ] ->>> type DB1 = '[ "sch0" ::: '[], "sch1" ::: '[ "vw" ::: 'View '[] ] ] ->>> :{ -let def :: Definition DB0 DB1 - def = alterViewSetSchema (#sch0 ! #vw) #sch1 -in printSQL def -:} -ALTER VIEW "sch0"."vw" SET SCHEMA "sch1"; --} -alterViewSetSchema - :: ( Has sch0 db schema0 - , Has vw schema0 ('View view) - , Has sch1 db schema1 ) - => QualifiedAlias sch0 vw -- ^ view to move - -> Alias sch1 -- ^ where to move it - -> Definition db (SetSchema sch0 sch1 schema0 schema1 vw 'View view db) -alterViewSetSchema ty sch = UnsafeDefinition $ - "ALTER VIEW" <+> renderSQL ty <+> "SET SCHEMA" <+> renderSQL sch <> ";" diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression.hs deleted file mode 100644 index ff37e690..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression.hs +++ /dev/null @@ -1,644 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Expression -Description: expressions -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -Expressions are the atoms used to build statements. --} - -{-# LANGUAGE - AllowAmbiguousTypes - , ConstraintKinds - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , FunctionalDependencies - , GADTs - , GeneralizedNewtypeDeriving - , LambdaCase - , MagicHash - , OverloadedStrings - , ScopedTypeVariables - , StandaloneDeriving - , TypeApplications - , TypeFamilies - , TypeInType - , TypeOperators - , UndecidableInstances - , RankNTypes -#-} - -module Squeal.PostgreSQL.Expression - ( -- * Expression - Expression (..) - , Expr - -- * Function - , type (-->) - , Fun - , unsafeFunction - , function - , unsafeLeftOp - , unsafeRightOp - -- * Operator - , Operator - , OperatorDB - , unsafeBinaryOp - , PGSubset (..) - , PGIntersect (..) - -- * Multivariable Function - , FunctionVar - , unsafeFunctionVar - , type (--->) - , FunN - , unsafeFunctionN - , functionN - -- * Re-export - , (&) - ) where - -import Control.Category -import Control.DeepSeq -import Data.Binary.Builder (toLazyByteString) -import Data.ByteString (ByteString) -import Data.ByteString.Builder (doubleDec, floatDec, int16Dec, int32Dec, int64Dec) -import Data.ByteString.Builder.Scientific (scientificBuilder) -import Data.ByteString.Lazy (toStrict) -import Data.Function ((&)) -import Data.Semigroup hiding (All) -import Data.String -import Generics.SOP hiding (All, from) -import GHC.OverloadedLabels -import GHC.TypeLits -import Numeric -import Prelude hiding (id, (.)) - -import qualified GHC.Generics as GHC - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - -{----------------------------------------- -column expressions ------------------------------------------} - -{- | `Expression`s are used in a variety of contexts, -such as in the target `Squeal.PostgreSQL.Query.List` of the -`Squeal.PostgreSQL.Query.select` command, -as new column values in `Squeal.PostgreSQL.Manipulation.insertInto` or -`Squeal.PostgreSQL.Manipulation.update`, -or in search `Squeal.PostgreSQL.Expression.Logic.Condition`s -in a number of commands. - -The expression syntax allows the calculation of -values from primitive expression using arithmetic, logical, -and other operations. - -The type parameters of `Expression` are - -* @lat :: @ `FromType`, the @from@ clauses of any lat queries in which - the `Expression` is a correlated subquery expression; -* @with :: @ `FromType`, the `Squeal.PostgreSQL.Query.CommonTableExpression`s - that are in scope for the `Expression`; -* @grp :: @ `Grouping`, the `Grouping` of the @from@ clause which may limit - which columns may be referenced by alias; -* @db :: @ `SchemasType`, the schemas of your database that are in - scope for the `Expression`; -* @from :: @ `FromType`, the @from@ clause which the `Expression` may use - to reference columns by alias; -* @ty :: @ `NullType`, the type of the `Expression`. --} -newtype Expression - (grp :: Grouping) - (lat :: FromType) - (with :: FromType) - (db :: SchemasType) - (params :: [NullType]) - (from :: FromType) - (ty :: NullType) - = UnsafeExpression { renderExpression :: ByteString } - deriving stock (GHC.Generic,Show,Eq,Ord) - deriving newtype (NFData) -instance RenderSQL (Expression grp lat with db params from ty) where - renderSQL = renderExpression - --- | An `Expr` is a closed `Expression`. --- It is a F@RankNType@ but don't be scared. --- Think of it as an expression which sees no --- namespaces, so you can't use parameters --- or alias references. It can be used as --- a simple piece of more complex `Expression`s. -type Expr x - = forall grp lat with db params from - . Expression grp lat with db params from x - -- ^ cannot reference aliases - --- | A @RankNType@ for binary operators. -type Operator x1 x2 y = forall db. OperatorDB db x1 x2 y - --- | Like `Operator` but depends on the schemas of the database -type OperatorDB db x1 x2 y - = forall grp lat with params from - . Expression grp lat with db params from x1 - -- ^ left input - -> Expression grp lat with db params from x2 - -- ^ right input - -> Expression grp lat with db params from y - -- ^ output - --- | A @RankNType@ for functions with a single argument. --- These could be either function calls or unary operators. --- This is a subtype of the usual Haskell function type `Prelude.->`, --- indeed a subcategory as it is closed under the usual --- `Prelude..` and `Prelude.id`. -type (-->) x y = forall db. Fun db x y - --- | Like `-->` but depends on the schemas of the database -type Fun db x y - = forall grp lat with params from - . Expression grp lat with db params from x - -- ^ input - -> Expression grp lat with db params from y - -- ^ output - -{- | A @RankNType@ for functions with a fixed-length list of heterogeneous arguments. -Use the `*:` operator to end your argument lists, like so. - ->>> printSQL (unsafeFunctionN "fun" (true :* false :* localTime *: true)) -fun(TRUE, FALSE, LOCALTIME, TRUE) --} -type (--->) xs y = forall db. FunN db xs y - --- | Like `--->` but depends on the schemas of the database -type FunN db xs y - = forall grp lat with params from - . NP (Expression grp lat with db params from) xs - -- ^ inputs - -> Expression grp lat with db params from y - -- ^ output - -{- | A @RankNType@ for functions with a variable-length list of -homogeneous arguments and at least 1 more argument. --} -type FunctionVar x0 x1 y - = forall grp lat with db params from - . [Expression grp lat with db params from x0] - -- ^ inputs - -> Expression grp lat with db params from x1 - -- ^ must have at least 1 input - -> Expression grp lat with db params from y - -- ^ output - -{- | >>> printSQL (unsafeFunctionVar "greatest" [true, null_] false) -greatest(TRUE, NULL, FALSE) --} -unsafeFunctionVar :: ByteString -> FunctionVar x0 x1 y -unsafeFunctionVar fun xs x = UnsafeExpression $ fun <> parenthesized - (commaSeparated (renderSQL <$> xs) <> ", " <> renderSQL x) - -instance (HasUnique tab (Join from lat) row, Has col row ty) - => IsLabel col (Expression 'Ungrouped lat with db params from ty) where - fromLabel = UnsafeExpression $ renderSQL (Alias @col) -instance (HasUnique tab (Join from lat) row, Has col row ty, tys ~ '[ty]) - => IsLabel col (NP (Expression 'Ungrouped lat with db params from) tys) where - fromLabel = fromLabel @col :* Nil -instance (HasUnique tab (Join from lat) row, Has col row ty, column ~ (col ::: ty)) - => IsLabel col - (Aliased (Expression 'Ungrouped lat with db params from) column) where - fromLabel = fromLabel @col `As` Alias -instance (HasUnique tab (Join from lat) row, Has col row ty, columns ~ '[col ::: ty]) - => IsLabel col - (NP (Aliased (Expression 'Ungrouped lat with db params from)) columns) where - fromLabel = fromLabel @col :* Nil - -instance (Has tab (Join from lat) row, Has col row ty) - => IsQualified tab col (Expression 'Ungrouped lat with db params from ty) where - tab ! col = UnsafeExpression $ - renderSQL tab <> "." <> renderSQL col -instance (Has tab (Join from lat) row, Has col row ty, tys ~ '[ty]) - => IsQualified tab col (NP (Expression 'Ungrouped lat with db params from) tys) where - tab ! col = tab ! col :* Nil -instance (Has tab (Join from lat) row, Has col row ty, column ~ (col ::: ty)) - => IsQualified tab col - (Aliased (Expression 'Ungrouped lat with db params from) column) where - tab ! col = tab ! col `As` col -instance (Has tab (Join from lat) row, Has col row ty, columns ~ '[col ::: ty]) - => IsQualified tab col - (NP (Aliased (Expression 'Ungrouped lat with db params from)) columns) where - tab ! col = tab ! col :* Nil - -instance - ( HasUnique tab (Join from lat) row - , Has col row ty - , GroupedBy tab col bys - ) => IsLabel col - (Expression ('Grouped bys) lat with db params from ty) where - fromLabel = UnsafeExpression $ renderSQL (Alias @col) -instance - ( HasUnique tab (Join from lat) row - , Has col row ty - , GroupedBy tab col bys - , tys ~ '[ty] - ) => IsLabel col - (NP (Expression ('Grouped bys) lat with db params from) tys) where - fromLabel = fromLabel @col :* Nil -instance - ( HasUnique tab (Join from lat) row - , Has col row ty - , GroupedBy tab col bys - , column ~ (col ::: ty) - ) => IsLabel col - (Aliased (Expression ('Grouped bys) lat with db params from) column) where - fromLabel = fromLabel @col `As` Alias -instance - ( HasUnique tab (Join from lat) row - , Has col row ty - , GroupedBy tab col bys - , columns ~ '[col ::: ty] - ) => IsLabel col - (NP (Aliased (Expression ('Grouped bys) lat with db params from)) columns) where - fromLabel = fromLabel @col :* Nil - -instance - ( Has tab (Join from lat) row - , Has col row ty - , GroupedBy tab col bys - ) => IsQualified tab col - (Expression ('Grouped bys) lat with db params from ty) where - tab ! col = UnsafeExpression $ - renderSQL tab <> "." <> renderSQL col -instance - ( Has tab (Join from lat) row - , Has col row ty - , GroupedBy tab col bys - , tys ~ '[ty] - ) => IsQualified tab col - (NP (Expression ('Grouped bys) lat with db params from) tys) where - tab ! col = tab ! col :* Nil -instance - ( Has tab (Join from lat) row - , Has col row ty - , GroupedBy tab col bys - , column ~ (col ::: ty) - ) => IsQualified tab col - (Aliased (Expression ('Grouped bys) lat with db params from) column) where - tab ! col = tab ! col `As` col -instance - ( Has tab (Join from lat) row - , Has col row ty - , GroupedBy tab col bys - , columns ~ '[col ::: ty] - ) => IsQualified tab col - (NP (Aliased (Expression ('Grouped bys) lat with db params from)) columns) where - tab ! col = tab ! col :* Nil - -instance (KnownSymbol label, label `In` labels) => IsPGlabel label - (Expression grp lat with db params from (null ('PGenum labels))) where - label = UnsafeExpression $ renderSQL (PGlabel @label) - --- | >>> printSQL $ unsafeBinaryOp "OR" true false --- (TRUE OR FALSE) -unsafeBinaryOp :: ByteString -> Operator ty0 ty1 ty2 -unsafeBinaryOp op x y = UnsafeExpression $ parenthesized $ - renderSQL x <+> op <+> renderSQL y - --- | >>> printSQL $ unsafeLeftOp "NOT" true --- (NOT TRUE) -unsafeLeftOp :: ByteString -> x --> y -unsafeLeftOp op x = UnsafeExpression $ parenthesized $ op <+> renderSQL x - --- | >>> printSQL $ true & unsafeRightOp "IS NOT TRUE" --- (TRUE IS NOT TRUE) -unsafeRightOp :: ByteString -> x --> y -unsafeRightOp op x = UnsafeExpression $ parenthesized $ renderSQL x <+> op - --- | >>> printSQL $ unsafeFunction "f" true --- f(TRUE) -unsafeFunction :: ByteString -> x --> y -unsafeFunction fun x = UnsafeExpression $ - fun <> parenthesized (renderSQL x) - -{- | Call a user defined function of a single variable - ->>> type Fn = '[ 'Null 'PGint4] :=> 'Returns ('NotNull 'PGnumeric) ->>> type Schema = '["fn" ::: 'Function Fn] ->>> :{ -let - fn :: Fun (Public Schema) ('Null 'PGint4) ('NotNull 'PGnumeric) - fn = function #fn -in - printSQL (fn 1) -:} -"fn"((1 :: int4)) --} -function - :: (Has sch db schema, Has fun schema ('Function ('[x] :=> 'Returns y))) - => QualifiedAlias sch fun -- ^ function name - -> Fun db x y -function f = unsafeFunction $ renderSQL f - --- | >>> printSQL $ unsafeFunctionN "f" (currentTime :* localTimestamp :* false *: inline 'a') --- f(CURRENT_TIME, LOCALTIMESTAMP, FALSE, (E'a' :: char(1))) -unsafeFunctionN :: SListI xs => ByteString -> xs ---> y -unsafeFunctionN fun xs = UnsafeExpression $ - fun <> parenthesized (renderCommaSeparated renderSQL xs) - -{- | Call a user defined multivariable function - ->>> type Fn = '[ 'Null 'PGint4, 'Null 'PGbool] :=> 'Returns ('NotNull 'PGnumeric) ->>> type Schema = '["fn" ::: 'Function Fn] ->>> :{ -let - fn :: FunN (Public Schema) '[ 'Null 'PGint4, 'Null 'PGbool] ('NotNull 'PGnumeric) - fn = functionN #fn -in - printSQL (fn (1 *: true)) -:} -"fn"((1 :: int4), TRUE) --} -functionN - :: ( Has sch db schema - , Has fun schema ('Function (xs :=> 'Returns y)) - , SListI xs ) - => QualifiedAlias sch fun -- ^ function alias - -> FunN db xs y -functionN f = unsafeFunctionN $ renderSQL f - -instance - Num (Expression grp lat with db params from (null 'PGint2)) where - (+) = unsafeBinaryOp "+" - (-) = unsafeBinaryOp "-" - (*) = unsafeBinaryOp "*" - abs = unsafeFunction "abs" - signum = unsafeFunction "sign" - fromInteger - = UnsafeExpression - . parenthesized - . (<> " :: int2") - . toStrict - . toLazyByteString - . int16Dec - . fromInteger -instance - Num (Expression grp lat with db params from (null 'PGint4)) where - (+) = unsafeBinaryOp "+" - (-) = unsafeBinaryOp "-" - (*) = unsafeBinaryOp "*" - abs = unsafeFunction "abs" - signum = unsafeFunction "sign" - fromInteger - = UnsafeExpression - . parenthesized - . (<> " :: int4") - . toStrict - . toLazyByteString - . int32Dec - . fromInteger -instance - Num (Expression grp lat with db params from (null 'PGint8)) where - (+) = unsafeBinaryOp "+" - (-) = unsafeBinaryOp "-" - (*) = unsafeBinaryOp "*" - abs = unsafeFunction "abs" - signum = unsafeFunction "sign" - fromInteger x = - let - y = fromInteger x - in - if y == minBound - -- For some reason Postgres throws an error with - -- (-9223372036854775808 :: int8) - -- even though it's a valid lowest value for int8 - then fromInteger (x+1) - 1 - else UnsafeExpression - . parenthesized - . (<> " :: int8") - . toStrict - . toLazyByteString - $ int64Dec y -instance - Num (Expression grp lat with db params from (null 'PGfloat4)) where - (+) = unsafeBinaryOp "+" - (-) = unsafeBinaryOp "-" - (*) = unsafeBinaryOp "*" - abs = unsafeFunction "abs" - signum = unsafeFunction "sign" - fromInteger x - = UnsafeExpression - . parenthesized - . (<> " :: float4") $ - let - y = fromInteger x - decimal = toStrict . toLazyByteString . floatDec - in - if isNaN y || isInfinite y - then singleQuotedUtf8 (decimal y) - else decimal y -instance - Num (Expression grp lat with db params from (null 'PGfloat8)) where - (+) = unsafeBinaryOp "+" - (-) = unsafeBinaryOp "-" - (*) = unsafeBinaryOp "*" - abs = unsafeFunction "abs" - signum = unsafeFunction "sign" - fromInteger x - = UnsafeExpression - . parenthesized - . (<> " :: float8") $ - let - y = fromInteger x - decimal = toStrict . toLazyByteString . doubleDec - in - if isNaN y || isInfinite y - then singleQuotedUtf8 (decimal y) - else decimal y -instance - Num (Expression grp lat with db params from (null 'PGnumeric)) where - (+) = unsafeBinaryOp "+" - (-) = unsafeBinaryOp "-" - (*) = unsafeBinaryOp "*" - abs = unsafeFunction "abs" - signum = unsafeFunction "sign" - fromInteger - = UnsafeExpression - . parenthesized - . (<> " :: numeric") - . toStrict - . toLazyByteString - . scientificBuilder - . fromInteger - -instance Fractional - (Expression grp lat with db params from (null 'PGfloat4)) where - (/) = unsafeBinaryOp "/" - fromRational x - = UnsafeExpression - . parenthesized - . (<> " :: float4") $ - let - y = fromRational x - decimal = toStrict . toLazyByteString . floatDec - in - if isNaN y || isInfinite y - then singleQuotedUtf8 (decimal y) - else decimal y -instance Fractional - (Expression grp lat with db params from (null 'PGfloat8)) where - (/) = unsafeBinaryOp "/" - fromRational x - = UnsafeExpression - . parenthesized - . (<> " :: float8") $ - let - y = fromRational x - decimal = toStrict . toLazyByteString . doubleDec - in - if isNaN y || isInfinite y - then singleQuotedUtf8 (decimal y) - else decimal y -instance Fractional - (Expression grp lat with db params from (null 'PGnumeric)) where - (/) = unsafeBinaryOp "/" - fromRational - = UnsafeExpression - . parenthesized - . (<> " :: numeric") - . toStrict - . toLazyByteString - . scientificBuilder - . fromRational - -instance Floating - (Expression grp lat with db params from (null 'PGfloat4)) where - pi = UnsafeExpression "pi()" - exp = unsafeFunction "exp" - log = unsafeFunction "ln" - sqrt = unsafeFunction "sqrt" - b ** x = UnsafeExpression $ - "power(" <> renderSQL b <> ", " <> renderSQL x <> ")" - logBase b y = log y / log b - sin = unsafeFunction "sin" - cos = unsafeFunction "cos" - tan = unsafeFunction "tan" - asin = unsafeFunction "asin" - acos = unsafeFunction "acos" - atan = unsafeFunction "atan" - sinh x = (exp x - exp (-x)) / 2 - cosh x = (exp x + exp (-x)) / 2 - tanh x = sinh x / cosh x - asinh x = log (x + sqrt (x*x + 1)) - acosh x = log (x + sqrt (x*x - 1)) - atanh x = log ((1 + x) / (1 - x)) / 2 -instance Floating - (Expression grp lat with db params from (null 'PGfloat8)) where - pi = UnsafeExpression "pi()" - exp = unsafeFunction "exp" - log = unsafeFunction "ln" - sqrt = unsafeFunction "sqrt" - b ** x = UnsafeExpression $ - "power(" <> renderSQL b <> ", " <> renderSQL x <> ")" - logBase b y = log y / log b - sin = unsafeFunction "sin" - cos = unsafeFunction "cos" - tan = unsafeFunction "tan" - asin = unsafeFunction "asin" - acos = unsafeFunction "acos" - atan = unsafeFunction "atan" - sinh x = (exp x - exp (-x)) / 2 - cosh x = (exp x + exp (-x)) / 2 - tanh x = sinh x / cosh x - asinh x = log (x + sqrt (x*x + 1)) - acosh x = log (x + sqrt (x*x - 1)) - atanh x = log ((1 + x) / (1 - x)) / 2 -instance Floating - (Expression grp lat with db params from (null 'PGnumeric)) where - pi = UnsafeExpression "pi()" - exp = unsafeFunction "exp" - log = unsafeFunction "ln" - sqrt = unsafeFunction "sqrt" - b ** x = UnsafeExpression $ - "power(" <> renderSQL b <> ", " <> renderSQL x <> ")" - logBase b y = log y / log b - sin = unsafeFunction "sin" - cos = unsafeFunction "cos" - tan = unsafeFunction "tan" - asin = unsafeFunction "asin" - acos = unsafeFunction "acos" - atan = unsafeFunction "atan" - sinh x = (exp x - exp (-x)) / 2 - cosh x = (exp x + exp (-x)) / 2 - tanh x = sinh x / cosh x - asinh x = log (x + sqrt (x*x + 1)) - acosh x = log (x + sqrt (x*x - 1)) - atanh x = log ((1 + x) / (1 - x)) / 2 - --- | Contained by operators -class PGSubset ty where - (@>) :: Operator (null0 ty) (null1 ty) ('Null 'PGbool) - (@>) = unsafeBinaryOp "@>" - (<@) :: Operator (null0 ty) (null1 ty) ('Null 'PGbool) - (<@) = unsafeBinaryOp "<@" -infix 4 @> -infix 4 <@ -instance PGSubset 'PGjsonb -instance PGSubset 'PGtsquery -instance PGSubset ('PGvararray ty) -instance PGSubset ('PGrange ty) - --- | Intersection operator -class PGIntersect ty where - (@&&) :: Operator (null0 ty) (null1 ty) ('Null 'PGbool) - (@&&) = unsafeBinaryOp "&&" -instance PGIntersect ('PGvararray ty) -instance PGIntersect ('PGrange ty) - -instance IsString - (Expression grp lat with db params from (null 'PGtext)) where - fromString - = UnsafeExpression - . parenthesized - . (<> " :: text") - . escapeQuotedString -instance IsString - (Expression grp lat with db params from (null 'PGtsvector)) where - fromString - = UnsafeExpression - . parenthesized - . (<> " :: tsvector") - . escapeQuotedString -instance IsString - (Expression grp lat with db params from (null 'PGtsquery)) where - fromString - = UnsafeExpression - . parenthesized - . (<> " :: tsquery") - . escapeQuotedString - -instance Semigroup - (Expression grp lat with db params from (null ('PGvararray ty))) where - (<>) = unsafeBinaryOp "||" -instance Semigroup - (Expression grp lat with db params from (null 'PGjsonb)) where - (<>) = unsafeBinaryOp "||" -instance Semigroup - (Expression grp lat with db params from (null 'PGtext)) where - (<>) = unsafeBinaryOp "||" -instance Semigroup - (Expression grp lat with db params from (null 'PGtsvector)) where - (<>) = unsafeBinaryOp "||" - -instance Monoid - (Expression grp lat with db params from (null 'PGtext)) where - mempty = fromString "" - mappend = (<>) -instance Monoid - (Expression grp lat with db params from (null 'PGtsvector)) where - mempty = fromString "" - mappend = (<>) diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Aggregate.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Aggregate.hs deleted file mode 100644 index ccf689a1..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Aggregate.hs +++ /dev/null @@ -1,644 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Expression.Aggregate -Description: aggregate functions and arguments -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -aggregate functions and arguments --} - -{-# LANGUAGE - DataKinds - , DeriveGeneric - , FlexibleContexts - , FlexibleInstances - , FunctionalDependencies - , LambdaCase - , MultiParamTypeClasses - , OverloadedStrings - , PatternSynonyms - , PolyKinds - , ScopedTypeVariables - , StandaloneDeriving - , TypeApplications - , TypeFamilies - , TypeOperators - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Expression.Aggregate - ( -- * Aggregate - Aggregate (..) - -- * Aggregate Arguments - , AggregateArg (..) - , pattern All - , pattern Alls - , allNotNull - , pattern Distinct - , pattern Distincts - , distinctNotNull - , FilterWhere (..) - -- * Aggregate Types - , PGSum - , PGAvg - ) where - -import Data.ByteString (ByteString) -import GHC.TypeLits - -import qualified Generics.SOP as SOP - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Expression.Logic -import Squeal.PostgreSQL.Expression.Null -import Squeal.PostgreSQL.Expression.Sort -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - -{- | -`Aggregate` functions compute a single result from a set of input values. -`Aggregate` functions can be used as `Grouped` `Expression`s as well -as `Squeal.PostgreSQL.Expression.Window.WindowFunction`s. --} -class Aggregate arg expr | expr -> arg where - - -- | A special aggregation that does not require an input - -- - -- >>> :{ - -- let - -- expression :: Expression ('Grouped bys) '[] with db params from ('NotNull 'PGint8) - -- expression = countStar - -- in printSQL expression - -- :} - -- count(*) - countStar :: expr lat with db params from ('NotNull 'PGint8) - - -- | >>> :{ - -- let - -- expression :: Expression ('Grouped bys) '[] with db params '[tab ::: '["col" ::: null ty]] ('NotNull 'PGint8) - -- expression = count (All #col) - -- in printSQL expression - -- :} - -- count(ALL "col") - count - :: arg '[ty] lat with db params from - -- ^ argument - -> expr lat with db params from ('NotNull 'PGint8) - - -- | >>> :{ - -- let - -- expression :: Expression ('Grouped bys) '[] with db params '[tab ::: '["col" ::: 'Null 'PGnumeric]] ('Null 'PGnumeric) - -- expression = sum_ (Distinct #col & filterWhere (#col .< 100)) - -- in printSQL expression - -- :} - -- sum(DISTINCT "col") FILTER (WHERE ("col" < (100.0 :: numeric))) - sum_ - :: arg '[null ty] lat with db params from - -- ^ argument - -> expr lat with db params from ('Null (PGSum ty)) - - -- | input values, including nulls, concatenated into an array - -- - -- >>> :{ - -- let - -- expression :: Expression ('Grouped bys) '[] with db params '[tab ::: '["col" ::: 'Null 'PGnumeric]] ('Null ('PGvararray ('Null 'PGnumeric))) - -- expression = arrayAgg (All #col & orderBy [AscNullsFirst #col] & filterWhere (#col .< 100)) - -- in printSQL expression - -- :} - -- array_agg(ALL "col" ORDER BY "col" ASC NULLS FIRST) FILTER (WHERE ("col" < (100.0 :: numeric))) - arrayAgg - :: arg '[ty] lat with db params from - -- ^ argument - -> expr lat with db params from ('Null ('PGvararray ty)) - - -- | aggregates values as a JSON array - jsonAgg - :: arg '[ty] lat with db params from - -- ^ argument - -> expr lat with db params from ('Null 'PGjson) - - -- | aggregates values as a JSON array - jsonbAgg - :: arg '[ty] lat with db params from - -- ^ argument - -> expr lat with db params from ('Null 'PGjsonb) - - {- | - the bitwise AND of all non-null input values, or null if none - - >>> :{ - let - expression :: Expression ('Grouped bys) '[] with db params '[tab ::: '["col" ::: null 'PGint4]] ('Null 'PGint4) - expression = bitAnd (Distinct #col) - in printSQL expression - :} - bit_and(DISTINCT "col") - -} - bitAnd - :: int `In` PGIntegral - => arg '[null int] lat with db params from - -- ^ argument - -> expr lat with db params from ('Null int) - - {- | - the bitwise OR of all non-null input values, or null if none - - >>> :{ - let - expression :: Expression ('Grouped bys) '[] with db params '[tab ::: '["col" ::: null 'PGint4]] ('Null 'PGint4) - expression = bitOr (All #col) - in printSQL expression - :} - bit_or(ALL "col") - -} - bitOr - :: int `In` PGIntegral - => arg '[null int] lat with db params from - -- ^ argument - -> expr lat with db params from ('Null int) - - {- | - true if all input values are true, otherwise false - - >>> :{ - let - winFun :: WindowFunction 'Ungrouped '[] with db params '[tab ::: '["col" ::: null 'PGbool]] ('Null 'PGbool) - winFun = boolAnd (Window #col) - in printSQL winFun - :} - bool_and("col") - -} - boolAnd - :: arg '[null 'PGbool] lat with db params from - -- ^ argument - -> expr lat with db params from ('Null 'PGbool) - - {- | - true if at least one input value is true, otherwise false - - >>> :{ - let - expression :: Expression ('Grouped bys) '[] with db params '[tab ::: '["col" ::: null 'PGbool]] ('Null 'PGbool) - expression = boolOr (All #col) - in printSQL expression - :} - bool_or(ALL "col") - -} - boolOr - :: arg '[null 'PGbool] lat with db params from - -- ^ argument - -> expr lat with db params from ('Null 'PGbool) - - {- | - equivalent to `boolAnd` - - >>> :{ - let - expression :: Expression ('Grouped bys) '[] with db params '[tab ::: '["col" ::: null 'PGbool]] ('Null 'PGbool) - expression = every (Distinct #col) - in printSQL expression - :} - every(DISTINCT "col") - -} - every - :: arg '[null 'PGbool] lat with db params from - -- ^ argument - -> expr lat with db params from ('Null 'PGbool) - - {- |maximum value of expression across all input values-} - max_ - :: arg '[null ty] lat with db params from - -- ^ argument - -> expr lat with db params from ('Null ty) - - -- | minimum value of expression across all input values - min_ - :: arg '[null ty] lat with db params from - -- ^ argument - -> expr lat with db params from ('Null ty) - - -- | the average (arithmetic mean) of all input values - avg - :: arg '[null ty] lat with db params from - -- ^ argument - -> expr lat with db params from ('Null (PGAvg ty)) - - {- | correlation coefficient - - >>> :{ - let - expression :: Expression ('Grouped g) '[] c s p '[t ::: '["x" ::: 'NotNull 'PGfloat8, "y" ::: 'NotNull 'PGfloat8]] ('Null 'PGfloat8) - expression = corr (Alls (#y *: #x)) - in printSQL expression - :} - corr(ALL "y", "x") - -} - corr - :: arg '[null 'PGfloat8, null 'PGfloat8] lat with db params from - -- ^ arguments - -> expr lat with db params from ('Null 'PGfloat8) - - {- | population covariance - - >>> :{ - let - expression :: Expression ('Grouped g) '[] c s p '[t ::: '["x" ::: 'NotNull 'PGfloat8, "y" ::: 'NotNull 'PGfloat8]] ('Null 'PGfloat8) - expression = covarPop (Alls (#y *: #x)) - in printSQL expression - :} - covar_pop(ALL "y", "x") - -} - covarPop - :: arg '[null 'PGfloat8, null 'PGfloat8] lat with db params from - -- ^ arguments - -> expr lat with db params from ('Null 'PGfloat8) - - {- | sample covariance - - >>> :{ - let - winFun :: WindowFunction 'Ungrouped '[] c s p '[t ::: '["x" ::: 'NotNull 'PGfloat8, "y" ::: 'NotNull 'PGfloat8]] ('Null 'PGfloat8) - winFun = covarSamp (Windows (#y *: #x)) - in printSQL winFun - :} - covar_samp("y", "x") - -} - covarSamp - :: arg '[null 'PGfloat8, null 'PGfloat8] lat with db params from - -- ^ arguments - -> expr lat with db params from ('Null 'PGfloat8) - - {- | average of the independent variable (sum(X)/N) - - >>> :{ - let - expression :: Expression ('Grouped g) '[] c s p '[t ::: '["x" ::: 'NotNull 'PGfloat8, "y" ::: 'NotNull 'PGfloat8]] ('Null 'PGfloat8) - expression = regrAvgX (Alls (#y *: #x)) - in printSQL expression - :} - regr_avgx(ALL "y", "x") - -} - regrAvgX - :: arg '[null 'PGfloat8, null 'PGfloat8] lat with db params from - -- ^ arguments - -> expr lat with db params from ('Null 'PGfloat8) - - {- | average of the dependent variable (sum(Y)/N) - - >>> :{ - let - winFun :: WindowFunction 'Ungrouped '[] c s p '[t ::: '["x" ::: 'NotNull 'PGfloat8, "y" ::: 'NotNull 'PGfloat8]] ('Null 'PGfloat8) - winFun = regrAvgY (Windows (#y *: #x)) - in printSQL winFun - :} - regr_avgy("y", "x") - -} - regrAvgY - :: arg '[null 'PGfloat8, null 'PGfloat8] lat with db params from - -- ^ arguments - -> expr lat with db params from ('Null 'PGfloat8) - - {- | number of input rows in which both expressions are nonnull - - >>> :{ - let - winFun :: WindowFunction 'Ungrouped '[] c s p '[t ::: '["x" ::: 'NotNull 'PGfloat8, "y" ::: 'NotNull 'PGfloat8]] ('Null 'PGint8) - winFun = regrCount (Windows (#y *: #x)) - in printSQL winFun - :} - regr_count("y", "x") - -} - regrCount - :: arg '[null 'PGfloat8, null 'PGfloat8] lat with db params from - -- ^ arguments - -> expr lat with db params from ('Null 'PGint8) - - {- | y-intercept of the least-squares-fit linear equation determined by the (X, Y) pairs - - >>> :{ - let - expression :: Expression ('Grouped g) '[] c s p '[t ::: '["x" ::: 'NotNull 'PGfloat8, "y" ::: 'NotNull 'PGfloat8]] ('Null 'PGfloat8) - expression = regrIntercept (Alls (#y *: #x)) - in printSQL expression - :} - regr_intercept(ALL "y", "x") - -} - regrIntercept - :: arg '[null 'PGfloat8, null 'PGfloat8] lat with db params from - -- ^ arguments - -> expr lat with db params from ('Null 'PGfloat8) - - -- | @regr_r2(Y, X)@, square of the correlation coefficient - regrR2 - :: arg '[null 'PGfloat8, null 'PGfloat8] lat with db params from - -- ^ arguments - -> expr lat with db params from ('Null 'PGfloat8) - - -- | @regr_slope(Y, X)@, slope of the least-squares-fit linear equation - -- determined by the (X, Y) pairs - regrSlope - :: arg '[null 'PGfloat8, null 'PGfloat8] lat with db params from - -- ^ arguments - -> expr lat with db params from ('Null 'PGfloat8) - - -- | @regr_sxx(Y, X)@, sum(X^2) - sum(X)^2/N - -- (“sum of squares” of the independent variable) - regrSxx - :: arg '[null 'PGfloat8, null 'PGfloat8] lat with db params from - -- ^ arguments - -> expr lat with db params from ('Null 'PGfloat8) - - -- | @regr_sxy(Y, X)@, sum(X*Y) - sum(X) * sum(Y)/N - -- (“sum of products” of independent times dependent variable) - regrSxy - :: arg '[null 'PGfloat8, null 'PGfloat8] lat with db params from - -- ^ arguments - -> expr lat with db params from ('Null 'PGfloat8) - - -- | @regr_syy(Y, X)@, sum(Y^2) - sum(Y)^2/N - -- (“sum of squares” of the dependent variable) - regrSyy - :: arg '[null 'PGfloat8, null 'PGfloat8] lat with db params from - -- ^ arguments - -> expr lat with db params from ('Null 'PGfloat8) - - -- | historical alias for `stddevSamp` - stddev - :: arg '[null ty] lat with db params from - -- ^ argument - -> expr lat with db params from ('Null (PGAvg ty)) - - -- | population standard deviation of the input values - stddevPop - :: arg '[null ty] lat with db params from - -- ^ argument - -> expr lat with db params from ('Null (PGAvg ty)) - - -- | sample standard deviation of the input values - stddevSamp - :: arg '[null ty] lat with db params from - -- ^ argument - -> expr lat with db params from ('Null (PGAvg ty)) - - -- | historical alias for `varSamp` - variance - :: arg '[null ty] lat with db params from - -- ^ argument - -> expr lat with db params from ('Null (PGAvg ty)) - - -- | population variance of the input values - -- (square of the population standard deviation) - varPop - :: arg '[null ty] lat with db params from - -- ^ argument - -> expr lat with db params from ('Null (PGAvg ty)) - - -- | sample variance of the input values - -- (square of the sample standard deviation) - varSamp - :: arg '[null ty] lat with db params from - -- ^ argument - -> expr lat with db params from ('Null (PGAvg ty)) - -{- | -`AggregateArg`s are used for the input of `Aggregate` `Expression`s. --} -data AggregateArg - (xs :: [NullType]) - (lat :: FromType) - (with :: FromType) - (db :: SchemasType) - (params :: [NullType]) - (from :: FromType) - = AggregateAll - { aggregateArgs :: NP (Expression 'Ungrouped lat with db params from) xs - , aggregateOrder :: [SortExpression 'Ungrouped lat with db params from] - -- ^ `orderBy` - , aggregateFilter :: [Condition 'Ungrouped lat with db params from] - -- ^ `filterWhere` - } - | AggregateDistinct - { aggregateArgs :: NP (Expression 'Ungrouped lat with db params from) xs - , aggregateOrder :: [SortExpression 'Ungrouped lat with db params from] - -- ^ `orderBy` - , aggregateFilter :: [Condition 'Ungrouped lat with db params from] - -- ^ `filterWhere` - } - -instance (HasUnique tab (Join from lat) row, Has col row ty) - => IsLabel col (AggregateArg '[ty] lat with db params from) where - fromLabel = All (fromLabel @col) -instance (Has tab (Join from lat) row, Has col row ty) - => IsQualified tab col (AggregateArg '[ty] lat with db params from) where - tab ! col = All (tab ! col) - -instance SOP.SListI xs => RenderSQL (AggregateArg xs lat with db params from) where - renderSQL = \case - AggregateAll args sorts filters -> - parenthesized - ("ALL" <+> renderCommaSeparated renderSQL args<> renderSQL sorts) - <> renderFilters filters - AggregateDistinct args sorts filters -> - parenthesized - ("DISTINCT" <+> renderCommaSeparated renderSQL args <> renderSQL sorts) - <> renderFilters filters - where - renderFilter wh = "FILTER" <+> parenthesized ("WHERE" <+> wh) - renderFilters = \case - [] -> "" - wh:whs -> " " <> renderFilter (renderSQL (foldr (.&&) wh whs)) - -instance OrderBy (AggregateArg xs) 'Ungrouped where - orderBy sorts1 = \case - AggregateAll xs sorts0 whs -> AggregateAll xs (sorts0 ++ sorts1) whs - AggregateDistinct xs sorts0 whs -> AggregateDistinct xs (sorts0 ++ sorts1) whs - --- | `All` invokes the aggregate on a single --- argument once for each input row. -pattern All - :: Expression 'Ungrouped lat with db params from x - -- ^ argument - -> AggregateArg '[x] lat with db params from -pattern All x = Alls (x :* Nil) - --- | `All` invokes the aggregate on multiple --- arguments once for each input row. -pattern Alls - :: NP (Expression 'Ungrouped lat with db params from) xs - -- ^ arguments - -> AggregateArg xs lat with db params from -pattern Alls xs = AggregateAll xs [] [] - --- | `allNotNull` invokes the aggregate on a single --- argument once for each input row where the argument --- is not null -allNotNull - :: Expression 'Ungrouped lat with db params from ('Null x) - -- ^ argument - -> AggregateArg '[ 'NotNull x] lat with db params from -allNotNull x = All (unsafeNotNull x) & filterWhere (not_ (isNull x)) - -{- | -`Distinct` invokes the aggregate once for each -distinct value of the expression found in the input. --} -pattern Distinct - :: Expression 'Ungrouped lat with db params from x - -- ^ argument - -> AggregateArg '[x] lat with db params from -pattern Distinct x = Distincts (x :* Nil) - -{- | -`Distincts` invokes the aggregate once for each -distinct set of values, for multiple expressions, found in the input. --} -pattern Distincts - :: NP (Expression 'Ungrouped lat with db params from) xs - -- ^ arguments - -> AggregateArg xs lat with db params from -pattern Distincts xs = AggregateDistinct xs [] [] - -{- | -`distinctNotNull` invokes the aggregate once for each -distinct, not null value of the expression found in the input. --} -distinctNotNull - :: Expression 'Ungrouped lat with db params from ('Null x) - -- ^ argument - -> AggregateArg '[ 'NotNull x] lat with db params from -distinctNotNull x = Distinct (unsafeNotNull x) & filterWhere (not_ (isNull x)) - --- | Permits filtering --- `Squeal.PostgreSQL.Expression.Window.WindowArg`s and `AggregateArg`s -class FilterWhere arg grp | arg -> grp where - {- | - If `filterWhere` is specified, then only the input rows for which - the `Condition` evaluates to true are fed to the aggregate function; - other rows are discarded. - -} - filterWhere - :: Condition grp lat with db params from - -- ^ include rows which evaluate to true - -> arg xs lat with db params from - -> arg xs lat with db params from -instance FilterWhere AggregateArg 'Ungrouped where - filterWhere wh = \case - AggregateAll xs sorts whs -> AggregateAll xs sorts (wh : whs) - AggregateDistinct xs sorts whs -> AggregateDistinct xs sorts (wh : whs) - -instance Aggregate AggregateArg (Expression ('Grouped bys)) where - countStar = UnsafeExpression "count(*)" - count = unsafeAggregate "count" - sum_ = unsafeAggregate "sum" - arrayAgg = unsafeAggregate "array_agg" - jsonAgg = unsafeAggregate "json_agg" - jsonbAgg = unsafeAggregate "jsonb_agg" - bitAnd = unsafeAggregate "bit_and" - bitOr = unsafeAggregate "bit_or" - boolAnd = unsafeAggregate "bool_and" - boolOr = unsafeAggregate "bool_or" - every = unsafeAggregate "every" - max_ = unsafeAggregate "max" - min_ = unsafeAggregate "min" - avg = unsafeAggregate "avg" - corr = unsafeAggregate "corr" - covarPop = unsafeAggregate "covar_pop" - covarSamp = unsafeAggregate "covar_samp" - regrAvgX = unsafeAggregate "regr_avgx" - regrAvgY = unsafeAggregate "regr_avgy" - regrCount = unsafeAggregate "regr_count" - regrIntercept = unsafeAggregate "regr_intercept" - regrR2 = unsafeAggregate "regr_r2" - regrSlope = unsafeAggregate "regr_slope" - regrSxx = unsafeAggregate "regr_sxx" - regrSxy = unsafeAggregate "regr_sxy" - regrSyy = unsafeAggregate "regr_syy" - stddev = unsafeAggregate "stddev" - stddevPop = unsafeAggregate "stddev_pop" - stddevSamp = unsafeAggregate "stddev_samp" - variance = unsafeAggregate "variance" - varPop = unsafeAggregate "var_pop" - varSamp = unsafeAggregate "var_samp" - --- provides a nicer type error when we forget to group by --- note that we need to make our 'a' polymorphic so that we can still match when it's ambiguous -instance ( TypeError ('Text "Cannot use aggregate functions to construct an Ungrouped Expression. Add a 'groupBy' to your TableExpression. If you want to aggregate across the entire result set, use 'groupBy Nil'.") - , a ~ AggregateArg - ) => Aggregate a (Expression 'Ungrouped) where - countStar = impossibleAggregateError - count = impossibleAggregateError - sum_ = impossibleAggregateError - arrayAgg = impossibleAggregateError - jsonAgg = impossibleAggregateError - jsonbAgg = impossibleAggregateError - bitAnd = impossibleAggregateError - bitOr = impossibleAggregateError - boolAnd = impossibleAggregateError - boolOr = impossibleAggregateError - every = impossibleAggregateError - max_ = impossibleAggregateError - min_ = impossibleAggregateError - avg = impossibleAggregateError - corr = impossibleAggregateError - covarPop = impossibleAggregateError - covarSamp = impossibleAggregateError - regrAvgX = impossibleAggregateError - regrAvgY = impossibleAggregateError - regrCount = impossibleAggregateError - regrIntercept = impossibleAggregateError - regrR2 = impossibleAggregateError - regrSlope = impossibleAggregateError - regrSxx = impossibleAggregateError - regrSxy = impossibleAggregateError - regrSyy = impossibleAggregateError - stddev = impossibleAggregateError - stddevPop = impossibleAggregateError - stddevSamp = impossibleAggregateError - variance = impossibleAggregateError - varPop = impossibleAggregateError - varSamp = impossibleAggregateError - --- | helper function for our errors above -impossibleAggregateError :: a -impossibleAggregateError = error "impossible; called aggregate function for Ungrouped even though the Aggregate instance has a type error constraint." - --- | escape hatch to define aggregate functions -unsafeAggregate - :: SOP.SListI xs - => ByteString -- ^ function - -> AggregateArg xs lat with db params from - -> Expression ('Grouped bys) lat with db params from y -unsafeAggregate fun xs = UnsafeExpression $ fun <> renderSQL xs - --- | A type family that calculates `PGSum` `PGType` of --- a given argument `PGType`. -type family PGSum ty where - PGSum 'PGint2 = 'PGint8 - PGSum 'PGint4 = 'PGint8 - PGSum 'PGint8 = 'PGnumeric - PGSum 'PGfloat4 = 'PGfloat4 - PGSum 'PGfloat8 = 'PGfloat8 - PGSum 'PGnumeric = 'PGnumeric - PGSum 'PGinterval = 'PGinterval - PGSum 'PGmoney = 'PGmoney - PGSum pg = TypeError - ( 'Text "Squeal type error: Cannot sum with argument type " - ':<>: 'ShowType pg ) - --- | A type family that calculates `PGAvg` type of a `PGType`. -type family PGAvg ty where - PGAvg 'PGint2 = 'PGnumeric - PGAvg 'PGint4 = 'PGnumeric - PGAvg 'PGint8 = 'PGnumeric - PGAvg 'PGnumeric = 'PGnumeric - PGAvg 'PGfloat4 = 'PGfloat8 - PGAvg 'PGfloat8 = 'PGfloat8 - PGAvg 'PGinterval = 'PGinterval - PGAvg pg = TypeError - ('Text "Squeal type error: No average for " ':<>: 'ShowType pg) diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Array.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Array.hs deleted file mode 100644 index f78f9558..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Array.hs +++ /dev/null @@ -1,242 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Expression.Array -Description: array functions -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -array functions --} - -{-# LANGUAGE - AllowAmbiguousTypes - , DataKinds - , FlexibleContexts - , FlexibleInstances - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeFamilies - , TypeOperators - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Expression.Array - ( -- * Array Functions - array - , array0 - , array1 - , array2 - , cardinality - , index - , index1 - , index2 - , unnest - , arrAny - , arrAll - ) where - -import Data.String -import Data.Word (Word64) -import GHC.TypeNats - -import qualified Generics.SOP as SOP - -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Expression.Logic -import Squeal.PostgreSQL.Expression.Type -import Squeal.PostgreSQL.Query.From.Set -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - --- | Construct an array. --- --- >>> printSQL $ array [null_, false, true] --- ARRAY[NULL, FALSE, TRUE] -array - :: [Expression grp lat with db params from ty] - -- ^ array elements - -> Expression grp lat with db params from (null ('PGvararray ty)) -array xs = UnsafeExpression $ "ARRAY" <> - bracketed (commaSeparated (renderSQL <$> xs)) - --- | Safely construct an empty array. --- --- >>> printSQL $ array0 text --- (ARRAY[] :: text[]) -array0 - :: TypeExpression db ty - -> Expression grp lat with db params from (null ('PGvararray ty)) -array0 ty = array [] & astype (vararray ty) - -{- | Construct a fixed length array. - ->>> printSQL $ array1 (null_ :* false *: true) -ARRAY[NULL, FALSE, TRUE] - ->>> :type array1 (null_ :* false *: true) -array1 (null_ :* false *: true) - :: Expression - grp - lat - with - db - params - from - (null ('PGfixarray '[3] ('Null 'PGbool))) --} -array1 - :: (n ~ Length tys, SOP.All ((~) ty) tys) - => NP (Expression grp lat with db params from) tys - -- ^ array elements - -> Expression grp lat with db params from (null ('PGfixarray '[n] ty)) -array1 xs = UnsafeExpression $ "ARRAY" <> - bracketed (renderCommaSeparated renderSQL xs) - -{- | Construct a fixed size matrix. - ->>> printSQL $ array2 ((null_ :* false *: true) *: (false :* null_ *: true)) -ARRAY[[NULL, FALSE, TRUE], [FALSE, NULL, TRUE]] - ->>> :type array2 ((null_ :* false *: true) *: (false :* null_ *: true)) -array2 ((null_ :* false *: true) *: (false :* null_ *: true)) - :: Expression - grp - lat - with - db - params - from - (null ('PGfixarray '[2, 3] ('Null 'PGbool))) --} -array2 - :: ( SOP.All ((~) tys) tyss - , SOP.All SOP.SListI tyss - , Length tyss ~ n1 - , SOP.All ((~) ty) tys - , Length tys ~ n2 ) - => NP (NP (Expression grp lat with db params from)) tyss - -- ^ matrix elements - -> Expression grp lat with db params from (null ('PGfixarray '[n1,n2] ty)) -array2 xss = UnsafeExpression $ "ARRAY" <> - bracketed (renderCommaSeparatedConstraint @SOP.SListI (bracketed . renderCommaSeparated renderSQL) xss) - --- | >>> printSQL $ cardinality (array [null_, false, true]) --- cardinality(ARRAY[NULL, FALSE, TRUE]) -cardinality :: null ('PGvararray ty) --> null 'PGint8 -cardinality = unsafeFunction "cardinality" - --- | >>> printSQL $ array [null_, false, true] & index 2 --- (ARRAY[NULL, FALSE, TRUE])[2] -index - :: Word64 -- ^ index - -> null ('PGvararray ty) --> NullifyType ty -index i arr = UnsafeExpression $ - parenthesized (renderSQL arr) <> "[" <> fromString (show i) <> "]" - --- | Typesafe indexing of fixed length arrays. --- --- >>> printSQL $ array1 (true *: false) & index1 @1 --- (ARRAY[TRUE, FALSE])[1] -index1 - :: forall i n ty - . (1 <= i, i <= n, KnownNat i) - => 'NotNull ('PGfixarray '[n] ty) --> ty - -- ^ vector index -index1 arr = UnsafeExpression $ - parenthesized (renderSQL arr) - <> "[" <> fromString (show (natVal (SOP.Proxy @i))) <> "]" - --- | Typesafe indexing of fixed size matrices. --- --- >>> printSQL $ array2 ((true *: false) *: (false *: true)) & index2 @1 @2 --- (ARRAY[[TRUE, FALSE], [FALSE, TRUE]])[1][2] -index2 - :: forall i j m n ty - . ( 1 <= i, i <= m, KnownNat i - , 1 <= j, j <= n, KnownNat j - ) - => 'NotNull ('PGfixarray '[m,n] ty) --> ty - -- ^ matrix index -index2 arr = UnsafeExpression $ - parenthesized (renderSQL arr) - <> "[" <> fromString (show (natVal (SOP.Proxy @i))) <> "]" - <> "[" <> fromString (show (natVal (SOP.Proxy @j))) <> "]" - --- | Expand an array to a set of rows --- --- >>> printSQL $ unnest (array [null_, false, true]) --- unnest(ARRAY[NULL, FALSE, TRUE]) -unnest :: null ('PGvararray ty) -|-> ("unnest" ::: '["unnest" ::: ty]) -unnest = unsafeSetFunction "unnest" - -{- | -The right-hand side is a parenthesized expression, -which must yield an array value. The left-hand expression -is evaluated and compared to each element of the array using -the given `Operator`, which must yield a Boolean result. -The result of `arrAll` is `true` if all comparisons yield true -(including the case where the array has zero elements). -The result is `false` if any false result is found. - -If the array expression yields a null array, -the result of `arrAll` will be null. If the left-hand expression yields null, -the result of `arrAll` is ordinarily null -(though a non-strict comparison `Operator` -could possibly yield a different result). -Also, if the right-hand array contains any null -elements and no false comparison result is obtained, -the result of `arrAll` will be null, not true -(again, assuming a strict comparison `Operator`). -This is in accordance with SQL's normal rules for Boolean -combinations of null values. - ->>> printSQL $ arrAll true (.==) (array [true, false, null_]) -(TRUE = ALL (ARRAY[TRUE, FALSE, NULL])) ->>> printSQL $ arrAll "hi" like (array ["bi","hi"]) -((E'hi' :: text) LIKE ALL (ARRAY[(E'bi' :: text), (E'hi' :: text)])) --} -arrAll - :: Expression grp lat with db params from ty1 -- ^ expression - -> Operator ty1 ty2 ('Null 'PGbool) -- ^ operator - -> Expression grp lat with db params from ('Null ('PGvararray ty2)) -- ^ array - -> Condition grp lat with db params from -arrAll x (?) xs = x ? (UnsafeExpression $ "ALL" <+> parenthesized (renderSQL xs)) - -{- | -The right-hand side is a parenthesized expression, which must yield an array -value. The left-hand expression is evaluated and compared to each element of -the array using the given `Operator`, which must yield a Boolean result. The -result of `arrAny` is `true` if any true result is obtained. The result is -`false` if no true result is found (including the case where the array -has zero elements). - -If the array expression yields a null array, the result of `arrAny` will -be null. If the left-hand expression yields null, the result of `arrAny` is -ordinarily null (though a non-strict comparison `Operator` could possibly -yield a different result). Also, if the right-hand array contains any -null elements and no true comparison result is obtained, the result of -`arrAny` will be null, not false -(again, assuming a strict comparison `Operator`). -This is in accordance with SQL's normal rules for -Boolean combinations of null values. - ->>> printSQL $ arrAny true (.==) (array [true, false, null_]) -(TRUE = ANY (ARRAY[TRUE, FALSE, NULL])) ->>> printSQL $ arrAny "hi" like (array ["bi","hi"]) -((E'hi' :: text) LIKE ANY (ARRAY[(E'bi' :: text), (E'hi' :: text)])) --} -arrAny - :: Expression grp lat with db params from ty1 -- ^ expression - -> Operator ty1 ty2 ('Null 'PGbool) -- ^ operator - -> Expression grp lat with db params from ('Null ('PGvararray ty2)) -- ^ array - -> Condition grp lat with db params from -arrAny x (?) xs = x ? (UnsafeExpression $ "ANY" <+> parenthesized (renderSQL xs)) diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Comparison.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Comparison.hs deleted file mode 100644 index 41c016a9..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Comparison.hs +++ /dev/null @@ -1,210 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Expression.Comparison -Description: comparison functions and operators -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -comparison functions and operators --} - -{-# LANGUAGE - OverloadedStrings - , RankNTypes - , TypeInType - , TypeOperators -#-} - -module Squeal.PostgreSQL.Expression.Comparison - ( -- * Comparison Operators - (.==) - , (./=) - , (.>=) - , (.<) - , (.<=) - , (.>) - -- * Comparison Functions - , greatest - , least - -- * Between - , BetweenExpr - , between - , notBetween - , betweenSymmetric - , notBetweenSymmetric - -- * Null Comparison - , isDistinctFrom - , isNotDistinctFrom - , isTrue - , isNotTrue - , isFalse - , isNotFalse - , isUnknown - , isNotUnknown - ) where - -import Data.ByteString - -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Expression.Logic -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - --- | Comparison operations like `.==`, `./=`, `.>`, `.>=`, `.<` and `.<=` --- will produce @NULL@s if one of their arguments is @NULL@. --- --- >>> printSQL $ true .== null_ --- (TRUE = NULL) -(.==) :: Operator (null0 ty) (null1 ty) ('Null 'PGbool) -(.==) = unsafeBinaryOp "=" -infix 4 .== - --- | >>> printSQL $ true ./= null_ --- (TRUE <> NULL) -(./=) :: Operator (null0 ty) (null1 ty) ('Null 'PGbool) -(./=) = unsafeBinaryOp "<>" -infix 4 ./= - --- | >>> printSQL $ true .>= null_ --- (TRUE >= NULL) -(.>=) :: Operator (null0 ty) (null1 ty) ('Null 'PGbool) -(.>=) = unsafeBinaryOp ">=" -infix 4 .>= - --- | >>> printSQL $ true .< null_ --- (TRUE < NULL) -(.<) :: Operator (null0 ty) (null1 ty) ('Null 'PGbool) -(.<) = unsafeBinaryOp "<" -infix 4 .< - --- | >>> printSQL $ true .<= null_ --- (TRUE <= NULL) -(.<=) :: Operator (null0 ty) (null1 ty) ('Null 'PGbool) -(.<=) = unsafeBinaryOp "<=" -infix 4 .<= - --- | >>> printSQL $ true .> null_ --- (TRUE > NULL) -(.>) :: Operator (null0 ty) (null1 ty) ('Null 'PGbool) -(.>) = unsafeBinaryOp ">" -infix 4 .> - --- | >>> let expr = greatest [param @1] currentTimestamp --- >>> printSQL expr --- GREATEST(($1 :: timestamp with time zone), CURRENT_TIMESTAMP) -greatest :: FunctionVar ty ty ty -greatest = unsafeFunctionVar "GREATEST" - --- | >>> printSQL $ least [null_] currentTimestamp --- LEAST(NULL, CURRENT_TIMESTAMP) -least :: FunctionVar ty ty ty -least = unsafeFunctionVar "LEAST" - -{- | -A @RankNType@ for comparison expressions like `between`. --} -type BetweenExpr - = forall grp lat with db params from ty - . Expression grp lat with db params from ty - -> ( Expression grp lat with db params from ty - , Expression grp lat with db params from ty ) -- ^ bounds - -> Condition grp lat with db params from - -unsafeBetweenExpr :: ByteString -> BetweenExpr -unsafeBetweenExpr fun a (x,y) = UnsafeExpression $ - renderSQL a <+> fun <+> renderSQL x <+> "AND" <+> renderSQL y - -{- | >>> printSQL $ true `between` (null_, false) -TRUE BETWEEN NULL AND FALSE --} -between :: BetweenExpr -between = unsafeBetweenExpr "BETWEEN" - -{- | >>> printSQL $ true `notBetween` (null_, false) -TRUE NOT BETWEEN NULL AND FALSE --} -notBetween :: BetweenExpr -notBetween = unsafeBetweenExpr "NOT BETWEEN" - -{- | between, after sorting the comparison values - ->>> printSQL $ true `betweenSymmetric` (null_, false) -TRUE BETWEEN SYMMETRIC NULL AND FALSE --} -betweenSymmetric :: BetweenExpr -betweenSymmetric = unsafeBetweenExpr "BETWEEN SYMMETRIC" - -{- | not between, after sorting the comparison values - ->>> printSQL $ true `notBetweenSymmetric` (null_, false) -TRUE NOT BETWEEN SYMMETRIC NULL AND FALSE --} -notBetweenSymmetric :: BetweenExpr -notBetweenSymmetric = unsafeBetweenExpr "NOT BETWEEN SYMMETRIC" - -{- | not equal, treating null like an ordinary value - ->>> printSQL $ true `isDistinctFrom` null_ -(TRUE IS DISTINCT FROM NULL) --} -isDistinctFrom :: Operator (null0 ty) (null1 ty) (null 'PGbool) -isDistinctFrom = unsafeBinaryOp "IS DISTINCT FROM" - -{- | equal, treating null like an ordinary value - ->>> printSQL $ true `isNotDistinctFrom` null_ -(TRUE IS NOT DISTINCT FROM NULL) --} -isNotDistinctFrom :: Operator (null0 ty) (null1 ty) (null 'PGbool) -isNotDistinctFrom = unsafeBinaryOp "IS NOT DISTINCT FROM" - -{- | is true - ->>> printSQL $ true & isTrue -(TRUE IS TRUE) --} -isTrue :: null0 'PGbool --> null1 'PGbool -isTrue = unsafeRightOp "IS TRUE" - -{- | is false or unknown - ->>> printSQL $ true & isNotTrue -(TRUE IS NOT TRUE) --} -isNotTrue :: null0 'PGbool --> null1 'PGbool -isNotTrue = unsafeRightOp "IS NOT TRUE" - -{- | is false - ->>> printSQL $ true & isFalse -(TRUE IS FALSE) --} -isFalse :: null0 'PGbool --> null1 'PGbool -isFalse = unsafeRightOp "IS FALSE" - -{- | is true or unknown - ->>> printSQL $ true & isNotFalse -(TRUE IS NOT FALSE) --} -isNotFalse :: null0 'PGbool --> null1 'PGbool -isNotFalse = unsafeRightOp "IS NOT FALSE" - -{- | is unknown - ->>> printSQL $ true & isUnknown -(TRUE IS UNKNOWN) --} -isUnknown :: null0 'PGbool --> null1 'PGbool -isUnknown = unsafeRightOp "IS UNKNOWN" - -{- | is true or false - ->>> printSQL $ true & isNotUnknown -(TRUE IS NOT UNKNOWN) --} -isNotUnknown :: null0 'PGbool --> null1 'PGbool -isNotUnknown = unsafeRightOp "IS NOT UNKNOWN" diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Composite.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Composite.hs deleted file mode 100644 index 30e4a32e..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Composite.hs +++ /dev/null @@ -1,93 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Expression.Composite -Description: composite functions -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -composite functions --} - -{-# LANGUAGE - AllowAmbiguousTypes - , DataKinds - , FlexibleContexts - , FlexibleInstances - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeFamilies - , TypeOperators - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Expression.Composite - ( -- * Composite Functions - row - , rowStar - , field - ) where - -import qualified Generics.SOP as SOP - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - --- | A row constructor is an expression that builds a row value --- (also called a composite value) using values for its member fields. --- --- >>> :{ --- type Complex = 'PGcomposite --- '[ "real" ::: 'NotNull 'PGfloat8 --- , "imaginary" ::: 'NotNull 'PGfloat8 ] --- :} --- --- >>> let i = row (0 `as` #real :* 1 `as` #imaginary) :: Expression grp lat with db params from ('NotNull Complex) --- >>> printSQL i --- ROW((0.0 :: float8), (1.0 :: float8)) -row - :: SOP.SListI row - => NP (Aliased (Expression grp lat with db params from)) row - -- ^ zero or more expressions for the row field values - -> Expression grp lat with db params from (null ('PGcomposite row)) -row exprs = UnsafeExpression $ "ROW" <> parenthesized - (renderCommaSeparated (\ (expr `As` _) -> renderSQL expr) exprs) - --- | A row constructor on all columns in a table expression. -rowStar - :: Has tab from row - => Alias tab -- ^ intermediate table - -> Expression grp lat with db params from (null ('PGcomposite row)) -rowStar tab = UnsafeExpression $ "ROW" <> - parenthesized (renderSQL tab <> ".*") - --- | >>> :{ --- type Complex = 'PGcomposite --- '[ "real" ::: 'NotNull 'PGfloat8 --- , "imaginary" ::: 'NotNull 'PGfloat8 ] --- type Schema = '["complex" ::: 'Typedef Complex] --- :} --- --- >>> let i = row (0 `as` #real :* 1 `as` #imaginary) :: Expression lat '[] grp (Public Schema) from params ('NotNull Complex) --- >>> printSQL $ i & field #complex #imaginary --- (ROW((0.0 :: float8), (1.0 :: float8))::"complex")."imaginary" -field - :: ( Has sch db schema - , Has tydef schema ('Typedef ('PGcomposite row)) - , Has field row ty) - => QualifiedAlias sch tydef -- ^ row type - -> Alias field -- ^ field name - -> Expression grp lat with db params from ('NotNull ('PGcomposite row)) - -> Expression grp lat with db params from ty -field td fld expr = UnsafeExpression $ - parenthesized (renderSQL expr <> "::" <> renderSQL td) - <> "." <> renderSQL fld diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Default.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Default.hs deleted file mode 100644 index 0dc36863..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Default.hs +++ /dev/null @@ -1,60 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Expression.Default -Description: optional expressions -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -optional expressions --} - -{-# LANGUAGE - DataKinds - , GADTs - , LambdaCase - , OverloadedStrings - , PatternSynonyms - , PolyKinds - , QuantifiedConstraints - , RankNTypes - , TypeOperators -#-} - -module Squeal.PostgreSQL.Expression.Default - ( -- * Default - Optional (..) - , mapOptional - , pattern NotDefault - ) where - -import Data.Kind -import Generics.SOP - -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- | `Optional` is either `Default` or `Set`ting of a value, --- parameterized by an appropriate `Optionality`. -data Optional (expr :: k -> Type) (ty :: (Optionality, k)) where - -- | Use the `Default` value for a column. - Default :: Optional expr ('Def :=> ty) - -- | `Set` a value for a column. - Set :: expr ty -> Optional expr (def :=> ty) - -instance (forall x. RenderSQL (expr x)) => RenderSQL (Optional expr ty) where - renderSQL = \case - Default -> "DEFAULT" - Set x -> renderSQL x - --- | Map a function over an `Optional` expression. -mapOptional - :: (expr x -> expr y) - -> Optional expr (def :=> x) - -> Optional expr (def :=> y) -mapOptional f = \case - Default -> Default - Set x -> Set (f x) - --- | `NotDefault` pattern analagous to `Just`. -pattern NotDefault :: ty -> Optional I ('Def :=> ty) -pattern NotDefault x = Set (I x) diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Inline.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Inline.hs deleted file mode 100644 index 0e6de289..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Inline.hs +++ /dev/null @@ -1,363 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Expression.Inline -Description: inline expressions -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -inline expressions --} - -{-# LANGUAGE - DataKinds - , FlexibleContexts - , FlexibleInstances - , LambdaCase - , MultiParamTypeClasses - , MultiWayIf - , OverloadedStrings - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeFamilies - , TypeOperators - , TypeSynonymInstances - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Expression.Inline - ( -- * Inline - Inline (..) - , InlineParam (..) - , InlineField (..) - , inlineFields - , InlineColumn (..) - , inlineColumns - ) where - -import Data.Binary.Builder (toLazyByteString) -import Data.ByteString.Lazy (toStrict) -import Data.ByteString.Builder (doubleDec, floatDec, int16Dec, int32Dec, int64Dec) -import Data.ByteString.Builder.Scientific (scientificBuilder) -import Data.Functor.Const (Const(Const)) -import Data.Functor.Constant (Constant(Constant)) -import Data.Int (Int16, Int32, Int64) -import Data.Kind (Type) -import Data.Scientific (Scientific) -import Data.String -import Data.Text (Text) -import Data.Time.Clock (DiffTime, diffTimeToPicoseconds, UTCTime) -import Data.Time.Format.ISO8601 (formatShow, timeOfDayAndOffsetFormat, FormatExtension(ExtendedFormat), iso8601Show) -import Data.Time.Calendar (Day) -import Data.Time.LocalTime (LocalTime, TimeOfDay, TimeZone) -import Data.UUID.Types (UUID, toASCIIBytes) -import Data.Vector (Vector, toList) -import Database.PostgreSQL.LibPQ (Oid(Oid)) -import GHC.TypeLits - -import qualified Data.Aeson as JSON -import qualified Data.Text as Text -import qualified Data.Text.Lazy as Lazy (Text) -import qualified Data.Text.Lazy as Lazy.Text -import qualified Generics.SOP as SOP -import qualified Generics.SOP.Record as SOP - -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Expression.Array -import Squeal.PostgreSQL.Expression.Default -import Squeal.PostgreSQL.Expression.Composite -import Squeal.PostgreSQL.Expression.Logic -import Squeal.PostgreSQL.Expression.Null -import Squeal.PostgreSQL.Expression.Range -import Squeal.PostgreSQL.Expression.Time -import Squeal.PostgreSQL.Expression.Type -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Type.PG -import Squeal.PostgreSQL.Type.Schema - -{- | -The `Inline` class allows embedding a Haskell value directly -as an `Expression` using `inline`. - ->>> printSQL (inline 'a') -(E'a' :: char(1)) - ->>> printSQL (inline (1 :: Double)) -(1.0 :: float8) - ->>> printSQL (inline (Json ([1, 2] :: [Double]))) -('[1.0,2.0]' :: json) - ->>> printSQL (inline (Enumerated GT)) -'GT' --} -class Inline x where inline :: x -> Expr (null (PG x)) -instance Inline Bool where - inline = \case - True -> true - False -> false -instance JSON.ToJSON x => Inline (Json x) where - inline (Json x) - = inferredtype - . UnsafeExpression - . singleQuotedUtf8 - . toStrict - . JSON.encode - $ x -instance JSON.ToJSON x => Inline (Jsonb x) where - inline (Jsonb x) - = inferredtype - . UnsafeExpression - . singleQuotedUtf8 - . toStrict - . JSON.encode - $ x -instance Inline Char where - inline chr = inferredtype . UnsafeExpression $ - "E\'" <> fromString (escape chr) <> "\'" -instance Inline String where inline x = fromString x -instance Inline Int16 where - inline x - = inferredtype - . UnsafeExpression - . toStrict - . toLazyByteString - . int16Dec - $ x -instance Inline Int32 where - inline x - = inferredtype - . UnsafeExpression - . toStrict - . toLazyByteString - . int32Dec - $ x -instance Inline Int64 where - inline x = - if x == minBound - -- For some reason Postgres throws an error with - -- (-9223372036854775808 :: int8) - -- even though it's a valid lowest value for int8 - then inline (x+1) - 1 - else inferredtype - . UnsafeExpression - . toStrict - . toLazyByteString - $ int64Dec x -instance Inline Float where - inline x = inferredtype . UnsafeExpression $ - if isNaN x || isInfinite x - then singleQuotedUtf8 (decimal x) - else decimal x - where - decimal = toStrict . toLazyByteString . floatDec -instance Inline Double where - inline x = inferredtype . UnsafeExpression $ - if isNaN x || isInfinite x - then singleQuotedUtf8 (decimal x) - else decimal x - where - decimal = toStrict . toLazyByteString . doubleDec -instance Inline Scientific where - inline x - = inferredtype - . UnsafeExpression - . toStrict - . toLazyByteString - . scientificBuilder - $ x -instance Inline Text where inline x = fromString . Text.unpack $ x -instance Inline Lazy.Text where inline x = fromString . Lazy.Text.unpack $ x -instance (KnownNat n, 1 <= n) => Inline (VarChar n) where - inline x - = inferredtype - . UnsafeExpression - . escapeQuotedText - . getVarChar - $ x -instance (KnownNat n, 1 <= n) => Inline (FixChar n) where - inline x - = inferredtype - . UnsafeExpression - . escapeQuotedText - . getFixChar - $ x -instance Inline x => Inline (Const x tag) where inline (Const x) = inline x -instance Inline x => Inline (SOP.K x tag) where inline (SOP.K x) = inline x -instance Inline x => Inline (Constant x tag) where - inline (Constant x) = inline x -instance Inline DiffTime where - inline dt = - let - picosecs = diffTimeToPicoseconds dt - (secs,leftover) = picosecs `quotRem` 1000000000000 - microsecs = leftover `quot` 1000000 - in - inferredtype $ - interval_ (fromIntegral secs) Seconds - +! interval_ (fromIntegral microsecs) Microseconds -instance Inline Day where - inline x - = inferredtype - . UnsafeExpression - . singleQuotedUtf8 - . fromString - . iso8601Show - $ x -instance Inline UTCTime where - inline x - = inferredtype - . UnsafeExpression - . singleQuotedUtf8 - . fromString - . iso8601Show - $ x -instance Inline (TimeOfDay, TimeZone) where - inline x - = inferredtype - . UnsafeExpression - . singleQuotedUtf8 - . fromString - . formatShow (timeOfDayAndOffsetFormat ExtendedFormat) - $ x -instance Inline TimeOfDay where - inline x - = inferredtype - . UnsafeExpression - . singleQuotedUtf8 - . fromString - . iso8601Show - $ x -instance Inline LocalTime where - inline x - = inferredtype - . UnsafeExpression - . singleQuotedUtf8 - . fromString - . iso8601Show - $ x -instance Inline (Range Int32) where - inline x = range int4range . fmap (\y -> inline y) $ x -instance Inline (Range Int64) where - inline x = range int8range . fmap (\y -> inline y) $ x -instance Inline (Range Scientific) where - inline x = range numrange . fmap (\y -> inline y) $ x -instance Inline (Range LocalTime) where - inline x = range tsrange . fmap (\y -> inline y) $ x -instance Inline (Range UTCTime) where - inline x = range tstzrange . fmap (\y -> inline y) $ x -instance Inline (Range Day) where - inline x = range daterange . fmap (\y -> inline y) $ x -instance Inline UUID where - inline x - = inferredtype - . UnsafeExpression - . singleQuotedUtf8 - . toASCIIBytes - $ x -instance Inline Money where - inline moolah = inferredtype . UnsafeExpression $ - fromString (show dollars) - <> "." <> fromString (show pennies) - where - (dollars,pennies) = cents moolah `divMod` 100 -instance InlineParam x (NullPG x) - => Inline (VarArray [x]) where - inline (VarArray xs) = array ((\x -> inlineParam x) <$> xs) -instance InlineParam x (NullPG x) - => Inline (VarArray (Vector x)) where - inline (VarArray xs) = array ((\x -> inlineParam x) <$> toList xs) -instance Inline Oid where - inline (Oid o) = inferredtype . UnsafeExpression . fromString $ show o -instance - ( SOP.IsEnumType x - , SOP.HasDatatypeInfo x - ) => Inline (Enumerated x) where - inline (Enumerated x) = - let - gshowConstructor - :: NP SOP.ConstructorInfo xss - -> SOP.SOP SOP.I xss - -> String - gshowConstructor Nil _ = "" - gshowConstructor (constructor :* _) (SOP.SOP (SOP.Z _)) = - SOP.constructorName constructor - gshowConstructor (_ :* constructors) (SOP.SOP (SOP.S xs)) = - gshowConstructor constructors (SOP.SOP xs) - in - UnsafeExpression - . singleQuotedUtf8 - . fromString - . gshowConstructor - (SOP.constructorInfo (SOP.datatypeInfo (SOP.Proxy @x))) - . SOP.from - $ x -instance - ( SOP.IsRecord x xs - , SOP.AllZip InlineField xs (RowPG x) - ) => Inline (Composite x) where - inline (Composite x) - = row - . SOP.htrans (SOP.Proxy @InlineField) inlineField - . SOP.toRecord - $ x - --- | Lifts `Inline` to `NullType`s. -class InlineParam x ty where inlineParam :: x -> Expr ty -instance (Inline x, pg ~ PG x) => InlineParam x ('NotNull pg) where inlineParam = inline -instance (Inline x, pg ~ PG x) => InlineParam (Maybe x) ('Null pg) where - inlineParam x = maybe null_ (\y -> inline y) x - --- | Lifts `Inline` to fields. -class InlineField - (field :: (Symbol, Type)) - (fieldpg :: (Symbol, NullType)) where - inlineField - :: SOP.P field - -> Aliased (Expression grp lat with db params from) fieldpg -instance (KnownSymbol alias, InlineParam x ty) - => InlineField (alias ::: x) (alias ::: ty) where - inlineField (SOP.P x) = inlineParam x `as` Alias @alias - --- | Inline a Haskell record as a row of expressions. -inlineFields - :: ( SOP.IsRecord hask fields - , SOP.AllZip InlineField fields row ) - => hask -- ^ record - -> NP (Aliased (Expression 'Ungrouped '[] with db params '[])) row -inlineFields - = SOP.htrans (SOP.Proxy @InlineField) inlineField - . SOP.toRecord - - --- | Lifts `Inline` to a column entry -class InlineColumn - (field :: (Symbol, Type)) - (column :: (Symbol, ColumnType)) where - -- | Haskell record field as a inline column - inlineColumn - :: SOP.P field - -> Aliased (Optional (Expression grp lat with db params from)) column -instance (KnownSymbol col, InlineParam x ty) - => InlineColumn (col ::: x) (col ::: 'NoDef :=> ty) where - inlineColumn (SOP.P x) = Set (inlineParam x) `as` (Alias @col) -instance (KnownSymbol col, InlineParam x ty) - => InlineColumn - (col ::: Optional SOP.I ('Def :=> x)) - (col ::: 'Def :=> ty) where - inlineColumn (SOP.P optional) = case optional of - Default -> Default `as` (Alias @col) - Set (SOP.I x) -> Set (inlineParam x) `as` (Alias @col) - --- | Inline a Haskell record as a list of columns. -inlineColumns - :: ( SOP.IsRecord hask xs - , SOP.AllZip InlineColumn xs columns ) - => hask -- ^ record - -> NP (Aliased (Optional (Expression 'Ungrouped '[] with db params '[]))) columns -inlineColumns - = SOP.htrans (SOP.Proxy @InlineColumn) inlineColumn - . SOP.toRecord diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Json.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Json.hs deleted file mode 100644 index 3a2c7283..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Json.hs +++ /dev/null @@ -1,484 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Expression.Json -Description: json and jsonb functions and operators -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -json and jsonb functions and operators --} - -{-# LANGUAGE - DataKinds - , FlexibleContexts - , FlexibleInstances - , GADTs - , OverloadedLabels - , OverloadedStrings - , PolyKinds - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeOperators - , UndecidableInstances - , UndecidableSuperClasses -#-} - -module Squeal.PostgreSQL.Expression.Json - ( -- * Json and Jsonb Operators - (.->) - , (.->>) - , (.#>) - , (.#>>) - -- * Jsonb Operators - , (.?) - , (.?|) - , (.?&) - , (.-.) - , (#-.) - -- * Json and Jsonb Functions - , toJson - , toJsonb - , arrayToJson - , rowToJson - , jsonBuildArray - , jsonbBuildArray - , JsonBuildObject (..) - , jsonObject - , jsonbObject - , jsonZipObject - , jsonbZipObject - , jsonArrayLength - , jsonbArrayLength - , jsonTypeof - , jsonbTypeof - , jsonStripNulls - , jsonbStripNulls - , jsonbSet - , jsonbInsert - , jsonbPretty - -- * Json and Jsonb Set Functions - , jsonEach - , jsonbEach - , jsonEachText - , jsonArrayElementsText - , jsonbEachText - , jsonbArrayElementsText - , jsonObjectKeys - , jsonbObjectKeys - , JsonPopulateFunction - , jsonPopulateRecord - , jsonbPopulateRecord - , jsonPopulateRecordSet - , jsonbPopulateRecordSet - , JsonToRecordFunction - , jsonToRecord - , jsonbToRecord - , jsonToRecordSet - , jsonbToRecordSet - ) where - -import Data.ByteString (ByteString) -import GHC.TypeLits - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Expression.Type -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Query.From -import Squeal.PostgreSQL.Query.From.Set -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - -import qualified Generics.SOP as SOP - --- $setup --- >>> import Squeal.PostgreSQL --- >>> import Data.Aeson - -{----------------------------------------- - -- json and jsonb support - -See https://www.postgresql.org/docs/10/static/functions-json.html -- most -comments lifted directly from this page. - -Table 9.44: json and jsonb operators ------------------------------------------} - --- | Get JSON value (object field or array element) at a key. -(.->) - :: (json `In` PGJsonType, key `In` PGJsonKey) - => Operator (null json) (null key) ('Null json) -infixl 8 .-> -(.->) = unsafeBinaryOp "->" - --- | Get JSON value (object field or array element) at a key, as text. -(.->>) - :: (json `In` PGJsonType, key `In` PGJsonKey) - => Operator (null json) (null key) ('Null 'PGtext) -infixl 8 .->> -(.->>) = unsafeBinaryOp "->>" - --- | Get JSON value at a specified path. -(.#>) - :: json `In` PGJsonType - => Operator (null json) (null ('PGvararray ('NotNull 'PGtext))) ('Null json) -infixl 8 .#> -(.#>) = unsafeBinaryOp "#>" - --- | Get JSON value at a specified path as text. -(.#>>) - :: json `In` PGJsonType - => Operator (null json) (null ('PGvararray ('NotNull 'PGtext))) ('Null 'PGtext) -infixl 8 .#>> -(.#>>) = unsafeBinaryOp "#>>" - --- Additional jsonb operators - --- | Does the string exist as a top-level key within the JSON value? -(.?) :: Operator (null 'PGjsonb) (null 'PGtext) ('Null 'PGbool) -infixl 9 .? -(.?) = unsafeBinaryOp "?" - --- | Do any of these array strings exist as top-level keys? -(.?|) :: Operator - (null 'PGjsonb) - (null ('PGvararray ('NotNull 'PGtext))) - ('Null 'PGbool) -infixl 9 .?| -(.?|) = unsafeBinaryOp "?|" - --- | Do all of these array strings exist as top-level keys? -(.?&) :: Operator - (null 'PGjsonb) - (null ('PGvararray ('NotNull 'PGtext))) - ('Null 'PGbool) -infixl 9 .?& -(.?&) = unsafeBinaryOp "?&" - --- | Delete a key or keys from a JSON object, or remove an array element. --- --- If the right operand is --- --- @ text @: Delete key / value pair or string element from left operand. --- Key / value pairs are matched based on their key value, --- --- @ text[] @: Delete multiple key / value pairs or string elements --- from left operand. Key / value pairs are matched based on their key value, --- --- @ integer @: Delete the array element with specified index (Negative integers --- count from the end). Throws an error if top level container is not an array. -(.-.) - :: key `In` '[ 'PGtext, 'PGvararray ('NotNull 'PGtext), 'PGint4, 'PGint2 ] - => Operator (null 'PGjsonb) (null key) (null 'PGjsonb) -infixl 6 .-. -(.-.) = unsafeBinaryOp "-" - --- | Delete the field or element with specified path (for JSON arrays, negative --- integers count from the end) -(#-.) :: Operator (null 'PGjsonb) (null ('PGvararray ('NotNull 'PGtext))) (null 'PGjsonb) -infixl 6 #-. -(#-.) = unsafeBinaryOp "#-" - -{----------------------------------------- -Table 9.45: JSON creation functions ------------------------------------------} - --- | Returns the value as json. Arrays and composites are converted --- (recursively) to arrays and objects; otherwise, if there is a cast from the --- type to json, the cast function will be used to perform the conversion; --- otherwise, a scalar value is produced. For any scalar type other than a --- number, a Boolean, or a null value, the text representation will be used, in --- such a fashion that it is a valid json value. -toJson :: null ty --> null 'PGjson -toJson = unsafeFunction "to_json" - --- | Returns the value as jsonb. Arrays and composites are converted --- (recursively) to arrays and objects; otherwise, if there is a cast from the --- type to json, the cast function will be used to perform the conversion; --- otherwise, a scalar value is produced. For any scalar type other than a --- number, a Boolean, or a null value, the text representation will be used, in --- such a fashion that it is a valid jsonb value. -toJsonb :: null ty --> null 'PGjsonb -toJsonb = unsafeFunction "to_jsonb" - --- | Returns the array as a JSON array. A PostgreSQL multidimensional array --- becomes a JSON array of arrays. -arrayToJson :: null ('PGvararray ty) --> null 'PGjson -arrayToJson = unsafeFunction "array_to_json" - --- | Returns the row as a JSON object. -rowToJson :: null ('PGcomposite ty) --> null 'PGjson -rowToJson = unsafeFunction "row_to_json" - --- | Builds a possibly-heterogeneously-typed JSON array out of a variadic --- argument list. -jsonBuildArray :: SOP.SListI tuple => tuple ---> null 'PGjson -jsonBuildArray = unsafeFunctionN "json_build_array" - --- | Builds a possibly-heterogeneously-typed (binary) JSON array out of a --- variadic argument list. -jsonbBuildArray :: SOP.SListI tuple => tuple ---> null 'PGjsonb -jsonbBuildArray = unsafeFunctionN "jsonb_build_array" - --- | Builds a possibly-heterogeneously-typed JSON object out of a variadic --- argument list. The elements of the argument list must alternate between text --- and values. -class SOP.SListI tys => JsonBuildObject tys where - - jsonBuildObject :: tys ---> null 'PGjson - jsonBuildObject = unsafeFunctionN "json_build_object" - - jsonbBuildObject :: tys ---> null 'PGjsonb - jsonbBuildObject = unsafeFunctionN "jsonb_build_object" - -instance JsonBuildObject '[] -instance (JsonBuildObject tys, key `In` PGJsonKey) - => JsonBuildObject ('NotNull key ': value ': tys) - --- | Builds a JSON object out of a text array. --- The array must have two dimensions --- such that each inner array has exactly two elements, --- which are taken as a key/value pair. -jsonObject - :: null ('PGfixarray '[n,2] ('NotNull 'PGtext)) - --> null 'PGjson -jsonObject = unsafeFunction "json_object" - --- | Builds a binary JSON object out of a text array. --- The array must have two dimensions --- such that each inner array has exactly two elements, --- which are taken as a key/value pair. -jsonbObject - :: null ('PGfixarray '[n,2] ('NotNull 'PGtext)) - --> null 'PGjsonb -jsonbObject = unsafeFunction "jsonb_object" - --- | This is an alternate form of 'jsonObject' that takes two arrays; one for --- keys and one for values, that are zipped pairwise to create a JSON object. -jsonZipObject :: - '[ null ('PGvararray ('NotNull 'PGtext)) - , null ('PGvararray ('NotNull 'PGtext)) ] - ---> null 'PGjson -jsonZipObject = unsafeFunctionN "json_object" - --- | This is an alternate form of 'jsonbObject' that takes two arrays; one for --- keys and one for values, that are zipped pairwise to create a binary JSON --- object. -jsonbZipObject :: - '[ null ('PGvararray ('NotNull 'PGtext)) - , null ('PGvararray ('NotNull 'PGtext)) ] - ---> null 'PGjsonb -jsonbZipObject = unsafeFunctionN "jsonb_object" - -{----------------------------------------- -Table 9.46: JSON processing functions ------------------------------------------} - --- | Returns the number of elements in the outermost JSON array. -jsonArrayLength :: null 'PGjson --> null 'PGint4 -jsonArrayLength = unsafeFunction "json_array_length" - --- | Returns the number of elements in the outermost binary JSON array. -jsonbArrayLength :: null 'PGjsonb --> null 'PGint4 -jsonbArrayLength = unsafeFunction "jsonb_array_length" - --- | Returns the type of the outermost JSON value as a text string. Possible --- types are object, array, string, number, boolean, and null. -jsonTypeof :: null 'PGjson --> null 'PGtext -jsonTypeof = unsafeFunction "json_typeof" - --- | Returns the type of the outermost binary JSON value as a text string. --- Possible types are object, array, string, number, boolean, and null. -jsonbTypeof :: null 'PGjsonb --> null 'PGtext -jsonbTypeof = unsafeFunction "jsonb_typeof" - --- | Returns its argument with all object fields that have null values omitted. --- Other null values are untouched. -jsonStripNulls :: null 'PGjson --> null 'PGjson -jsonStripNulls = unsafeFunction "json_strip_nulls" - --- | Returns its argument with all object fields that have null values omitted. --- Other null values are untouched. -jsonbStripNulls :: null 'PGjsonb --> null 'PGjsonb -jsonbStripNulls = unsafeFunction "jsonb_strip_nulls" - --- | @ jsonbSet target path new_value create_missing @ --- --- Returns target with the section designated by path replaced by @new_value@, --- or with @new_value@ added if create_missing is --- `Squeal.PostgreSQL.Expression.Logic.true` and the --- item designated by path does not exist. As with the path orientated --- operators, negative integers that appear in path count from the end of JSON --- arrays. -jsonbSet :: - '[ null 'PGjsonb, null ('PGvararray ('NotNull 'PGtext)) - , null 'PGjsonb, null 'PGbool ] ---> null 'PGjsonb -jsonbSet = unsafeFunctionN "jsonbSet" - --- | @ jsonbInsert target path new_value insert_after @ --- --- Returns target with @new_value@ inserted. If target section designated by --- path is in a JSONB array, @new_value@ will be inserted before target or after --- if @insert_after@ is `Squeal.PostgreSQL.Expression.Logic.true`. --- If target section designated by --- path is in JSONB object, @new_value@ will be inserted only if target does not --- exist. As with the path orientated operators, negative integers that appear --- in path count from the end of JSON arrays. -jsonbInsert :: - '[ null 'PGjsonb, null ('PGvararray ('NotNull 'PGtext)) - , null 'PGjsonb, null 'PGbool ] ---> null 'PGjsonb -jsonbInsert = unsafeFunctionN "jsonb_insert" - --- | Returns its argument as indented JSON text. -jsonbPretty :: null 'PGjsonb --> null 'PGtext -jsonbPretty = unsafeFunction "jsonb_pretty" - -{- | Expands the outermost JSON object into a set of key/value pairs. - ->>> printSQL (select Star (from (jsonEach (inline (Json (object ["a" .= "foo", "b" .= "bar"])))))) -SELECT * FROM json_each(('{"b":"bar","a":"foo"}' :: json)) --} -jsonEach :: null 'PGjson -|-> - ("json_each" ::: '["key" ::: 'NotNull 'PGtext, "value" ::: 'NotNull 'PGjson]) -jsonEach = unsafeSetFunction "json_each" - -{- | Expands the outermost binary JSON object into a set of key/value pairs. - ->>> printSQL (select Star (from (jsonbEach (inline (Jsonb (object ["a" .= "foo", "b" .= "bar"])))))) -SELECT * FROM jsonb_each(('{"b":"bar","a":"foo"}' :: jsonb)) --} -jsonbEach - :: null 'PGjsonb -|-> - ("jsonb_each" ::: '["key" ::: 'NotNull 'PGtext, "value" ::: 'NotNull 'PGjson]) -jsonbEach = unsafeSetFunction "jsonb_each" - -{- | Expands the outermost JSON object into a set of key/value pairs. - ->>> printSQL (select Star (from (jsonEachText (inline (Json (object ["a" .= "foo", "b" .= "bar"])))))) -SELECT * FROM json_each_text(('{"b":"bar","a":"foo"}' :: json)) --} -jsonEachText - :: null 'PGjson -|-> - ("json_each_text" ::: '["key" ::: 'NotNull 'PGtext, "value" ::: 'NotNull 'PGtext]) -jsonEachText = unsafeSetFunction "json_each_text" - -{- | Returns a set of text values from a JSON array - ->>> printSQL (select Star (from (jsonArrayElementsText (inline (Json (toJSON ["monkey", "pony", "bear"] )))))) -SELECT * FROM json_array_elements_text(('["monkey","pony","bear"]' :: json)) --} -jsonArrayElementsText - :: null 'PGjson -|-> - ("json_array_elements_text" ::: '["value" ::: 'NotNull 'PGtext]) -jsonArrayElementsText = unsafeSetFunction "json_array_elements_text" - -{- | Expands the outermost binary JSON object into a set of key/value pairs. - ->>> printSQL (select Star (from (jsonbEachText (inline (Jsonb (object ["a" .= "foo", "b" .= "bar"])))))) -SELECT * FROM jsonb_each_text(('{"b":"bar","a":"foo"}' :: jsonb)) --} -jsonbEachText - :: null 'PGjsonb -|-> - ("jsonb_each_text" ::: '["key" ::: 'NotNull 'PGtext, "value" ::: 'NotNull 'PGtext]) -jsonbEachText = unsafeSetFunction "jsonb_each_text" - -{- | Returns set of keys in the outermost JSON object. - ->>> printSQL (jsonObjectKeys (inline (Json (object ["a" .= "foo", "b" .= "bar"])))) -json_object_keys(('{"b":"bar","a":"foo"}' :: json)) --} -jsonObjectKeys - :: null 'PGjson -|-> - ("json_object_keys" ::: '["json_object_keys" ::: 'NotNull 'PGtext]) -jsonObjectKeys = unsafeSetFunction "json_object_keys" - -{- | Returns set of keys in the outermost JSON object. - ->>> printSQL (jsonbObjectKeys (inline (Jsonb (object ["a" .= "foo", "b" .= "bar"])))) -jsonb_object_keys(('{"b":"bar","a":"foo"}' :: jsonb)) --} -jsonbObjectKeys - :: null 'PGjsonb -|-> - ("jsonb_object_keys" ::: '["jsonb_object_keys" ::: 'NotNull 'PGtext]) -jsonbObjectKeys = unsafeSetFunction "jsonb_object_keys" - -{- | Returns a set of text values from a binary JSON array - ->>> printSQL (select Star (from (jsonbArrayElementsText (inline (Jsonb (toJSON ["red", "green", "cyan"] )))))) -SELECT * FROM jsonb_array_elements_text(('["red","green","cyan"]' :: jsonb)) --} -jsonbArrayElementsText - :: null 'PGjsonb -|-> - ("jsonb_array_elements_text" ::: '["value" ::: 'NotNull 'PGtext]) -jsonbArrayElementsText = unsafeSetFunction "jsonb_array_elements_text" - --- | Build rows from Json types. -type JsonPopulateFunction fun json - = forall db row lat with params - . json `In` PGJsonType - => TypeExpression db ('NotNull ('PGcomposite row)) -- ^ row type - -> Expression 'Ungrouped lat with db params '[] ('NotNull json) - -- ^ json type - -> FromClause lat with db params '[fun ::: row] - -unsafePopulateFunction - :: forall fun ty - . KnownSymbol fun => Alias fun -> JsonPopulateFunction fun ty -unsafePopulateFunction _fun ty expr = UnsafeFromClause $ renderSymbol @fun - <> parenthesized ("null::" <> renderSQL ty <> ", " <> renderSQL expr) - --- | Expands the JSON expression to a row whose columns match the record --- type defined by the given table. -jsonPopulateRecord :: JsonPopulateFunction "json_populate_record" 'PGjson -jsonPopulateRecord = unsafePopulateFunction #json_populate_record - --- | Expands the binary JSON expression to a row whose columns match the record --- type defined by the given table. -jsonbPopulateRecord :: JsonPopulateFunction "jsonb_populate_record" 'PGjsonb -jsonbPopulateRecord = unsafePopulateFunction #jsonb_populate_record - --- | Expands the outermost array of objects in the given JSON expression to a --- set of rows whose columns match the record type defined by the given table. -jsonPopulateRecordSet :: JsonPopulateFunction "json_populate_record_set" 'PGjson -jsonPopulateRecordSet = unsafePopulateFunction #json_populate_record_set - --- | Expands the outermost array of objects in the given binary JSON expression --- to a set of rows whose columns match the record type defined by the given --- table. -jsonbPopulateRecordSet :: JsonPopulateFunction "jsonb_populate_record_set" 'PGjsonb -jsonbPopulateRecordSet = unsafePopulateFunction #jsonb_populate_record_set - --- | Build rows from Json types. -type JsonToRecordFunction json - = forall lat with db params tab row - . (SOP.SListI row, json `In` PGJsonType) - => Expression 'Ungrouped lat with db params '[] ('NotNull json) - -- ^ json type - -> Aliased (NP (Aliased (TypeExpression db))) (tab ::: row) - -- ^ row type - -> FromClause lat with db params '[tab ::: row] - -unsafeRecordFunction :: ByteString -> JsonToRecordFunction json -unsafeRecordFunction fun expr (types `As` tab) = UnsafeFromClause $ - fun <> parenthesized (renderSQL expr) <+> "AS" <+> renderSQL tab - <> parenthesized (renderCommaSeparated renderTy types) - where - renderTy :: Aliased (TypeExpression db) ty -> ByteString - renderTy (ty `As` alias) = renderSQL alias <+> renderSQL ty - --- | Builds an arbitrary record from a JSON object. -jsonToRecord :: JsonToRecordFunction 'PGjson -jsonToRecord = unsafeRecordFunction "json_to_record" - --- | Builds an arbitrary record from a binary JSON object. -jsonbToRecord :: JsonToRecordFunction 'PGjsonb -jsonbToRecord = unsafeRecordFunction "jsonb_to_record" - --- | Builds an arbitrary set of records from a JSON array of objects. -jsonToRecordSet :: JsonToRecordFunction 'PGjson -jsonToRecordSet = unsafeRecordFunction "json_to_record_set" - --- | Builds an arbitrary set of records from a binary JSON array of objects. -jsonbToRecordSet :: JsonToRecordFunction 'PGjsonb -jsonbToRecordSet = unsafeRecordFunction "jsonb_to_record_set" diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Logic.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Logic.hs deleted file mode 100644 index 9b405865..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Logic.hs +++ /dev/null @@ -1,108 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Expression.Logic -Description: logical expressions and operators -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -logical expressions and operators --} - -{-# LANGUAGE - DataKinds - , OverloadedStrings - , TypeOperators -#-} - -module Squeal.PostgreSQL.Expression.Logic - ( -- * Condition - Condition - , true - , false - -- * Logic - , not_ - , (.&&) - , (.||) - -- * Conditional - , caseWhenThenElse - , ifThenElse - ) where - -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- | A `Condition` is an `Expression`, which can evaluate --- to `true`, `false` or `Squeal.PostgreSQL.Null.null_`. This is because SQL uses --- a three valued logic. -type Condition grp lat with db params from = - Expression grp lat with db params from ('Null 'PGbool) - --- | >>> printSQL true --- TRUE -true :: Expr (null 'PGbool) -true = UnsafeExpression "TRUE" - --- | >>> printSQL false --- FALSE -false :: Expr (null 'PGbool) -false = UnsafeExpression "FALSE" - --- | >>> printSQL $ not_ true --- (NOT TRUE) -not_ :: null 'PGbool --> null 'PGbool -not_ = unsafeLeftOp "NOT" - --- | >>> printSQL $ true .&& false --- (TRUE AND FALSE) -(.&&) :: Operator (null 'PGbool) (null 'PGbool) (null 'PGbool) -infixr 3 .&& -(.&&) = unsafeBinaryOp "AND" - --- | >>> printSQL $ true .|| false --- (TRUE OR FALSE) -(.||) :: Operator (null 'PGbool) (null 'PGbool) (null 'PGbool) -infixr 2 .|| -(.||) = unsafeBinaryOp "OR" - --- | >>> :{ --- let --- expression :: Expression grp lat with db params from (null 'PGint2) --- expression = caseWhenThenElse [(true, 1), (false, 2)] 3 --- in printSQL expression --- :} --- CASE WHEN TRUE THEN (1 :: int2) WHEN FALSE THEN (2 :: int2) ELSE (3 :: int2) END -caseWhenThenElse - :: [ ( Condition grp lat with db params from - , Expression grp lat with db params from ty - ) ] - -- ^ whens and thens - -> Expression grp lat with db params from ty - -- ^ else - -> Expression grp lat with db params from ty -caseWhenThenElse whenThens else_ = UnsafeExpression $ mconcat - [ "CASE" - , mconcat - [ mconcat - [ " WHEN ", renderSQL when_ - , " THEN ", renderSQL then_ - ] - | (when_,then_) <- whenThens - ] - , " ELSE ", renderSQL else_ - , " END" - ] - --- | >>> :{ --- let --- expression :: Expression grp lat with db params from (null 'PGint2) --- expression = ifThenElse true 1 0 --- in printSQL expression --- :} --- CASE WHEN TRUE THEN (1 :: int2) ELSE (0 :: int2) END -ifThenElse - :: Condition grp lat with db params from - -> Expression grp lat with db params from ty -- ^ then - -> Expression grp lat with db params from ty -- ^ else - -> Expression grp lat with db params from ty -ifThenElse if_ then_ else_ = caseWhenThenElse [(if_,then_)] else_ diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Math.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Math.hs deleted file mode 100644 index f1d589a4..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Math.hs +++ /dev/null @@ -1,101 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Expression.Math -Description: math functions -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -math functions --} - -{-# LANGUAGE - DataKinds - , OverloadedStrings - , TypeOperators -#-} - -module Squeal.PostgreSQL.Expression.Math - ( -- * Math Function - atan2_ - , quot_ - , rem_ - , trunc - , round_ - , ceiling_ - ) where - -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - --- | >>> :{ --- let --- expression :: Expr (null 'PGfloat4) --- expression = atan2_ (pi *: 2) --- in printSQL expression --- :} --- atan2(pi(), (2.0 :: float4)) -atan2_ :: float `In` PGFloating => '[ null float, null float] ---> null float -atan2_ = unsafeFunctionN "atan2" - - --- | integer division, truncates the result --- --- >>> :{ --- let --- expression :: Expression grp lat with db params from (null 'PGint2) --- expression = 5 `quot_` 2 --- in printSQL expression --- :} --- ((5 :: int2) / (2 :: int2)) -quot_ - :: int `In` PGIntegral - => Operator (null int) (null int) (null int) -quot_ = unsafeBinaryOp "/" - --- | remainder upon integer division --- --- >>> :{ --- let --- expression :: Expression grp lat with db params from (null 'PGint2) --- expression = 5 `rem_` 2 --- in printSQL expression --- :} --- ((5 :: int2) % (2 :: int2)) -rem_ - :: int `In` PGIntegral - => Operator (null int) (null int) (null int) -rem_ = unsafeBinaryOp "%" - --- | >>> :{ --- let --- expression :: Expression grp lat with db params from (null 'PGfloat4) --- expression = trunc pi --- in printSQL expression --- :} --- trunc(pi()) -trunc :: frac `In` PGFloating => null frac --> null frac -trunc = unsafeFunction "trunc" - --- | >>> :{ --- let --- expression :: Expression grp lat with db params from (null 'PGfloat4) --- expression = round_ pi --- in printSQL expression --- :} --- round(pi()) -round_ :: frac `In` PGFloating => null frac --> null frac -round_ = unsafeFunction "round" - --- | >>> :{ --- let --- expression :: Expression grp lat with db params from (null 'PGfloat4) --- expression = ceiling_ pi --- in printSQL expression --- :} --- ceiling(pi()) -ceiling_ :: frac `In` PGFloating => null frac --> null frac -ceiling_ = unsafeFunction "ceiling" diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Null.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Null.hs deleted file mode 100644 index f4ee6e13..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Null.hs +++ /dev/null @@ -1,134 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Expression.Null -Description: null expressions and handlers -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -null expressions and handlers --} - -{-# LANGUAGE - DataKinds - , KindSignatures - , OverloadedStrings - , RankNTypes - , TypeFamilies - , TypeOperators -#-} - -module Squeal.PostgreSQL.Expression.Null - ( -- * Null - null_ - , notNull - , unsafeNotNull - , monoNotNull - , coalesce - , fromNull - , isNull - , isNotNull - , matchNull - , nullIf - , CombineNullity - ) where - -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Expression.Logic -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - --- | analagous to `Nothing` --- --- >>> printSQL null_ --- NULL -null_ :: Expr ('Null ty) -null_ = UnsafeExpression "NULL" - --- | analagous to `Just` --- --- >>> printSQL $ notNull true --- TRUE -notNull :: 'NotNull ty --> 'Null ty -notNull = UnsafeExpression . renderSQL - --- | Analagous to `Data.Maybe.fromJust` inverse to `notNull`, --- useful when you know an `Expression` is `NotNull`, --- because, for instance, you've filtered out @NULL@ --- values in a column. -unsafeNotNull :: 'Null ty --> 'NotNull ty -unsafeNotNull = UnsafeExpression . renderSQL - --- | Some expressions are null polymorphic which may raise --- inference issues. Use `monoNotNull` to fix their --- nullity as `NotNull`. -monoNotNull - :: (forall null. Expression grp lat with db params from (null ty)) - -- ^ null polymorphic - -> Expression grp lat with db params from ('NotNull ty) -monoNotNull x = x - --- | return the leftmost value which is not NULL --- --- >>> printSQL $ coalesce [null_, true] false --- COALESCE(NULL, TRUE, FALSE) -coalesce :: FunctionVar ('Null ty) (null ty) (null ty) -coalesce nullxs notNullx = UnsafeExpression $ - "COALESCE" <> parenthesized (commaSeparated - ((renderSQL <$> nullxs) <> [renderSQL notNullx])) - --- | analagous to `Data.Maybe.fromMaybe` using @COALESCE@ --- --- >>> printSQL $ fromNull true null_ --- COALESCE(NULL, TRUE) -fromNull - :: Expression grp lat with db params from ('NotNull ty) - -- ^ what to convert @NULL@ to - -> Expression grp lat with db params from ('Null ty) - -> Expression grp lat with db params from ('NotNull ty) -fromNull notNullx nullx = coalesce [nullx] notNullx - --- | >>> printSQL $ null_ & isNull --- NULL IS NULL -isNull :: 'Null ty --> null 'PGbool -isNull x = UnsafeExpression $ renderSQL x <+> "IS NULL" - --- | >>> printSQL $ null_ & isNotNull --- NULL IS NOT NULL -isNotNull :: 'Null ty --> null 'PGbool -isNotNull x = UnsafeExpression $ renderSQL x <+> "IS NOT NULL" - --- | analagous to `maybe` using @IS NULL@ --- --- >>> printSQL $ matchNull true not_ null_ --- CASE WHEN NULL IS NULL THEN TRUE ELSE (NOT NULL) END -matchNull - :: Expression grp lat with db params from (nullty) - -- ^ what to convert @NULL@ to - -> ( Expression grp lat with db params from ('NotNull ty) - -> Expression grp lat with db params from (nullty) ) - -- ^ function to perform when @NULL@ is absent - -> Expression grp lat with db params from ('Null ty) - -> Expression grp lat with db params from (nullty) -matchNull y f x = ifThenElse (isNull x) y - (f (UnsafeExpression (renderSQL x))) - -{-| right inverse to `fromNull`, if its arguments are equal then -`nullIf` gives @NULL@. - ->>> :set -XTypeApplications ->>> printSQL (nullIf (false *: param @1)) -NULLIF(FALSE, ($1 :: bool)) --} -nullIf :: '[ 'NotNull ty, 'NotNull ty] ---> 'Null ty -nullIf = unsafeFunctionN "NULLIF" - -{-| Make the return type of the type family `NotNull` if both arguments are, - or `Null` otherwise. --} -type family CombineNullity - (lhs :: PGType -> NullType) (rhs :: PGType -> NullType) :: PGType -> NullType where - CombineNullity 'NotNull 'NotNull = 'NotNull - CombineNullity _ _ = 'Null diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Parameter.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Parameter.hs deleted file mode 100644 index 8be9bca0..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Parameter.hs +++ /dev/null @@ -1,130 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Expression.Parameter -Description: out-of-line parameters -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -out-of-line parameters --} - -{-# LANGUAGE - AllowAmbiguousTypes - , DataKinds - , FlexibleContexts - , FlexibleInstances - , FunctionalDependencies - , GADTs - , KindSignatures - , MultiParamTypeClasses - , OverloadedStrings - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeFamilies - , TypeOperators - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Expression.Parameter - ( -- * Parameter - HasParameter (parameter) - , param - ) where - -import Data.Kind (Constraint) -import GHC.Exts (Any) -import GHC.TypeLits - -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Expression.Type -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - -{- | A `HasParameter` constraint is used to indicate a value that is -supplied externally to a SQL statement. -`Squeal.PostgreSQL.Session.manipulateParams`, -`Squeal.PostgreSQL.Session.queryParams` and -`Squeal.PostgreSQL.Session.traversePrepared` support specifying data values -separately from the SQL command string, in which case `param`s are used to -refer to the out-of-line data values. --} -class KnownNat ix => HasParameter - (ix :: Nat) - (params :: [NullType]) - (ty :: NullType) - | ix params -> ty where - -- | `parameter` takes a `Nat` using type application and a `TypeExpression`. - -- - -- >>> printSQL (parameter @1 int4) - -- ($1 :: int4) - parameter - :: TypeExpression db ty - -> Expression grp lat with db params from ty - parameter ty = UnsafeExpression $ parenthesized $ - "$" <> renderNat @ix <+> "::" - <+> renderSQL ty - --- we could do the check for 0 in @HasParameter'@, but this way forces checking 'ix' before delegating, --- which has the nice effect of ambiguous 'ix' errors mentioning 'HasParameter' instead of @HasParameter'@ -instance {-# OVERLAPS #-} (TypeError ('Text "Tried to get the param at index 0, but params are 1-indexed"), x ~ Any) => HasParameter 0 params x -instance {-# OVERLAPS #-} (KnownNat ix, HasParameter' ix params ix params x) => HasParameter ix params x - --- | @HasParameter'@ is an implementation detail of 'HasParameter' allowing us to --- include the full parameter list in our errors. Generally speaking it shouldn't leak to users --- of the library -class KnownNat ix => HasParameter' - (originalIx :: Nat) - (allParams :: [NullType]) - (ix :: Nat) - (params :: [NullType]) - (ty :: NullType) - | ix params -> ty where -instance {-# OVERLAPS #-} - ( params ~ (y ': xs) - , y ~ x -- having a separate 'y' type variable is required for 'ParamTypeMismatchError' - , ParamOutOfBoundsError originalIx allParams params - , ParamTypeMismatchError originalIx allParams x y - ) => HasParameter' originalIx allParams 1 params x -instance {-# OVERLAPS #-} - ( KnownNat ix - , HasParameter' originalIx allParams (ix-1) xs x - , params ~ (y ': xs) - , ParamOutOfBoundsError originalIx allParams params - ) - => HasParameter' originalIx allParams ix params x - --- | @ParamOutOfBoundsError@ reports a nicer error with more context when we try to do an out-of-bounds lookup successfully do a lookup but --- find a different field than we expected, or when we find ourself out of bounds -type family ParamOutOfBoundsError (originalIx :: Nat) (allParams :: [NullType]) (params :: [NullType]) :: Constraint where - ParamOutOfBoundsError originalIx allParams '[] = TypeError - ('Text "Index " ':<>: 'ShowType originalIx ':<>: 'Text " is out of bounds in 1-indexed parameter list:" ':$$: 'ShowType allParams) - ParamOutOfBoundsError _ _ _ = () - --- | @ParamTypeMismatchError@ reports a nicer error with more context when we successfully do a lookup but --- find a different field than we expected, or when we find ourself out of bounds -type family ParamTypeMismatchError (originalIx :: Nat) (allParams :: [NullType]) (found :: NullType) (expected :: NullType) :: Constraint where - ParamTypeMismatchError _ _ found found = () - ParamTypeMismatchError originalIx allParams found expected = TypeError - ( 'Text "Type mismatch when looking up param at index " ':<>: 'ShowType originalIx - ':$$: 'Text "in 1-indexed parameter list:" - ':$$: 'Text " " ':<>: 'ShowType allParams - ':$$: 'Text "" - ':$$: 'Text "Expected: " ':<>: 'ShowType expected - ':$$: 'Text "But found: " ':<>: 'ShowType found - ':$$: 'Text "" - ) - --- | `param` takes a `Nat` using type application and for basic types, --- infers a `TypeExpression`. --- --- >>> printSQL (param @1 @('Null 'PGint4)) --- ($1 :: int4) -param - :: forall n ty lat with db params from grp - . (NullTyped db ty, HasParameter n params ty) - => Expression grp lat with db params from ty -- ^ param -param = parameter @n (nulltype @db) diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Range.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Range.hs deleted file mode 100644 index 5c0aa315..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Range.hs +++ /dev/null @@ -1,227 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Expression.Range -Description: range types and functions -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -range types and functions --} - -{-# LANGUAGE - AllowAmbiguousTypes - , DataKinds - , DeriveAnyClass - , DeriveGeneric - , DeriveFoldable - , DerivingStrategies - , DeriveTraversable - , FlexibleContexts - , FlexibleInstances - , LambdaCase - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , PatternSynonyms - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeFamilies - , TypeOperators - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Expression.Range - ( -- * Range - Range (..) - , (<=..<=), (<..<), (<=..<), (<..<=) - , moreThan, atLeast, lessThan, atMost - , singleton, whole - , Bound (..) - -- * Range Function - -- ** Range Construction - , range - -- ** Range Operator - , (.<@) - , (@>.) - , (<<@) - , (@>>) - , (&<) - , (&>) - , (-|-) - , (@+) - , (@*) - , (@-) - -- ** Range Function - , lowerBound - , upperBound - , isEmpty - , lowerInc - , lowerInf - , upperInc - , upperInf - , rangeMerge - ) where - -import qualified GHC.Generics as GHC -import qualified Generics.SOP as SOP - -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Expression.Type hiding (bool) -import Squeal.PostgreSQL.Type.PG -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL (tstzrange, numrange, int4range, now, printSQL) - --- | Construct a `range` --- --- >>> printSQL $ range tstzrange (atLeast now) --- tstzrange(now(), NULL, '[)') --- >>> printSQL $ range numrange (0 <=..< 2*pi) --- numrange((0.0 :: numeric), ((2.0 :: numeric) * pi()), '[)') --- >>> printSQL $ range int4range Empty --- ('empty' :: int4range) -range - :: TypeExpression db (null ('PGrange ty)) - -- ^ range type - -> Range (Expression grp lat with db params from ('NotNull ty)) - -- ^ range of values - -> Expression grp lat with db params from (null ('PGrange ty)) -range ty = \case - Empty -> UnsafeExpression $ parenthesized - (emp <+> "::" <+> renderSQL ty) - NonEmpty l u -> UnsafeExpression $ renderSQL ty <> parenthesized - (commaSeparated (args l u)) - where - emp = singleQuote <> "empty" <> singleQuote - args l u = [arg l, arg u, singleQuote <> bra l <> ket u <> singleQuote] - singleQuote = "\'" - arg = \case - Infinite -> "NULL"; Closed x -> renderSQL x; Open x -> renderSQL x - bra = \case Infinite -> "("; Closed _ -> "["; Open _ -> "(" - ket = \case Infinite -> ")"; Closed _ -> "]"; Open _ -> ")" - --- | The type of `Bound` for a `Range`. -data Bound x - = Infinite -- ^ unbounded - | Closed x -- ^ inclusive - | Open x -- ^ exclusive - deriving - ( Eq, Ord, Show, Read, GHC.Generic - , Functor, Foldable, Traversable ) - --- | A `Range` datatype that comprises connected subsets of --- the real line. -data Range x = Empty | NonEmpty (Bound x) (Bound x) - deriving - ( Eq, Ord, Show, Read, GHC.Generic - , Functor, Foldable, Traversable ) - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) --- | `PGrange` @(@`PG` @hask)@ -instance IsPG hask => IsPG (Range hask) where - type PG (Range hask) = 'PGrange (PG hask) - --- | Finite `Range` constructor -(<=..<=), (<..<), (<=..<), (<..<=) :: x -> x -> Range x -infix 4 <=..<=, <..<, <=..<, <..<= -x <=..<= y = NonEmpty (Closed x) (Closed y) -x <..< y = NonEmpty (Open x) (Open y) -x <=..< y = NonEmpty (Closed x) (Open y) -x <..<= y = NonEmpty (Open x) (Closed y) - --- | Half-infinite `Range` constructor -moreThan, atLeast, lessThan, atMost :: x -> Range x -moreThan x = NonEmpty (Open x) Infinite -atLeast x = NonEmpty (Closed x) Infinite -lessThan x = NonEmpty Infinite (Open x) -atMost x = NonEmpty Infinite (Closed x) - --- | A point on the line -singleton :: x -> Range x -singleton x = x <=..<= x - --- | The `whole` line -whole :: Range x -whole = NonEmpty Infinite Infinite - --- | range is contained by -(.<@) :: Operator ('NotNull ty) (null ('PGrange ty)) ('Null 'PGbool) -(.<@) = unsafeBinaryOp "<@" - --- | contains range -(@>.) :: Operator (null ('PGrange ty)) ('NotNull ty) ('Null 'PGbool) -(@>.) = unsafeBinaryOp "@>" - --- | strictly left of, --- return false when an empty range is involved -(<<@) :: Operator (null ('PGrange ty)) (null ('PGrange ty)) ('Null 'PGbool) -(<<@) = unsafeBinaryOp "<<" - --- | strictly right of, --- return false when an empty range is involved -(@>>) :: Operator (null ('PGrange ty)) (null ('PGrange ty)) ('Null 'PGbool) -(@>>) = unsafeBinaryOp ">>" - --- | does not extend to the right of, --- return false when an empty range is involved -(&<) :: Operator (null ('PGrange ty)) (null ('PGrange ty)) ('Null 'PGbool) -(&<) = unsafeBinaryOp "&<" - --- | does not extend to the left of, --- return false when an empty range is involved -(&>) :: Operator (null ('PGrange ty)) (null ('PGrange ty)) ('Null 'PGbool) -(&>) = unsafeBinaryOp "&>" - --- | is adjacent to, return false when an empty range is involved -(-|-) :: Operator (null ('PGrange ty)) (null ('PGrange ty)) ('Null 'PGbool) -(-|-) = unsafeBinaryOp "-|-" - --- | union, will fail if the resulting range would --- need to contain two disjoint sub-ranges -(@+) :: Operator (null ('PGrange ty)) (null ('PGrange ty)) (null ('PGrange ty)) -(@+) = unsafeBinaryOp "+" - --- | intersection -(@*) :: Operator (null ('PGrange ty)) (null ('PGrange ty)) (null ('PGrange ty)) -(@*) = unsafeBinaryOp "*" - --- | difference, will fail if the resulting range would --- need to contain two disjoint sub-ranges -(@-) :: Operator (null ('PGrange ty)) (null ('PGrange ty)) (null ('PGrange ty)) -(@-) = unsafeBinaryOp "-" - --- | lower bound of range -lowerBound :: null ('PGrange ty) --> 'Null ty -lowerBound = unsafeFunction "lower" - --- | upper bound of range -upperBound :: null ('PGrange ty) --> 'Null ty -upperBound = unsafeFunction "upper" - --- | is the range empty? -isEmpty :: null ('PGrange ty) --> 'Null 'PGbool -isEmpty = unsafeFunction "isempty" - --- | is the lower bound inclusive? -lowerInc :: null ('PGrange ty) --> 'Null 'PGbool -lowerInc = unsafeFunction "lower_inc" - --- | is the lower bound infinite? -lowerInf :: null ('PGrange ty) --> 'Null 'PGbool -lowerInf = unsafeFunction "lower_inf" - --- | is the upper bound inclusive? -upperInc :: null ('PGrange ty) --> 'Null 'PGbool -upperInc = unsafeFunction "upper_inc" - --- | is the upper bound infinite? -upperInf :: null ('PGrange ty) --> 'Null 'PGbool -upperInf = unsafeFunction "upper_inf" - --- | the smallest range which includes both of the given ranges -rangeMerge :: - '[null ('PGrange ty), null ('PGrange ty)] - ---> null ('PGrange ty) -rangeMerge = unsafeFunctionN "range_merge" diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Sort.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Sort.hs deleted file mode 100644 index 7d631e78..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Sort.hs +++ /dev/null @@ -1,101 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Expression.Sort -Description: sort expressions -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -sort expressions --} - -{-# LANGUAGE - DataKinds - , FlexibleInstances - , FunctionalDependencies - , GADTs - , LambdaCase - , MultiParamTypeClasses - , OverloadedStrings - , StandaloneDeriving -#-} - -module Squeal.PostgreSQL.Expression.Sort - ( -- * Sort - SortExpression (..) - , OrderBy (..) - ) where - -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- | `SortExpression`s are used by `orderBy` to optionally sort the results --- of a `Squeal.PostgreSQL.Query.Query`. `Asc` or `Desc` --- set the sort direction of a `NotNull` result --- column to ascending or descending. Ascending order puts smaller values --- first, where "smaller" is defined in terms of the --- `Squeal.PostgreSQL.Expression.Comparison..<` operator. Similarly, --- descending order is determined with the --- `Squeal.PostgreSQL.Expression.Comparison..>` operator. `AscNullsFirst`, --- `AscNullsLast`, `DescNullsFirst` and `DescNullsLast` options are used to --- determine whether nulls appear before or after non-null values in the sort --- ordering of a `Null` result column. -data SortExpression grp lat with db params from where - Asc - :: Expression grp lat with db params from ('NotNull ty) - -- ^ sort by - -> SortExpression grp lat with db params from - Desc - :: Expression grp lat with db params from ('NotNull ty) - -- ^ sort by - -> SortExpression grp lat with db params from - AscNullsFirst - :: Expression grp lat with db params from ('Null ty) - -- ^ sort by - -> SortExpression grp lat with db params from - AscNullsLast - :: Expression grp lat with db params from ('Null ty) - -- ^ sort by - -> SortExpression grp lat with db params from - DescNullsFirst - :: Expression grp lat with db params from ('Null ty) - -- ^ sort by - -> SortExpression grp lat with db params from - DescNullsLast - :: Expression grp lat with db params from ('Null ty) - -- ^ sort by - -> SortExpression grp lat with db params from -deriving instance Show (SortExpression grp lat with db params from) -instance RenderSQL (SortExpression grp lat with db params from) where - renderSQL = \case - Asc expression -> renderSQL expression <+> "ASC" - Desc expression -> renderSQL expression <+> "DESC" - AscNullsFirst expression -> renderSQL expression - <+> "ASC NULLS FIRST" - DescNullsFirst expression -> renderSQL expression - <+> "DESC NULLS FIRST" - AscNullsLast expression -> renderSQL expression <+> "ASC NULLS LAST" - DescNullsLast expression -> renderSQL expression <+> "DESC NULLS LAST" -instance RenderSQL [SortExpression grp lat with db params from] where - renderSQL = \case - [] -> "" - srts -> " ORDER BY" - <+> commaSeparated (renderSQL <$> srts) - -{- | -The `orderBy` clause causes the result rows of a `Squeal.PostgreSQL.Query.TableExpression` -to be sorted according to the specified `SortExpression`(s). -If two rows are equal according to the leftmost expression, -they are compared according to the next expression and so on. -If they are equal according to all specified expressions, -they are returned in an implementation-dependent order. - -You can also control the order in which rows are processed by window functions -using `orderBy` within `Squeal.PostgreSQL.Query.Over`. --} -class OrderBy expr grp | expr -> grp where - orderBy - :: [SortExpression grp lat with db params from] - -- ^ sorts - -> expr lat with db params from - -> expr lat with db params from diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Subquery.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Subquery.hs deleted file mode 100644 index e16cf1b7..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Subquery.hs +++ /dev/null @@ -1,124 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Expression.Subquery -Description: subquery expressions -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -subquery expressions --} - -{-# LANGUAGE - DataKinds - , OverloadedStrings - , RankNTypes - , TypeOperators -#-} - -module Squeal.PostgreSQL.Expression.Subquery - ( -- * Subquery - exists - , in_ - , notIn - , subAll - , subAny - ) where - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Expression.Logic -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Query -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - -{- | -The argument of `exists` is an arbitrary subquery. The subquery is evaluated -to determine whether it returns any rows. If it returns at least one row, -the result of `exists` is `true`; if the subquery returns no rows, -the result of `exists` is `false`. - -The subquery can refer to variables from the surrounding query, -which will act as constants during any one evaluation of the subquery. - -The subquery will generally only be executed long enough to determine whether -at least one row is returned, not all the way to completion. --} -exists - :: Query (Join lat from) with db params row - -- ^ subquery - -> Expression grp lat with db params from (null 'PGbool) -exists query = UnsafeExpression $ "EXISTS" <+> parenthesized (renderSQL query) - -{- | -The right-hand side is a parenthesized subquery, which must return -exactly one column. The left-hand expression is evaluated and compared to each -row of the subquery result using the given `Operator`, -which must yield a Boolean result. The result of `subAll` is `true` -if all rows yield true (including the case where the subquery returns no rows). -The result is `false` if any `false` result is found. -The result is `Squeal.PostgreSQL.Expression.Null.null_` if - no comparison with a subquery row returns `false`, -and at least one comparison returns `Squeal.PostgreSQL.Expression.Null.null_`. - ->>> printSQL $ subAll true (.==) (values_ (true `as` #foo)) -(TRUE = ALL (SELECT * FROM (VALUES (TRUE)) AS t ("foo"))) --} -subAll - :: Expression grp lat with db params from ty1 -- ^ expression - -> Operator ty1 ty2 ('Null 'PGbool) -- ^ operator - -> Query (Join lat from) with db params '[col ::: ty2] -- ^ subquery - -> Condition grp lat with db params from -subAll expr (?) qry = expr ? - (UnsafeExpression $ "ALL" <+> parenthesized (renderSQL qry)) - -{- | -The right-hand side is a parenthesized subquery, which must return exactly one column. -The left-hand expression is evaluated and compared to each row of the subquery result -using the given `Operator`, which must yield a Boolean result. The result of `subAny` is `true` -if any `true` result is obtained. The result is `false` if no true result is found -(including the case where the subquery returns no rows). - ->>> printSQL $ subAny "foo" like (values_ ("foobar" `as` #foo)) -((E'foo' :: text) LIKE ANY (SELECT * FROM (VALUES ((E'foobar' :: text))) AS t ("foo"))) --} -subAny - :: Expression grp lat with db params from ty1 -- ^ expression - -> Operator ty1 ty2 ('Null 'PGbool) -- ^ operator - -> Query (Join lat from) with db params '[col ::: ty2] -- ^ subquery - -> Condition grp lat with db params from -subAny expr (?) qry = expr ? - (UnsafeExpression $ "ANY" <+> parenthesized (renderSQL qry)) - -{- | -The result is `true` if the left-hand expression's result is equal -to any of the right-hand expressions. - ->>> printSQL $ true `in_` [true, false, null_] -TRUE IN (TRUE, FALSE, NULL) --} -in_ - :: Expression grp lat with db params from ty -- ^ expression - -> [Expression grp lat with db params from ty] - -> Expression grp lat with db params from ('Null 'PGbool) -_ `in_` [] = false -expr `in_` exprs = UnsafeExpression $ renderSQL expr <+> "IN" - <+> parenthesized (commaSeparated (renderSQL <$> exprs)) - -{- | -The result is `true` if the left-hand expression's result is not equal -to any of the right-hand expressions. - ->>> printSQL $ true `notIn` [false, null_] -TRUE NOT IN (FALSE, NULL) --} -notIn - :: Expression grp lat with db params from ty -- ^ expression - -> [Expression grp lat with db params from ty] - -> Expression grp lat with db params from ('Null 'PGbool) -_ `notIn` [] = true -expr `notIn` exprs = UnsafeExpression $ renderSQL expr <+> "NOT IN" - <+> parenthesized (commaSeparated (renderSQL <$> exprs)) diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Text.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Text.hs deleted file mode 100644 index 1d1dedc0..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Text.hs +++ /dev/null @@ -1,88 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Expression.Text -Description: text functions and operators -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -text functions and operators --} - -{-# LANGUAGE - DataKinds - , OverloadedStrings - , RankNTypes - , ScopedTypeVariables - , TypeOperators -#-} - -module Squeal.PostgreSQL.Expression.Text - ( -- * Text Function - lower - , upper - , charLength - , like - , ilike - , replace - , strpos - ) where - -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - --- | >>> printSQL $ lower "ARRRGGG" --- lower((E'ARRRGGG' :: text)) -lower :: null 'PGtext --> null 'PGtext -lower = unsafeFunction "lower" - --- | >>> printSQL $ upper "eeee" --- upper((E'eeee' :: text)) -upper :: null 'PGtext --> null 'PGtext -upper = unsafeFunction "upper" - --- | >>> printSQL $ charLength "four" --- char_length((E'four' :: text)) -charLength :: null 'PGtext --> null 'PGint4 -charLength = unsafeFunction "char_length" - --- | The `like` expression returns true if the @string@ matches --- the supplied @pattern@. If @pattern@ does not contain percent signs --- or underscores, then the pattern only represents the string itself; --- in that case `like` acts like the equals operator. An underscore (_) --- in pattern stands for (matches) any single character; a percent sign (%) --- matches any sequence of zero or more characters. --- --- >>> printSQL $ "abc" `like` "a%" --- ((E'abc' :: text) LIKE (E'a%' :: text)) -like :: Operator (null 'PGtext) (null 'PGtext) ('Null 'PGbool) -like = unsafeBinaryOp "LIKE" - --- | The key word ILIKE can be used instead of LIKE to make the --- match case-insensitive according to the active locale. --- --- >>> printSQL $ "abc" `ilike` "a%" --- ((E'abc' :: text) ILIKE (E'a%' :: text)) -ilike :: Operator (null 'PGtext) (null 'PGtext) ('Null 'PGbool) -ilike = unsafeBinaryOp "ILIKE" - --- | Determines the location of the substring match using the `strpos` --- function. Returns the 1-based index of the first match, if no --- match exists the function returns (0). --- --- >>> printSQL $ strpos ("string" *: "substring") --- strpos((E'string' :: text), (E'substring' :: text)) -strpos - :: '[null 'PGtext, null 'PGtext] ---> null 'PGint4 -strpos = unsafeFunctionN "strpos" - --- | Over the string in the first argument, replace all occurrences of --- the second argument with the third and return the modified string. --- --- >>> printSQL $ replace ("string" :* "from" *: "to") --- replace((E'string' :: text), (E'from' :: text), (E'to' :: text)) -replace - :: '[ null 'PGtext, null 'PGtext, null 'PGtext ] ---> null 'PGtext -replace = unsafeFunctionN "replace" diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/TextSearch.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/TextSearch.hs deleted file mode 100644 index 92e791ec..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/TextSearch.hs +++ /dev/null @@ -1,159 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Expression.TextSearch -Description: text search functions and operators -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -text search functions and operators --} - -{-# LANGUAGE - DataKinds - , OverloadedStrings - , TypeOperators -#-} - -module Squeal.PostgreSQL.Expression.TextSearch - ( -- * Text Search Operator - (@@) - , (.&) - , (.|) - , (.!) - , (<->) - -- * Text Search Function - , arrayToTSvector - , tsvectorLength - , numnode - , plainToTSquery - , phraseToTSquery - , websearchToTSquery - , queryTree - , toTSquery - , toTSvector - , setWeight - , strip - , jsonToTSvector - , jsonbToTSvector - , tsDelete - , tsFilter - , tsHeadline - ) where - -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Type.Schema - --- | `Squeal.PostgreSQL.Expression.Type.tsvector` matches tsquery ? -(@@) :: Operator (null 'PGtsvector) (null 'PGtsquery) ('Null 'PGbool) -(@@) = unsafeBinaryOp "@@" - --- | AND `Squeal.PostgreSQL.Expression.Type.tsquery`s together -(.&) :: Operator (null 'PGtsquery) (null 'PGtsquery) (null 'PGtsquery) -(.&) = unsafeBinaryOp "&&" - --- | OR `Squeal.PostgreSQL.Expression.Type.tsquery`s together -(.|) :: Operator (null 'PGtsquery) (null 'PGtsquery) (null 'PGtsquery) -(.|) = unsafeBinaryOp "||" - --- | negate a `Squeal.PostgreSQL.Expression.Type.tsquery` -(.!) :: null 'PGtsquery --> null 'PGtsquery -(.!) = unsafeLeftOp "!!" - --- | `Squeal.PostgreSQL.Expression.Type.tsquery` followed by --- `Squeal.PostgreSQL.Expression.Type.tsquery` -(<->) :: Operator (null 'PGtsquery) (null 'PGtsquery) (null 'PGtsquery) -(<->) = unsafeBinaryOp "<->" - --- | convert array of lexemes to `Squeal.PostgreSQL.Expression.Type.tsvector` -arrayToTSvector - :: null ('PGvararray ('NotNull 'PGtext)) - --> null 'PGtsvector -arrayToTSvector = unsafeFunction "array_to_tsvector" - --- | number of lexemes in `Squeal.PostgreSQL.Expression.Type.tsvector` -tsvectorLength :: null 'PGtsvector --> null 'PGint4 -tsvectorLength = unsafeFunction "length" - --- | number of lexemes plus operators in `Squeal.PostgreSQL.Expression.Type.tsquery` -numnode :: null 'PGtsquery --> null 'PGint4 -numnode = unsafeFunction "numnode" - --- | produce `Squeal.PostgreSQL.Expression.Type.tsquery` ignoring punctuation -plainToTSquery :: null 'PGtext --> null 'PGtsquery -plainToTSquery = unsafeFunction "plainto_tsquery" - --- | produce `Squeal.PostgreSQL.Expression.Type.tsquery` that searches for a phrase, --- ignoring punctuation -phraseToTSquery :: null 'PGtext --> null 'PGtsquery -phraseToTSquery = unsafeFunction "phraseto_tsquery" - --- | produce `Squeal.PostgreSQL.Expression.Type.tsquery` from a web search style query -websearchToTSquery :: null 'PGtext --> null 'PGtsquery -websearchToTSquery = unsafeFunction "websearch_to_tsquery" - --- | get indexable part of a `Squeal.PostgreSQL.Expression.Type.tsquery` -queryTree :: null 'PGtsquery --> null 'PGtext -queryTree = unsafeFunction "query_tree" - --- | normalize words and convert to `Squeal.PostgreSQL.Expression.Type.tsquery` -toTSquery :: null 'PGtext --> null 'PGtsquery -toTSquery = unsafeFunction "to_tsquery" - --- | reduce document text to `Squeal.PostgreSQL.Expression.Type.tsvector` -toTSvector - :: ty `In` '[ 'PGtext, 'PGjson, 'PGjsonb] - => null ty --> null 'PGtsvector -toTSvector = unsafeFunction "to_tsvector" - --- | assign weight to each element of `Squeal.PostgreSQL.Expression.Type.tsvector` -setWeight :: '[null 'PGtsvector, null ('PGchar 1)] ---> null 'PGtsvector -setWeight = unsafeFunctionN "set_weight" - --- | remove positions and weights from `Squeal.PostgreSQL.Expression.Type.tsvector` -strip :: null 'PGtsvector --> null 'PGtsvector -strip = unsafeFunction "strip" - --- | @jsonToTSvector (document *: filter)@ --- reduce each value in the document, specified by filter to a `Squeal.PostgreSQL.Expression.Type.tsvector`, --- and then concatenate those in document order to produce a single `Squeal.PostgreSQL.Expression.Type.tsvector`. --- filter is a `Squeal.PostgreSQL.Expression.Type.json` array, that enumerates what kind of elements --- need to be included into the resulting `Squeal.PostgreSQL.Expression.Type.tsvector`. --- Possible values for filter are "string" (to include all string values), --- "numeric" (to include all numeric values in the string format), --- "boolean" (to include all Boolean values in the string format "true"/"false"), --- "key" (to include all keys) or "all" (to include all above). --- These values can be combined together to include, e.g. all string and numeric values. -jsonToTSvector :: '[null 'PGjson, null 'PGjson] ---> null 'PGtsvector -jsonToTSvector = unsafeFunctionN "json_to_tsvector" - --- | @jsonbToTSvector (document *: filter)@ --- reduce each value in the document, specified by filter to a `Squeal.PostgreSQL.Expression.Type.tsvector`, --- and then concatenate those in document order to produce a single `Squeal.PostgreSQL.Expression.Type.tsvector`. --- filter is a `Squeal.PostgreSQL.Expression.Type.jsonb` array, that enumerates what kind of elements --- need to be included into the resulting `Squeal.PostgreSQL.Expression.Type.tsvector`. --- Possible values for filter are "string" (to include all string values), --- "numeric" (to include all numeric values in the string format), --- "boolean" (to include all Boolean values in the string format "true"/"false"), --- "key" (to include all keys) or "all" (to include all above). --- These values can be combined together to include, e.g. all string and numeric values. -jsonbToTSvector :: '[null 'PGjsonb, null 'PGjsonb] ---> null 'PGtsvector -jsonbToTSvector = unsafeFunctionN "jsonb_to_tsvector" - --- | remove given lexeme from `Squeal.PostgreSQL.Expression.Type.tsvector` -tsDelete :: - '[null 'PGtsvector, null ('PGvararray ('NotNull 'PGtext))] - ---> null 'PGtsvector -tsDelete = unsafeFunctionN "ts_delete" - --- | select only elements with given weights from `Squeal.PostgreSQL.Expression.Type.tsvector` -tsFilter :: - '[null 'PGtsvector, null ('PGvararray ('NotNull ('PGchar 1)))] - ---> null 'PGtsvector -tsFilter = unsafeFunctionN "ts_filter" - --- | display a `Squeal.PostgreSQL.Expression.Type.tsquery` match -tsHeadline - :: document `In` '[ 'PGtext, 'PGjson, 'PGjsonb] - => '[null document, null 'PGtsquery] ---> null 'PGtext -tsHeadline = unsafeFunctionN "ts_headline" diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Time.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Time.hs deleted file mode 100644 index 3552cb13..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Time.hs +++ /dev/null @@ -1,247 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Expression.Time -Description: date/time functions and operators -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -date/time functions and operators --} - -{-# LANGUAGE - DataKinds - , DeriveGeneric - , FunctionalDependencies - , LambdaCase - , MultiParamTypeClasses - , OverloadedStrings - , PolyKinds - , RankNTypes - , TypeFamilies - , TypeOperators - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Expression.Time - ( -- * Time Operation - TimeOp (..) - -- * Time Function - , currentDate - , currentTime - , currentTimestamp - , dateTrunc - , localTime - , localTimestamp - , now - , makeDate - , makeTime - , makeTimestamp - , makeTimestamptz - , atTimeZone - , PGAtTimeZone - -- * Interval - , interval_ - , TimeUnit (..) - ) where - -import Data.Fixed -import Data.String -import GHC.TypeLits - -import qualified GHC.Generics as GHC -import qualified Generics.SOP as SOP - -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - --- | >>> printSQL currentDate --- CURRENT_DATE -currentDate :: Expr (null 'PGdate) -currentDate = UnsafeExpression "CURRENT_DATE" - --- | >>> printSQL currentTime --- CURRENT_TIME -currentTime :: Expr (null 'PGtimetz) -currentTime = UnsafeExpression "CURRENT_TIME" - --- | >>> printSQL currentTimestamp --- CURRENT_TIMESTAMP -currentTimestamp :: Expr (null 'PGtimestamptz) -currentTimestamp = UnsafeExpression "CURRENT_TIMESTAMP" - --- | >>> printSQL localTime --- LOCALTIME -localTime :: Expr (null 'PGtime) -localTime = UnsafeExpression "LOCALTIME" - --- | >>> printSQL localTimestamp --- LOCALTIMESTAMP -localTimestamp :: Expr (null 'PGtimestamp) -localTimestamp = UnsafeExpression "LOCALTIMESTAMP" - --- | Current date and time (equivalent to `currentTimestamp`) --- --- >>> printSQL now --- now() -now :: Expr (null 'PGtimestamptz) -now = UnsafeExpression "now()" - -{-| -Create date from year, month and day fields - ->>> printSQL (makeDate (1984 :* 7 *: 3)) -make_date((1984 :: int4), (7 :: int4), (3 :: int4)) --} -makeDate :: '[ null 'PGint4, null 'PGint4, null 'PGint4 ] ---> null 'PGdate -makeDate = unsafeFunctionN "make_date" - -{-| -Create time from hour, minute and seconds fields - ->>> printSQL (makeTime (8 :* 15 *: 23.5)) -make_time((8 :: int4), (15 :: int4), (23.5 :: float8)) --} -makeTime :: '[ null 'PGint4, null 'PGint4, null 'PGfloat8 ] ---> null 'PGtime -makeTime = unsafeFunctionN "make_time" - -{-| -Create timestamp from year, month, day, hour, minute and seconds fields - ->>> printSQL (makeTimestamp (2013 :* 7 :* 15 :* 8 :* 15 *: 23.5)) -make_timestamp((2013 :: int4), (7 :: int4), (15 :: int4), (8 :: int4), (15 :: int4), (23.5 :: float8)) --} -makeTimestamp :: - '[ null 'PGint4, null 'PGint4, null 'PGint4 - , null 'PGint4, null 'PGint4, null 'PGfloat8 ] ---> null 'PGtimestamp -makeTimestamp = unsafeFunctionN "make_timestamp" - -{-| -Create timestamp with time zone from -year, month, day, hour, minute and seconds fields; -the current time zone is used - ->>> printSQL (makeTimestamptz (2013 :* 7 :* 15 :* 8 :* 15 *: 23.5)) -make_timestamptz((2013 :: int4), (7 :: int4), (15 :: int4), (8 :: int4), (15 :: int4), (23.5 :: float8)) --} -makeTimestamptz :: - '[ null 'PGint4, null 'PGint4, null 'PGint4 - , null 'PGint4, null 'PGint4, null 'PGfloat8 ] ---> null 'PGtimestamptz -makeTimestamptz = unsafeFunctionN "make_timestamptz" - -{-| -Truncate a timestamp with the specified precision - ->>> printSQL $ dateTrunc Quarter (makeTimestamp (2010 :* 5 :* 6 :* 14 :* 45 *: 11.4)) -date_trunc('quarter', make_timestamp((2010 :: int4), (5 :: int4), (6 :: int4), (14 :: int4), (45 :: int4), (11.4 :: float8))) --} -dateTrunc - :: time `In` '[ 'PGtimestamp, 'PGtimestamptz ] - => TimeUnit -> null time --> null time -dateTrunc tUnit args = unsafeFunctionN "date_trunc" (timeUnitExpr *: args) - where - timeUnitExpr :: forall grp lat with db params from null0. - Expression grp lat with db params from (null0 'PGtext) - timeUnitExpr = UnsafeExpression . singleQuotedUtf8 . renderSQL $ tUnit - --- | Calculate the return time type of the `atTimeZone` `Operator`. -type family PGAtTimeZone ty where - PGAtTimeZone 'PGtimestamptz = 'PGtimestamp - PGAtTimeZone 'PGtimestamp = 'PGtimestamptz - PGAtTimeZone 'PGtimetz = 'PGtimetz - PGAtTimeZone pg = TypeError - ( 'Text "Squeal type error: AT TIME ZONE cannot be applied to " - ':<>: 'ShowType pg ) - -{-| -Convert a timestamp, timestamp with time zone, or time of day with timezone to a different timezone using an interval offset or specific timezone denoted by text. When using the interval offset, the interval duration must be less than one day or 24 hours. - ->>> printSQL $ (makeTimestamp (2009 :* 7 :* 22 :* 19 :* 45 *: 11.4)) `atTimeZone` (interval_ 8 Hours) -(make_timestamp((2009 :: int4), (7 :: int4), (22 :: int4), (19 :: int4), (45 :: int4), (11.4 :: float8)) AT TIME ZONE (INTERVAL '8.000 hours')) - ->>> :{ - let - timezone :: Expr (null 'PGtext) - timezone = "EST" - in printSQL $ (makeTimestamptz (2015 :* 9 :* 15 :* 4 :* 45 *: 11.4)) `atTimeZone` timezone -:} -(make_timestamptz((2015 :: int4), (9 :: int4), (15 :: int4), (4 :: int4), (45 :: int4), (11.4 :: float8)) AT TIME ZONE (E'EST' :: text)) --} -atTimeZone - :: zone `In` '[ 'PGtext, 'PGinterval] - => Operator (null time) (null zone) (null (PGAtTimeZone time)) -atTimeZone = unsafeBinaryOp "AT TIME ZONE" - -{-| -Affine space operations on time types. --} -class TimeOp time diff | time -> diff where - {-| - >>> printSQL (makeDate (1984 :* 7 *: 3) !+ 365) - (make_date((1984 :: int4), (7 :: int4), (3 :: int4)) + (365 :: int4)) - -} - (!+) :: Operator (null time) (null diff) (null time) - (!+) = unsafeBinaryOp "+" - {-| - >>> printSQL (365 +! makeDate (1984 :* 7 *: 3)) - ((365 :: int4) + make_date((1984 :: int4), (7 :: int4), (3 :: int4))) - -} - (+!) :: Operator (null diff) (null time) (null time) - (+!) = unsafeBinaryOp "+" - {-| - >>> printSQL (makeDate (1984 :* 7 *: 3) !- 365) - (make_date((1984 :: int4), (7 :: int4), (3 :: int4)) - (365 :: int4)) - -} - (!-) :: Operator (null time) (null diff) (null time) - (!-) = unsafeBinaryOp "-" - {-| - >>> printSQL (makeDate (1984 :* 7 *: 3) !-! currentDate) - (make_date((1984 :: int4), (7 :: int4), (3 :: int4)) - CURRENT_DATE) - -} - (!-!) :: Operator (null time) (null time) (null diff) - (!-!) = unsafeBinaryOp "-" -instance TimeOp 'PGtimestamp 'PGinterval -instance TimeOp 'PGtimestamptz 'PGinterval -instance TimeOp 'PGtime 'PGinterval -instance TimeOp 'PGtimetz 'PGinterval -instance TimeOp 'PGinterval 'PGinterval -instance TimeOp 'PGdate 'PGint4 -infixl 6 !+ -infixl 6 +! -infixl 6 !- -infixl 6 !-! - --- | A `TimeUnit` to use in `interval_` construction. -data TimeUnit - = Years | Quarter | Months | Weeks | Days - | Hours | Minutes | Seconds - | Microseconds | Milliseconds - | Decades | Centuries | Millennia - deriving (Eq, Ord, Show, Read, Enum, GHC.Generic) -instance SOP.Generic TimeUnit -instance SOP.HasDatatypeInfo TimeUnit -instance RenderSQL TimeUnit where - renderSQL = \case - Years -> "years" - Quarter -> "quarter" - Months -> "months" - Weeks -> "weeks" - Days -> "days" - Hours -> "hours" - Minutes -> "minutes" - Seconds -> "seconds" - Microseconds -> "microseconds" - Milliseconds -> "milliseconds" - Decades -> "decades" - Centuries -> "centuries" - Millennia -> "millennia" - --- | >>> printSQL $ interval_ 7 Days --- (INTERVAL '7.000 days') -interval_ :: Milli -> TimeUnit -> Expr (null 'PGinterval) -interval_ num unit = UnsafeExpression . parenthesized $ "INTERVAL" <+> - "'" <> fromString (show num) <+> renderSQL unit <> "'" diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Type.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Type.hs deleted file mode 100644 index 27b24b90..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Type.hs +++ /dev/null @@ -1,506 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Expression -Description: type expressions -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -type expressions --} - -{-# LANGUAGE - AllowAmbiguousTypes - , DataKinds - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , GeneralizedNewtypeDeriving - , KindSignatures - , MultiParamTypeClasses - , OverloadedStrings - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeOperators - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Expression.Type - ( -- * Type Cast - cast - , astype - , inferredtype - -- * Type Expression - , TypeExpression (..) - , typedef - , typetable - , typeview - , bool - , int2 - , smallint - , int4 - , int - , integer - , int8 - , bigint - , numeric - , float4 - , real - , float8 - , doublePrecision - , money - , text - , char - , character - , varchar - , characterVarying - , bytea - , timestamp - , timestampWithTimeZone - , timestamptz - , date - , time - , timeWithTimeZone - , timetz - , interval - , uuid - , inet - , json - , jsonb - , vararray - , fixarray - , tsvector - , tsquery - , oid - , int4range - , int8range - , numrange - , tsrange - , tstzrange - , daterange - , record - -- * Column Type - , ColumnTypeExpression (..) - , nullable - , notNullable - , default_ - , serial2 - , smallserial - , serial4 - , serial - , serial8 - , bigserial - -- * Type Inference - , PGTyped (..) - , pgtypeFrom - , NullTyped (..) - , nulltypeFrom - , ColumnTyped (..) - , columntypeFrom - , FieldTyped (..) - ) where - -import Control.DeepSeq -import Data.ByteString -import Data.String -import GHC.TypeLits - -import qualified Data.ByteString as ByteString -import qualified GHC.Generics as GHC -import qualified Generics.SOP as SOP - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Type.PG -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - --- When a `cast` is applied to an `Expression` of a known type, it --- represents a run-time type conversion. The cast will succeed only if a --- suitable type conversion operation has been defined. --- --- | >>> printSQL $ true & cast int4 --- (TRUE :: int4) -cast - :: TypeExpression db ty1 - -- ^ type to cast as - -> Expression grp lat with db params from ty0 - -- ^ value to convert - -> Expression grp lat with db params from ty1 -cast ty x = UnsafeExpression $ parenthesized $ - renderSQL x <+> "::" <+> renderSQL ty - --- | A safe version of `cast` which just matches a value with its type. --- --- >>> printSQL (1 & astype int) --- ((1 :: int4) :: int) -astype - :: TypeExpression db ty - -- ^ type to specify as - -> Expression grp lat with db params from ty - -- ^ value - -> Expression grp lat with db params from ty -astype = cast - --- | `inferredtype` will add a type annotation to an `Expression` --- which can be useful for fixing the storage type of a value. --- --- >>> printSQL (inferredtype true) --- (TRUE :: bool) -inferredtype - :: NullTyped db ty - => Expression lat common grp db params from ty - -- ^ value - -> Expression lat common grp db params from ty -inferredtype = astype nulltype - -{----------------------------------------- -type expressions ------------------------------------------} - --- | `TypeExpression`s are used in `cast`s and --- `Squeal.PostgreSQL.Definition.createTable` commands. -newtype TypeExpression (db :: SchemasType) (ty :: NullType) - = UnsafeTypeExpression { renderTypeExpression :: ByteString } - deriving stock (GHC.Generic,Show,Eq,Ord) - deriving newtype (NFData) -instance RenderSQL (TypeExpression db ty) where - renderSQL = renderTypeExpression - --- | The enum or composite type in a `Typedef` can be expressed by its alias. -typedef - :: (Has sch db schema, Has td schema ('Typedef ty)) - => QualifiedAlias sch td - -- ^ type alias - -> TypeExpression db (null ty) -typedef = UnsafeTypeExpression . renderSQL - --- | The composite type corresponding to a `Table` definition can be expressed --- by its alias. -typetable - :: (Has sch db schema, Has tab schema ('Table table)) - => QualifiedAlias sch tab - -- ^ table alias - -> TypeExpression db (null ('PGcomposite (TableToRow table))) -typetable = UnsafeTypeExpression . renderSQL - --- | The composite type corresponding to a `View` definition can be expressed --- by its alias. -typeview - :: (Has sch db schema, Has vw schema ('View view)) - => QualifiedAlias sch vw - -- ^ view alias - -> TypeExpression db (null ('PGcomposite view)) -typeview = UnsafeTypeExpression . renderSQL - --- | logical Boolean (true/false) -bool :: TypeExpression db (null 'PGbool) -bool = UnsafeTypeExpression "bool" --- | signed two-byte integer -int2, smallint :: TypeExpression db (null 'PGint2) -int2 = UnsafeTypeExpression "int2" -smallint = UnsafeTypeExpression "smallint" --- | signed four-byte integer -int4, int, integer :: TypeExpression db (null 'PGint4) -int4 = UnsafeTypeExpression "int4" -int = UnsafeTypeExpression "int" -integer = UnsafeTypeExpression "integer" --- | signed eight-byte integer -int8, bigint :: TypeExpression db (null 'PGint8) -int8 = UnsafeTypeExpression "int8" -bigint = UnsafeTypeExpression "bigint" --- | arbitrary precision numeric type -numeric :: TypeExpression db (null 'PGnumeric) -numeric = UnsafeTypeExpression "numeric" --- | single precision floating-point number (4 bytes) -float4, real :: TypeExpression db (null 'PGfloat4) -float4 = UnsafeTypeExpression "float4" -real = UnsafeTypeExpression "real" --- | double precision floating-point number (8 bytes) -float8, doublePrecision :: TypeExpression db (null 'PGfloat8) -float8 = UnsafeTypeExpression "float8" -doublePrecision = UnsafeTypeExpression "double precision" --- | currency amount -money :: TypeExpression schema (null 'PGmoney) -money = UnsafeTypeExpression "money" --- | variable-length character string -text :: TypeExpression db (null 'PGtext) -text = UnsafeTypeExpression "text" --- | fixed-length character string -char, character - :: forall n db null. (KnownNat n, 1 <= n) - => TypeExpression db (null ('PGchar n)) -char = UnsafeTypeExpression $ "char(" <> renderNat @n <> ")" -character = UnsafeTypeExpression $ "character(" <> renderNat @n <> ")" --- | variable-length character string -varchar, characterVarying - :: forall n db null. (KnownNat n, 1 <= n) - => TypeExpression db (null ('PGvarchar n)) -varchar = UnsafeTypeExpression $ "varchar(" <> renderNat @n <> ")" -characterVarying = UnsafeTypeExpression $ - "character varying(" <> renderNat @n <> ")" --- | binary data ("byte array") -bytea :: TypeExpression db (null 'PGbytea) -bytea = UnsafeTypeExpression "bytea" --- | date and time (no time zone) -timestamp :: TypeExpression db (null 'PGtimestamp) -timestamp = UnsafeTypeExpression "timestamp" --- | date and time, including time zone -timestampWithTimeZone, timestamptz :: TypeExpression db (null 'PGtimestamptz) -timestampWithTimeZone = UnsafeTypeExpression "timestamp with time zone" -timestamptz = UnsafeTypeExpression "timestamptz" --- | calendar date (year, month, day) -date :: TypeExpression db (null 'PGdate) -date = UnsafeTypeExpression "date" --- | time of day (no time zone) -time :: TypeExpression db (null 'PGtime) -time = UnsafeTypeExpression "time" --- | time of day, including time zone -timeWithTimeZone, timetz :: TypeExpression db (null 'PGtimetz) -timeWithTimeZone = UnsafeTypeExpression "time with time zone" -timetz = UnsafeTypeExpression "timetz" --- | time span -interval :: TypeExpression db (null 'PGinterval) -interval = UnsafeTypeExpression "interval" --- | universally unique identifier -uuid :: TypeExpression db (null 'PGuuid) -uuid = UnsafeTypeExpression "uuid" --- | IPv4 or IPv6 host address -inet :: TypeExpression db (null 'PGinet) -inet = UnsafeTypeExpression "inet" --- | textual JSON data -json :: TypeExpression db (null 'PGjson) -json = UnsafeTypeExpression "json" --- | binary JSON data, decomposed -jsonb :: TypeExpression db (null 'PGjsonb) -jsonb = UnsafeTypeExpression "jsonb" --- | variable length array -vararray - :: TypeExpression db pg - -> TypeExpression db (null ('PGvararray pg)) -vararray ty = UnsafeTypeExpression $ renderSQL ty <> "[]" --- | fixed length array --- --- >>> renderSQL (fixarray @'[2] json) --- "json[2]" -fixarray - :: forall dims db null pg. SOP.All KnownNat dims - => TypeExpression db pg - -> TypeExpression db (null ('PGfixarray dims pg)) -fixarray ty = UnsafeTypeExpression $ - renderSQL ty <> renderDims @dims - where - renderDims :: forall ns. SOP.All KnownNat ns => ByteString - renderDims = - ("[" <>) - . (<> "]") - . ByteString.intercalate "][" - . SOP.hcollapse - $ SOP.hcmap (SOP.Proxy @KnownNat) - (SOP.K . fromString . show . natVal) - (SOP.hpure SOP.Proxy :: SOP.NP SOP.Proxy ns) --- | text search query -tsvector :: TypeExpression db (null 'PGtsvector) -tsvector = UnsafeTypeExpression "tsvector" --- | text search document -tsquery :: TypeExpression db (null 'PGtsquery) -tsquery = UnsafeTypeExpression "tsquery" --- | Object identifiers (OIDs) are used internally by PostgreSQL --- as primary keys for various system tables. -oid :: TypeExpression db (null 'PGoid) -oid = UnsafeTypeExpression "oid" --- | Range of integer -int4range :: TypeExpression db (null ('PGrange 'PGint4)) -int4range = UnsafeTypeExpression "int4range" --- | Range of bigint -int8range :: TypeExpression db (null ('PGrange 'PGint8)) -int8range = UnsafeTypeExpression "int8range" --- | Range of numeric -numrange :: TypeExpression db (null ('PGrange 'PGnumeric)) -numrange = UnsafeTypeExpression "numrange" --- | Range of timestamp without time zone -tsrange :: TypeExpression db (null ('PGrange 'PGtimestamp)) -tsrange = UnsafeTypeExpression "tsrange" --- | Range of timestamp with time zone -tstzrange :: TypeExpression db (null ('PGrange 'PGtimestamptz)) -tstzrange = UnsafeTypeExpression "tstzrange" --- | Range of date -daterange :: TypeExpression db (null ('PGrange 'PGdate)) -daterange = UnsafeTypeExpression "daterange" --- | Anonymous composite record -record :: TypeExpression db (null ('PGcomposite record)) -record = UnsafeTypeExpression "record" - --- | `pgtype` is a demoted version of a `PGType` -class PGTyped db (ty :: PGType) where pgtype :: TypeExpression db (null ty) -instance PGTyped db 'PGbool where pgtype = bool -instance PGTyped db 'PGint2 where pgtype = int2 -instance PGTyped db 'PGint4 where pgtype = int4 -instance PGTyped db 'PGint8 where pgtype = int8 -instance PGTyped db 'PGnumeric where pgtype = numeric -instance PGTyped db 'PGfloat4 where pgtype = float4 -instance PGTyped db 'PGfloat8 where pgtype = float8 -instance PGTyped db 'PGmoney where pgtype = money -instance PGTyped db 'PGtext where pgtype = text -instance (KnownNat n, 1 <= n) - => PGTyped db ('PGchar n) where pgtype = char @n -instance (KnownNat n, 1 <= n) - => PGTyped db ('PGvarchar n) where pgtype = varchar @n -instance PGTyped db 'PGbytea where pgtype = bytea -instance PGTyped db 'PGtimestamp where pgtype = timestamp -instance PGTyped db 'PGtimestamptz where pgtype = timestampWithTimeZone -instance PGTyped db 'PGdate where pgtype = date -instance PGTyped db 'PGtime where pgtype = time -instance PGTyped db 'PGtimetz where pgtype = timeWithTimeZone -instance PGTyped db 'PGinterval where pgtype = interval -instance PGTyped db 'PGuuid where pgtype = uuid -instance PGTyped db 'PGinet where pgtype = inet -instance PGTyped db 'PGjson where pgtype = json -instance PGTyped db 'PGjsonb where pgtype = jsonb -instance PGTyped db pg => PGTyped db ('PGvararray (null pg)) where - pgtype = vararray (pgtype @db @pg) -instance (SOP.All KnownNat dims, PGTyped db pg) - => PGTyped db ('PGfixarray dims (null pg)) where - pgtype = fixarray @dims (pgtype @db @pg) -instance PGTyped db 'PGtsvector where pgtype = tsvector -instance PGTyped db 'PGtsquery where pgtype = tsquery -instance PGTyped db 'PGoid where pgtype = oid -instance PGTyped db ('PGrange 'PGint4) where pgtype = int4range -instance PGTyped db ('PGrange 'PGint8) where pgtype = int8range -instance PGTyped db ('PGrange 'PGnumeric) where pgtype = numrange -instance PGTyped db ('PGrange 'PGtimestamp) where pgtype = tsrange -instance PGTyped db ('PGrange 'PGtimestamptz) where pgtype = tstzrange -instance PGTyped db ('PGrange 'PGdate) where pgtype = daterange -instance - ( UserType db ('PGcomposite row) ~ '(sch,td) - , Has sch db schema - , Has td schema ('Typedef ('PGcomposite row)) - ) => PGTyped db ('PGcomposite row) where - pgtype = typedef (QualifiedAlias @sch @td) -instance - ( UserType db ('PGenum labels) ~ '(sch,td) - , Has sch db schema - , Has td schema ('Typedef ('PGenum labels)) - ) => PGTyped db ('PGenum labels) where - pgtype = typedef (QualifiedAlias @sch @td) - --- | Specify `TypeExpression` from a Haskell type. --- --- >>> printSQL $ pgtypeFrom @String --- text --- --- >>> printSQL $ pgtypeFrom @Double --- float8 -pgtypeFrom - :: forall hask db null. PGTyped db (PG hask) - => TypeExpression db (null (PG hask)) -pgtypeFrom = pgtype @db @(PG hask) - --- | Lift `PGTyped` to a field -class FieldTyped db ty where fieldtype :: Aliased (TypeExpression db) ty -instance (KnownSymbol alias, NullTyped db ty) - => FieldTyped db (alias ::: ty) where - fieldtype = nulltype `As` Alias - --- | `ColumnTypeExpression`s are used in --- `Squeal.PostgreSQL.Definition.createTable` commands. -newtype ColumnTypeExpression (db :: SchemasType) (ty :: ColumnType) - = UnsafeColumnTypeExpression { renderColumnTypeExpression :: ByteString } - deriving stock (GHC.Generic,Show,Eq,Ord) - deriving newtype (NFData) -instance RenderSQL (ColumnTypeExpression db ty) where - renderSQL = renderColumnTypeExpression - --- | used in `Squeal.PostgreSQL.Definition.createTable` --- commands as a column constraint to note that --- @NULL@ may be present in a column -nullable - :: TypeExpression db (null ty) - -- ^ type - -> ColumnTypeExpression db ('NoDef :=> 'Null ty) -nullable ty = UnsafeColumnTypeExpression $ renderSQL ty <+> "NULL" - --- | used in `Squeal.PostgreSQL.Definition.createTable` --- commands as a column constraint to ensure --- @NULL@ is not present in a column -notNullable - :: TypeExpression db (null ty) - -- ^ type - -> ColumnTypeExpression db ('NoDef :=> 'NotNull ty) -notNullable ty = UnsafeColumnTypeExpression $ renderSQL ty <+> "NOT NULL" - --- | used in `Squeal.PostgreSQL.Definition.createTable` --- commands as a column constraint to give a default -default_ - :: Expression 'Ungrouped '[] '[] db '[] '[] ty - -- ^ default value - -> ColumnTypeExpression db ('NoDef :=> ty) - -- ^ column type - -> ColumnTypeExpression db ('Def :=> ty) -default_ x ty = UnsafeColumnTypeExpression $ - renderSQL ty <+> "DEFAULT" <+> renderExpression x - --- | not a true type, but merely a notational convenience for creating --- unique identifier columns with type `PGint2` -serial2, smallserial - :: ColumnTypeExpression db ('Def :=> 'NotNull 'PGint2) -serial2 = UnsafeColumnTypeExpression "serial2" -smallserial = UnsafeColumnTypeExpression "smallserial" --- | not a true type, but merely a notational convenience for creating --- unique identifier columns with type `PGint4` -serial4, serial - :: ColumnTypeExpression db ('Def :=> 'NotNull 'PGint4) -serial4 = UnsafeColumnTypeExpression "serial4" -serial = UnsafeColumnTypeExpression "serial" --- | not a true type, but merely a notational convenience for creating --- unique identifier columns with type `PGint8` -serial8, bigserial - :: ColumnTypeExpression db ('Def :=> 'NotNull 'PGint8) -serial8 = UnsafeColumnTypeExpression "serial8" -bigserial = UnsafeColumnTypeExpression "bigserial" - --- | Like @PGTyped@ but also accounts for null. -class NullTyped db (ty :: NullType) where - nulltype :: TypeExpression db ty - -instance PGTyped db ty => NullTyped db (null ty) where - nulltype = pgtype @db @ty - --- | Specify null `TypeExpression` from a Haskell type. --- --- >>> printSQL $ nulltypeFrom @(Maybe String) --- text --- --- >>> printSQL $ nulltypeFrom @Double --- float8 -nulltypeFrom - :: forall hask db. NullTyped db (NullPG hask) - => TypeExpression db (NullPG hask) -nulltypeFrom = nulltype @db @(NullPG hask) - --- | Like @PGTyped@ but also accounts for null. -class ColumnTyped db (column :: ColumnType) where - columntype :: ColumnTypeExpression db column -instance NullTyped db ('Null ty) - => ColumnTyped db ('NoDef :=> 'Null ty) where - columntype = nullable (nulltype @db @('Null ty)) -instance NullTyped db ('NotNull ty) - => ColumnTyped db ('NoDef :=> 'NotNull ty) where - columntype = notNullable (nulltype @db @('NotNull ty)) - --- | Specify `ColumnTypeExpression` from a Haskell type. --- --- >>> printSQL $ columntypeFrom @(Maybe String) --- text NULL --- --- >>> printSQL $ columntypeFrom @Double --- float8 NOT NULL -columntypeFrom - :: forall hask db. (ColumnTyped db ('NoDef :=> NullPG hask)) - => ColumnTypeExpression db ('NoDef :=> NullPG hask) -columntypeFrom = columntype @db @('NoDef :=> NullPG hask) diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Window.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Window.hs deleted file mode 100644 index 3d21c6c9..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Expression/Window.hs +++ /dev/null @@ -1,348 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Expression.Window -Description: window functions, arguments and definitions -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -window functions, arguments and definitions --} - -{-# LANGUAGE - DataKinds - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , GeneralizedNewtypeDeriving - , KindSignatures - , LambdaCase - , MultiParamTypeClasses - , OverloadedStrings - , PatternSynonyms - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeOperators - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Expression.Window - ( -- * Window Definition - WindowDefinition (..) - , partitionBy - -- * Window Function - -- ** Types - , WindowFunction (..) - , WindowArg (..) - , pattern Window - , pattern Windows - , WinFun0 - , type (-#->) - , type (--#->) - -- ** Functions - , rank - , rowNumber - , denseRank - , percentRank - , cumeDist - , ntile - , lag - , lead - , firstValue - , lastValue - , nthValue - , unsafeWindowFunction1 - , unsafeWindowFunctionN - ) where - -import Control.DeepSeq -import Data.ByteString (ByteString) - -import qualified GHC.Generics as GHC -import qualified Generics.SOP as SOP - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Expression.Aggregate -import Squeal.PostgreSQL.Expression.Logic -import Squeal.PostgreSQL.Expression.Sort -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - -instance Aggregate (WindowArg grp) (WindowFunction grp) where - countStar = UnsafeWindowFunction "count(*)" - count = unsafeWindowFunction1 "count" - sum_ = unsafeWindowFunction1 "sum" - arrayAgg = unsafeWindowFunction1 "array_agg" - jsonAgg = unsafeWindowFunction1 "json_agg" - jsonbAgg = unsafeWindowFunction1 "jsonb_agg" - bitAnd = unsafeWindowFunction1 "bit_and" - bitOr = unsafeWindowFunction1 "bit_or" - boolAnd = unsafeWindowFunction1 "bool_and" - boolOr = unsafeWindowFunction1 "bool_or" - every = unsafeWindowFunction1 "every" - max_ = unsafeWindowFunction1 "max" - min_ = unsafeWindowFunction1 "min" - avg = unsafeWindowFunction1 "avg" - corr = unsafeWindowFunctionN "corr" - covarPop = unsafeWindowFunctionN "covar_pop" - covarSamp = unsafeWindowFunctionN "covar_samp" - regrAvgX = unsafeWindowFunctionN "regr_avgx" - regrAvgY = unsafeWindowFunctionN "regr_avgy" - regrCount = unsafeWindowFunctionN "regr_count" - regrIntercept = unsafeWindowFunctionN "regr_intercept" - regrR2 = unsafeWindowFunctionN "regr_r2" - regrSlope = unsafeWindowFunctionN "regr_slope" - regrSxx = unsafeWindowFunctionN "regr_sxx" - regrSxy = unsafeWindowFunctionN "regr_sxy" - regrSyy = unsafeWindowFunctionN "regr_syy" - stddev = unsafeWindowFunction1 "stddev" - stddevPop = unsafeWindowFunction1 "stddev_pop" - stddevSamp = unsafeWindowFunction1 "stddev_samp" - variance = unsafeWindowFunction1 "variance" - varPop = unsafeWindowFunction1 "var_pop" - varSamp = unsafeWindowFunction1 "var_samp" - --- | A `WindowDefinition` is a set of table rows that are somehow related --- to the current row -data WindowDefinition grp lat with db params from where - WindowDefinition - :: SOP.SListI bys - => NP (Expression grp lat with db params from) bys - -- ^ `partitionBy` clause - -> [SortExpression grp lat with db params from] - -- ^ `Squeal.PostgreSQL.Expression.Sort.orderBy` clause - -> WindowDefinition grp lat with db params from - -instance OrderBy (WindowDefinition grp) grp where - orderBy sortsR (WindowDefinition parts sortsL) - = WindowDefinition parts (sortsL ++ sortsR) - -instance RenderSQL (WindowDefinition grp lat with db params from) where - renderSQL (WindowDefinition part ord) = - renderPartitionByClause part <> renderSQL ord - where - renderPartitionByClause = \case - Nil -> "" - parts -> "PARTITION" <+> "BY" <+> renderCommaSeparated renderExpression parts - -{- | -The `partitionBy` clause within `Squeal.PostgreSQL.Query.Over` divides the rows into groups, -or partitions, that share the same values of the `partitionBy` `Expression`(s). -For each row, the window function is computed across the rows that fall into -the same partition as the current row. --} -partitionBy - :: SOP.SListI bys - => NP (Expression grp lat with db params from) bys -- ^ partitions - -> WindowDefinition grp lat with db params from -partitionBy bys = WindowDefinition bys [] - -{- | -A window function performs a calculation across a set of table rows -that are somehow related to the current row. This is comparable to the type -of calculation that can be done with an aggregate function. -However, window functions do not cause rows to become grouped into a single -output row like non-window aggregate calls would. -Instead, the rows retain their separate identities. -Behind the scenes, the window function is able to access more than -just the current row of the query result. --} -newtype WindowFunction - (grp :: Grouping) - (lat :: FromType) - (with :: FromType) - (db :: SchemasType) - (params :: [NullType]) - (from :: FromType) - (ty :: NullType) - = UnsafeWindowFunction { renderWindowFunction :: ByteString } - deriving stock (GHC.Generic,Show,Eq,Ord) - deriving newtype (NFData) - -{- | -`WindowArg`s are used for the input of `WindowFunction`s. --} -data WindowArg - (grp :: Grouping) - (args :: [NullType]) - (lat :: FromType) - (with :: FromType) - (db :: SchemasType) - (params :: [NullType]) - (from :: FromType) - = WindowArg - { windowArgs :: NP (Expression grp lat with db params from) args - -- ^ `Window` or `Windows` - , windowFilter :: [Condition grp lat with db params from] - -- ^ `filterWhere` - } deriving stock (GHC.Generic) - -instance (HasUnique tab (Join from lat) row, Has col row ty) - => IsLabel col (WindowArg 'Ungrouped '[ty] lat with db params from) where - fromLabel = Window (fromLabel @col) -instance (Has tab (Join from lat) row, Has col row ty) - => IsQualified tab col (WindowArg 'Ungrouped '[ty] lat with db params from) where - tab ! col = Window (tab ! col) -instance (HasUnique tab (Join from lat) row, Has col row ty, GroupedBy tab col bys) - => IsLabel col (WindowArg ('Grouped bys) '[ty] lat with db params from) where - fromLabel = Window (fromLabel @col) -instance (Has tab (Join from lat) row, Has col row ty, GroupedBy tab col bys) - => IsQualified tab col (WindowArg ('Grouped bys) '[ty] lat with db params from) where - tab ! col = Window (tab ! col) - -instance SOP.SListI args - => RenderSQL (WindowArg grp args lat with db params from) where - renderSQL (WindowArg args filters) = - parenthesized (renderCommaSeparated renderSQL args) - & renderFilters filters - where - renderFilter wh = "FILTER" <+> parenthesized ("WHERE" <+> wh) - renderFilters = \case - [] -> id - wh:whs -> (<+> renderFilter (renderSQL (foldr (.&&) wh whs))) - -instance FilterWhere (WindowArg grp) grp where - filterWhere wh (WindowArg args filters) = WindowArg args (wh : filters) - --- | `Window` invokes a `WindowFunction` on a single argument. -pattern Window - :: Expression grp lat with db params from arg - -- ^ argument - -> WindowArg grp '[arg] lat with db params from -pattern Window x = Windows (x :* Nil) - --- | `Windows` invokes a `WindowFunction` on multiple argument. -pattern Windows - :: NP (Expression grp lat with db params from) args - -- ^ arguments - -> WindowArg grp args lat with db params from -pattern Windows xs = WindowArg xs [] - -instance RenderSQL (WindowFunction grp lat with db params from ty) where - renderSQL = renderWindowFunction - -{- | -A @RankNType@ for window functions with no arguments. --} -type WinFun0 x - = forall grp lat with db params from - . WindowFunction grp lat with db params from x - -- ^ cannot reference aliases - -{- | -A @RankNType@ for window functions with 1 argument. --} -type (-#->) x y - = forall grp lat with db params from - . WindowArg grp '[x] lat with db params from - -- ^ input - -> WindowFunction grp lat with db params from y - -- ^ output - -{- | A @RankNType@ for window functions with a fixed-length -list of heterogeneous arguments. -Use the `*:` operator to end your argument lists. --} -type (--#->) xs y - = forall grp lat with db params from - . WindowArg grp xs lat with db params from - -- ^ inputs - -> WindowFunction grp lat with db params from y - -- ^ output - --- | escape hatch for defining window functions -unsafeWindowFunction1 :: ByteString -> x -#-> y -unsafeWindowFunction1 fun x - = UnsafeWindowFunction $ fun <> renderSQL x - --- | escape hatch for defining multi-argument window functions -unsafeWindowFunctionN :: SOP.SListI xs => ByteString -> xs --#-> y -unsafeWindowFunctionN fun xs = UnsafeWindowFunction $ fun <> renderSQL xs - -{- | rank of the current row with gaps; same as `rowNumber` of its first peer - ->>> printSQL rank -rank() --} -rank :: WinFun0 ('NotNull 'PGint8) -rank = UnsafeWindowFunction "rank()" - -{- | number of the current row within its partition, counting from 1 - ->>> printSQL rowNumber -row_number() --} -rowNumber :: WinFun0 ('NotNull 'PGint8) -rowNumber = UnsafeWindowFunction "row_number()" - -{- | rank of the current row without gaps; this function counts peer groups - ->>> printSQL denseRank -dense_rank() --} -denseRank :: WinFun0 ('NotNull 'PGint8) -denseRank = UnsafeWindowFunction "dense_rank()" - -{- | relative rank of the current row: (rank - 1) / (total partition rows - 1) - ->>> printSQL percentRank -percent_rank() --} -percentRank :: WinFun0 ('NotNull 'PGfloat8) -percentRank = UnsafeWindowFunction "percent_rank()" - -{- | cumulative distribution: (number of partition rows -preceding or peer with current row) / total partition rows - ->>> printSQL cumeDist -cume_dist() --} -cumeDist :: WinFun0 ('NotNull 'PGfloat8) -cumeDist = UnsafeWindowFunction "cume_dist()" - -{- | integer ranging from 1 to the argument value, -dividing the partition as equally as possible - ->>> printSQL $ ntile (Window 5) -ntile((5 :: int4)) --} -ntile :: 'NotNull 'PGint4 -#-> 'NotNull 'PGint4 -ntile = unsafeWindowFunction1 "ntile" - -{- | returns value evaluated at the row that is offset rows before the current -row within the partition; if there is no such row, instead return default -(which must be of the same type as value). Both offset and default are -evaluated with respect to the current row. --} -lag :: '[ty, 'NotNull 'PGint4, ty] --#-> ty -lag = unsafeWindowFunctionN "lag" - -{- | returns value evaluated at the row that is offset rows after the current -row within the partition; if there is no such row, instead return default -(which must be of the same type as value). Both offset and default are -evaluated with respect to the current row. --} -lead :: '[ty, 'NotNull 'PGint4, ty] --#-> ty -lead = unsafeWindowFunctionN "lead" - -{- | returns value evaluated at the row that is the -first row of the window frame --} -firstValue :: ty -#-> ty -firstValue = unsafeWindowFunction1 "first_value" - -{- | returns value evaluated at the row that is the -last row of the window frame --} -lastValue :: ty -#-> ty -lastValue = unsafeWindowFunction1 "last_value" - -{- | returns value evaluated at the row that is the nth -row of the window frame (counting from 1); null if no such row --} -nthValue :: '[null ty, 'NotNull 'PGint4] --#-> 'Null ty -nthValue = unsafeWindowFunctionN "nth_value" diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Manipulation.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Manipulation.hs deleted file mode 100644 index fbf9f5da..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Manipulation.hs +++ /dev/null @@ -1,343 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Manipulation -Description: data manipulation language -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -data manipulation language --} - -{-# LANGUAGE - DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , GeneralizedNewtypeDeriving - , LambdaCase - , MultiParamTypeClasses - , OverloadedStrings - , PatternSynonyms - , QuantifiedConstraints - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeFamilies - , TypeInType - , TypeOperators - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Manipulation - ( -- * Manipulation - Manipulation (..) - , Manipulation_ - , queryStatement - , ReturningClause (..) - , pattern Returning_ - , UsingClause (..) - ) where - -import Control.DeepSeq -import Data.ByteString hiding (foldr) -import Data.Kind (Type) -import Data.Quiver.Functor - -import qualified Generics.SOP as SOP -import qualified GHC.Generics as GHC - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Type.PG -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Query -import Squeal.PostgreSQL.Query.From -import Squeal.PostgreSQL.Query.Select -import Squeal.PostgreSQL.Query.With -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL --- >>> import Data.Int --- >>> import Data.Time - -{- | -A `Manipulation` is a statement which may modify data in the database, -but does not alter its schemas. Examples are -`Squeal.PostgreSQL.Manipulation.Insert.insertInto`s, -`Squeal.PostgreSQL.Manipulation.Update.update`s and -`Squeal.PostgreSQL.Manipulation.Delete.deleteFrom`s. -A `queryStatement` is also considered a `Manipulation` even though it does not modify data. - -The general `Manipulation` type is parameterized by - -* @with :: FromType@ - scope for all `Squeal.PostgreSQL.Query.From.common` table expressions, -* @db :: SchemasType@ - scope for all `Squeal.PostgreSQL.Query.From.table`s and `Squeal.PostgreSQL.Query.From.view`s, -* @params :: [NullType]@ - scope for all `Squeal.Expression.Parameter.parameter`s, -* @row :: RowType@ - return type of the `Manipulation`. - -Let's see some examples of `Manipulation`s. - -simple insert: - ->>> type Columns = '["col1" ::: 'NoDef :=> 'Null 'PGint4, "col2" ::: 'Def :=> 'NotNull 'PGint4] ->>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] ->>> :{ -let - manp :: Manipulation with (Public Schema) '[] '[] - manp = - insertInto_ #tab (Values_ (Set 2 `as` #col1 :* Default `as` #col2)) -in printSQL manp -:} -INSERT INTO "tab" AS "tab" ("col1", "col2") VALUES ((2 :: int4), DEFAULT) - -out-of-line parameterized insert: - ->>> type Columns = '["col1" ::: 'Def :=> 'NotNull 'PGint4, "col2" ::: 'NoDef :=> 'NotNull 'PGint4] ->>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] ->>> :{ -let - manp :: Manipulation with (Public Schema) '[ 'NotNull 'PGint4] '[] - manp = - insertInto_ #tab $ Values_ - (Default `as` #col1 :* Set (param @1) `as` #col2) -in printSQL manp -:} -INSERT INTO "tab" AS "tab" ("col1", "col2") VALUES (DEFAULT, ($1 :: int4)) - -in-line parameterized insert: - ->>> type Columns = '["col1" ::: 'Def :=> 'NotNull 'PGint4, "col2" ::: 'NoDef :=> 'NotNull 'PGint4] ->>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] ->>> :{ -data Row = Row { col1 :: Optional SOP.I ('Def :=> Int32), col2 :: Int32 } - deriving stock (GHC.Generic) - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) -:} - ->>> :{ -let - manp :: Row -> Row -> Manipulation with (Public Schema) '[] '[] - manp row1 row2 = insertInto_ #tab $ inlineValues row1 [row2] - row1 = Row {col1 = Default, col2 = 2 :: Int32} - row2 = Row {col1 = NotDefault (3 :: Int32), col2 = 4 :: Int32} -in printSQL (manp row1 row2) -:} -INSERT INTO "tab" AS "tab" ("col1", "col2") VALUES (DEFAULT, (2 :: int4)), ((3 :: int4), (4 :: int4)) - -returning insert: - ->>> :{ -let - manp :: Manipulation with (Public Schema) '[] '["col1" ::: 'NotNull 'PGint4] - manp = - insertInto #tab (Values_ (Set 2 `as` #col1 :* Set 3 `as` #col2)) - OnConflictDoRaise (Returning #col1) -in printSQL manp -:} -INSERT INTO "tab" AS "tab" ("col1", "col2") VALUES ((2 :: int4), (3 :: int4)) RETURNING "col1" AS "col1" - -upsert: - ->>> type CustomersColumns = '["name" ::: 'NoDef :=> 'NotNull 'PGtext, "email" ::: 'NoDef :=> 'NotNull 'PGtext] ->>> type CustomersConstraints = '["uq" ::: 'Unique '["name"]] ->>> type CustomersSchema = '["customers" ::: 'Table (CustomersConstraints :=> CustomersColumns)] ->>> :{ -let - manp :: Manipulation with (Public CustomersSchema) '[] '[] - manp = - insertInto #customers - (Values_ (Set "John Smith" `as` #name :* Set "john@smith.com" `as` #email)) - (OnConflict (OnConstraint #uq) - (DoUpdate (Set (#excluded ! #email <> "; " <> #customers ! #email) `as` #email) [])) - (Returning_ Nil) -in printSQL manp -:} -INSERT INTO "customers" AS "customers" ("name", "email") VALUES ((E'John Smith' :: text), (E'john@smith.com' :: text)) ON CONFLICT ON CONSTRAINT "uq" DO UPDATE SET "email" = ("excluded"."email" || ((E'; ' :: text) || "customers"."email")) - -query insert: - ->>> :{ -let - manp :: Manipulation with (Public Schema) '[] '[] - manp = insertInto_ #tab (Subquery (select Star (from (table #tab)))) -in printSQL manp -:} -INSERT INTO "tab" AS "tab" SELECT * FROM "tab" AS "tab" - -update: - ->>> :{ -let - manp :: Manipulation with (Public Schema) '[] '[] - manp = update_ #tab (Set 2 `as` #col1) (#col1 ./= #col2) -in printSQL manp -:} -UPDATE "tab" AS "tab" SET "col1" = (2 :: int4) WHERE ("col1" <> "col2") - -delete: - ->>> :{ -let - manp :: Manipulation with (Public Schema) '[] '["col1" ::: 'NotNull 'PGint4, "col2" ::: 'NotNull 'PGint4] - manp = deleteFrom #tab NoUsing (#col1 .== #col2) (Returning Star) -in printSQL manp -:} -DELETE FROM "tab" AS "tab" WHERE ("col1" = "col2") RETURNING * - -delete and using clause: - ->>> :{ -type Schema3 = - '[ "tab" ::: 'Table ('[] :=> Columns) - , "other_tab" ::: 'Table ('[] :=> Columns) - , "third_tab" ::: 'Table ('[] :=> Columns) ] -:} - ->>> :{ -let - manp :: Manipulation with (Public Schema3) '[] '[] - manp = - deleteFrom #tab (Using (table #other_tab & also (table #third_tab))) - ( (#tab ! #col2 .== #other_tab ! #col2) - .&& (#tab ! #col2 .== #third_tab ! #col2) ) - (Returning_ Nil) -in printSQL manp -:} -DELETE FROM "tab" AS "tab" USING "other_tab" AS "other_tab", "third_tab" AS "third_tab" WHERE (("tab"."col2" = "other_tab"."col2") AND ("tab"."col2" = "third_tab"."col2")) - -with manipulation: - ->>> type ProductsColumns = '["product" ::: 'NoDef :=> 'NotNull 'PGtext, "date" ::: 'Def :=> 'NotNull 'PGdate] ->>> type ProductsSchema = '["products" ::: 'Table ('[] :=> ProductsColumns), "products_deleted" ::: 'Table ('[] :=> ProductsColumns)] ->>> :{ -let - manp :: Manipulation with (Public ProductsSchema) '[ 'NotNull 'PGdate] '[] - manp = with - (deleteFrom #products NoUsing (#date .< param @1) (Returning Star) `as` #del) - (insertInto_ #products_deleted (Subquery (select Star (from (common #del))))) -in printSQL manp -:} -WITH "del" AS (DELETE FROM "products" AS "products" WHERE ("date" < ($1 :: date)) RETURNING *) INSERT INTO "products_deleted" AS "products_deleted" SELECT * FROM "del" AS "del" --} -newtype Manipulation - (with :: FromType) - (db :: SchemasType) - (params :: [NullType]) - (columns :: RowType) - = UnsafeManipulation { renderManipulation :: ByteString } - deriving stock (GHC.Generic,Show,Eq,Ord) - deriving newtype (NFData) -instance RenderSQL (Manipulation with db params columns) where - renderSQL = renderManipulation -instance With Manipulation where - with Done manip = manip - with ctes manip = UnsafeManipulation $ - "WITH" <+> commaSeparated (qtoList renderSQL ctes) <+> renderSQL manip - -{- | -The `Manipulation_` type is parameterized by a @db@ `SchemasType`, -against which it is type-checked, an input @params@ Haskell `Type`, -and an ouput row Haskell `Type`. - -Generally, @params@ will be a Haskell tuple or record whose entries -may be referenced using positional -`Squeal.PostgreSQL.Expression.Parameter.param`s and @row@ will be a -Haskell record, whose entries will be targeted using overloaded labels. - -A `Manipulation_` can be run -using `Squeal.PostgreSQL.Session.manipulateParams`, or if @params = ()@ -using `Squeal.PostgreSQL.Session.manipulate`. - -`Manipulation_` is a type family which resolves into a `Manipulation`, -so don't be fooled by the input params and output row Haskell `Type`s, -which are converted into appropriate -Postgres @[@`NullType`@]@ params and `RowType` rows. -Use `Squeal.PostgreSQL.Session.Statement.manipulation` to -fix actual Haskell input params and output rows. - ->>> :set -XDeriveAnyClass -XDerivingStrategies ->>> type Columns = '["col1" ::: 'NoDef :=> 'Null 'PGint8, "col2" ::: 'Def :=> 'NotNull 'PGtext] ->>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] ->>> :{ -data Row = Row { col1 :: Maybe Int64, col2 :: String } - deriving stock (GHC.Generic) - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) -:} - ->>> :{ -let - manp :: Manipulation_ (Public Schema) (Int64, Int64) Row - manp = deleteFrom #tab NoUsing (#col1 .== param @1 + param @2) (Returning Star) - stmt :: Statement (Public Schema) (Int64, Int64) Row - stmt = manipulation manp -:} - ->>> :type manp -manp - :: Manipulation - '[] - '["public" ::: '["tab" ::: 'Table ('[] :=> Columns)]] - '[ 'NotNull 'PGint8, 'NotNull 'PGint8] - '["col1" ::: 'Null 'PGint8, "col2" ::: 'NotNull 'PGtext] ->>> :type stmt -stmt - :: Statement - '["public" ::: '["tab" ::: 'Table ('[] :=> Columns)]] - (Int64, Int64) - Row --} -type family Manipulation_ (db :: SchemasType) (params :: Type) (row :: Type) where - Manipulation_ db params row = Manipulation '[] db (TuplePG params) (RowPG row) - --- | Convert a `Query` into a `Manipulation`. -queryStatement - :: Query '[] with db params columns - -- ^ `Query` to embed as a `Manipulation` - -> Manipulation with db params columns -queryStatement q = UnsafeManipulation $ renderSQL q - --- | A `ReturningClause` computes and returns value(s) based --- on each row actually inserted, updated or deleted. This is primarily --- useful for obtaining values that were supplied by defaults, such as a --- serial sequence number. However, any expression using the table's columns --- is allowed. Only rows that were successfully inserted or updated or --- deleted will be returned. For example, if a row was locked --- but not updated because an `Squeal.PostgreSQL.Manipulation.Insert.OnConflict` --- `Squeal.PostgreSQL.Manipulation.Insert.DoUpdate` condition was not satisfied, --- the row will not be returned. `Returning` `Star` will return all columns --- in the row. Use `Returning_` `Nil` in the common case where no return --- values are desired. -newtype ReturningClause with db params from row = - Returning (Selection 'Ungrouped '[] with db params from row) - -instance RenderSQL (ReturningClause with db params from row) where - renderSQL = \case - Returning (List Nil) -> "" - Returning selection -> " RETURNING" <+> renderSQL selection - --- | `Returning` a `List` -pattern Returning_ - :: SOP.SListI row - => NP (Aliased (Expression 'Ungrouped '[] with db params from)) row - -- ^ row of values - -> ReturningClause with db params from row -pattern Returning_ list = Returning (List list) - --- | Specify additional tables with `Using` --- an `also` list of table expressions, allowing columns --- from other tables to appear in the WHERE condition. --- This is similar to the list of tables that can be specified --- in the FROM Clause of a SELECT statement; --- for example, an alias for the table name can be specified. --- Do not repeat the target table in the `Using` list, --- unless you wish to set up a self-join. --- `NoUsing` if no additional tables are to be used. -data UsingClause with db params from where - NoUsing :: UsingClause with db params '[] - Using - :: FromClause '[] with db params from - -- ^ what to use - -> UsingClause with db params from diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Manipulation/Call.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Manipulation/Call.hs deleted file mode 100644 index e3bcc828..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Manipulation/Call.hs +++ /dev/null @@ -1,117 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Call -Description: call statements -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -call statements --} - -{-# LANGUAGE - DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , GeneralizedNewtypeDeriving - , LambdaCase - , MultiParamTypeClasses - , OverloadedStrings - , PatternSynonyms - , QuantifiedConstraints - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeFamilies - , TypeInType - , TypeOperators - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Manipulation.Call - ( -- * Call - call - , unsafeCall - , callN - , unsafeCallN - ) where - -import Data.ByteString hiding (foldr) - -import Generics.SOP (SListI) - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Manipulation -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - -{- | ->>> printSQL $ unsafeCall "p" true -CALL p(TRUE) --} -unsafeCall - :: ByteString -- ^ procedure to call - -> Expression 'Ungrouped '[] with db params '[] x -- ^ arguments - -> Manipulation with db params '[] -unsafeCall pro x = UnsafeManipulation $ - "CALL" <+> pro <> parenthesized (renderSQL x) - -{- | Call a user defined procedure of one variable. - ->>> type Schema = '[ "p" ::: 'Procedure '[ 'NotNull 'PGint4 ] ] ->>> :{ -let - p :: Manipulation '[] (Public Schema) '[] '[] - p = call #p 1 -in - printSQL p -:} -CALL "p"((1 :: int4)) --} -call - :: ( Has sch db schema - , Has pro schema ('Procedure '[x]) ) - => QualifiedAlias sch pro -- ^ procedure to call - -> Expression 'Ungrouped '[] with db params '[] x -- ^ arguments - -> Manipulation with db params '[] -call = unsafeCall . renderSQL - - -{- | ->>> printSQL $ unsafeCallN "p" (true *: false) -CALL p(TRUE, FALSE) --} -unsafeCallN - :: SListI xs - => ByteString -- ^ procedure to call - -> NP (Expression 'Ungrouped '[] with db params '[]) xs -- ^ arguments - -> Manipulation with db params '[] -unsafeCallN pro xs = UnsafeManipulation $ - "CALL" <+> pro <> parenthesized (renderCommaSeparated renderSQL xs) - -{- | Call a user defined procedure. - ->>> type Schema = '[ "p" ::: 'Procedure '[ 'NotNull 'PGint4, 'NotNull 'PGtext ] ] ->>> :{ -let - p :: Manipulation '[] (Public Schema) '[] '[] - p = callN #p (1 *: "hi") -in - printSQL p -:} -CALL "p"((1 :: int4), (E'hi' :: text)) --} -callN - :: ( Has sch db schema - , Has pro schema ('Procedure xs) - , SListI xs ) - => QualifiedAlias sch pro -- ^ procedure to call - -> NP (Expression 'Ungrouped '[] with db params '[]) xs -- ^ arguments - -> Manipulation with db params '[] -callN = unsafeCallN . renderSQL diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Manipulation/Delete.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Manipulation/Delete.hs deleted file mode 100644 index 13f9dd44..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Manipulation/Delete.hs +++ /dev/null @@ -1,105 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Delete -Description: delete statements -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -delete statements --} - -{-# LANGUAGE - DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , GeneralizedNewtypeDeriving - , LambdaCase - , MultiParamTypeClasses - , OverloadedStrings - , PatternSynonyms - , QuantifiedConstraints - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeFamilies - , TypeInType - , TypeOperators - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Manipulation.Delete - ( -- * Delete - deleteFrom - , deleteFrom_ - ) where - -import qualified Generics.SOP as SOP - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Expression.Logic -import Squeal.PostgreSQL.Manipulation -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - -{----------------------------------------- -DELETE statements ------------------------------------------} - -{- | Delete rows from a table. - ->>> type Columns = '["col1" ::: 'Def :=> 'NotNull 'PGint4, "col2" ::: 'NoDef :=> 'NotNull 'PGint4] ->>> type Schema = '["tab1" ::: 'Table ('[] :=> Columns), "tab2" ::: 'Table ('[] :=> Columns)] ->>> :{ -let - manp :: Manipulation with (Public Schema) '[] '["col1" ::: 'NotNull 'PGint4, "col2" ::: 'NotNull 'PGint4] - manp = deleteFrom #tab1 (Using (table #tab2)) (#tab1 ! #col1 .== #tab2 ! #col2) (Returning (#tab1 & DotStar)) -in printSQL manp -:} -DELETE FROM "tab1" AS "tab1" USING "tab2" AS "tab2" WHERE ("tab1"."col1" = "tab2"."col2") RETURNING "tab1".* --} -deleteFrom - :: ( SOP.SListI row - , Has sch db schema - , Has tab0 schema ('Table table) ) - => Aliased (QualifiedAlias sch) (tab ::: tab0) -- ^ table to delete from - -> UsingClause with db params from - -> Condition 'Ungrouped '[] with db params (tab ::: TableToRow table ': from) - -- ^ condition under which to delete a row - -> ReturningClause with db params (tab ::: TableToRow table ': from) row - -- ^ results to return - -> Manipulation with db params row -deleteFrom (tab0 `As` tab) using wh returning = UnsafeManipulation $ - "DELETE FROM" - <+> renderSQL tab0 <+> "AS" <+> renderSQL tab - <> case using of - NoUsing -> "" - Using tables -> " USING" <+> renderSQL tables - <+> "WHERE" <+> renderSQL wh - <> renderSQL returning - -{- | Delete rows returning `Nil`. - ->>> type Columns = '["col1" ::: 'Def :=> 'NotNull 'PGint4] ->>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] ->>> :{ -let - manp :: Manipulation with (Public Schema) '[ 'NotNull 'PGint4] '[] - manp = deleteFrom_ (#tab `as` #t) (#t ! #col1 .== param @1) -in printSQL manp -:} -DELETE FROM "tab" AS "t" WHERE ("t"."col1" = ($1 :: int4)) --} -deleteFrom_ - :: ( Has sch db schema - , Has tab0 schema ('Table table) ) - => Aliased (QualifiedAlias sch) (tab ::: tab0) -- ^ table to delete from - -> Condition 'Ungrouped '[] with db params '[tab ::: TableToRow table] - -- ^ condition under which to delete a row - -> Manipulation with db params '[] -deleteFrom_ tab wh = deleteFrom tab NoUsing wh (Returning_ Nil) diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Manipulation/Insert.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Manipulation/Insert.hs deleted file mode 100644 index fc7eb881..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Manipulation/Insert.hs +++ /dev/null @@ -1,288 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Manipulation.Insert -Description: insert statements -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -insert statements --} - -{-# LANGUAGE - DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , GeneralizedNewtypeDeriving - , LambdaCase - , MultiParamTypeClasses - , OverloadedStrings - , PatternSynonyms - , QuantifiedConstraints - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeFamilies - , TypeInType - , TypeOperators - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Manipulation.Insert - ( -- * Insert - insertInto - , insertInto_ - -- * Clauses - , QueryClause (..) - , pattern Values_ - , inlineValues - , inlineValues_ - , ConflictClause (..) - , ConflictTarget (..) - , ConflictAction (..) - ) where - -import Data.ByteString hiding (foldr) - -import qualified Generics.SOP as SOP -import qualified Generics.SOP.Record as SOP - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Expression.Default -import Squeal.PostgreSQL.Expression.Inline -import Squeal.PostgreSQL.Expression.Logic -import Squeal.PostgreSQL.Manipulation -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Query -import Squeal.PostgreSQL.Query.Table -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - -{----------------------------------------- -INSERT statements ------------------------------------------} - -{- | -When a table is created, it contains no data. The first thing to do -before a database can be of much use is to insert data. Data is -conceptually inserted one row at a time. Of course you can also insert -more than one row, but there is no way to insert less than one row. -Even if you know only some column values, a complete row must be created. - ->>> type CustomersColumns = '["name" ::: 'NoDef :=> 'NotNull 'PGtext, "email" ::: 'NoDef :=> 'NotNull 'PGtext] ->>> type CustomersConstraints = '["uq" ::: 'Unique '["name"]] ->>> type CustomersSchema = '["customers" ::: 'Table (CustomersConstraints :=> CustomersColumns)] ->>> :{ -let - manp :: Manipulation with (Public CustomersSchema) '[] '[] - manp = - insertInto #customers - (Values_ (Set "John Smith" `as` #name :* Set "john@smith.com" `as` #email)) - (OnConflict (OnConstraint #uq) - (DoUpdate (Set (#excluded ! #email <> "; " <> #customers ! #email) `as` #email) [])) - (Returning_ Nil) -in printSQL manp -:} -INSERT INTO "customers" AS "customers" ("name", "email") VALUES ((E'John Smith' :: text), (E'john@smith.com' :: text)) ON CONFLICT ON CONSTRAINT "uq" DO UPDATE SET "email" = ("excluded"."email" || ((E'; ' :: text) || "customers"."email")) --} -insertInto - :: ( Has sch db schema - , Has tab0 schema ('Table table) - , SOP.SListI (TableToColumns table) - , SOP.SListI row ) - => Aliased (QualifiedAlias sch) (tab ::: tab0) - -- ^ table - -> QueryClause with db params (TableToColumns table) - -- ^ what to insert - -> ConflictClause tab with db params table - -- ^ what to do in case of conflict - -> ReturningClause with db params '[tab ::: TableToRow table] row - -- ^ what to return - -> Manipulation with db params row -insertInto (tab0 `As` tab) qry conflict ret = UnsafeManipulation $ - "INSERT" <+> "INTO" - <+> renderSQL tab0 <+> "AS" <+> renderSQL tab - <+> renderSQL qry - <> renderSQL conflict - <> renderSQL ret - -{- | Like `insertInto` but with `OnConflictDoRaise` and no `ReturningClause`. - ->>> type Columns = '["col1" ::: 'NoDef :=> 'Null 'PGint4, "col2" ::: 'Def :=> 'NotNull 'PGint4] ->>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] ->>> :{ -let - manp :: Manipulation with (Public Schema) '[] '[] - manp = - insertInto_ #tab (Values_ (Set 2 `as` #col1 :* Default `as` #col2)) -in printSQL manp -:} -INSERT INTO "tab" AS "tab" ("col1", "col2") VALUES ((2 :: int4), DEFAULT) --} -insertInto_ - :: ( Has sch db schema - , Has tab0 schema ('Table table) - , SOP.SListI (TableToColumns table) ) - => Aliased (QualifiedAlias sch) (tab ::: tab0) - -- ^ table - -> QueryClause with db params (TableToColumns table) - -- ^ what to insert - -> Manipulation with db params '[] -insertInto_ tab qry = - insertInto tab qry OnConflictDoRaise (Returning_ Nil) - --- | A `QueryClause` describes what to `insertInto` a table. -data QueryClause with db params columns where - Values - :: SOP.SListI columns - => NP (Aliased (Optional (Expression 'Ungrouped '[] with db params from))) columns - -- ^ row of values - -> [NP (Aliased (Optional (Expression 'Ungrouped '[] with db params from))) columns] - -- ^ additional rows of values - -> QueryClause with db params columns - Select - :: SOP.SListI columns - => NP (Aliased (Optional (Expression grp '[] with db params from))) columns - -- ^ row of values - -> TableExpression grp '[] with db params from - -- ^ from a table expression - -> QueryClause with db params columns - Subquery - :: ColumnsToRow columns ~ row - => Query '[] with db params row - -- ^ subquery to insert - -> QueryClause with db params columns - -instance RenderSQL (QueryClause with db params columns) where - renderSQL = \case - Values row0 rows -> - parenthesized (renderCommaSeparated renderSQLPart row0) - <+> "VALUES" - <+> commaSeparated - ( parenthesized - . renderCommaSeparated renderValuePart <$> row0 : rows ) - Select row0 tab -> - parenthesized (renderCommaSeparatedMaybe renderSQLPartMaybe row0) - <+> "SELECT" - <+> renderCommaSeparatedMaybe renderValuePartMaybe row0 - <+> renderSQL tab - Subquery qry -> renderQuery qry - where - renderSQLPartMaybe, renderValuePartMaybe - :: Aliased (Optional (Expression grp '[] with db params from)) column - -> Maybe ByteString - renderSQLPartMaybe = \case - Default `As` _ -> Nothing - Set _ `As` name -> Just $ renderSQL name - renderValuePartMaybe = \case - Default `As` _ -> Nothing - Set value `As` _ -> Just $ renderExpression value - renderSQLPart, renderValuePart - :: Aliased (Optional (Expression grp '[] with db params from)) column - -> ByteString - renderSQLPart (_ `As` name) = renderSQL name - renderValuePart (value `As` _) = renderSQL value - --- | `Values_` describes a single `NP` list of `Aliased` `Optional` `Expression`s --- whose `ColumnsType` must match the tables'. -pattern Values_ - :: SOP.SListI columns - => NP (Aliased (Optional (Expression 'Ungrouped '[] with db params from))) columns - -- ^ row of values - -> QueryClause with db params columns -pattern Values_ vals = Values vals [] - --- | `inlineValues_` a Haskell record in `insertInto`. -inlineValues_ - :: ( SOP.IsRecord hask xs - , SOP.AllZip InlineColumn xs columns ) - => hask -- ^ record - -> QueryClause with db params columns -inlineValues_ = Values_ . inlineColumns - --- | `inlineValues` Haskell records in `insertInto`. -inlineValues - :: ( SOP.IsRecord hask xs - , SOP.AllZip InlineColumn xs columns ) - => hask -- ^ record - -> [hask] -- ^ more - -> QueryClause with db params columns -inlineValues hask hasks = Values (inlineColumns hask) (inlineColumns <$> hasks) - --- | A `ConflictClause` specifies an action to perform upon a constraint --- violation. `OnConflictDoRaise` will raise an error. --- `OnConflict` `DoNothing` simply avoids inserting a row. --- `OnConflict` `DoUpdate` updates the existing row that conflicts with the row --- proposed for insertion. -data ConflictClause tab with db params table where - OnConflictDoRaise :: ConflictClause tab with db params table - OnConflict - :: ConflictTarget table - -- ^ conflict target - -> ConflictAction tab with db params table - -- ^ conflict action - -> ConflictClause tab with db params table - --- | Render a `ConflictClause`. -instance SOP.SListI (TableToColumns table) - => RenderSQL (ConflictClause tab with db params table) where - renderSQL = \case - OnConflictDoRaise -> "" - OnConflict target action -> " ON CONFLICT" - <+> renderSQL target <+> renderSQL action - -{- | -`ConflictAction` specifies an alternative `OnConflict` action. -It can be either `DoNothing`, or a `DoUpdate` clause specifying -the exact details of the update action to be performed in case of a conflict. -The `Set` and WHERE `Condition`s in `OnConflict` `DoUpdate` have access to the -existing row using the table's name, and to rows proposed -for insertion using the special @#excluded@ row. -`OnConflict` `DoNothing` simply avoids inserting a row as its alternative action. -`OnConflict` `DoUpdate` updates the existing row that conflicts -with the row proposed for insertion as its alternative action. --} -data ConflictAction tab with db params table where - DoNothing :: ConflictAction tab with db params table - DoUpdate - :: ( row ~ TableToRow table - , from ~ '[tab ::: row, "excluded" ::: row] - , Updatable table updates ) - => NP (Aliased (Optional (Expression 'Ungrouped '[] with db params from))) updates - -> [Condition 'Ungrouped '[] with db params from] - -- ^ WHERE `Condition`s - -> ConflictAction tab with db params table - -instance RenderSQL (ConflictAction tab with db params table) where - renderSQL = \case - DoNothing -> "DO NOTHING" - DoUpdate updates whs' - -> "DO UPDATE SET" - <+> renderCommaSeparated renderUpdate updates - <> case whs' of - [] -> "" - wh:whs -> " WHERE" <+> renderSQL (foldr (.&&) wh whs) - -renderUpdate - :: (forall x. RenderSQL (expr x)) - => Aliased (Optional expr) ty - -> ByteString -renderUpdate (expr `As` col) = renderSQL col <+> "=" <+> renderSQL expr - --- | A `ConflictTarget` specifies the constraint violation that triggers a --- `ConflictAction`. -data ConflictTarget table where - OnConstraint - :: Has con constraints constraint - => Alias con - -> ConflictTarget (constraints :=> columns) - --- | Render a `ConflictTarget` -instance RenderSQL (ConflictTarget constraints) where - renderSQL (OnConstraint con) = - "ON" <+> "CONSTRAINT" <+> renderSQL con diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Manipulation/Update.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Manipulation/Update.hs deleted file mode 100644 index af6b3351..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Manipulation/Update.hs +++ /dev/null @@ -1,134 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Update -Description: update statements -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -update statements --} - -{-# LANGUAGE - DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , GeneralizedNewtypeDeriving - , LambdaCase - , MultiParamTypeClasses - , OverloadedStrings - , PatternSynonyms - , QuantifiedConstraints - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeFamilies - , TypeInType - , TypeOperators - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Manipulation.Update - ( -- * Update - update - , update_ - ) where - -import Data.ByteString hiding (foldr) -import GHC.TypeLits - -import qualified Generics.SOP as SOP - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Expression.Default -import Squeal.PostgreSQL.Expression.Logic -import Squeal.PostgreSQL.Manipulation -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - -renderUpdate - :: (forall x. RenderSQL (expr x)) - => Aliased (Optional expr) ty - -> ByteString -renderUpdate (expr `As` col) = renderSQL col <+> "=" <+> renderSQL expr - -{----------------------------------------- -UPDATE statements ------------------------------------------} - -{- | An `update` command changes the values of the specified columns -in all rows that satisfy the condition. - ->>> type Columns = '["col1" ::: 'Def :=> 'NotNull 'PGint4, "col2" ::: 'NoDef :=> 'NotNull 'PGint4] ->>> type Schema = '["tab1" ::: 'Table ('[] :=> Columns), "tab2" ::: 'Table ('[] :=> Columns)] ->>> :{ -let - manp :: Manipulation with (Public Schema) '[] - '["col1" ::: 'NotNull 'PGint4, - "col2" ::: 'NotNull 'PGint4] - manp = update - (#tab1 `as` #t1) - (Set (2 + #t2 ! #col2) `as` #col1) - (Using (table (#tab2 `as` #t2))) - (#t1 ! #col1 ./= #t2 ! #col2) - (Returning (#t1 & DotStar)) -in printSQL manp -:} -UPDATE "tab1" AS "t1" SET "col1" = ((2 :: int4) + "t2"."col2") FROM "tab2" AS "t2" WHERE ("t1"."col1" <> "t2"."col2") RETURNING "t1".* --} -update - :: ( Has sch db schema - , Has tab0 schema ('Table table) - , Updatable table updates - , SOP.SListI row ) - => Aliased (QualifiedAlias sch) (tab ::: tab0) -- ^ table to update - -> NP (Aliased (Optional (Expression 'Ungrouped '[] with db params (tab ::: TableToRow table ': from)))) updates - -- ^ update expressions, modified values to replace old values - -> UsingClause with db params from - -- ^ FROM A table expression allowing columns from other tables to appear - -- in the WHERE condition and update expressions. - -> Condition 'Ungrouped '[] with db params (tab ::: TableToRow table ': from) - -- ^ WHERE condition under which to perform update on a row - -> ReturningClause with db params (tab ::: TableToRow table ': from) row -- ^ results to return - -> Manipulation with db params row -update (tab0 `As` tab) columns using wh returning = UnsafeManipulation $ - "UPDATE" - <+> renderSQL tab0 <+> "AS" <+> renderSQL tab - <+> "SET" - <+> renderCommaSeparated renderUpdate columns - <> case using of - NoUsing -> "" - Using tables -> " FROM" <+> renderSQL tables - <+> "WHERE" <+> renderSQL wh - <> renderSQL returning - -{- | Update a row returning `Nil`. - ->>> type Columns = '["col1" ::: 'Def :=> 'NotNull 'PGint4, "col2" ::: 'NoDef :=> 'NotNull 'PGint4] ->>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] ->>> :{ -let - manp :: Manipulation with (Public Schema) '[] '[] - manp = update_ #tab (Set 2 `as` #col1) (#col1 ./= #col2) -in printSQL manp -:} -UPDATE "tab" AS "tab" SET "col1" = (2 :: int4) WHERE ("col1" <> "col2") --} -update_ - :: ( Has sch db schema - , Has tab0 schema ('Table table) - , KnownSymbol tab - , Updatable table updates ) - => Aliased (QualifiedAlias sch) (tab ::: tab0) -- ^ table to update - -> NP (Aliased (Optional (Expression 'Ungrouped '[] with db params '[tab ::: TableToRow table]))) updates - -- ^ modified values to replace old values - -> Condition 'Ungrouped '[] with db params '[tab ::: TableToRow table] - -- ^ condition under which to perform update on a row - -> Manipulation with db params '[] -update_ tab columns wh = update tab columns NoUsing wh (Returning_ Nil) diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Query.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Query.hs deleted file mode 100644 index ca9715b6..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Query.hs +++ /dev/null @@ -1,417 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Query -Description: structured query language -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -structured query language --} - -{-# LANGUAGE - ConstraintKinds - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , GeneralizedNewtypeDeriving - , LambdaCase - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , QuantifiedConstraints - , ScopedTypeVariables - , StandaloneDeriving - , TypeApplications - , TypeFamilies - , TypeInType - , TypeOperators - , RankNTypes - , UndecidableInstances - #-} - -module Squeal.PostgreSQL.Query - ( -- * Query - Query (..) - , Query_ - -- ** Set Operations - , union - , unionAll - , intersect - , intersectAll - , except - , exceptAll - ) where - -import Control.DeepSeq -import Data.ByteString (ByteString) -import Data.Kind (Type) - -import qualified GHC.Generics as GHC - -import Squeal.PostgreSQL.Type.PG -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL --- >>> import Data.Int (Int32, Int64) --- >>> import Data.Monoid (Sum (..)) --- >>> import Data.Text (Text) --- >>> import qualified Generics.SOP as SOP - -{- | -The process of retrieving or the command to retrieve data from -a database is called a `Query`. - -The general `Query` type is parameterized by - -* @lat :: FromType@ - scope for `Squeal.PostgreSQL.Query.From.Join.JoinLateral` and subquery expressions, -* @with :: FromType@ - scope for all `Squeal.PostgreSQL.Query.From.common` table expressions, -* @db :: SchemasType@ - scope for all `Squeal.PostgreSQL.Query.From.table`s and `Squeal.PostgreSQL.Query.From.view`s, -* @params :: [NullType]@ - scope for all `Squeal.Expression.Parameter.parameter`s, -* @row :: RowType@ - return type of the `Query`. - -Let's see some `Query` examples. - -simple query: - ->>> type Columns = '["col1" ::: 'NoDef :=> 'NotNull 'PGint4, "col2" ::: 'NoDef :=> 'NotNull 'PGint4] ->>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] ->>> :{ -let - qry :: Query lat with (Public Schema) '[] '["col1" ::: 'NotNull 'PGint4, "col2" ::: 'NotNull 'PGint4] - qry = select Star (from (table #tab)) -in printSQL qry -:} -SELECT * FROM "tab" AS "tab" - -restricted query: - ->>> :{ -let - qry :: Query '[] with (Public Schema) params '["col1" ::: 'NotNull 'PGint4, "col2" ::: 'NotNull 'PGint4] - qry = - select_ ((#col1 + #col2) `as` #col1 :* #col1 `as` #col2) - ( from (table #tab) - & where_ (#col1 .> #col2) - & where_ (#col2 .> 0) ) -in printSQL qry -:} -SELECT ("col1" + "col2") AS "col1", "col1" AS "col2" FROM "tab" AS "tab" WHERE (("col1" > "col2") AND ("col2" > (0 :: int4))) - -subquery: - ->>> :{ -let - qry :: Query lat with (Public Schema) params '["col1" ::: 'NotNull 'PGint4, "col2" ::: 'NotNull 'PGint4] - qry = select Star (from (subquery (select Star (from (table #tab)) `as` #sub))) -in printSQL qry -:} -SELECT * FROM (SELECT * FROM "tab" AS "tab") AS "sub" - -limits and offsets: - ->>> :{ -let - qry :: Query lat with (Public Schema) params '["col1" ::: 'NotNull 'PGint4, "col2" ::: 'NotNull 'PGint4] - qry = select Star (from (table #tab) & limit 100 & offset 2 & limit 50 & offset 2) -in printSQL qry -:} -SELECT * FROM "tab" AS "tab" LIMIT 50 OFFSET 4 - -parameterized query: - ->>> :{ -let - qry :: Query '[] with (Public Schema) '[ 'NotNull 'PGint4] '["col1" ::: 'NotNull 'PGint4, "col2" ::: 'NotNull 'PGint4] - qry = select Star (from (table #tab) & where_ (#col1 .> param @1)) -in printSQL qry -:} -SELECT * FROM "tab" AS "tab" WHERE ("col1" > ($1 :: int4)) - -aggregation query: - ->>> :{ -let - qry :: Query '[] with (Public Schema) params '["col1" ::: 'NotNull 'PGint8, "col2" ::: 'NotNull 'PGint4] - qry = - select_ ((fromNull 0 (sum_ (All #col2))) `as` #col1 :* #col1 `as` #col2) - ( from (table (#tab `as` #table1)) - & groupBy #col1 - & having (sum_ (Distinct #col2) .> 1) ) -in printSQL qry -:} -SELECT COALESCE(sum(ALL "col2"), (0 :: int8)) AS "col1", "col1" AS "col2" FROM "tab" AS "table1" GROUP BY "col1" HAVING (sum(DISTINCT "col2") > (1 :: int8)) - -sorted query: - ->>> :{ -let - qry :: Query '[] with (Public Schema) params '["col1" ::: 'NotNull 'PGint4, "col2" ::: 'NotNull 'PGint4] - qry = select Star (from (table #tab) & orderBy [#col1 & Asc]) -in printSQL qry -:} -SELECT * FROM "tab" AS "tab" ORDER BY "col1" ASC - -joins: - ->>> :{ -type OrdersColumns = - '[ "id" ::: 'NoDef :=> 'NotNull 'PGint4 - , "price" ::: 'NoDef :=> 'NotNull 'PGfloat4 - , "customer_id" ::: 'NoDef :=> 'NotNull 'PGint4 - , "shipper_id" ::: 'NoDef :=> 'NotNull 'PGint4 ] -:} - ->>> :{ -type OrdersConstraints = - '["pk_orders" ::: PrimaryKey '["id"] - ,"fk_customers" ::: ForeignKey '["customer_id"] "public" "customers" '["id"] - ,"fk_shippers" ::: ForeignKey '["shipper_id"] "public" "shippers" '["id"] ] -:} - ->>> type NamesColumns = '["id" ::: 'NoDef :=> 'NotNull 'PGint4, "name" ::: 'NoDef :=> 'NotNull 'PGtext] ->>> type CustomersConstraints = '["pk_customers" ::: PrimaryKey '["id"]] ->>> type ShippersConstraints = '["pk_shippers" ::: PrimaryKey '["id"]] ->>> :{ -type OrdersSchema = - '[ "orders" ::: 'Table (OrdersConstraints :=> OrdersColumns) - , "customers" ::: 'Table (CustomersConstraints :=> NamesColumns) - , "shippers" ::: 'Table (ShippersConstraints :=> NamesColumns) ] -:} - ->>> :{ -type OrderRow = - '[ "price" ::: 'NotNull 'PGfloat4 - , "customerName" ::: 'NotNull 'PGtext - , "shipperName" ::: 'NotNull 'PGtext - ] -:} - ->>> :{ -let - qry :: Query lat with (Public OrdersSchema) params OrderRow - qry = select_ - ( #o ! #price `as` #price :* - #c ! #name `as` #customerName :* - #s ! #name `as` #shipperName ) - ( from (table (#orders `as` #o) - & innerJoin (table (#customers `as` #c)) - (#o ! #customer_id .== #c ! #id) - & innerJoin (table (#shippers `as` #s)) - (#o ! #shipper_id .== #s ! #id)) ) -in printSQL qry -:} -SELECT "o"."price" AS "price", "c"."name" AS "customerName", "s"."name" AS "shipperName" FROM "orders" AS "o" INNER JOIN "customers" AS "c" ON ("o"."customer_id" = "c"."id") INNER JOIN "shippers" AS "s" ON ("o"."shipper_id" = "s"."id") - -self-join: - ->>> :{ -let - qry :: Query lat with (Public Schema) params '["col1" ::: 'NotNull 'PGint4, "col2" ::: 'NotNull 'PGint4] - qry = select - (#t1 & DotStar) - (from (table (#tab `as` #t1) & crossJoin (table (#tab `as` #t2)))) -in printSQL qry -:} -SELECT "t1".* FROM "tab" AS "t1" CROSS JOIN "tab" AS "t2" - -value queries: - ->>> :{ -let - qry :: Query lat with db params '["col1" ::: 'NotNull 'PGtext, "col2" ::: 'NotNull 'PGbool] - qry = values - ("true" `as` #col1 :* true `as` #col2) - ["false" `as` #col1 :* false `as` #col2] -in printSQL qry -:} -SELECT * FROM (VALUES ((E'true' :: text), TRUE), ((E'false' :: text), FALSE)) AS t ("col1", "col2") - -set operations: - ->>> :{ -let - qry :: Query lat with (Public Schema) params '["col1" ::: 'NotNull 'PGint4, "col2" ::: 'NotNull 'PGint4] - qry = select Star (from (table #tab)) `unionAll` select Star (from (table #tab)) -in printSQL qry -:} -(SELECT * FROM "tab" AS "tab") UNION ALL (SELECT * FROM "tab" AS "tab") - -with query: - ->>> :{ -let - qry :: Query lat with (Public Schema) params '["col1" ::: 'NotNull 'PGint4, "col2" ::: 'NotNull 'PGint4] - qry = with ( - select Star (from (table #tab)) `as` #cte1 :>> - select Star (from (common #cte1)) `as` #cte2 - ) (select Star (from (common #cte2))) -in printSQL qry -:} -WITH "cte1" AS (SELECT * FROM "tab" AS "tab"), "cte2" AS (SELECT * FROM "cte1" AS "cte1") SELECT * FROM "cte2" AS "cte2" - -window functions: - ->>> :{ -let - qry :: Query '[] with (Public Schema) db '["col1" ::: 'NotNull 'PGint4, "col2" ::: 'NotNull 'PGint8] - qry = select - (#col1 & Also (rank `as` #col2 `Over` (partitionBy #col1 & orderBy [#col2 & Asc]))) - (from (table #tab)) -in printSQL qry -:} -SELECT "col1" AS "col1", rank() OVER (PARTITION BY "col1" ORDER BY "col2" ASC) AS "col2" FROM "tab" AS "tab" - -correlated subqueries: - ->>> :{ -let - qry :: Query '[] with (Public Schema) params '["col1" ::: 'NotNull 'PGint4] - qry = - select #col1 (from (table (#tab `as` #t1)) - & where_ (exists ( - select Star (from (table (#tab `as` #t2)) - & where_ (#t2 ! #col2 .== #t1 ! #col1))))) -in printSQL qry -:} -SELECT "col1" AS "col1" FROM "tab" AS "t1" WHERE EXISTS (SELECT * FROM "tab" AS "t2" WHERE ("t2"."col2" = "t1"."col1")) --} -newtype Query - (lat :: FromType) - (with :: FromType) - (db :: SchemasType) - (params :: [NullType]) - (row :: RowType) - = UnsafeQuery { renderQuery :: ByteString } - deriving stock (GHC.Generic,Show,Eq,Ord) - deriving newtype (NFData) -instance RenderSQL (Query lat with db params row) where renderSQL = renderQuery - -{- | -The `Query_` type is parameterized by a @db@ `SchemasType`, -against which the query is type-checked, an input @params@ Haskell `Type`, -and an ouput row Haskell `Type`. - -A `Query_` can be run -using `Squeal.PostgreSQL.Session.runQueryParams`, or if @params = ()@ -using `Squeal.PostgreSQL.Session.runQuery`. - -Generally, @params@ will be a Haskell tuple or record whose entries -may be referenced using positional -`Squeal.PostgreSQL.Expression.Parameter.parameter`s and @row@ will be a -Haskell record, whose entries will be targeted using overloaded labels. - -`Query_` is a type family which resolves into a `Query`, -so don't be fooled by the input params and output row Haskell `Type`s, -which are converted into appropriate -Postgres @[@`NullType`@]@ params and `RowType` rows. -Use `Squeal.PostgreSQL.Session.Statement.query` to -fix actual Haskell input params and output rows. - ->>> :set -XDeriveAnyClass -XDerivingStrategies ->>> type Columns = '["col1" ::: 'NoDef :=> 'Null 'PGint8, "col2" ::: 'Def :=> 'NotNull 'PGtext] ->>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] ->>> :{ -data Row = Row { col1 :: Maybe Int64, col2 :: String } - deriving stock (GHC.Generic) - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) -:} - ->>> :{ -let - qry :: Query_ (Public Schema) (Int64, Bool) Row - qry = select Star (from (table #tab) & where_ (#col1 .> param @1 .&& notNull (param @2))) - stmt :: Statement (Public Schema) (Int64, Bool) Row - stmt = query qry -:} - ->>> :type qry -qry - :: Query - '[] - '[] - '["public" ::: '["tab" ::: 'Table ('[] :=> Columns)]] - '[ 'NotNull 'PGint8, 'NotNull 'PGbool] - '["col1" ::: 'Null 'PGint8, "col2" ::: 'NotNull 'PGtext] ->>> :type stmt -stmt - :: Statement - '["public" ::: '["tab" ::: 'Table ('[] :=> Columns)]] - (Int64, Bool) - Row --} -type family Query_ - (db :: SchemasType) - (params :: Type) - (row :: Type) where - Query_ db params row = - Query '[] '[] db (TuplePG params) (RowPG row) - --- | The results of two queries can be combined using the set operation --- `union`. Duplicate rows are eliminated. -union - :: Query lat with db params columns -- ^ - -> Query lat with db params columns - -> Query lat with db params columns -q1 `union` q2 = UnsafeQuery $ - parenthesized (renderSQL q1) - <+> "UNION" - <+> parenthesized (renderSQL q2) - --- | The results of two queries can be combined using the set operation --- `unionAll`, the disjoint union. Duplicate rows are retained. -unionAll - :: Query lat with db params columns -- ^ - -> Query lat with db params columns - -> Query lat with db params columns -q1 `unionAll` q2 = UnsafeQuery $ - parenthesized (renderSQL q1) - <+> "UNION" <+> "ALL" - <+> parenthesized (renderSQL q2) - --- | The results of two queries can be combined using the set operation --- `intersect`, the intersection. Duplicate rows are eliminated. -intersect - :: Query lat with db params columns -- ^ - -> Query lat with db params columns - -> Query lat with db params columns -q1 `intersect` q2 = UnsafeQuery $ - parenthesized (renderSQL q1) - <+> "INTERSECT" - <+> parenthesized (renderSQL q2) - --- | The results of two queries can be combined using the set operation --- `intersectAll`, the intersection. Duplicate rows are retained. -intersectAll - :: Query lat with db params columns -- ^ - -> Query lat with db params columns - -> Query lat with db params columns -q1 `intersectAll` q2 = UnsafeQuery $ - parenthesized (renderSQL q1) - <+> "INTERSECT" <+> "ALL" - <+> parenthesized (renderSQL q2) - --- | The results of two queries can be combined using the set operation --- `except`, the set difference. Duplicate rows are eliminated. -except - :: Query lat with db params columns -- ^ - -> Query lat with db params columns - -> Query lat with db params columns -q1 `except` q2 = UnsafeQuery $ - parenthesized (renderSQL q1) - <+> "EXCEPT" - <+> parenthesized (renderSQL q2) - --- | The results of two queries can be combined using the set operation --- `exceptAll`, the set difference. Duplicate rows are retained. -exceptAll - :: Query lat with db params columns -- ^ - -> Query lat with db params columns - -> Query lat with db params columns -q1 `exceptAll` q2 = UnsafeQuery $ - parenthesized (renderSQL q1) - <+> "EXCEPT" <+> "ALL" - <+> parenthesized (renderSQL q2) diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Query/From.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Query/From.hs deleted file mode 100644 index 860380de..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Query/From.hs +++ /dev/null @@ -1,114 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Query.From -Description: from clauses -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -from clauses --} - -{-# LANGUAGE - ConstraintKinds - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , GeneralizedNewtypeDeriving - , LambdaCase - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , QuantifiedConstraints - , ScopedTypeVariables - , StandaloneDeriving - , TypeApplications - , TypeFamilies - , TypeInType - , TypeOperators - , RankNTypes - , UndecidableInstances - #-} - -module Squeal.PostgreSQL.Query.From - ( -- * From Clause - FromClause (..) - , table - , subquery - , view - , common - ) where - -import Control.DeepSeq -import Data.ByteString (ByteString) - -import qualified GHC.Generics as GHC - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Query -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - -{----------------------------------------- -FROM clauses ------------------------------------------} - -{- | -A `FromClause` can be a table name, or a derived table such -as a subquery, a @JOIN@ construct, or complex combinations of these. --} -newtype FromClause - (lat :: FromType) - (with :: FromType) - (db :: SchemasType) - (params :: [NullType]) - (from :: FromType) - = UnsafeFromClause { renderFromClause :: ByteString } - deriving stock (GHC.Generic,Show,Eq,Ord) - deriving newtype (NFData) -instance RenderSQL (FromClause lat with db params from) where - renderSQL = renderFromClause - --- | A real `table` is a table from the database. -table - :: (Has sch db schema, Has tab schema ('Table table)) - => Aliased (QualifiedAlias sch) (alias ::: tab) -- ^ (renamable) table alias - -> FromClause lat with db params '[alias ::: TableToRow table] -table (tab `As` alias) = UnsafeFromClause $ - renderSQL tab <+> "AS" <+> renderSQL alias - -{- | `subquery` derives a table from a `Query`. -The subquery may not reference columns provided by preceding `FromClause` items. -Use `Squeal.PostgreSQL.Query.From.Join.JoinLateral` -if the subquery must reference columns provided by preceding `FromClause` items. --} -subquery - :: Aliased (Query lat with db params) query - -- ^ aliased `Query` - -> FromClause lat with db params '[query] -subquery = UnsafeFromClause . renderAliased (parenthesized . renderSQL) - --- | `view` derives a table from a `View`. -view - :: (Has sch db schema, Has vw schema ('View view)) - => Aliased (QualifiedAlias sch) (alias ::: vw) -- ^ (renamable) view alias - -> FromClause lat with db params '[alias ::: view] -view (vw `As` alias) = UnsafeFromClause $ - renderSQL vw <+> "AS" <+> renderSQL alias - --- | `common` derives a table from a common table expression. -common - :: Has cte with common - => Aliased Alias (alias ::: cte) -- ^ (renamable) common table expression alias - -> FromClause lat with db params '[alias ::: common] -common (cte `As` alias) = UnsafeFromClause $ - renderSQL cte <+> "AS" <+> renderSQL alias - -instance Additional (FromClause lat with db params) where - also right left = UnsafeFromClause $ - renderSQL left <> ", " <> renderSQL right diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Query/From/Join.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Query/From/Join.hs deleted file mode 100644 index af651693..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Query/From/Join.hs +++ /dev/null @@ -1,294 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Query.From.Join -Description: Squeal joins -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -Squeal joins --} - -{-# LANGUAGE - ConstraintKinds - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , GeneralizedNewtypeDeriving - , LambdaCase - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , QuantifiedConstraints - , ScopedTypeVariables - , StandaloneDeriving - , TypeApplications - , TypeFamilies - , TypeInType - , TypeOperators - , RankNTypes - , UndecidableInstances - #-} - -module Squeal.PostgreSQL.Query.From.Join - ( -- * Join - JoinItem (..) - , cross, crossJoin, crossJoinLateral - , inner, innerJoin, innerJoinLateral - , leftOuter, leftOuterJoin, leftOuterJoinLateral - , rightOuter, rightOuterJoin, rightOuterJoinLateral - , fullOuter, fullOuterJoin, fullOuterJoinLateral - ) where - -import Generics.SOP hiding (from) - -import qualified Generics.SOP as SOP - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Expression.Logic -import Squeal.PostgreSQL.Query -import Squeal.PostgreSQL.Query.From -import Squeal.PostgreSQL.Query.From.Set -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - -{- | -A `JoinItem` is the right hand side of a `cross`, -`inner`, `leftOuter`, `rightOuter`, `fullOuter` join of -`FromClause`s. --} -data JoinItem - (lat :: FromType) - (with :: FromType) - (db :: SchemasType) - (params :: [NullType]) - (left :: FromType) - (right :: FromType) where - Join - :: FromClause lat with db params right - -- ^ A standard `Squeal.PostgreSQL.Query.Join`. - -- It is not allowed to reference columns provided - -- by preceding `FromClause` items. - -> JoinItem lat with db params left right - JoinLateral - :: Aliased (Query (Join lat left) with db params) query - -- ^ Subqueries can be preceded by `JoinLateral`. - -- This allows them to reference columns provided - -- by preceding `FromClause` items. - -> JoinItem lat with db params left '[query] - JoinFunction - :: SetFun db arg set - -- ^ Set returning functions can be preceded by `JoinFunction`. - -- This allows them to reference columns provided - -- by preceding `FromClause` items. - -> Expression 'Ungrouped lat with db params left arg - -- ^ argument - -> JoinItem lat with db params left '[set] - JoinFunctionN - :: SListI args - => SetFunN db args set - -- ^ Set returning multi-argument functions - -- can be preceded by `JoinFunctionN`. - -- This allows them to reference columns provided - -- by preceding `FromClause` items. - -> NP (Expression 'Ungrouped lat with db params left) args - -- ^ arguments - -> JoinItem lat with db params left '[set] -instance RenderSQL (JoinItem lat with db params left right) where - renderSQL = \case - Join tab -> "JOIN" <+> renderSQL tab - JoinLateral qry -> "JOIN LATERAL" <+> - renderAliased (parenthesized . renderSQL) qry - JoinFunction fun x -> "JOIN" <+> - renderSQL (fun (UnsafeExpression (renderSQL x))) - JoinFunctionN fun xs -> "JOIN" <+> - renderSQL (fun (SOP.hmap (UnsafeExpression . renderSQL) xs)) - -{- | -@left & cross (Join right)@. For every possible combination of rows from -@left@ and @right@ (i.e., a Cartesian product), the joined table will contain -a row consisting of all columns in @left@ followed by all columns in @right@. -If the tables have @n@ and @m@ rows respectively, the joined table will -have @n * m@ rows. --} -cross - :: JoinItem lat with db params left right -- ^ right - -> FromClause lat with db params left -- ^ left - -> FromClause lat with db params (Join left right) -cross item tab = UnsafeFromClause $ - renderSQL tab <+> "CROSS" <+> renderSQL item - -{- | -@left & crossJoin right@. For every possible combination of rows from -@left@ and @right@ (i.e., a Cartesian product), the joined table will contain -a row consisting of all columns in @left@ followed by all columns in @right@. -If the tables have @n@ and @m@ rows respectively, the joined table will -have @n * m@ rows. --} -crossJoin - :: FromClause lat with db params right -- ^ right - -> FromClause lat with db params left -- ^ left - -> FromClause lat with db params (Join left right) -crossJoin = cross . Join - -{- | -Like `crossJoin` with a `subquery` but allowed to reference columns provided -by preceding `FromClause` items. --} -crossJoinLateral - :: Aliased (Query (Join lat left) with db params) query -- ^ right subquery - -> FromClause lat with db params left -- ^ left - -> FromClause lat with db params (Join left '[query]) -crossJoinLateral = cross . JoinLateral - -{- | @left & inner (Join right) on@. The joined table is filtered by -the @on@ condition. --} -inner - :: JoinItem lat with db params left right -- ^ right - -> Condition 'Ungrouped lat with db params (Join left right) -- ^ @ON@ condition - -> FromClause lat with db params left -- ^ left - -> FromClause lat with db params (Join left right) -inner item on tab = UnsafeFromClause $ - renderSQL tab <+> "INNER" <+> renderSQL item <+> "ON" <+> renderSQL on - -{- | @left & innerJoin right on@. The joined table is filtered by -the @on@ condition. --} -innerJoin - :: FromClause lat with db params right -- ^ right - -> Condition 'Ungrouped lat with db params (Join left right) -- ^ @ON@ condition - -> FromClause lat with db params left -- ^ left - -> FromClause lat with db params (Join left right) -innerJoin = inner . Join - -{- | -Like `innerJoin` with a `subquery` but allowed to reference columns provided -by preceding `FromClause` items. --} -innerJoinLateral - :: Aliased (Query (Join lat left) with db params) query -- ^ right subquery - -> Condition 'Ungrouped lat with db params (Join left '[query]) -- ^ @ON@ condition - -> FromClause lat with db params left -- ^ left - -> FromClause lat with db params (Join left '[query]) -innerJoinLateral = inner . JoinLateral - -{- | @left & leftOuter (Join right) on@. First, an inner join is performed. -Then, for each row in @left@ that does not satisfy the @on@ condition with -any row in @right@, a joined row is added with null values in columns of @right@. -Thus, the joined table always has at least one row for each row in @left@. --} -leftOuter - :: JoinItem lat with db params left right -- ^ right - -> Condition 'Ungrouped lat with db params (Join left right) -- ^ @ON@ condition - -> FromClause lat with db params left -- ^ left - -> FromClause lat with db params (Join left (NullifyFrom right)) -leftOuter item on tab = UnsafeFromClause $ - renderSQL tab <+> "LEFT OUTER" <+> renderSQL item <+> "ON" <+> renderSQL on - -{- | @left & leftOuterJoin right on@. First, an inner join is performed. -Then, for each row in @left@ that does not satisfy the @on@ condition with -any row in @right@, a joined row is added with null values in columns of @right@. -Thus, the joined table always has at least one row for each row in @left@. --} -leftOuterJoin - :: FromClause lat with db params right -- ^ right - -> Condition 'Ungrouped lat with db params (Join left right) -- ^ @ON@ condition - -> FromClause lat with db params left -- ^ left - -> FromClause lat with db params (Join left (NullifyFrom right)) -leftOuterJoin = leftOuter . Join - -{- | -Like `leftOuterJoin` with a `subquery` but allowed to reference columns provided -by preceding `FromClause` items. --} -leftOuterJoinLateral - :: Aliased (Query (Join lat left) with db params) query -- ^ right subquery - -> Condition 'Ungrouped lat with db params (Join left '[query]) -- ^ @ON@ condition - -> FromClause lat with db params left -- ^ left - -> FromClause lat with db params (Join left (NullifyFrom '[query])) -leftOuterJoinLateral = leftOuter . JoinLateral - -{- | @left & rightOuter (Join right) on@. First, an inner join is performed. -Then, for each row in @right@ that does not satisfy the @on@ condition with -any row in @left@, a joined row is added with null values in columns of @left@. -This is the converse of a left join: the result table will always -have a row for each row in @right@. --} -rightOuter - :: JoinItem lat with db params left right -- ^ right - -> Condition 'Ungrouped lat with db params (Join left right) -- ^ @ON@ condition - -> FromClause lat with db params left -- ^ left - -> FromClause lat with db params (Join (NullifyFrom left) right) -rightOuter item on tab = UnsafeFromClause $ - renderSQL tab <+> "RIGHT OUTER" <+> renderSQL item <+> "ON" <+> renderSQL on - -{- | @left & rightOuterJoin right on@. First, an inner join is performed. -Then, for each row in @right@ that does not satisfy the @on@ condition with -any row in @left@, a joined row is added with null values in columns of @left@. -This is the converse of a left join: the result table will always -have a row for each row in @right@. --} -rightOuterJoin - :: FromClause lat with db params right -- ^ right - -> Condition 'Ungrouped lat with db params (Join left right) -- ^ @ON@ condition - -> FromClause lat with db params left -- ^ left - -> FromClause lat with db params (Join (NullifyFrom left) right) -rightOuterJoin = rightOuter . Join - -{- | -Like `rightOuterJoin` with a `subquery` but allowed to reference columns provided -by preceding `FromClause` items. --} -rightOuterJoinLateral - :: Aliased (Query (Join lat left) with db params) query -- ^ right subquery - -> Condition 'Ungrouped lat with db params (Join left '[query]) -- ^ @ON@ condition - -> FromClause lat with db params left -- ^ left - -> FromClause lat with db params (Join (NullifyFrom left) '[query]) -rightOuterJoinLateral = rightOuter . JoinLateral - -{- | @left & fullOuter (Join right) on@. First, an inner join is performed. -Then, for each row in @left@ that does not satisfy the @on@ condition with -any row in @right@, a joined row is added with null values in columns of @right@. -Also, for each row of @right@ that does not satisfy the join condition -with any row in @left@, a joined row with null values in the columns of @left@ -is added. --} -fullOuter - :: JoinItem lat with db params left right -- ^ right - -> Condition 'Ungrouped lat with db params (Join left right) -- ^ @ON@ condition - -> FromClause lat with db params left -- ^ left - -> FromClause lat with db params (NullifyFrom (Join left right)) -fullOuter item on tab = UnsafeFromClause $ - renderSQL tab <+> "FULL OUTER" <+> renderSQL item <+> "ON" <+> renderSQL on - -{- | @left & fullOuterJoin right on@. First, an inner join is performed. -Then, for each row in @left@ that does not satisfy the @on@ condition with -any row in @right@, a joined row is added with null values in columns of @right@. -Also, for each row of @right@ that does not satisfy the join condition -with any row in @left@, a joined row with null values in the columns of @left@ -is added. --} -fullOuterJoin - :: FromClause lat with db params right -- ^ right - -> Condition 'Ungrouped lat with db params (Join left right) -- ^ @ON@ condition - -> FromClause lat with db params left -- ^ left - -> FromClause lat with db params (NullifyFrom (Join left right)) -fullOuterJoin = fullOuter . Join - -{- | -Like `fullOuterJoin` with a `subquery` but allowed to reference columns provided -by preceding `FromClause` items. --} -fullOuterJoinLateral - :: Aliased (Query (Join lat left) with db params) query -- ^ right subquery - -> Condition 'Ungrouped lat with db params (Join left '[query]) -- ^ @ON@ condition - -> FromClause lat with db params left -- ^ left - -> FromClause lat with db params (NullifyFrom (Join left '[query])) -fullOuterJoinLateral = fullOuter . JoinLateral diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Query/From/Set.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Query/From/Set.hs deleted file mode 100644 index a0697de5..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Query/From/Set.hs +++ /dev/null @@ -1,204 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Query.From.Set -Description: set returning functions -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -set returning functions --} - -{-# LANGUAGE - ConstraintKinds - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , GeneralizedNewtypeDeriving - , LambdaCase - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , QuantifiedConstraints - , ScopedTypeVariables - , StandaloneDeriving - , TypeApplications - , TypeFamilies - , TypeInType - , TypeOperators - , RankNTypes - , UndecidableInstances - #-} - -module Squeal.PostgreSQL.Query.From.Set - ( -- * Set Functions - type (-|->) - , type (--|->) - , SetFun - , SetFunN - , generateSeries - , generateSeriesStep - , generateSeriesTimestamp - , unsafeSetFunction - , setFunction - , unsafeSetFunctionN - , setFunctionN - ) where - -import Data.ByteString (ByteString) -import Generics.SOP hiding (from) -import GHC.TypeLits - -import qualified Generics.SOP as SOP - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Query.From -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Type.Schema - -{- | -A @RankNType@ for set returning functions with 1 argument. --} -type (-|->) arg set = forall db. SetFun db arg set - -{- | -A @RankNType@ for set returning functions with multiple argument. --} -type (--|->) arg set = forall db. SetFunN db arg set - -- ^ output - -{- | -Like `-|->` but depends on the schemas of the database --} -type SetFun db arg row - = forall lat with params - . Expression 'Ungrouped lat with db params '[] arg - -- ^ input - -> FromClause lat with db params '[row] - -- ^ output - -{- | -Like `--|->` but depends on the schemas of the database --} -type SetFunN db args set - = forall lat with params - . NP (Expression 'Ungrouped lat with db params '[]) args - -- ^ input - -> FromClause lat with db params '[set] - -- ^ output - --- $setup --- >>> import Squeal.PostgreSQL - --- | Escape hatch for a set returning function of a single variable -unsafeSetFunction - :: forall fun ty row. KnownSymbol fun - => ByteString - -> ty -|-> (fun ::: row) -- ^ set returning function -unsafeSetFunction fun x = UnsafeFromClause $ - fun <> parenthesized (renderSQL x) - -{- | Call a user defined set returning function of a single variable - ->>> type Fn = '[ 'Null 'PGbool] :=> 'ReturnsTable '["ret" ::: 'NotNull 'PGnumeric] ->>> type Schema = '["fn" ::: 'Function Fn] ->>> :{ -let - fn :: SetFun (Public Schema) ('Null 'PGbool) ("fn" ::: '["ret" ::: 'NotNull 'PGnumeric]) - fn = setFunction #fn -in - printSQL (fn true) -:} -"fn"(TRUE) --} -setFunction - :: ( Has sch db schema - , Has fun schema ('Function ('[ty] :=> 'ReturnsTable row)) ) - => QualifiedAlias sch fun -- ^ function alias - -> SetFun db ty (fun ::: row) -setFunction fun = unsafeSetFunction (renderSQL fun) - -{- | Escape hatch for a multivariable set returning function-} -unsafeSetFunctionN - :: forall fun tys row. (SOP.SListI tys, KnownSymbol fun) - => ByteString - -> tys --|-> (fun ::: row) -- ^ set returning function -unsafeSetFunctionN fun xs = UnsafeFromClause $ - fun <> parenthesized (renderCommaSeparated renderSQL xs) - -{- | Call a user defined multivariable set returning function - ->>> type Fn = '[ 'Null 'PGbool, 'Null 'PGtext] :=> 'ReturnsTable '["ret" ::: 'NotNull 'PGnumeric] ->>> type Schema = '["fn" ::: 'Function Fn] ->>> :{ -let - fn :: SetFunN (Public Schema) - '[ 'Null 'PGbool, 'Null 'PGtext] - ("fn" ::: '["ret" ::: 'NotNull 'PGnumeric]) - fn = setFunctionN #fn -in - printSQL (fn (true *: "hi")) -:} -"fn"(TRUE, (E'hi' :: text)) --} -setFunctionN - :: ( Has sch db schema - , Has fun schema ('Function (tys :=> 'ReturnsTable row)) - , SOP.SListI tys ) - => QualifiedAlias sch fun -- ^ function alias - -> SetFunN db tys (fun ::: row) -setFunctionN fun = unsafeSetFunctionN (renderSQL fun) - -{- | @generateSeries (start :* stop)@ - -Generate a series of values, -from @start@ to @stop@ with a step size of one - ->>> printSQL (generateSeries @'PGint4 (1 *: 10)) -generate_series((1 :: int4), (10 :: int4)) --} -generateSeries - :: ty `In` '[ 'PGint4, 'PGint8, 'PGnumeric] - => '[ null ty, null ty] --|-> - ("generate_series" ::: '["generate_series" ::: null ty]) - -- ^ set returning function -generateSeries = unsafeSetFunctionN "generate_series" - -{- | @generateSeriesStep (start :* stop *: step)@ - -Generate a series of values, -from @start@ to @stop@ with a step size of @step@ - ->>> printSQL (generateSeriesStep @'PGint8 (2 :* 100 *: 2)) -generate_series((2 :: int8), (100 :: int8), (2 :: int8)) --} -generateSeriesStep - :: ty `In` '[ 'PGint4, 'PGint8, 'PGnumeric] - => '[null ty, null ty, null ty] --|-> - ("generate_series" ::: '["generate_series" ::: null ty]) - -- ^ set returning function -generateSeriesStep = unsafeSetFunctionN "generate_series" - -{- | @generateSeriesTimestamp (start :* stop *: step)@ - -Generate a series of timestamps, -from @start@ to @stop@ with a step size of @step@ - ->>> :{ -let - start = now - stop = now !+ interval_ 10 Years - step = interval_ 1 Months -in printSQL (generateSeriesTimestamp (start :* stop *: step)) -:} -generate_series(now(), (now() + (INTERVAL '10.000 years')), (INTERVAL '1.000 months')) --} -generateSeriesTimestamp - :: ty `In` '[ 'PGtimestamp, 'PGtimestamptz] - => '[null ty, null ty, null 'PGinterval] --|-> - ("generate_series" ::: '["generate_series" ::: null ty]) - -- ^ set returning function -generateSeriesTimestamp = unsafeSetFunctionN "generate_series" diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Query/Select.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Query/Select.hs deleted file mode 100644 index d4c64de6..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Query/Select.hs +++ /dev/null @@ -1,252 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Query.Select -Description: select statements -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -select statements --} - -{-# LANGUAGE - ConstraintKinds - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , GeneralizedNewtypeDeriving - , LambdaCase - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , QuantifiedConstraints - , ScopedTypeVariables - , StandaloneDeriving - , TypeApplications - , TypeFamilies - , TypeInType - , TypeOperators - , RankNTypes - , UndecidableInstances - #-} - -module Squeal.PostgreSQL.Query.Select - ( -- ** Select - select - , select_ - , selectDistinct - , selectDistinct_ - , selectDistinctOn - , selectDistinctOn_ - , Selection (..) - ) where - -import Data.ByteString (ByteString) -import Data.String -import Generics.SOP hiding (from) -import GHC.TypeLits - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Expression.Sort -import Squeal.PostgreSQL.Expression.Window -import Squeal.PostgreSQL.Query -import Squeal.PostgreSQL.Query.Table -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - -{----------------------------------------- -SELECT queries ------------------------------------------} - -{- | The simplest kinds of `Selection` are `Star` and `DotStar` which -emits all columns that a `TableExpression` produces. A select `List` -is a list of `Expression`s. A `Selection` could be a list of -`WindowFunction`s `Over` `WindowDefinition`. `Additional` `Selection`s can -be selected with `Also`. --} -data Selection grp lat with db params from row where - Star - :: HasUnique tab from row - => Selection 'Ungrouped lat with db params from row - -- ^ `HasUnique` table in the `Squeal.PostgreSQL.Query.From.FromClause` - DotStar - :: Has tab from row - => Alias tab - -- ^ `Has` table with `Alias` - -> Selection 'Ungrouped lat with db params from row - List - :: SListI row - => NP (Aliased (Expression grp lat with db params from)) row - -- ^ `NP` list of `Aliased` `Expression`s - -> Selection grp lat with db params from row - Over - :: SListI row - => NP (Aliased (WindowFunction grp lat with db params from)) row - -- ^ `NP` list of `Aliased` `WindowFunction`s - -> WindowDefinition grp lat with db params from - -> Selection grp lat with db params from row - Also - :: Selection grp lat with db params from right - -- ^ `Additional` `Selection` - -> Selection grp lat with db params from left - -> Selection grp lat with db params from (Join left right) -instance Additional (Selection grp lat with db params from) where - also = Also -instance (KnownSymbol col, row ~ '[col ::: ty]) - => Aliasable col - (Expression grp lat with db params from ty) - (Selection grp lat with db params from row) where - expr `as` col = List (expr `as` col) -instance (Has tab (Join from lat) row0, Has col row0 ty, row1 ~ '[col ::: ty]) - => IsQualified tab col - (Selection 'Ungrouped lat with db params from row1) where - tab ! col = tab ! col `as` col -instance - ( Has tab (Join from lat) row0 - , Has col row0 ty - , row1 ~ '[col ::: ty] - , GroupedBy tab col bys ) - => IsQualified tab col - (Selection ('Grouped bys) lat with db params from row1) where - tab ! col = tab ! col `as` col -instance (HasUnique tab (Join from lat) row0, Has col row0 ty, row1 ~ '[col ::: ty]) - => IsLabel col - (Selection 'Ungrouped lat with db params from row1) where - fromLabel = fromLabel @col `as` Alias -instance - ( HasUnique tab (Join from lat) row0 - , Has col row0 ty - , row1 ~ '[col ::: ty] - , GroupedBy tab col bys ) - => IsLabel col - (Selection ('Grouped bys) lat with db params from row1) where - fromLabel = fromLabel @col `as` Alias - -instance RenderSQL (Selection grp lat with db params from row) where - renderSQL = \case - List list -> renderCommaSeparated (renderAliased renderSQL) list - Star -> "*" - DotStar tab -> renderSQL tab <> ".*" - Also right left -> renderSQL left <> ", " <> renderSQL right - Over winFns winDef -> - let - renderOver - :: Aliased (WindowFunction grp lat with db params from) field - -> ByteString - renderOver (winFn `As` col) = renderSQL winFn - <+> "OVER" <+> parenthesized (renderSQL winDef) - <+> "AS" <+> renderSQL col - in - renderCommaSeparated renderOver winFns - -instance IsString - (Selection grp lat with db params from '["fromOnly" ::: 'NotNull 'PGtext]) where - fromString str = fromString str `as` Alias - --- | the `TableExpression` in the `select` command constructs an intermediate --- virtual table by possibly combining tables, views, eliminating rows, --- grouping, etc. This table is finally passed on to processing by --- the select list. The `Selection` determines which columns of --- the intermediate table are actually output. -select - :: (SListI row, row ~ (x ': xs)) - => Selection grp lat with db params from row - -- ^ selection - -> TableExpression grp lat with db params from - -- ^ intermediate virtual table - -> Query lat with db params row -select selection tabexpr = UnsafeQuery $ - "SELECT" - <+> renderSQL selection - <+> renderSQL tabexpr - --- | Like `select` but takes an `NP` list of `Expression`s instead --- of a general `Selection`. -select_ - :: (SListI row, row ~ (x ': xs)) - => NP (Aliased (Expression grp lat with db params from)) row - -- ^ select list - -> TableExpression grp lat with db params from - -- ^ intermediate virtual table - -> Query lat with db params row -select_ = select . List - --- | After the select list has been processed, the result table can --- be subject to the elimination of duplicate rows using `selectDistinct`. -selectDistinct - :: (SListI columns, columns ~ (col ': cols)) - => Selection grp lat with db params from columns - -- ^ selection - -> TableExpression grp lat with db params from - -- ^ intermediate virtual table - -> Query lat with db params columns -selectDistinct selection tabexpr = UnsafeQuery $ - "SELECT DISTINCT" - <+> renderSQL selection - <+> renderSQL tabexpr - --- | Like `selectDistinct` but takes an `NP` list of `Expression`s instead --- of a general `Selection`. -selectDistinct_ - :: (SListI columns, columns ~ (col ': cols)) - => NP (Aliased (Expression grp lat with db params from)) columns - -- ^ select list - -> TableExpression grp lat with db params from - -- ^ intermediate virtual table - -> Query lat with db params columns -selectDistinct_ = selectDistinct . List - -{-| -`selectDistinctOn` keeps only the first row of each set of rows where -the given expressions evaluate to equal. The DISTINCT ON expressions are -interpreted using the same rules as for ORDER BY. ORDER BY is used to -ensure that the desired row appears first. - -The DISTINCT ON expression(s) must match the leftmost ORDER BY expression(s). -The ORDER BY clause will normally contain additional expression(s) that -determine the desired precedence of rows within each DISTINCT ON group. - -In order to guarantee they match and reduce redundancy, this function -will prepend the The DISTINCT ON expressions to the ORDER BY clause. --} -selectDistinctOn - :: (SListI columns, columns ~ (col ': cols)) - => [SortExpression grp lat with db params from] - -- ^ DISTINCT ON expression(s) and prepended to ORDER BY clause - -> Selection grp lat with db params from columns - -- ^ selection - -> TableExpression grp lat with db params from - -- ^ intermediate virtual table - -> Query lat with db params columns -selectDistinctOn distincts selection tab = UnsafeQuery $ - "SELECT DISTINCT ON" - <+> parenthesized (commaSeparated (renderDistinctOn <$> distincts)) - <+> renderSQL selection - <+> renderSQL (tab {orderByClause = distincts <> orderByClause tab}) - where - renderDistinctOn = \case - Asc expression -> renderSQL expression - Desc expression -> renderSQL expression - AscNullsFirst expression -> renderSQL expression - DescNullsFirst expression -> renderSQL expression - AscNullsLast expression -> renderSQL expression - DescNullsLast expression -> renderSQL expression - --- | Like `selectDistinctOn` but takes an `NP` list of `Expression`s instead --- of a general `Selection`. -selectDistinctOn_ - :: (SListI columns, columns ~ (col ': cols)) - => [SortExpression grp lat with db params from] - -- ^ distinct on and return the first row in ordering - -> NP (Aliased (Expression grp lat with db params from)) columns - -- ^ selection - -> TableExpression grp lat with db params from - -- ^ intermediate virtual table - -> Query lat with db params columns -selectDistinctOn_ distincts = selectDistinctOn distincts . List diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Query/Table.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Query/Table.hs deleted file mode 100644 index 95803b76..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Query/Table.hs +++ /dev/null @@ -1,409 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Query.Table -Description: intermediate table expressions -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -intermediate table expressions --} - -{-# LANGUAGE - ConstraintKinds - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , GeneralizedNewtypeDeriving - , LambdaCase - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , QuantifiedConstraints - , ScopedTypeVariables - , StandaloneDeriving - , TypeApplications - , TypeFamilies - , TypeInType - , TypeOperators - , RankNTypes - , UndecidableInstances - #-} - -module Squeal.PostgreSQL.Query.Table - ( -- * Table Expression - TableExpression (..) - , from - , where_ - , groupBy - , having - , limit - , offset - , lockRows - -- * Grouping - , By (..) - , GroupByClause (..) - , HavingClause (..) - -- * Row Locks - , LockingClause (..) - , LockStrength (..) - , Waiting (..) - ) where - -import Control.DeepSeq -import Data.ByteString (ByteString) -import Data.String -import Data.Word -import Generics.SOP hiding (from) -import GHC.TypeLits - -import qualified GHC.Generics as GHC - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Expression.Logic -import Squeal.PostgreSQL.Expression.Sort -import Squeal.PostgreSQL.Query.From -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - -{----------------------------------------- -Table Expressions ------------------------------------------} - --- | A `TableExpression` computes a table. The table expression contains --- a `fromClause` that is optionally followed by a `whereClause`, --- `groupByClause`, `havingClause`, `orderByClause`, `limitClause` --- `offsetClause` and `lockingClauses`. Trivial table expressions simply refer --- to a table on disk, a so-called base table, but more complex expressions --- can be used to modify or combine base tables in various ways. -data TableExpression - (grp :: Grouping) - (lat :: FromType) - (with :: FromType) - (db :: SchemasType) - (params :: [NullType]) - (from :: FromType) - = TableExpression - { fromClause :: FromClause lat with db params from - -- ^ A table reference that can be a table name, or a derived table such - -- as a subquery, a @JOIN@ construct, or complex combinations of these. - , whereClause :: [Condition 'Ungrouped lat with db params from] - -- ^ optional search coditions, combined with `.&&`. After the processing - -- of the `fromClause` is done, each row of the derived virtual table - -- is checked against the search condition. If the result of the - -- condition is true, the row is kept in the output table, - -- otherwise it is discarded. The search condition typically references - -- at least one column of the table generated in the `fromClause`; - -- this is not required, but otherwise the WHERE clause will - -- be fairly useless. - , groupByClause :: GroupByClause grp from - -- ^ The `groupByClause` is used to group together those rows in a table - -- that have the same values in all the columns listed. The order in which - -- the columns are listed does not matter. The effect is to combine each - -- set of rows having common values into one group row that represents all - -- rows in the group. This is done to eliminate redundancy in the output - -- and/or compute aggregates that apply to these groups. - , havingClause :: HavingClause grp lat with db params from - -- ^ If a table has been grouped using `groupBy`, but only certain groups - -- are of interest, the `havingClause` can be used, much like a - -- `whereClause`, to eliminate groups from the result. Expressions in the - -- `havingClause` can refer both to grouped expressions and to ungrouped - -- expressions (which necessarily involve an aggregate function). - , orderByClause :: [SortExpression grp lat with db params from] - -- ^ The `orderByClause` is for optional sorting. When more than one - -- `SortExpression` is specified, the later (right) values are used to sort - -- rows that are equal according to the earlier (left) values. - , limitClause :: [Word64] - -- ^ The `limitClause` is combined with `min` to give a limit count - -- if nonempty. If a limit count is given, no more than that many rows - -- will be returned (but possibly fewer, if the query itself yields - -- fewer rows). - , offsetClause :: [Word64] - -- ^ The `offsetClause` is combined with `Prelude.+` to give an offset count - -- if nonempty. The offset count says to skip that many rows before - -- beginning to return rows. The rows are skipped before the limit count - -- is applied. - , lockingClauses :: [LockingClause from] - -- ^ `lockingClauses` can be added to a table expression with `lockRows`. - } deriving (GHC.Generic) - --- | Render a `TableExpression` -instance RenderSQL (TableExpression grp lat with db params from) where - renderSQL - (TableExpression frm' whs' grps' hvs' srts' lims' offs' lks') = mconcat - [ "FROM ", renderSQL frm' - , renderWheres whs' - , renderSQL grps' - , renderSQL hvs' - , renderSQL srts' - , renderLimits lims' - , renderOffsets offs' - , renderLocks lks' ] - where - renderWheres = \case - [] -> "" - wh:whs -> " WHERE" <+> renderSQL (foldr (.&&) wh whs) - renderLimits = \case - [] -> "" - lims -> " LIMIT" <+> fromString (show (minimum lims)) - renderOffsets = \case - [] -> "" - offs -> " OFFSET" <+> fromString (show (sum offs)) - renderLocks = foldr (\l b -> b <+> renderSQL l) "" - --- | A `from` generates a `TableExpression` from a table reference that can be --- a table name, or a derived table such as a subquery, a JOIN construct, --- or complex combinations of these. A `from` may be transformed by `where_`, --- `groupBy`, `having`, `orderBy`, `limit` and `offset`, --- using the `Data.Function.&` operator --- to match the left-to-right sequencing of their placement in SQL. -from - :: FromClause lat with db params from -- ^ table reference - -> TableExpression 'Ungrouped lat with db params from -from tab = TableExpression tab [] noGroups NoHaving [] [] [] [] - --- | A `where_` is an endomorphism of `TableExpression`s which adds a --- search condition to the `whereClause`. -where_ - :: Condition 'Ungrouped lat with db params from -- ^ filtering condition - -> TableExpression grp lat with db params from - -> TableExpression grp lat with db params from -where_ wh rels = rels {whereClause = wh : whereClause rels} - --- | A `groupBy` is a transformation of `TableExpression`s which switches --- its `Grouping` from `Ungrouped` to `Grouped`. Use @groupBy Nil@ to perform --- a "grand total" aggregation query. -groupBy - :: SListI bys - => NP (By from) bys -- ^ grouped columns - -> TableExpression 'Ungrouped lat with db params from - -> TableExpression ('Grouped bys) lat with db params from -groupBy bys rels = TableExpression - { fromClause = fromClause rels - , whereClause = whereClause rels - , groupByClause = group bys - , havingClause = Having [] - , orderByClause = [] - , limitClause = limitClause rels - , offsetClause = offsetClause rels - , lockingClauses = lockingClauses rels - } - --- | A `having` is an endomorphism of `TableExpression`s which adds a --- search condition to the `havingClause`. -having - :: Condition ('Grouped bys) lat with db params from -- ^ having condition - -> TableExpression ('Grouped bys) lat with db params from - -> TableExpression ('Grouped bys) lat with db params from -having hv rels = rels - { havingClause = case havingClause rels of Having hvs -> Having (hv:hvs) } - -instance OrderBy (TableExpression grp) grp where - orderBy srts rels = rels {orderByClause = orderByClause rels ++ srts} - --- | A `limit` is an endomorphism of `TableExpression`s which adds to the --- `limitClause`. -limit - :: Word64 -- ^ limit parameter - -> TableExpression grp lat with db params from - -> TableExpression grp lat with db params from -limit lim rels = rels {limitClause = lim : limitClause rels} - --- | An `offset` is an endomorphism of `TableExpression`s which adds to the --- `offsetClause`. -offset - :: Word64 -- ^ offset parameter - -> TableExpression grp lat with db params from - -> TableExpression grp lat with db params from -offset off rels = rels {offsetClause = off : offsetClause rels} - -{- | Add a `LockingClause` to a `TableExpression`. -Multiple `LockingClause`s can be written if it is necessary -to specify different locking behavior for different tables. -If the same table is mentioned (or implicitly affected) -by more than one locking clause, then it is processed -as if it was only specified by the strongest one. -Similarly, a table is processed as `NoWait` if that is specified -in any of the clauses affecting it. Otherwise, it is processed -as `SkipLocked` if that is specified in any of the clauses affecting it. -Further, a `LockingClause` cannot be added to a grouped table expression. --} -lockRows - :: LockingClause from -- ^ row-level lock - -> TableExpression 'Ungrouped lat with db params from - -> TableExpression 'Ungrouped lat with db params from -lockRows lck tab = tab {lockingClauses = lck : lockingClauses tab} - -{----------------------------------------- -Grouping ------------------------------------------} - --- | `By`s are used in `groupBy` to reference a list of columns which are then --- used to group together those rows in a table that have the same values --- in all the columns listed. @By \#col@ will reference an unambiguous --- column @col@; otherwise @By2 (\#tab \! \#col)@ will reference a table --- qualified column @tab.col@. -data By - (from :: FromType) - (by :: (Symbol,Symbol)) where - By1 - :: (HasUnique table from columns, Has column columns ty) - => Alias column - -> By from '(table, column) - By2 - :: (Has table from columns, Has column columns ty) - => Alias table - -> Alias column - -> By from '(table, column) -deriving instance Show (By from by) -deriving instance Eq (By from by) -deriving instance Ord (By from by) -instance RenderSQL (By from by) where - renderSQL = \case - By1 column -> renderSQL column - By2 rel column -> renderSQL rel <> "." <> renderSQL column - -instance (HasUnique rel rels cols, Has col cols ty, by ~ '(rel, col)) - => IsLabel col (By rels by) where fromLabel = By1 fromLabel -instance (HasUnique rel rels cols, Has col cols ty, bys ~ '[ '(rel, col)]) - => IsLabel col (NP (By rels) bys) where fromLabel = By1 fromLabel :* Nil -instance (Has rel rels cols, Has col cols ty, by ~ '(rel, col)) - => IsQualified rel col (By rels by) where (!) = By2 -instance (Has rel rels cols, Has col cols ty, bys ~ '[ '(rel, col)]) - => IsQualified rel col (NP (By rels) bys) where - rel ! col = By2 rel col :* Nil - --- | A `GroupByClause` indicates the `Grouping` of a `TableExpression`. -newtype GroupByClause grp from = UnsafeGroupByClause - { renderGroupByClause :: ByteString } - deriving stock (GHC.Generic,Show,Eq,Ord) - deriving newtype (NFData) -instance RenderSQL (GroupByClause grp from) where - renderSQL = renderGroupByClause -noGroups :: GroupByClause 'Ungrouped from -noGroups = UnsafeGroupByClause "" -group - :: SListI bys - => NP (By from) bys - -> GroupByClause ('Grouped bys) from -group bys = UnsafeGroupByClause $ case bys of - Nil -> "" - _ -> " GROUP BY" <+> renderCommaSeparated renderSQL bys - --- | A `HavingClause` is used to eliminate groups that are not of interest. --- An `Ungrouped` `TableExpression` may only use `NoHaving` while a `Grouped` --- `TableExpression` must use `Having` whose conditions are combined with --- `.&&`. -data HavingClause grp lat with db params from where - NoHaving :: HavingClause 'Ungrouped lat with db params from - Having - :: [Condition ('Grouped bys) lat with db params from] - -> HavingClause ('Grouped bys) lat with db params from -deriving instance Show (HavingClause grp lat with db params from) -deriving instance Eq (HavingClause grp lat with db params from) -deriving instance Ord (HavingClause grp lat with db params from) - --- | Render a `HavingClause`. -instance RenderSQL (HavingClause grp lat with db params from) where - renderSQL = \case - NoHaving -> "" - Having [] -> "" - Having conditions -> - " HAVING" <+> commaSeparated (renderSQL <$> conditions) - -{- | -If specific tables are named in a locking clause, -then only rows coming from those tables are locked; -any other tables used in the `Squeal.PostgreSQL.Query.Select.select` are simply read as usual. -A locking clause with a `Nil` table list affects all tables used in the statement. -If a locking clause is applied to a `view` or `subquery`, -it affects all tables used in the `view` or `subquery`. -However, these clauses do not apply to `Squeal.PostgreSQL.Query.With.with` queries referenced by the primary query. -If you want row locking to occur within a `Squeal.PostgreSQL.Query.With.with` query, -specify a `LockingClause` within the `Squeal.PostgreSQL.Query.With.with` query. --} -data LockingClause from where - For - :: HasAll tabs from tables - => LockStrength -- ^ lock strength - -> NP Alias tabs -- ^ table list - -> Waiting -- ^ wait or not - -> LockingClause from -instance RenderSQL (LockingClause from) where - renderSQL (For str tabs wt) = - "FOR" <+> renderSQL str - <> case tabs of - Nil -> "" - _ -> " OF" <+> renderSQL tabs - <> renderSQL wt - -{- | -Row-level locks, which are listed as below with the contexts -in which they are used automatically by PostgreSQL. -Note that a transaction can hold conflicting locks on the same row, -even in different subtransactions; but other than that, -two transactions can never hold conflicting locks on the same row. -Row-level locks do not affect data querying; -they block only writers and lockers to the same row. -Row-level locks are released at transaction end or during savepoint rollback. --} -data LockStrength - = Update - {- ^ `For` `Update` causes the rows retrieved by the `Squeal.PostgreSQL.Query.Select.select` statement - to be locked as though for update. This prevents them from being locked, - modified or deleted by other transactions until the current transaction ends. - That is, other transactions that attempt `Squeal.PostgreSQL.Manipulation.Update.update`, `Squeal.PostgreSQL.Manipulation.Delete.deleteFrom`, - `Squeal.PostgreSQL.Query.Select.select` `For` `Update`, `Squeal.PostgreSQL.Query.Select.select` `For` `NoKeyUpdate`, - `Squeal.PostgreSQL.Query.Select.select` `For` `Share` or `Squeal.PostgreSQL.Query.Select.select` `For` `KeyShare` of these rows will be blocked - until the current transaction ends; conversely, `Squeal.PostgreSQL.Query.Select.select` `For` `Update` will wait - for a concurrent transaction that has run any of those commands on the same row, - and will then lock and return the updated row (or no row, if the row was deleted). - Within a `Squeal.PostgreSQL.Session.Transaction.RepeatableRead` or `Squeal.PostgreSQL.Session.Transaction.Serializable` transaction, however, an error will be - thrown if a row to be locked has changed since the transaction started. - - The `For` `Update` lock mode is also acquired by any `Squeal.PostgreSQL.Manipulation.Delete.deleteFrom` a row, - and also by an `Update` that modifies the values on certain columns. - Currently, the set of columns considered for the `Squeal.PostgreSQL.Manipulation.Update.update` case are those - that have a unique index on them that can be used in a foreign key - (so partial indexes and expressional indexes are not considered), - but this may change in the future.-} - | NoKeyUpdate - {- | Behaves similarly to `For` `Update`, except that the lock acquired is weaker: - this lock will not block `Squeal.PostgreSQL.Query.Select.select` `For` `KeyShare` commands that attempt to acquire - a lock on the same rows. This lock mode is also acquired by any `Squeal.PostgreSQL.Manipulation.Update.update` - that does not acquire a `For` `Update` lock.-} - | Share - {- | Behaves similarly to `For` `Share`, except that the lock is weaker: - `Squeal.PostgreSQL.Query.Select.select` `For` `Update` is blocked, but not `Squeal.PostgreSQL.Query.Select.select` `For` `NoKeyUpdate`. - A key-shared lock blocks other transactions from performing - `Squeal.PostgreSQL.Manipulation.Delete.deleteFrom` or any `Squeal.PostgreSQL.Manipulation.Update.update` that changes the key values, - but not other `Update`, and neither does it prevent `Squeal.PostgreSQL.Query.Select.select` `For` `NoKeyUpdate`, - `Squeal.PostgreSQL.Query.Select.select` `For` `Share`, or `Squeal.PostgreSQL.Query.Select.select` `For` `KeyShare`.-} - | KeyShare - deriving (Eq, Ord, Show, Read, Enum, GHC.Generic) -instance RenderSQL LockStrength where - renderSQL = \case - Update -> "UPDATE" - NoKeyUpdate -> "NO KEY UPDATE" - Share -> "SHARE" - KeyShare -> "KEY SHARE" - --- | To prevent the operation from `Waiting` for other transactions to commit, --- use either the `NoWait` or `SkipLocked` option. -data Waiting - = Wait - -- ^ wait for other transactions to commit - | NoWait - -- ^ reports an error, rather than waiting - | SkipLocked - -- ^ any selected rows that cannot be immediately locked are skipped - deriving (Eq, Ord, Show, Read, Enum, GHC.Generic) -instance RenderSQL Waiting where - renderSQL = \case - Wait -> "" - NoWait -> " NOWAIT" - SkipLocked -> " SKIP LOCKED" diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Query/Values.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Query/Values.hs deleted file mode 100644 index 2d81fd03..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Query/Values.hs +++ /dev/null @@ -1,88 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Query.Values -Description: values statements -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -values statements --} - -{-# LANGUAGE - ConstraintKinds - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , GeneralizedNewtypeDeriving - , LambdaCase - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , QuantifiedConstraints - , ScopedTypeVariables - , StandaloneDeriving - , TypeApplications - , TypeFamilies - , TypeInType - , TypeOperators - , RankNTypes - , UndecidableInstances - #-} - -module Squeal.PostgreSQL.Query.Values - ( -- ** Values - values - , values_ - ) where - -import Data.ByteString (ByteString) -import Generics.SOP hiding (from) - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Expression -import Squeal.PostgreSQL.Query -import Squeal.PostgreSQL.Render - --- $setup --- >>> import Squeal.PostgreSQL - --- | `values` computes a row value or set of row values --- specified by value expressions. It is most commonly used --- to generate a “constant table” within a larger command, --- but it can be used on its own. --- --- >>> type Row = '["a" ::: 'NotNull 'PGint4, "b" ::: 'NotNull 'PGtext] --- >>> let query = values (1 `as` #a :* "one" `as` #b) [] :: Query lat with db '[] Row --- >>> printSQL query --- SELECT * FROM (VALUES ((1 :: int4), (E'one' :: text))) AS t ("a", "b") -values - :: SListI cols - => NP (Aliased (Expression 'Ungrouped lat with db params '[] )) cols - -> [NP (Aliased (Expression 'Ungrouped lat with db params '[] )) cols] - -- ^ When more than one row is specified, all the rows must - -- must have the same number of elements - -> Query lat with db params cols -values rw rws = UnsafeQuery $ "SELECT * FROM" - <+> parenthesized ( - "VALUES" - <+> commaSeparated - ( parenthesized - . renderCommaSeparated renderValuePart <$> rw:rws ) - ) <+> "AS t" - <+> parenthesized (renderCommaSeparated renderAliasPart rw) - where - renderAliasPart, renderValuePart - :: Aliased (Expression 'Ungrouped lat with db params '[] ) ty -> ByteString - renderAliasPart (_ `As` name) = renderSQL name - renderValuePart (value `As` _) = renderSQL value - --- | `values_` computes a row value or set of row values --- specified by value expressions. -values_ - :: SListI cols - => NP (Aliased (Expression 'Ungrouped lat with db params '[] )) cols - -- ^ one row of values - -> Query lat with db params cols -values_ rw = values rw [] diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Query/With.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Query/With.hs deleted file mode 100644 index 7e7ade72..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Query/With.hs +++ /dev/null @@ -1,245 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Query.With -Description: with statements -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -with statements --} - -{-# LANGUAGE - ConstraintKinds - , DeriveGeneric - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , GADTs - , GeneralizedNewtypeDeriving - , LambdaCase - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , QuantifiedConstraints - , ScopedTypeVariables - , StandaloneDeriving - , TypeApplications - , TypeFamilies - , TypeInType - , TypeOperators - , RankNTypes - , UndecidableInstances - #-} - -module Squeal.PostgreSQL.Query.With - ( -- ** With - With (..) - , CommonTableExpression (..) - , withRecursive - , Materialization (..) - , materialized - , notMaterialized - ) where - -import Data.Quiver.Functor -import GHC.TypeLits - -import qualified GHC.Generics as GHC -import qualified Generics.SOP as SOP - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Query -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - -{- | `with` provides a way to write auxiliary statements for use in a larger query. -These statements, referred to as `CommonTableExpression`s, can be thought of as -defining temporary tables that exist just for one query. - -`with` can be used for a `Query`. Multiple `CommonTableExpression`s can be -chained together with the `Path` constructor `:>>`, and each `CommonTableExpression` -is constructed via overloaded `as`. - ->>> type Columns = '["col1" ::: 'NoDef :=> 'NotNull 'PGint4, "col2" ::: 'NoDef :=> 'NotNull 'PGint4] ->>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] ->>> :{ -let - qry :: Query lat with (Public Schema) params '["col1" ::: 'NotNull 'PGint4, "col2" ::: 'NotNull 'PGint4] - qry = with ( - select Star (from (table #tab)) `as` #cte1 :>> - select Star (from (common #cte1)) `as` #cte2 - ) (select Star (from (common #cte2))) -in printSQL qry -:} -WITH "cte1" AS (SELECT * FROM "tab" AS "tab"), "cte2" AS (SELECT * FROM "cte1" AS "cte1") SELECT * FROM "cte2" AS "cte2" - -You can use data-modifying statements in `with`. This allows you to perform several -different operations in the same query. An example is: - ->>> type ProductsColumns = '["product" ::: 'NoDef :=> 'NotNull 'PGtext, "date" ::: 'Def :=> 'NotNull 'PGdate] ->>> type ProductsSchema = '["products" ::: 'Table ('[] :=> ProductsColumns), "products_deleted" ::: 'Table ('[] :=> ProductsColumns)] ->>> :{ -let - manp :: Manipulation with (Public ProductsSchema) '[ 'NotNull 'PGdate] '[] - manp = with - (deleteFrom #products NoUsing (#date .< param @1) (Returning Star) `as` #del) - (insertInto_ #products_deleted (Subquery (select Star (from (common #del))))) -in printSQL manp -:} -WITH "del" AS (DELETE FROM "products" AS "products" WHERE ("date" < ($1 :: date)) RETURNING *) INSERT INTO "products_deleted" AS "products_deleted" SELECT * FROM "del" AS "del" --} -class With statement where - with - :: Path (CommonTableExpression statement db params) with0 with1 - -- ^ common table expressions - -> statement with1 db params row - -- ^ larger query - -> statement with0 db params row -instance With (Query lat) where - with Done query = query - with ctes query = UnsafeQuery $ - "WITH" <+> commaSeparated (qtoList renderSQL ctes) <+> renderSQL query - -{- | A `withRecursive` `Query` can refer to its own output. -A very simple example is this query to sum the integers from 1 through 100: - ->>> import Data.Monoid (Sum (..)) ->>> import Data.Int (Int64) ->>> :{ - let - sum100 :: Statement db () (Sum Int64) - sum100 = query $ - withRecursive - ( values_ ((1 & astype int) `as` #n) - `unionAll` - select_ ((#n + 1) `as` #n) - (from (common #t) & where_ (#n .< 100)) `as` #t ) - ( select_ - (fromNull 0 (sum_ (All #n)) `as` #getSum) - (from (common #t) & groupBy Nil) ) - in printSQL sum100 -:} -WITH RECURSIVE "t" AS ((SELECT * FROM (VALUES (((1 :: int4) :: int))) AS t ("n")) UNION ALL (SELECT ("n" + (1 :: int4)) AS "n" FROM "t" AS "t" WHERE ("n" < (100 :: int4)))) SELECT COALESCE(sum(ALL "n"), (0 :: int8)) AS "getSum" FROM "t" AS "t" - -The general form of a recursive WITH query is always a non-recursive term, -then `union` (or `unionAll`), then a recursive term, where -only the recursive term can contain a reference to the query's own output. --} -withRecursive - :: Aliased (Query lat (recursive ': with) db params) recursive - -- ^ recursive query - -> Query lat (recursive ': with) db params row - -- ^ larger query - -> Query lat with db params row -withRecursive (recursive `As` cte) query = UnsafeQuery $ - "WITH RECURSIVE" <+> renderSQL cte - <+> "AS" <+> parenthesized (renderSQL recursive) - <+> renderSQL query - --- | Whether the contents of the WITH clause are materialized. --- If a WITH query is non-recursive and side-effect-free (that is, it is a SELECT containing no volatile functions) then it can be folded into the parent query, allowing joint optimization of the two query levels. --- --- Note: Use of `Materialized` or `NotMaterialized` requires PostgreSQL version 12 or higher. For earlier versions, use `DefaultMaterialization` which in those earlier versions of PostgreSQL behaves as `Materialized`. PostgreSQL 12 both changes the default behavior as well as adds options for customizing the materialization behavior. -data Materialization = - DefaultMaterialization -- ^ By default, folding happens if the parent query references the WITH query just once, but not if it references the WITH query more than once. Note: this is the behavior in PostgreSQL 12+. In PostgreSQL 11 and earlier, all CTEs are materialized. - | Materialized -- ^ You can override that decision by specifying MATERIALIZED to force separate calculation of the WITH query. Requires PostgreSQL 12+. - | NotMaterialized -- ^ or by specifying NOT MATERIALIZED to force it to be merged into the parent query. Requires PostgreSQL 12+. - deriving (Eq, Ord, Show, Read, Enum, GHC.Generic) -instance SOP.Generic Materialization -instance SOP.HasDatatypeInfo Materialization -instance RenderSQL Materialization where - renderSQL = \case - DefaultMaterialization -> "" - Materialized -> "MATERIALIZED" - NotMaterialized -> "NOT MATERIALIZED" - --- | A `CommonTableExpression` is an auxiliary statement in a `with` clause. -data CommonTableExpression statement - (db :: SchemasType) - (params :: [NullType]) - (with0 :: FromType) - (with1 :: FromType) where - CommonTableExpression - :: Aliased (statement with db params) (cte ::: common) - -- ^ aliased statement - -> Materialization - -- ^ materialization of the CTE output - -> CommonTableExpression statement db params with (cte ::: common ': with) -instance - ( KnownSymbol cte - , with1 ~ (cte ::: common ': with) - ) => Aliasable cte - (statement with db params common) - (CommonTableExpression statement db params with with1) where - statement `as` cte = CommonTableExpression (statement `as` cte) DefaultMaterialization -instance - ( KnownSymbol cte - , with1 ~ (cte ::: common ': with) - ) => Aliasable cte - (statement with db params common) - (Path (CommonTableExpression statement db params) with with1) where - statement `as` cte = qsingle (statement `as` cte) - -instance (forall c s p r. RenderSQL (statement c s p r)) => RenderSQL - (CommonTableExpression statement db params with0 with1) where - renderSQL (CommonTableExpression (statement `As` cte) materialization) = - renderSQL cte - <+> "AS" - <+> renderSQL materialization - <> case materialization of - DefaultMaterialization -> "" - _ -> " " - <> parenthesized (renderSQL statement) - -{- | Force separate calculation of the WITH query. - ->>> type Columns = '["col1" ::: 'NoDef :=> 'NotNull 'PGint4, "col2" ::: 'NoDef :=> 'NotNull 'PGint4] ->>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] ->>> :{ -let - qry :: Query lat with (Public Schema) params '["col1" ::: 'NotNull 'PGint4, "col2" ::: 'NotNull 'PGint4] - qry = with ( - materialized (select Star (from (table #tab)) `as` #cte1) :>> - select Star (from (common #cte1)) `as` #cte2 - ) (select Star (from (common #cte2))) -in printSQL qry -:} -WITH "cte1" AS MATERIALIZED (SELECT * FROM "tab" AS "tab"), "cte2" AS (SELECT * FROM "cte1" AS "cte1") SELECT * FROM "cte2" AS "cte2" - -Note: if the last CTE has `materialized` or `notMaterialized` you must add `:>> Done`. - -Requires PostgreSQL 12 or higher. --} -materialized - :: Aliased (statement with db params) (cte ::: common) -- ^ CTE - -> CommonTableExpression statement db params with (cte ::: common ': with) -materialized stmt = CommonTableExpression stmt Materialized - -{- | Force the WITH query to be merged into the parent query. - ->>> type Columns = '["col1" ::: 'NoDef :=> 'NotNull 'PGint4, "col2" ::: 'NoDef :=> 'NotNull 'PGint4] ->>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] ->>> :{ -let - qry :: Query lat with (Public Schema) params '["col1" ::: 'NotNull 'PGint4, "col2" ::: 'NotNull 'PGint4] - qry = with ( - select Star (from (table #tab)) `as` #cte1 :>> - notMaterialized (select Star (from (common #cte1)) `as` #cte2) :>> - Done - ) (select Star (from (common #cte2))) -in printSQL qry -:} -WITH "cte1" AS (SELECT * FROM "tab" AS "tab"), "cte2" AS NOT MATERIALIZED (SELECT * FROM "cte1" AS "cte1") SELECT * FROM "cte2" AS "cte2" - -Note: if the last CTE has `materialized` or `notMaterialized` you must add `:>> Done` to finish the `Path`. - -Requires PostgreSQL 12 or higher. --} -notMaterialized - :: Aliased (statement with db params) (cte ::: common) -- ^ CTE - -> CommonTableExpression statement db params with (cte ::: common ': with) -notMaterialized stmt = CommonTableExpression stmt NotMaterialized diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Render.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Render.hs deleted file mode 100644 index 4c433ebc..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Render.hs +++ /dev/null @@ -1,155 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Render -Description: render functions -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -render functions --} - -{-# LANGUAGE - AllowAmbiguousTypes - , ConstraintKinds - , FlexibleContexts - , LambdaCase - , MagicHash - , OverloadedStrings - , PolyKinds - , RankNTypes - , ScopedTypeVariables - , TypeApplications -#-} - -module Squeal.PostgreSQL.Render - ( -- * Render - RenderSQL (..) - , printSQL - , escape - , parenthesized - , bracketed - , (<+>) - , commaSeparated - , doubleQuoted - , singleQuotedText - , singleQuotedUtf8 - , escapeQuotedString - , escapeQuotedText - , renderCommaSeparated - , renderCommaSeparatedConstraint - , renderCommaSeparatedMaybe - , renderNat - , renderSymbol - ) where - -import Control.Monad.IO.Class (MonadIO (..)) -import Data.ByteString (ByteString) -import Data.Maybe (catMaybes) -import Data.Text (Text) -import Generics.SOP -import GHC.Exts -import GHC.TypeLits hiding (Text) - -import qualified Data.Text as Text -import qualified Data.Text.Encoding as Text -import qualified Data.ByteString as ByteString -import qualified Data.ByteString.Char8 as Char8 - --- | Parenthesize a `ByteString`. -parenthesized :: ByteString -> ByteString -parenthesized str = "(" <> str <> ")" - --- | Square bracket a `ByteString` -bracketed :: ByteString -> ByteString -bracketed str = "[" <> str <> "]" - --- | Concatenate two `ByteString`s with a space between. -(<+>) :: ByteString -> ByteString -> ByteString -infixr 7 <+> -str1 <+> str2 = str1 <> " " <> str2 - --- | Comma separate a list of `ByteString`s. -commaSeparated :: [ByteString] -> ByteString -commaSeparated = ByteString.intercalate ", " - --- | Add double quotes around a `ByteString`. -doubleQuoted :: ByteString -> ByteString -doubleQuoted str = "\"" <> str <> "\"" - --- | Add single quotes around a `Text` and escape single quotes within it. -singleQuotedText :: Text -> ByteString -singleQuotedText str = - "'" <> Text.encodeUtf8 (Text.replace "'" "''" str) <> "'" - --- | Add single quotes around a `ByteString` and escape single quotes within it. -singleQuotedUtf8 :: ByteString -> ByteString -singleQuotedUtf8 = singleQuotedText . Text.decodeUtf8 - --- | Escape quote a string. -escapeQuotedString :: String -> ByteString -escapeQuotedString x = "E\'" <> Text.encodeUtf8 (fromString (escape =<< x)) <> "\'" - --- | Escape quote a string. -escapeQuotedText :: Text -> ByteString -escapeQuotedText x = - "E\'" <> Text.encodeUtf8 (Text.concatMap (fromString . escape) x) <> "\'" - --- | Comma separate the renderings of a heterogeneous list. -renderCommaSeparated - :: SListI xs - => (forall x. expression x -> ByteString) - -> NP expression xs -> ByteString -renderCommaSeparated render - = commaSeparated - . hcollapse - . hmap (K . render) - --- | Comma separate the renderings of a heterogeneous list. -renderCommaSeparatedConstraint - :: forall c xs expression. (All c xs, SListI xs) - => (forall x. c x => expression x -> ByteString) - -> NP expression xs -> ByteString -renderCommaSeparatedConstraint render - = commaSeparated - . hcollapse - . hcmap (Proxy @c) (K . render) - --- | Comma separate the `Maybe` renderings of a heterogeneous list, dropping --- `Nothing`s. -renderCommaSeparatedMaybe - :: SListI xs - => (forall x. expression x -> Maybe ByteString) - -> NP expression xs -> ByteString -renderCommaSeparatedMaybe render - = commaSeparated - . catMaybes - . hcollapse - . hmap (K . render) - --- | Render a promoted `Nat`. -renderNat :: forall n. KnownNat n => ByteString -renderNat = fromString (show (natVal' (proxy# :: Proxy# n))) - --- | Render a promoted `Symbol`. -renderSymbol :: forall s. KnownSymbol s => ByteString -renderSymbol = fromString (symbolVal' (proxy# :: Proxy# s)) - --- | A class for rendering SQL -class RenderSQL sql where renderSQL :: sql -> ByteString - --- | Print SQL. -printSQL :: (RenderSQL sql, MonadIO io) => sql -> io () -printSQL = liftIO . Char8.putStrLn . renderSQL - --- | `escape` a character to prevent injection -escape :: Char -> String -escape = \case - '\NUL' -> "" - '\'' -> "''" - '"' -> "\\\"" - '\b' -> "\\b" - '\n' -> "\\n" - '\r' -> "\\r" - '\t' -> "\\t" - '\\' -> "\\\\" - c -> [c] diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Session.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Session.hs deleted file mode 100644 index 6e29c502..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Session.hs +++ /dev/null @@ -1,362 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Session -Description: sessions -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -Using Squeal in your application will come down to defining -the @DB :: @`SchemasType` of your database and including @PQ DB DB@ in your -application's monad transformer stack, giving it an instance of `MonadPQ` @DB@. --} - -{-# OPTIONS_GHC -fno-warn-redundant-constraints #-} -{-# LANGUAGE - DefaultSignatures - , FunctionalDependencies - , FlexibleContexts - , FlexibleInstances - , InstanceSigs - , OverloadedStrings - , PolyKinds - , QuantifiedConstraints - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeFamilies - , TypeInType - , TypeOperators - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Session - ( PQ (PQ, unPQ) - , runPQ - , execPQ - , evalPQ - , withConnection - ) where - -import Control.Category -import Control.Monad.Base (MonadBase(..)) -import Control.Monad.Catch -import Control.Monad.Except -import Control.Monad.Morph -import Control.Monad.Reader -import Control.Monad.Trans.Control (MonadBaseControl(..), MonadTransControl(..)) -import UnliftIO (MonadUnliftIO(..)) -import Data.ByteString (ByteString) -import Data.Foldable -import Data.Functor ((<&>)) -import Data.Kind -import Data.Traversable -import Generics.SOP -import PostgreSQL.Binary.Encoding (encodingBytes) -import Prelude hiding (id, (.)) - -import qualified Control.Monad.Fail as Fail -import qualified Database.PostgreSQL.LibPQ as LibPQ -import qualified PostgreSQL.Binary.Encoding as Encoding - -import Squeal.PostgreSQL.Definition -import Squeal.PostgreSQL.Manipulation -import Squeal.PostgreSQL.Session.Connection -import Squeal.PostgreSQL.Session.Encode -import Squeal.PostgreSQL.Session.Exception -import Squeal.PostgreSQL.Session.Indexed -import Squeal.PostgreSQL.Session.Oid -import Squeal.PostgreSQL.Session.Monad -import Squeal.PostgreSQL.Session.Result -import Squeal.PostgreSQL.Session.Statement -import Squeal.PostgreSQL.Type.Schema - --- | We keep track of the schema via an Atkey indexed state monad transformer, --- `PQ`. -newtype PQ - (db0 :: SchemasType) - (db1 :: SchemasType) - (m :: Type -> Type) - (x :: Type) = - PQ { unPQ :: K LibPQ.Connection db0 -> m (K x db1) } - -instance Monad m => Functor (PQ db0 db1 m) where - fmap f (PQ pq) = PQ $ \ conn -> do - K x <- pq conn - return $ K (f x) - --- | Run a `PQ` and keep the result and the `LibPQ.Connection`. -runPQ - :: Functor m - => PQ db0 db1 m x - -> K LibPQ.Connection db0 - -> m (x, K LibPQ.Connection db1) -runPQ (PQ pq) conn = (\ x -> (unK x, K (unK conn))) <$> pq conn - -- K x <- pq conn - -- return (x, K (unK conn)) - --- | Execute a `PQ` and discard the result but keep the `LibPQ.Connection`. -execPQ - :: Functor m - => PQ db0 db1 m x - -> K LibPQ.Connection db0 - -> m (K LibPQ.Connection db1) -execPQ (PQ pq) conn = mapKK (\ _ -> unK conn) <$> pq conn - --- | Evaluate a `PQ` and discard the `LibPQ.Connection` but keep the result. -evalPQ - :: Functor m - => PQ db0 db1 m x - -> K LibPQ.Connection db0 - -> m x -evalPQ (PQ pq) conn = unK <$> pq conn - -instance IndexedMonadTrans PQ where - - pqAp (PQ f) (PQ x) = PQ $ \ conn -> do - K f' <- f conn - K x' <- x (K (unK conn)) - return $ K (f' x') - - pqBind f (PQ x) = PQ $ \ conn -> do - K x' <- x conn - unPQ (f x') (K (unK conn)) - -instance IndexedMonadTransPQ PQ where - - define (UnsafeDefinition q) = PQ $ \ (K conn) -> liftIO $ do - resultMaybe <- LibPQ.exec conn q - case resultMaybe of - Nothing -> throwM $ ConnectionException "LibPQ.exec" - Just result -> K <$> okResult_ result - -instance (MonadIO io, db0 ~ db, db1 ~ db) => MonadPQ db (PQ db0 db1 io) where - - executeParams (Manipulation encode decode (UnsafeManipulation q)) x = - PQ $ \ kconn@(K conn) -> liftIO $ do - let - formatParam - :: forall param. OidOfNull db param - => K (Maybe Encoding.Encoding) param - -> IO (K (Maybe (LibPQ.Oid, ByteString, LibPQ.Format)) param) - formatParam (K maybeEncoding) = do - oid <- runReaderT (oidOfNull @db @param) kconn - return . K $ maybeEncoding <&> \encoding -> - (oid, encodingBytes encoding, LibPQ.Binary) - encodedParams <- runReaderT (runEncodeParams encode x) kconn - formattedParams <- hcollapse <$> - hctraverse' (Proxy @(OidOfNull db)) formatParam encodedParams - resultMaybe <- - LibPQ.execParams conn (q <> ";") formattedParams LibPQ.Binary - case resultMaybe of - Nothing -> throwM $ ConnectionException "LibPQ.execParams" - Just result -> do - okResult_ result - return $ K (Result decode result) - executeParams (Query encode decode q) x = - executeParams (Manipulation encode decode (queryStatement q)) x - - executePrepared (Manipulation encode decode (UnsafeManipulation q :: Manipulation '[] db params row)) list = - PQ $ \ kconn@(K conn) -> liftIO $ do - let - - temp = "temporary_statement" - - oidOfParam :: forall p. OidOfNull db p => (IO :.: K LibPQ.Oid) p - oidOfParam = Comp $ K <$> runReaderT (oidOfNull @db @p) kconn - oidsOfParams :: NP (IO :.: K LibPQ.Oid) params - oidsOfParams = hcpure (Proxy @(OidOfNull db)) oidOfParam - - prepare = do - oids <- hcollapse <$> hsequence' oidsOfParams - prepResultMaybe <- LibPQ.prepare conn temp (q <> ";") (Just oids) - case prepResultMaybe of - Nothing -> throwM $ ConnectionException "LibPQ.prepare" - Just prepResult -> okResult_ prepResult - - deallocate = do - deallocResultMaybe <- LibPQ.exec conn ("DEALLOCATE " <> temp <> ";") - case deallocResultMaybe of - Nothing -> throwM $ ConnectionException "LibPQ.exec" - Just deallocResult -> okResult_ deallocResult - - execPrepared = for list $ \ params -> do - encodedParams <- runReaderT (runEncodeParams encode params) kconn - let - formatParam encoding = (encodingBytes encoding, LibPQ.Binary) - formattedParams = - [ formatParam <$> maybeParam - | maybeParam <- hcollapse encodedParams - ] - resultMaybe <- - LibPQ.execPrepared conn temp formattedParams LibPQ.Binary - case resultMaybe of - Nothing -> throwM $ ConnectionException "LibPQ.execPrepared" - Just result -> do - okResult_ result - return $ Result decode result - - liftIO (K <$> bracket_ prepare deallocate execPrepared) - - executePrepared (Query encode decode q) list = - executePrepared (Manipulation encode decode (queryStatement q)) list - - executePrepared_ (Manipulation encode _ (UnsafeManipulation q :: Manipulation '[] db params row)) list = - PQ $ \ kconn@(K conn) -> do - let - - temp = "temporary_statement" - - oidOfParam :: forall p. OidOfNull db p => (IO :.: K LibPQ.Oid) p - oidOfParam = Comp $ K <$> runReaderT (oidOfNull @db @p) kconn - oidsOfParams :: NP (IO :.: K LibPQ.Oid) params - oidsOfParams = hcpure (Proxy @(OidOfNull db)) oidOfParam - - prepare = do - oids <- hcollapse <$> hsequence' oidsOfParams - prepResultMaybe <- LibPQ.prepare conn temp (q <> ";") (Just oids) - case prepResultMaybe of - Nothing -> throwM $ ConnectionException "LibPQ.prepare" - Just prepResult -> okResult_ prepResult - - deallocate = do - deallocResultMaybe <- LibPQ.exec conn ("DEALLOCATE " <> temp <> ";") - case deallocResultMaybe of - Nothing -> throwM $ ConnectionException "LibPQ.exec" - Just deallocResult -> okResult_ deallocResult - - execPrepared_ = for_ list $ \ params -> do - encodedParams <- runReaderT (runEncodeParams encode params) kconn - let - formatParam encoding = (encodingBytes encoding, LibPQ.Binary) - formattedParams = - [ formatParam <$> maybeParam - | maybeParam <- hcollapse encodedParams - ] - resultMaybe <- - LibPQ.execPrepared conn temp formattedParams LibPQ.Binary - case resultMaybe of - Nothing -> throwM $ ConnectionException "LibPQ.execPrepared" - Just result -> okResult_ result - - liftIO (K <$> bracket_ prepare deallocate execPrepared_) - - executePrepared_ (Query encode decode q) list = - executePrepared_ (Manipulation encode decode (queryStatement q)) list - -instance (Monad m, db0 ~ db1) - => Applicative (PQ db0 db1 m) where - pure x = PQ $ \ _conn -> pure (K x) - (<*>) = pqAp - -instance (Monad m, db0 ~ db1) - => Monad (PQ db0 db1 m) where - return = pure - (>>=) = flip pqBind - -instance (Monad m, db0 ~ db1) - => Fail.MonadFail (PQ db0 db1 m) where - fail = Fail.fail - -instance db0 ~ db1 => MFunctor (PQ db0 db1) where - hoist f (PQ pq) = PQ (f . pq) - -instance db0 ~ db1 => MonadTrans (PQ db0 db1) where - lift m = PQ $ \ _conn -> do - x <- m - return (K x) - -instance db0 ~ db1 => MMonad (PQ db0 db1) where - embed f (PQ pq) = PQ $ \ conn -> do - evalPQ (f (pq conn)) conn - -instance (MonadIO m, schema0 ~ schema1) - => MonadIO (PQ schema0 schema1 m) where - liftIO = lift . liftIO - -instance (MonadUnliftIO m, db0 ~ db1) - => MonadUnliftIO (PQ db0 db1 m) where - withRunInIO - :: ((forall a . PQ db0 schema1 m a -> IO a) -> IO b) - -> PQ db0 schema1 m b - withRunInIO inner = PQ $ \conn -> - withRunInIO $ \(run :: (forall x . m x -> IO x)) -> - K <$> inner (\pq -> run $ unK <$> unPQ pq conn) - -instance (MonadBase b m) - => MonadBase b (PQ schema schema m) where - liftBase = lift . liftBase - -instance db0 ~ db1 => MonadTransControl (PQ db0 db1) where - type StT (PQ db0 db1) a = a - liftWith f = PQ $ \conn -> K <$> (f $ \pq -> unK <$> unPQ pq conn) - restoreT ma = PQ . const $ K <$> ma - --- | A snapshot of the state of a `PQ` computation, used in MonadBaseControl Instance -type PQRun schema = - forall m x. Monad m => PQ schema schema m x -> m (K x schema) - -instance (MonadBaseControl b m, schema0 ~ schema1) - => MonadBaseControl b (PQ schema0 schema1 m) where - type StM (PQ schema0 schema1 m) x = StM m (K x schema0) - restoreM = PQ . const . restoreM - liftBaseWith f = - pqliftWith $ \ run -> liftBaseWith $ \ runInBase -> f $ runInBase . run - where - pqliftWith :: Functor m => (PQRun schema -> m a) -> PQ schema schema m a - pqliftWith g = PQ $ \ conn -> - fmap K (g $ \ pq -> unPQ pq conn) - -instance (MonadThrow m, db0 ~ db1) - => MonadThrow (PQ db0 db1 m) where - throwM = lift . throwM - -instance (MonadCatch m, db0 ~ db1) - => MonadCatch (PQ db0 db1 m) where - catch (PQ m) f = PQ $ \k -> m k `catch` \e -> unPQ (f e) k - -instance (MonadMask m, db0 ~ db1) - => MonadMask (PQ db0 db1 m) where - mask a = PQ $ \e -> mask $ \u -> unPQ (a $ q u) e - where q u (PQ b) = PQ (u . b) - - uninterruptibleMask a = - PQ $ \k -> uninterruptibleMask $ \u -> unPQ (a $ q u) k - where q u (PQ b) = PQ (u . b) - - generalBracket acquire release use = PQ $ \k -> - K <$> generalBracket - (unK <$> unPQ acquire k) - (\resource exitCase -> unK <$> unPQ (release resource exitCase) k) - (\resource -> unK <$> unPQ (use resource) k) - -instance (Monad m, Semigroup r, db0 ~ db1) => Semigroup (PQ db0 db1 m r) where - f <> g = pqAp (fmap (<>) f) g - -instance (Monad m, Monoid r, db0 ~ db1) => Monoid (PQ db0 db1 m r) where - mempty = pure mempty - --- | Do `connectdb` and `finish` before and after a computation. -withConnection - :: forall db0 db1 io x - . (MonadIO io, MonadMask io) - => ByteString - -> PQ db0 db1 io x - -> io x -withConnection connString action = - unK <$> bracket (connectdb connString) finish (unPQ action) - -okResult_ :: MonadIO io => LibPQ.Result -> io () -okResult_ result = liftIO $ do - status <- LibPQ.resultStatus result - case status of - LibPQ.CommandOk -> return () - LibPQ.TuplesOk -> return () - _ -> do - stateCodeMaybe <- LibPQ.resultErrorField result LibPQ.DiagSqlstate - case stateCodeMaybe of - Nothing -> throwM $ ConnectionException "LibPQ.resultErrorField" - Just stateCode -> do - msgMaybe <- LibPQ.resultErrorMessage result - case msgMaybe of - Nothing -> throwM $ ConnectionException "LibPQ.resultErrorMessage" - Just msg -> throwM . SQLException $ SQLState status stateCode msg diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Connection.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Session/Connection.hs deleted file mode 100644 index 03d34727..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Connection.hs +++ /dev/null @@ -1,78 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Session.Connection -Description: database connections -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -database connections --} - -{-# LANGUAGE - DataKinds - , PolyKinds - , RankNTypes - , TypeOperators -#-} - -module Squeal.PostgreSQL.Session.Connection - ( LibPQ.Connection - , connectdb - , finish - , lowerConnection - , SOP.K (..) - , SOP.unK - ) where - -import Control.Monad.IO.Class -import Data.ByteString (ByteString) - -import Squeal.PostgreSQL.Type.Schema - -import qualified Generics.SOP as SOP -import qualified Database.PostgreSQL.LibPQ as LibPQ - --- $setup --- >>> import Squeal.PostgreSQL - -{- | Makes a new connection to the database server. - -This function opens a new database connection using the parameters taken -from the string conninfo. - -The passed string can be empty to use all default parameters, or it can -contain one or more parameter settings separated by whitespace. -Each parameter setting is in the form keyword = value. Spaces around the equal -sign are optional. To write an empty value or a value containing spaces, -surround it with single quotes, e.g., keyword = 'a value'. Single quotes and -backslashes within the value must be escaped with a backslash, i.e., ' and \. - -To specify the schema you wish to connect with, use type application. - ->>> :set -XDataKinds ->>> :set -XPolyKinds ->>> :set -XTypeOperators ->>> type DB = '["public" ::: '["tab" ::: 'Table ('[] :=> '["col" ::: 'NoDef :=> 'Null 'PGint2])]] ->>> :set -XTypeApplications ->>> :set -XOverloadedStrings ->>> conn <- connectdb @DB "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" - -Note that, for now, squeal doesn't offer any protection from connecting -with the wrong schema! --} -connectdb - :: forall (db :: SchemasType) io - . MonadIO io - => ByteString -- ^ conninfo - -> io (SOP.K LibPQ.Connection db) -connectdb = fmap SOP.K . liftIO . LibPQ.connectdb - --- | Closes the connection to the server. -finish :: MonadIO io => SOP.K LibPQ.Connection db -> io () -finish = liftIO . LibPQ.finish . SOP.unK - --- | Safely `lowerConnection` to a smaller schema. -lowerConnection - :: SOP.K LibPQ.Connection (schema ': db) - -> SOP.K LibPQ.Connection db -lowerConnection (SOP.K conn) = SOP.K conn diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Decode.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Session/Decode.hs deleted file mode 100644 index 86292395..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Decode.hs +++ /dev/null @@ -1,603 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Session.Decode -Description: decoding of result values -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -decoding of result values --} - -{-# LANGUAGE - AllowAmbiguousTypes - , CPP - , DataKinds - , DerivingStrategies - , FlexibleContexts - , FlexibleInstances - , FunctionalDependencies - , GeneralizedNewtypeDeriving - , LambdaCase - , MultiParamTypeClasses - , OverloadedStrings - , PolyKinds - , ScopedTypeVariables - , TypeApplications - , TypeFamilies - , TypeOperators - , UndecidableInstances - , UndecidableSuperClasses -#-} - -module Squeal.PostgreSQL.Session.Decode - ( -- * Decode Types - FromPG (..) - , devalue - , rowValue - , enumValue - -- * Decode Rows - , DecodeRow (..) - , decodeRow - , runDecodeRow - , GenericRow (..) - , appendRows - , consRow - -- * Decoding Classes - , FromValue (..) - , FromField (..) - , FromArray (..) - , StateT (..) - , ExceptT (..) - ) where - -import BinaryParser -import Control.Applicative -import Control.Arrow -import Control.Monad -#if MIN_VERSION_base(4,13,0) -#else -import Control.Monad.Fail -#endif -import Control.Monad.Except -import Control.Monad.Reader -import Control.Monad.State.Strict -import Control.Monad.Trans.Maybe -import Data.Bits -import Data.Coerce (coerce) -import Data.Functor.Constant (Constant(Constant)) -import Data.Int (Int16, Int32, Int64) -import Data.Kind -import Data.Scientific (Scientific) -import Data.String (fromString) -import Data.Text (Text) -import Data.Time (Day, TimeOfDay, TimeZone, LocalTime, UTCTime, DiffTime) -import Data.UUID.Types (UUID) -import Data.Vector (Vector) -import Database.PostgreSQL.LibPQ (Oid(Oid)) -import GHC.OverloadedLabels -import GHC.TypeLits -import Network.IP.Addr (NetAddr, IP) -import PostgreSQL.Binary.Decoding hiding (Composite) -import Unsafe.Coerce - -import qualified Data.Aeson as Aeson -import qualified Data.ByteString.Lazy as Lazy (ByteString) -import qualified Data.ByteString as Strict (ByteString) -import qualified Data.Text.Lazy as Lazy (Text) -import qualified Data.Text as Strict (Text) -import qualified Data.Text as Strict.Text -import qualified Data.Vector as Vector -import qualified Generics.SOP as SOP -import qualified Generics.SOP.Record as SOP - -import Squeal.PostgreSQL.Expression.Range -import Squeal.PostgreSQL.Type -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Type.PG -import Squeal.PostgreSQL.Type.Schema - --- | Converts a `Value` type from @postgresql-binary@ for use in --- the `fromPG` method of `FromPG`. -devalue :: Value x -> StateT Strict.ByteString (Except Strict.Text) x -devalue = unsafeCoerce - -revalue :: StateT Strict.ByteString (Except Strict.Text) x -> Value x -revalue = unsafeCoerce - -{- | ->>> :set -XTypeFamilies ->>> :{ -data Complex = Complex - { real :: Double - , imaginary :: Double - } -instance IsPG Complex where - type PG Complex = 'PGcomposite '[ - "re" ::: 'NotNull 'PGfloat8, - "im" ::: 'NotNull 'PGfloat8] -instance FromPG Complex where - fromPG = rowValue $ do - re <- #re - im <- #im - return Complex {real = re, imaginary = im} -:} --} -rowValue - :: (PG y ~ 'PGcomposite row, SOP.SListI row) - => DecodeRow row y -- ^ fields - -> StateT Strict.ByteString (Except Strict.Text) y -rowValue decoder = devalue $ - let - -- - -- [for each field] - -- - -- [if value is NULL] - -- <-1: 4 bytes> - -- [else] - -- - -- bytes> - -- [end if] - -- [end for] - comp = valueParser $ do - unitOfSize 4 - SOP.hsequence' $ SOP.hpure $ SOP.Comp $ do - unitOfSize 4 - len :: Int32 <- sized 4 int - if len == -1 - then return (SOP.K Nothing) - else SOP.K . Just <$> bytesOfSize (fromIntegral len) - in fn (runDecodeRow decoder <=< comp) - --- | A `FromPG` constraint gives a parser from the binary format of --- a PostgreSQL `PGType` into a Haskell `Type`. -class IsPG y => FromPG y where - {- | - >>> :set -XMultiParamTypeClasses -XGeneralizedNewtypeDeriving -XDerivingStrategies -XDerivingVia -XUndecidableInstances - >>> import GHC.Generics as GHC - >>> :{ - newtype UserId = UserId { getId :: Int64 } - deriving newtype (IsPG, FromPG) - :} - - >>> :{ - data Complex = Complex - { real :: Double - , imaginary :: Double - } deriving stock GHC.Generic - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) - deriving (IsPG, FromPG) via Composite Complex - :} - - >>> :{ - data Direction = North | South | East | West - deriving stock GHC.Generic - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) - deriving (IsPG, FromPG) via Enumerated Direction - :} - - -} - fromPG :: StateT Strict.ByteString (Except Strict.Text) y -instance FromPG Bool where - fromPG = devalue bool -instance FromPG Int16 where - fromPG = devalue int -instance FromPG Int32 where - fromPG = devalue int -instance FromPG Int64 where - fromPG = devalue int -instance FromPG Oid where - fromPG = devalue $ Oid <$> int -instance FromPG Float where - fromPG = devalue float4 -instance FromPG Double where - fromPG = devalue float8 -instance FromPG Scientific where - fromPG = devalue numeric -instance FromPG Money where - fromPG = devalue $ Money <$> int -instance FromPG UUID where - fromPG = devalue uuid -instance FromPG (NetAddr IP) where - fromPG = devalue inet -instance FromPG Char where - fromPG = devalue char -instance FromPG Strict.Text where - fromPG = devalue text_strict -instance FromPG Lazy.Text where - fromPG = devalue text_lazy -instance FromPG String where - fromPG = devalue $ Strict.Text.unpack <$> text_strict -instance FromPG Strict.ByteString where - fromPG = devalue bytea_strict -instance FromPG Lazy.ByteString where - fromPG = devalue bytea_lazy -instance KnownNat n => FromPG (VarChar n) where - fromPG = devalue $ text_strict >>= \t -> - case varChar t of - Nothing -> throwError $ Strict.Text.pack $ concat - [ "Source for VarChar has wrong length" - , "; expected length " - , show (natVal (SOP.Proxy @n)) - , ", actual length " - , show (Strict.Text.length t) - , "." - ] - Just x -> pure x -instance KnownNat n => FromPG (FixChar n) where - fromPG = devalue $ text_strict >>= \t -> - case fixChar t of - Nothing -> throwError $ Strict.Text.pack $ concat - [ "Source for FixChar has wrong length" - , "; expected length " - , show (natVal (SOP.Proxy @n)) - , ", actual length " - , show (Strict.Text.length t) - , "." - ] - Just x -> pure x -instance FromPG x => FromPG (Const x tag) where - fromPG = coerce $ fromPG @x -instance FromPG x => FromPG (SOP.K x tag) where - fromPG = coerce $ fromPG @x -instance FromPG x => FromPG (Constant x tag) where - fromPG = coerce $ fromPG @x -instance FromPG Day where - fromPG = devalue date -instance FromPG TimeOfDay where - fromPG = devalue time_int -instance FromPG (TimeOfDay, TimeZone) where - fromPG = devalue timetz_int -instance FromPG LocalTime where - fromPG = devalue timestamp_int -instance FromPG UTCTime where - fromPG = devalue timestamptz_int -instance FromPG DiffTime where - fromPG = devalue interval_int -instance FromPG Aeson.Value where - fromPG = devalue json_ast -instance Aeson.FromJSON x => FromPG (Json x) where - fromPG = devalue $ Json <$> - json_bytes (left Strict.Text.pack . Aeson.eitherDecodeStrict) -instance Aeson.FromJSON x => FromPG (Jsonb x) where - fromPG = devalue $ Jsonb <$> - jsonb_bytes (left Strict.Text.pack . Aeson.eitherDecodeStrict) -instance (FromArray '[] ty y, ty ~ NullPG y) - => FromPG (VarArray (Vector y)) where - fromPG = - let - rep n x = VarArray <$> Vector.replicateM n x - in - devalue . array $ dimensionArray rep - (fromArray @'[] @(NullPG y)) -instance (FromArray '[] ty y, ty ~ NullPG y) - => FromPG (VarArray [y]) where - fromPG = - let - rep n x = VarArray <$> replicateM n x - in - devalue . array $ dimensionArray rep - (fromArray @'[] @(NullPG y)) -instance FromArray dims ty y => FromPG (FixArray y) where - fromPG = devalue $ FixArray <$> array (fromArray @dims @ty @y) -instance - ( SOP.IsEnumType y - , SOP.HasDatatypeInfo y - , LabelsPG y ~ labels - ) => FromPG (Enumerated y) where - fromPG = - let - greadConstructor - :: SOP.All ((~) '[]) xss - => NP SOP.ConstructorInfo xss - -> String - -> Maybe (SOP.SOP SOP.I xss) - greadConstructor Nil _ = Nothing - greadConstructor (constructor :* constructors) name = - if name == SOP.constructorName constructor - then Just (SOP.SOP (SOP.Z Nil)) - else SOP.SOP . SOP.S . SOP.unSOP <$> - greadConstructor constructors name - in - devalue - $ fmap Enumerated - . enum - $ fmap SOP.to - . greadConstructor - (SOP.constructorInfo (SOP.datatypeInfo (SOP.Proxy @y))) - . Strict.Text.unpack -instance - ( SOP.IsRecord y ys - , SOP.AllZip FromField row ys - , RowPG y ~ row - ) => FromPG (Composite y) where - fromPG = rowValue (Composite <$> genericRow) -instance FromPG y => FromPG (Range y) where - fromPG = devalue $ do - flag <- byte - if testBit flag 0 then return Empty else do - lower <- - if testBit flag 3 - then return Infinite - else do - len <- sized 4 int - l <- sized len (revalue fromPG) - return $ if testBit flag 1 then Closed l else Open l - upper <- - if testBit flag 4 - then return Infinite - else do - len <- sized 4 int - l <- sized len (revalue fromPG) - return $ if testBit flag 2 then Closed l else Open l - return $ NonEmpty lower upper - --- | A `FromValue` constraint lifts the `FromPG` parser --- to a decoding of a @NullityType@ to a `Type`, --- decoding `Null`s to `Maybe`s. You should not define instances for --- `FromValue`, just use the provided instances. -class FromValue (ty :: NullType) (y :: Type) where - fromValue :: Maybe Strict.ByteString -> Either Strict.Text y -instance (FromPG y, pg ~ PG y) => FromValue ('NotNull pg) y where - fromValue = \case - Nothing -> throwError "fromField: saw NULL when expecting NOT NULL" - Just bytestring -> valueParser (revalue fromPG) bytestring -instance (FromPG y, pg ~ PG y) => FromValue ('Null pg) (Maybe y) where - fromValue = \case - Nothing -> return Nothing - Just bytestring -> fmap Just $ valueParser (revalue fromPG) bytestring - --- | A `FromField` constraint lifts the `FromPG` parser --- to a decoding of a @(Symbol, NullityType)@ to a `Type`, --- decoding `Null`s to `Maybe`s. You should not define instances for --- `FromField`, just use the provided instances. -class FromField (field :: (Symbol, NullType)) (y :: (Symbol, Type)) where - fromField :: Maybe Strict.ByteString -> Either Strict.Text (SOP.P y) -instance (FromValue ty y, fld0 ~ fld1) - => FromField (fld0 ::: ty) (fld1 ::: y) where - fromField = fmap SOP.P . fromValue @ty - --- | A `FromArray` constraint gives a decoding to a Haskell `Type` --- from the binary format of a PostgreSQL fixed-length array. --- You should not define instances for --- `FromArray`, just use the provided instances. -class FromArray (dims :: [Nat]) (ty :: NullType) (y :: Type) where - fromArray :: Array y -instance (FromPG y, pg ~ PG y) => FromArray '[] ('NotNull pg) y where - fromArray = valueArray (revalue fromPG) -instance (FromPG y, pg ~ PG y) => FromArray '[] ('Null pg) (Maybe y) where - fromArray = nullableValueArray (revalue fromPG) -instance - ( SOP.IsProductType product ys - , Length ys ~ dim - , SOP.All ((~) y) ys - , FromArray dims ty y ) - => FromArray (dim ': dims) ty product where - fromArray = - let - rep _ = fmap (SOP.to . SOP.SOP . SOP.Z) . replicateMN - in - dimensionArray rep (fromArray @dims @ty @y) - -replicateMN - :: forall x xs m. (SOP.All ((~) x) xs, Monad m, SOP.SListI xs) - => m x -> m (SOP.NP SOP.I xs) -replicateMN mx = SOP.hsequence' $ - SOP.hcpure (SOP.Proxy :: SOP.Proxy ((~) x)) (SOP.Comp (SOP.I <$> mx)) - -{- | -`DecodeRow` describes a decoding of a PostgreSQL `RowType` -into a Haskell `Type`. - -`DecodeRow` has an interface given by the classes -`Functor`, `Applicative`, `Alternative`, `Monad`, -`MonadPlus`, `MonadError` `Strict.Text`, and `IsLabel`. - ->>> :set -XOverloadedLabels ->>> :{ -let - decode :: DecodeRow - '[ "fst" ::: 'NotNull 'PGint2, "snd" ::: 'NotNull ('PGchar 1)] - (Int16, Char) - decode = (,) <$> #fst <*> #snd -in runDecodeRow decode (SOP.K (Just "\NUL\SOH") :* SOP.K (Just "a") :* Nil) -:} -Right (1,'a') - -There is also an `IsLabel` instance for `MaybeT` `DecodeRow`s, useful -for decoding outer joined rows. - ->>> :{ -let - decode :: DecodeRow - '[ "fst" ::: 'Null 'PGint2, "snd" ::: 'Null ('PGchar 1)] - (Maybe (Int16, Char)) - decode = runMaybeT $ (,) <$> #fst <*> #snd -in runDecodeRow decode (SOP.K (Just "\NUL\SOH") :* SOP.K (Just "a") :* Nil) -:} -Right (Just (1,'a')) - --} -newtype DecodeRow (row :: RowType) (y :: Type) = DecodeRow - { unDecodeRow :: ReaderT - (SOP.NP (SOP.K (Maybe Strict.ByteString)) row) (Except Strict.Text) y } - deriving newtype - ( Functor - , Applicative - , Alternative - , Monad - , MonadPlus - , MonadError Strict.Text ) -instance MonadFail (DecodeRow row) where - fail = throwError . fromString - --- | Run a `DecodeRow`. -runDecodeRow - :: DecodeRow row y - -> SOP.NP (SOP.K (Maybe Strict.ByteString)) row - -> Either Strict.Text y -runDecodeRow = fmap runExcept . runReaderT . unDecodeRow - -{- | Append two row decoders with a combining function. - ->>> import GHC.Generics as GHC ->>> :{ -data L = L {fst :: Int16, snd :: Char} - deriving stock (GHC.Generic, Show) - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) -data R = R {thrd :: Bool, frth :: Bool} - deriving stock (GHC.Generic, Show) - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) -type Row = '[ - "fst" ::: 'NotNull 'PGint2, - "snd" ::: 'NotNull ('PGchar 1), - "thrd" ::: 'NotNull 'PGbool, - "frth" ::: 'NotNull 'PGbool] -:} - ->>> :{ -let - decode :: DecodeRow Row (L,R) - decode = appendRows (,) genericRow genericRow - row4 = - SOP.K (Just "\NUL\SOH") :* - SOP.K (Just "a") :* - SOP.K (Just "\NUL") :* - SOP.K (Just "\NUL") :* Nil -in runDecodeRow decode row4 -:} -Right (L {fst = 1, snd = 'a'},R {thrd = False, frth = False}) --} -appendRows - :: SOP.SListI left - => (l -> r -> z) -- ^ combining function - -> DecodeRow left l -- ^ left decoder - -> DecodeRow right r -- ^ right decoder - -> DecodeRow (Join left right) z -appendRows f decL decR = decodeRow $ \row -> case disjoin row of - (rowL, rowR) -> f <$> runDecodeRow decL rowL <*> runDecodeRow decR rowR - -{- | Cons a column and a row decoder with a combining function. - ->>> :{ -let - decode :: DecodeRow - '["fst" ::: 'NotNull 'PGtext, "snd" ::: 'NotNull 'PGint2, "thrd" ::: 'NotNull ('PGchar 1)] - (String, (Int16, Char)) - decode = consRow (,) #fst (consRow (,) #snd #thrd) -in runDecodeRow decode (SOP.K (Just "hi") :* SOP.K (Just "\NUL\SOH") :* SOP.K (Just "a") :* Nil) -:} -Right ("hi",(1,'a')) --} -consRow - :: FromValue head h - => (h -> t -> z) -- ^ combining function - -> Alias col -- ^ alias of head - -> DecodeRow tail t -- ^ tail decoder - -> DecodeRow (col ::: head ': tail) z -consRow f _ dec = decodeRow $ \case - (SOP.K h :: SOP.K (Maybe Strict.ByteString) (col ::: head)) :* t - -> f <$> fromValue @head h <*> runDecodeRow dec t - --- | Smart constructor for a `DecodeRow`. -decodeRow - :: (SOP.NP (SOP.K (Maybe Strict.ByteString)) row -> Either Strict.Text y) - -> DecodeRow row y -decodeRow dec = DecodeRow . ReaderT $ liftEither . dec -instance {-# OVERLAPPING #-} (KnownSymbol fld, FromValue ty y) - => IsLabel fld (DecodeRow (fld ::: ty ': row) y) where - fromLabel = decodeRow $ \(SOP.K b SOP.:* _) -> do - let - flderr = mconcat - [ "field name: " - , "\"", fromString (symbolVal (SOP.Proxy @fld)), "\"; " - ] - left (flderr <>) $ fromValue @ty b -instance {-# OVERLAPPABLE #-} IsLabel fld (DecodeRow row y) - => IsLabel fld (DecodeRow (field ': row) y) where - fromLabel = decodeRow $ \(_ SOP.:* bs) -> - runDecodeRow (fromLabel @fld) bs -instance {-# OVERLAPPING #-} (KnownSymbol fld, FromValue ty (Maybe y)) - => IsLabel fld (MaybeT (DecodeRow (fld ::: ty ': row)) y) where - fromLabel = MaybeT . decodeRow $ \(SOP.K b SOP.:* _) -> do - let - flderr = mconcat - [ "field name: " - , "\"", fromString (symbolVal (SOP.Proxy @fld)), "\"; " - ] - left (flderr <>) $ fromValue @ty b -instance {-# OVERLAPPABLE #-} IsLabel fld (MaybeT (DecodeRow row) y) - => IsLabel fld (MaybeT (DecodeRow (field ': row)) y) where - fromLabel = MaybeT . decodeRow $ \(_ SOP.:* bs) -> - runDecodeRow (runMaybeT (fromLabel @fld)) bs - --- | A `GenericRow` constraint to ensure that a Haskell type --- is a record type, --- has a `RowPG`, --- and all its fields and can be decoded from corresponding Postgres fields. -class - ( SOP.IsRecord y ys - , row ~ RowPG y - , SOP.AllZip FromField row ys - ) => GenericRow row y ys where - {- | Row decoder for `SOP.Generic` records. - - >>> import qualified GHC.Generics as GHC - >>> import qualified Generics.SOP as SOP - >>> data Two = Two {frst :: Int16, scnd :: String} deriving (Show, GHC.Generic, SOP.Generic, SOP.HasDatatypeInfo) - >>> :{ - let - decode :: DecodeRow '[ "frst" ::: 'NotNull 'PGint2, "scnd" ::: 'NotNull 'PGtext] Two - decode = genericRow - in runDecodeRow decode (SOP.K (Just "\NUL\STX") :* SOP.K (Just "two") :* Nil) - :} - Right (Two {frst = 2, scnd = "two"}) - -} - genericRow :: DecodeRow row y -instance - ( row ~ RowPG y - , SOP.IsRecord y ys - , SOP.AllZip FromField row ys - ) => GenericRow row y ys where - genericRow - = DecodeRow - . ReaderT - $ fmap SOP.fromRecord - . SOP.hsequence' - . SOP.htrans (SOP.Proxy @FromField) (SOP.Comp . runField) - where - runField - :: forall ty z. FromField ty z - => SOP.K (Maybe Strict.ByteString) ty - -> Except Strict.Text (SOP.P z) - runField = liftEither . fromField @ty . SOP.unK - -{- | ->>> :{ -data Dir = North | East | South | West -instance IsPG Dir where - type PG Dir = 'PGenum '["north", "south", "east", "west"] -instance FromPG Dir where - fromPG = enumValue $ - label @"north" North :* - label @"south" South :* - label @"east" East :* - label @"west" West -:} --} -enumValue - :: (SOP.All KnownSymbol labels, PG y ~ 'PGenum labels) - => NP (SOP.K y) labels -- ^ labels - -> StateT Strict.ByteString (Except Strict.Text) y -enumValue = devalue . enum . labels - where - labels - :: SOP.All KnownSymbol labels - => NP (SOP.K y) labels - -> Text -> Maybe y - labels = \case - Nil -> \_ -> Nothing - ((y :: SOP.K y label) :* ys) -> \ str -> - if str == fromString (symbolVal (SOP.Proxy @label)) - then Just (SOP.unK y) - else labels ys str diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Encode.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Session/Encode.hs deleted file mode 100644 index 0ccfc6fc..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Encode.hs +++ /dev/null @@ -1,535 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Session.Encode -Description: encoding of statement parameters -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -encoding of statement parameters --} - -{-# LANGUAGE - AllowAmbiguousTypes - , ConstraintKinds - , DataKinds - , DefaultSignatures - , FlexibleContexts - , FlexibleInstances - , LambdaCase - , MultiParamTypeClasses - , PolyKinds - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeFamilies - , TypeOperators - , UndecidableInstances - , UndecidableSuperClasses -#-} - -module Squeal.PostgreSQL.Session.Encode - ( -- * Encode Parameters - EncodeParams (..) - , GenericParams (..) - , nilParams - , (.*) - , (*.) - , aParam - , appendParams - -- * Encoding Classes - , ToPG (..) - , ToParam (..) - , ToField (..) - , ToArray (..) - ) where - -import ByteString.StrictBuilder -import Control.Monad -import Control.Monad.Reader -import Data.Bits -import Data.ByteString as Strict (ByteString) -import Data.ByteString.Lazy as Lazy (ByteString) -import Data.Coerce (coerce) -import Data.Functor.Const (Const(Const)) -import Data.Functor.Constant (Constant(Constant)) -import Data.Functor.Contravariant -import Data.Int (Int16, Int32, Int64) -import Data.Kind -import Data.Scientific (Scientific) -import Data.Text as Strict (Text) -import Data.Text.Lazy as Lazy (Text) -import Data.Time (Day, TimeOfDay, TimeZone, LocalTime, UTCTime, DiffTime) -import Data.UUID.Types (UUID) -import Data.Vector (Vector) -import Data.Word (Word32) -import Foreign.C.Types (CUInt(CUInt)) -import GHC.TypeLits -import Network.IP.Addr (NetAddr, IP) -import PostgreSQL.Binary.Encoding - -import qualified Data.Aeson as Aeson -import qualified Data.ByteString.Lazy as Lazy.ByteString -import qualified Data.Text as Strict.Text -import qualified Database.PostgreSQL.LibPQ as LibPQ -import qualified Generics.SOP as SOP -import qualified Generics.SOP.Record as SOP - -import Squeal.PostgreSQL.Expression.Range -import Squeal.PostgreSQL.Session.Oid -import Squeal.PostgreSQL.Type -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Type.PG -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL (connectdb, finish) - --- | A `ToPG` constraint gives an encoding of a Haskell `Type` into --- into the binary format of a PostgreSQL `PGType`. -class IsPG x => ToPG (db :: SchemasType) (x :: Type) where - -- | >>> :set -XTypeApplications -XDataKinds - -- >>> conn <- connectdb @'[] "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" - -- >>> runReaderT (toPG @'[] False) conn - -- "\NUL" - -- - -- >>> runReaderT (toPG @'[] (0 :: Int16)) conn - -- "\NUL\NUL" - -- - -- >>> runReaderT (toPG @'[] (0 :: Int32)) conn - -- "\NUL\NUL\NUL\NUL" - -- - -- >>> :set -XMultiParamTypeClasses -XGeneralizedNewtypeDeriving - -- >>> newtype UserId = UserId { getUserId :: Int64 } deriving newtype (IsPG, ToPG db) - -- >>> runReaderT (toPG @'[] (UserId 0)) conn - -- "\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL" - -- - -- >>> finish conn - toPG :: x -> ReaderT (SOP.K LibPQ.Connection db) IO Encoding -instance ToPG db Bool where toPG = pure . bool -instance ToPG db Int16 where toPG = pure . int2_int16 -instance ToPG db Int32 where toPG = pure . int4_int32 -instance ToPG db Int64 where toPG = pure . int8_int64 -instance ToPG db Oid where toPG = pure . int4_word32 . getOid -instance ToPG db Float where toPG = pure . float4 -instance ToPG db Double where toPG = pure . float8 -instance ToPG db Scientific where toPG = pure . numeric -instance ToPG db Money where toPG = pure . int8_int64 . cents -instance ToPG db UUID where toPG = pure . uuid -instance ToPG db (NetAddr IP) where toPG = pure . inet -instance ToPG db Char where toPG = pure . char_utf8 -instance ToPG db Strict.Text where toPG = pure . text_strict -instance ToPG db Lazy.Text where toPG = pure . text_lazy -instance ToPG db String where - toPG = pure . text_strict . Strict.Text.pack -instance ToPG db Strict.ByteString where toPG = pure . bytea_strict -instance ToPG db Lazy.ByteString where toPG = pure . bytea_lazy -instance ToPG db (VarChar n) where toPG = pure . text_strict . getVarChar -instance ToPG db (FixChar n) where toPG = pure . text_strict . getFixChar -instance ToPG db x => ToPG db (Const x tag) where toPG = toPG @db @x . coerce -instance ToPG db x => ToPG db (SOP.K x tag) where toPG = toPG @db @x . coerce -instance ToPG db x => ToPG db (Constant x tag) where toPG = toPG @db @x . coerce -instance ToPG db Day where toPG = pure . date -instance ToPG db TimeOfDay where toPG = pure . time_int -instance ToPG db (TimeOfDay, TimeZone) where toPG = pure . timetz_int -instance ToPG db LocalTime where toPG = pure . timestamp_int -instance ToPG db UTCTime where toPG = pure . timestamptz_int -instance ToPG db DiffTime where toPG = pure . interval_int -instance ToPG db Aeson.Value where toPG = pure . json_ast -instance Aeson.ToJSON x => ToPG db (Json x) where - toPG = pure . json_bytes - . Lazy.ByteString.toStrict . Aeson.encode . getJson -instance Aeson.ToJSON x => ToPG db (Jsonb x) where - toPG = pure . jsonb_bytes - . Lazy.ByteString.toStrict . Aeson.encode . getJsonb -instance (NullPG x ~ ty, ToArray db '[] ty x, OidOfNull db ty) - => ToPG db (VarArray [x]) where - toPG (VarArray arr) = do - oid <- oidOfNull @db @ty - let - dims = [fromIntegral (length arr)] - nulls = arrayNulls @db @'[] @ty @x - payload <- dimArray foldM (arrayPayload @db @'[] @ty @x) arr - return $ encodeArray 1 nulls oid dims payload -instance (NullPG x ~ ty, ToArray db '[] ty x, OidOfNull db ty) - => ToPG db (VarArray (Vector x)) where - toPG (VarArray arr) = do - oid <- oidOfNull @db @ty - let - dims = [fromIntegral (length arr)] - nulls = arrayNulls @db @'[] @ty @x - payload <- dimArray foldM (arrayPayload @db @'[] @ty @x) arr - return $ encodeArray 1 nulls oid dims payload -instance (ToArray db dims ty x, OidOfNull db ty) - => ToPG db (FixArray x) where - toPG (FixArray arr) = do - oid <- oidOfNull @db @ty - payload <- arrayPayload @db @dims @ty arr - let - dims = arrayDims @db @dims @ty @x - nulls = arrayNulls @db @dims @ty @x - ndims = fromIntegral (length dims) - return $ encodeArray ndims nulls oid dims payload -instance - ( SOP.IsEnumType x - , SOP.HasDatatypeInfo x - , LabelsPG x ~ labels - ) => ToPG db (Enumerated x) where - toPG = - let - gshowConstructor - :: NP SOP.ConstructorInfo xss - -> SOP.SOP SOP.I xss - -> String - gshowConstructor Nil _ = "" - gshowConstructor (constructor :* _) (SOP.SOP (SOP.Z _)) = - SOP.constructorName constructor - gshowConstructor (_ :* constructors) (SOP.SOP (SOP.S xs)) = - gshowConstructor constructors (SOP.SOP xs) - in - pure - . text_strict - . Strict.Text.pack - . gshowConstructor - (SOP.constructorInfo (SOP.datatypeInfo (SOP.Proxy @x))) - . SOP.from - . getEnumerated -instance - ( SOP.SListI fields - , SOP.IsRecord x xs - , SOP.AllZip (ToField db) fields xs - , SOP.All (OidOfField db) fields - , RowPG x ~ fields - ) => ToPG db (Composite x) where - toPG (Composite x) = do - let - compositeSize - = int4_int32 - $ fromIntegral - $ SOP.lengthSList - $ SOP.Proxy @xs - each - :: OidOfField db field - => SOP.K (Maybe Encoding) field - -> ReaderT (SOP.K LibPQ.Connection db) IO Encoding - each (SOP.K field :: SOP.K (Maybe Encoding) field) = do - oid <- getOid <$> oidOfField @db @field - return $ int4_word32 oid <> maybe null4 sized field - fields :: NP (SOP.K (Maybe Encoding)) fields <- hctransverse - (SOP.Proxy @(ToField db)) (toField @db) (SOP.toRecord x) - compositePayload <- hcfoldMapM - (SOP.Proxy @(OidOfField db)) each fields - return $ compositeSize <> compositePayload -instance ToPG db x => ToPG db (Range x) where - toPG r = do - payload <- case r of - Empty -> return mempty - NonEmpty lower upper -> (<>) <$> putBound lower <*> putBound upper - return $ word8 (setFlags r 0) <> payload - where - putBound = \case - Infinite -> return mempty - Closed value -> sized <$> toPG @db value - Open value -> sized <$> toPG @db value - setFlags = \case - Empty -> (`setBit` 0) - NonEmpty lower upper -> - setLowerFlags lower . setUpperFlags upper - setLowerFlags = \case - Infinite -> (`setBit` 3) - Closed _ -> (`setBit` 1) - Open _ -> id - setUpperFlags = \case - Infinite -> (`setBit` 4) - Closed _ -> (`setBit` 2) - Open _ -> id - --- | A `ToParam` constraint gives an encoding of a Haskell `Type` into --- into the binary format of a PostgreSQL `NullType`. --- You should not define instances for `ToParam`, --- just use the provided instances. -class ToParam (db :: SchemasType) (ty :: NullType) (x :: Type) where - toParam :: x -> ReaderT (SOP.K LibPQ.Connection db) IO (Maybe Encoding) -instance (ToPG db x, pg ~ PG x) => ToParam db ('NotNull pg) x where - toParam = fmap Just . toPG @db -instance (ToPG db x, pg ~ PG x) => ToParam db ('Null pg) (Maybe x) where - toParam = maybe (pure Nothing) (fmap Just . toPG @db) - --- | A `ToField` constraint lifts the `ToPG` parser --- to an encoding of a @(Symbol, Type)@ to a @(Symbol, NullityType)@, --- encoding `Null`s to `Maybe`s. You should not define instances for --- `ToField`, just use the provided instances. -class ToField - (db :: SchemasType) - (field :: (Symbol, NullType)) - (x :: (Symbol, Type)) where - toField :: SOP.P x - -> ReaderT (SOP.K LibPQ.Connection db) IO (SOP.K (Maybe Encoding) field) -instance (fld0 ~ fld1, ToParam db ty x) - => ToField db (fld0 ::: ty) (fld1 ::: x) where - toField (SOP.P x) = SOP.K <$> toParam @db @ty x - --- | A `ToArray` constraint gives an encoding of a Haskell `Type` --- into the binary format of a PostgreSQL fixed-length array. --- You should not define instances for --- `ToArray`, just use the provided instances. -class ToArray - (db :: SchemasType) - (dims :: [Nat]) - (ty :: NullType) - (x :: Type) where - arrayPayload :: x -> ReaderT (SOP.K LibPQ.Connection db) IO Encoding - arrayDims :: [Int32] - arrayNulls :: Bool -instance (ToPG db x, pg ~ PG x) - => ToArray db '[] ('NotNull pg) x where - arrayPayload = fmap sized . toPG @db @x - arrayDims = [] - arrayNulls = False -instance (ToPG db x, pg ~ PG x) - => ToArray db '[] ('Null pg) (Maybe x) where - arrayPayload = maybe (pure null4) (fmap sized . toPG @db @x) - arrayDims = [] - arrayNulls = True -instance - ( SOP.IsProductType tuple xs - , Length xs ~ dim - , SOP.All ((~) x) xs - , ToArray db dims ty x - , KnownNat dim ) - => ToArray db (dim ': dims) ty tuple where - arrayPayload - = dimArray foldlNP (arrayPayload @db @dims @ty @x) - . SOP.unZ . SOP.unSOP . SOP.from - arrayDims - = fromIntegral (natVal (SOP.Proxy @dim)) - : arrayDims @db @dims @ty @x - arrayNulls = arrayNulls @db @dims @ty @x -foldlNP - :: (SOP.All ((~) x) xs, Monad m) - => (z -> x -> m z) -> z -> NP SOP.I xs -> m z -foldlNP f z = \case - Nil -> pure z - SOP.I x :* xs -> do - z' <- f z x - foldlNP f z' xs - -{- | -`EncodeParams` describes an encoding of a Haskell `Type` -into a list of parameter `NullType`s. - ->>> conn <- connectdb @'[] "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" ->>> :{ -let - encode :: EncodeParams '[] - '[ 'NotNull 'PGint2, 'NotNull ('PGchar 1), 'NotNull 'PGtext] - (Int16, (Char, String)) - encode = fst .* fst.snd *. snd.snd -in runReaderT (runEncodeParams encode (1,('a',"foo"))) conn -:} -K (Just "\NUL\SOH") :* K (Just "a") :* K (Just "foo") :* Nil - ->>> finish conn --} -newtype EncodeParams - (db :: SchemasType) - (tys :: [NullType]) - (x :: Type) = EncodeParams - { runEncodeParams :: x - -> ReaderT (SOP.K LibPQ.Connection db) IO (NP (SOP.K (Maybe Encoding)) tys) } -instance Contravariant (EncodeParams db tys) where - contramap f (EncodeParams g) = EncodeParams (g . f) - --- | A `GenericParams` constraint to ensure that a Haskell type --- is a product type, --- has a `TuplePG`, --- and all its terms have known Oids, --- and can be encoded to corresponding Postgres types. -class - ( SOP.IsProductType x xs - , params ~ TuplePG x - , SOP.All (OidOfNull db) params - , SOP.AllZip (ToParam db) params xs - ) => GenericParams db params x xs where - {- | Parameter encoding for `SOP.Generic` tuples and records. - - >>> import qualified GHC.Generics as GHC - >>> import qualified Generics.SOP as SOP - >>> data Two = Two Int16 String deriving (GHC.Generic, SOP.Generic) - >>> conn <- connectdb @'[] "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" - >>> :{ - let - encode :: EncodeParams '[] '[ 'NotNull 'PGint2, 'NotNull 'PGtext] Two - encode = genericParams - in runReaderT (runEncodeParams encode (Two 2 "two")) conn - :} - K (Just "\NUL\STX") :* K (Just "two") :* Nil - - >>> :{ - let - encode :: EncodeParams '[] '[ 'NotNull 'PGint2, 'NotNull 'PGtext] (Int16, String) - encode = genericParams - in runReaderT (runEncodeParams encode (2, "two")) conn - :} - K (Just "\NUL\STX") :* K (Just "two") :* Nil - - >>> finish conn - -} - genericParams :: EncodeParams db params x -instance - ( params ~ TuplePG x - , SOP.All (OidOfNull db) params - , SOP.IsProductType x xs - , SOP.AllZip (ToParam db) params xs - ) => GenericParams db params x xs where - genericParams = EncodeParams - $ hctransverse (SOP.Proxy @(ToParam db)) encodeNullParam - . SOP.unZ . SOP.unSOP . SOP.from - where - encodeNullParam - :: forall ty y. ToParam db ty y - => SOP.I y -> ReaderT (SOP.K LibPQ.Connection db) IO (SOP.K (Maybe Encoding) ty) - encodeNullParam = fmap SOP.K . toParam @db @ty . SOP.unI - --- | Encode 0 parameters. -nilParams :: EncodeParams db '[] x -nilParams = EncodeParams $ \ _ -> pure Nil - -{- | Cons a parameter encoding. - ->>> conn <- connectdb @'[] "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" ->>> :{ -let - encode :: EncodeParams '[] - '[ 'Null 'PGint4, 'NotNull 'PGtext] - (Maybe Int32, String) - encode = fst .* snd .* nilParams -in runReaderT (runEncodeParams encode (Nothing, "foo")) conn -:} -K Nothing :* K (Just "foo") :* Nil - ->>> finish conn --} -(.*) - :: forall db x0 ty x tys. (ToParam db ty x0, ty ~ NullPG x0) - => (x -> x0) -- ^ head - -> EncodeParams db tys x -- ^ tail - -> EncodeParams db (ty ': tys) x -f .* EncodeParams params = EncodeParams $ \x -> - (:*) <$> (SOP.K <$> toParam @db @ty (f x)) <*> params x -infixr 5 .* - -{- | End a parameter encoding. - ->>> conn <- connectdb @'[] "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" ->>> :{ -let - encode :: EncodeParams '[] - '[ 'Null 'PGint4, 'NotNull 'PGtext, 'NotNull ('PGchar 1)] - (Maybe Int32, String, Char) - encode = (\(x,_,_) -> x) .* (\(_,y,_) -> y) *. (\(_,_,z) -> z) -in runReaderT (runEncodeParams encode (Nothing, "foo", 'z')) conn -:} -K Nothing :* K (Just "foo") :* K (Just "z") :* Nil - ->>> finish conn --} -(*.) - :: forall db x x0 ty0 x1 ty1 - . ( ToParam db ty0 x0 - , ty0 ~ NullPG x0 - , ToParam db ty1 x1 - , ty1 ~ NullPG x1 - ) - => (x -> x0) -- ^ second to last - -> (x -> x1) -- ^ last - -> EncodeParams db '[ty0, ty1] x -f *. g = f .* g .* nilParams -infixl 8 *. - -{- | Encode 1 parameter. - ->>> conn <- connectdb @'[] "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" ->>> :{ -let - encode :: EncodeParams '[] '[ 'NotNull 'PGint4] Int32 - encode = aParam -in runReaderT (runEncodeParams encode 1776) conn -:} -K (Just "\NUL\NUL\ACK\240") :* Nil - ->>> finish conn --} -aParam - :: forall db x ty. (ToParam db ty x, ty ~ NullPG x) - => EncodeParams db '[ty] x -aParam = EncodeParams $ - fmap (\param -> SOP.K param :* Nil) . toParam @db @(NullPG x) - -{- | Append parameter encodings. - ->>> conn <- connectdb @'[] "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" ->>> :{ -let - encode :: EncodeParams '[] - '[ 'NotNull 'PGint4, 'NotNull 'PGint2] - (Int32, Int16) - encode = contramap fst aParam `appendParams` contramap snd aParam -in runReaderT (runEncodeParams encode (1776, 2)) conn -:} -K (Just "\NUL\NUL\ACK\240") :* K (Just "\NUL\STX") :* Nil - ->>> finish conn --} -appendParams - :: EncodeParams db params0 x -- ^ left - -> EncodeParams db params1 x -- ^ right - -> EncodeParams db (Join params0 params1) x -appendParams encode0 encode1 = EncodeParams $ \x -> also - <$> runEncodeParams encode1 x - <*> runEncodeParams encode0 x - -getOid :: LibPQ.Oid -> Word32 -getOid (LibPQ.Oid (CUInt oid)) = oid - -encodeArray :: Int32 -> Bool -> LibPQ.Oid -> [Int32] -> Encoding -> Encoding -encodeArray ndim nulls oid dimensions payload = mconcat - [ int4_int32 ndim - , if nulls then true4 else false4 - , int4_word32 (getOid oid) - , foldMap (\dimension -> int4_int32 dimension <> true4) dimensions - , payload ] - -dimArray - :: Functor m - => (forall b. (b -> a -> m b) -> b -> c -> m b) - -> (a -> m Encoding) -> c -> m Encoding -dimArray folder elementArray = folder step mempty - where - step builder element = (builder <>) <$> elementArray element - -null4, true4, false4 :: Encoding -null4 = int4_int32 (-1) -true4 = int4_word32 1 -false4 = int4_word32 0 - -sized :: Encoding -> Encoding -sized bs = int4_int32 (fromIntegral (builderLength bs)) <> bs - -hctransverse - :: (SOP.AllZip c ys xs, Applicative m) - => SOP.Proxy c - -> (forall y x. c y x => f x -> m (g y)) - -> NP f xs -> m (NP g ys) -hctransverse c f = \case - Nil -> pure Nil - x :* xs -> (:*) <$> f x <*> hctransverse c f xs - -hcfoldMapM - :: (Monoid r, Applicative m, SOP.All c xs) - => SOP.Proxy c - -> (forall x. c x => f x -> m r) - -> NP f xs -> m r -hcfoldMapM c f = \case - Nil -> pure mempty - x :* xs -> (<>) <$> f x <*> hcfoldMapM c f xs diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Exception.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Session/Exception.hs deleted file mode 100644 index 18f7b57e..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Exception.hs +++ /dev/null @@ -1,100 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Session.Exception -Description: exceptions -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -exceptions --} - -{-# LANGUAGE - OverloadedStrings - , PatternSynonyms -#-} - -module Squeal.PostgreSQL.Session.Exception - ( SquealException (..) - , pattern UniqueViolation - , pattern CheckViolation - , pattern SerializationFailure - , pattern DeadlockDetected - , SQLState (..) - , LibPQ.ExecStatus (..) - , catchSqueal - , handleSqueal - , trySqueal - , throwSqueal - ) where - -import Control.Monad.Catch -import Data.ByteString (ByteString) -import Data.Text (Text) - -import qualified Database.PostgreSQL.LibPQ as LibPQ - --- $setup --- >>> import Squeal.PostgreSQL - --- | the state of LibPQ -data SQLState = SQLState - { sqlExecStatus :: LibPQ.ExecStatus - , sqlStateCode :: ByteString - -- ^ https://www.postgresql.org/docs/current/static/errcodes-appendix.html - , sqlErrorMessage :: ByteString - } deriving (Eq, Show) - --- | `Exception`s that can be thrown by Squeal. -data SquealException - = SQLException SQLState - -- ^ SQL exception state - | ConnectionException Text - -- ^ `Database.PostgreSQL.LibPQ` function connection exception - | DecodingException Text Text - -- ^ decoding exception function and error message - | ColumnsException Text LibPQ.Column - -- ^ unexpected number of columns - | RowsException Text LibPQ.Row LibPQ.Row - -- ^ too few rows, expected at least and actual number of rows - deriving (Eq, Show) -instance Exception SquealException - --- | A pattern for unique violation exceptions. -pattern UniqueViolation :: ByteString -> SquealException -pattern UniqueViolation msg = - SQLException (SQLState LibPQ.FatalError "23505" msg) --- | A pattern for check constraint violation exceptions. -pattern CheckViolation :: ByteString -> SquealException -pattern CheckViolation msg = - SQLException (SQLState LibPQ.FatalError "23514" msg) --- | A pattern for serialization failure exceptions. -pattern SerializationFailure :: ByteString -> SquealException -pattern SerializationFailure msg = - SQLException (SQLState LibPQ.FatalError "40001" msg) --- | A pattern for deadlock detection exceptions. -pattern DeadlockDetected :: ByteString -> SquealException -pattern DeadlockDetected msg = - SQLException (SQLState LibPQ.FatalError "40P01" msg) - --- | Catch `SquealException`s. -catchSqueal - :: MonadCatch m - => m a - -> (SquealException -> m a) -- ^ handler - -> m a -catchSqueal = catch - --- | Handle `SquealException`s. -handleSqueal - :: MonadCatch m - => (SquealException -> m a) -- ^ handler - -> m a -> m a -handleSqueal = handle - --- | `Either` return a `SquealException` or a result. -trySqueal :: MonadCatch m => m a -> m (Either SquealException a) -trySqueal = try - --- | Throw `SquealException`s. -throwSqueal :: MonadThrow m => SquealException -> m a -throwSqueal = throwM diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Indexed.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Session/Indexed.hs deleted file mode 100644 index ec7eb66d..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Indexed.hs +++ /dev/null @@ -1,126 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Session.Indexed -Description: indexed session monad -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -`Squeal.PostgreSQL.Indexed` provides an indexed monad transformer -class and a class extending it to run `Definition`s. --} - -{-# LANGUAGE - DataKinds - , DefaultSignatures - , FlexibleContexts - , FlexibleInstances - , FunctionalDependencies - , PolyKinds - , MultiParamTypeClasses - , QuantifiedConstraints - , RankNTypes - , TypeApplications - , TypeFamilies - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Session.Indexed - ( IndexedMonadTrans (..) - , Indexed (..) - , IndexedMonadTransPQ (..) - , indexedDefine - ) where - -import Control.Category (Category (..)) -import Control.Monad -import Control.Monad.IO.Class -import Control.Monad.Trans -import Data.Function ((&)) -import Prelude hiding (id, (.)) - -import Squeal.PostgreSQL.Definition - -{- | An [Atkey indexed monad] -(https://bentnib.org/paramnotions-jfp.pdf) -is a `Functor` [enriched category] -(https://ncatlab.org/nlab/show/enriched+category). -An indexed monad transformer transforms a `Monad` into an indexed monad, -and is a monad transformer when its source and target are the same, -enabling use of standard @do@ notation for endo-index operations. --} -class - ( forall i j m. Monad m => Functor (t i j m) - , forall i m. Monad m => Monad (t i i m) - , forall i. MonadTrans (t i i) - ) => IndexedMonadTrans t where - - {-# MINIMAL pqJoin | pqBind #-} - - -- | indexed analog of `<*>` - pqAp - :: Monad m - => t i j m (x -> y) - -> t j k m x - -> t i k m y - pqAp tf tx = pqBind (<$> tx) tf - - -- | indexed analog of `join` - pqJoin - :: Monad m - => t i j m (t j k m y) - -> t i k m y - pqJoin t = t & pqBind id - - -- | indexed analog of `=<<` - pqBind - :: Monad m - => (x -> t j k m y) - -> t i j m x - -> t i k m y - pqBind f t = pqJoin (f <$> t) - - -- | indexed analog of flipped `>>` - pqThen - :: Monad m - => t j k m y - -> t i j m x - -> t i k m y - pqThen pq2 pq1 = pq1 & pqBind (\ _ -> pq2) - - -- | indexed analog of `<=<` - pqAndThen - :: Monad m - => (y -> t j k m z) - -> (x -> t i j m y) - -> x -> t i k m z - pqAndThen g f x = pqBind g (f x) - -{- | `Indexed` reshuffles the type parameters of an `IndexedMonadTrans`, -exposing its `Category` instance.-} -newtype Indexed t m r i j = Indexed {runIndexed :: t i j m r} -instance - ( IndexedMonadTrans t - , Monad m - , Monoid r - ) => Category (Indexed t m r) where - id = Indexed (pure mempty) - Indexed g . Indexed f = Indexed $ pqAp (fmap (<>) f) g - -{- | `IndexedMonadTransPQ` is a class for indexed monad transformers -that support running `Definition`s using `define` which acts functorially in effect. - -* @define id = return ()@ -* @define (statement1 >>> statement2) = define statement1 & pqThen (define statement2)@ --} -class IndexedMonadTrans pq => IndexedMonadTransPQ pq where - define :: MonadIO io => Definition db0 db1 -> pq db0 db1 io () - -{- | Run a pure SQL `Definition` functorially in effect - -* @indexedDefine id = id@ -* @indexedDefine (def1 >>> def2) = indexedDefine def1 >>> indexedDefine def2@ --} -indexedDefine - :: (IndexedMonadTransPQ pq, MonadIO io) - => Definition db0 db1 -> Indexed pq io () db0 db1 -indexedDefine = Indexed . define diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Migration.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Session/Migration.hs deleted file mode 100644 index 050ea3db..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Migration.hs +++ /dev/null @@ -1,456 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Session.Migration -Description: migrations -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -This module defines a `Migration` type to safely -change the schema of your database over time. Let's see an example! - -First turn on some extensions. - ->>> :set -XDataKinds -XOverloadedLabels ->>> :set -XOverloadedStrings -XFlexibleContexts -XTypeOperators - -Next, let's define our `TableType`s. - ->>> :{ -type UsersTable = - '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=> - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "name" ::: 'NoDef :=> 'NotNull 'PGtext - ] -:} - ->>> :{ -type EmailsTable = - '[ "pk_emails" ::: 'PrimaryKey '["id"] - , "fk_user_id" ::: 'ForeignKey '["user_id"] "public" "users" '["id"] - ] :=> - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4 - , "email" ::: 'NoDef :=> 'Null 'PGtext - ] -:} - -Now we can define some `Migration`s to make our tables. - -`Migration`s are parameterized giving the option of a - -* pure one-way `Migration` `Definition` -* impure one-way `Migration` @(@`Indexed` `PQ` `IO`@)@ -* pure reversible `Migration` @(@`IsoQ` `Definition`@)@ -* impure reversible `Migration` @(@`IsoQ` @(@`Indexed` `PQ` `IO`@)@@)@ - -For this example, we'll use pure reversible `Migration`s. - ->>> :{ -let - makeUsers :: Migration (IsoQ Definition) - '["public" ::: '[]] - '["public" ::: '["users" ::: 'Table UsersTable]] - makeUsers = Migration "make users table" IsoQ - { up = createTable #users - ( serial `as` #id :* - notNullable text `as` #name ) - ( primaryKey #id `as` #pk_users ) - , down = dropTable #users - } -:} - ->>> :{ -let - makeEmails :: Migration (IsoQ Definition) - '["public" ::: '["users" ::: 'Table UsersTable]] - '["public" ::: '["users" ::: 'Table UsersTable, "emails" ::: 'Table EmailsTable]] - makeEmails = Migration "make emails table" IsoQ - { up = createTable #emails - ( serial `as` #id :* - notNullable int `as` #user_id :* - nullable text `as` #email ) - ( primaryKey #id `as` #pk_emails :* - foreignKey #user_id #users #id - (OnDelete Cascade) (OnUpdate Cascade) `as` #fk_user_id ) - , down = dropTable #emails - } -:} - -Now that we have a couple migrations we can chain them together into a `Path`. - ->>> let migrations = makeUsers :>> makeEmails :>> Done - -Now run the migrations. - ->>> import Control.Monad.IO.Class ->>> :{ -withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $ - manipulate_ (UnsafeManipulation "SET client_min_messages TO WARNING;") - -- suppress notices - & pqThen (liftIO (putStrLn "Migrate")) - & pqThen (migrateUp migrations) - & pqThen (liftIO (putStrLn "Rollback")) - & pqThen (migrateDown migrations) -:} -Migrate -Rollback - -We can also create a simple executable using `mainMigrateIso`. - ->>> let main = mainMigrateIso "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" migrations - ->>> withArgs [] main -Invalid command: "". Use: -migrate to run all available migrations -rollback to rollback all available migrations -status to display migrations run and migrations left to run - ->>> withArgs ["status"] main -Migrations already run: - None -Migrations left to run: - - make users table - - make emails table - ->>> withArgs ["migrate"] main -Migrations already run: - - make users table - - make emails table -Migrations left to run: - None - ->>> withArgs ["rollback"] main -Migrations already run: - None -Migrations left to run: - - make users table - - make emails table - -In addition to enabling `Migration`s using pure SQL `Definition`s for -the `up` and `down` migrations, you can also perform impure `IO` actions -by using a `Migration`s over the `Indexed` `PQ` `IO` category. --} - -{-# LANGUAGE - DataKinds - , DeriveGeneric - , FlexibleContexts - , FlexibleInstances - , FunctionalDependencies - , GADTs - , LambdaCase - , MultiParamTypeClasses - , OverloadedLabels - , OverloadedStrings - , PolyKinds - , QuantifiedConstraints - , RankNTypes - , TypeApplications - , TypeOperators -#-} - -module Squeal.PostgreSQL.Session.Migration - ( -- * Migration - Migration (..) - , Migratory (..) - , migrate - , migrateUp - , migrateDown - , MigrationsTable - -- * Executable - , mainMigrate - , mainMigrateIso - -- * Re-export - , IsoQ (..) - ) where - -import Control.Category -import Control.Category.Free -import Control.Monad -import Control.Monad.IO.Class -import Data.ByteString (ByteString) -import Data.Foldable (traverse_) -import Data.Function ((&)) -import Data.List ((\\)) -import Data.Quiver -import Data.Quiver.Functor -import Data.Text (Text) -import Data.Time (UTCTime) -import Prelude hiding ((.), id) -import System.Environment - -import qualified Data.Text.IO as Text (putStrLn) -import qualified Generics.SOP as SOP -import qualified GHC.Generics as GHC - -import Squeal.PostgreSQL.Definition -import Squeal.PostgreSQL.Definition.Constraint -import Squeal.PostgreSQL.Definition.Table -import Squeal.PostgreSQL.Expression.Comparison -import Squeal.PostgreSQL.Expression.Default -import Squeal.PostgreSQL.Expression.Parameter -import Squeal.PostgreSQL.Expression.Time -import Squeal.PostgreSQL.Expression.Type -import Squeal.PostgreSQL.Manipulation -import Squeal.PostgreSQL.Manipulation.Delete -import Squeal.PostgreSQL.Manipulation.Insert -import Squeal.PostgreSQL.Session -import Squeal.PostgreSQL.Session.Decode -import Squeal.PostgreSQL.Session.Encode -import Squeal.PostgreSQL.Session.Indexed -import Squeal.PostgreSQL.Session.Monad -import Squeal.PostgreSQL.Session.Result -import Squeal.PostgreSQL.Session.Statement -import Squeal.PostgreSQL.Session.Transaction.Unsafe -import Squeal.PostgreSQL.Query.From -import Squeal.PostgreSQL.Query.Select -import Squeal.PostgreSQL.Query.Table -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Type.Schema - --- | A `Migration` consists of a name and a migration definition. -data Migration def db0 db1 = Migration - { migrationName :: Text -- ^ The name of a `Migration`. - -- Each `migrationName` should be unique. - , migrationDef :: def db0 db1 -- ^ The migration of a `Migration`. - } deriving (GHC.Generic) -instance QFunctor Migration where - qmap f (Migration n i) = Migration n (f i) - -{- | -A `Migratory` `Category` can run or -possibly rewind a `Path` of `Migration`s. --} -class (Category def, Category run) => Migratory def run | def -> run where - {- | Run a `Path` of `Migration`s.-} - runMigrations :: Path (Migration def) db0 db1 -> run db0 db1 --- | impure migrations -instance Migratory (Indexed PQ IO ()) (Indexed PQ IO ()) where - runMigrations path = Indexed . unsafePQ . transactionally_ $ do - define createMigrations - qtoMonoid upMigration path - where - upMigration step = do - executed <- do - result <- executeParams selectMigration (migrationName step) - ntuples (result :: Result UTCTime) - unless (executed == 1) $ do - _ <- unsafePQ . runIndexed $ migrationDef step - executeParams_ insertMigration (migrationName step) --- | pure migrations -instance Migratory Definition (Indexed PQ IO ()) where - runMigrations = runMigrations . qmap (qmap ixDefine) --- | impure rewinds -instance Migratory (OpQ (Indexed PQ IO ())) (OpQ (Indexed PQ IO ())) where - runMigrations path = OpQ . Indexed . unsafePQ . transactionally_ $ do - define createMigrations - qtoMonoid @FoldPath downMigration (reversePath path) - where - downMigration (OpQ step) = do - executed <- do - result <- executeParams selectMigration (migrationName step) - ntuples (result :: Result UTCTime) - unless (executed == 0) $ do - _ <- unsafePQ . runIndexed . getOpQ $ migrationDef step - executeParams_ deleteMigration (migrationName step) --- | pure rewinds -instance Migratory (OpQ Definition) (OpQ (Indexed PQ IO ())) where - runMigrations = runMigrations . qmap (qmap (qmap ixDefine)) --- | impure rewindable migrations -instance Migratory - (IsoQ (Indexed PQ IO ())) - (IsoQ (Indexed PQ IO ())) where - runMigrations path = IsoQ - (runMigrations (qmap (qmap up) path)) - (getOpQ (runMigrations (qmap (qmap (OpQ . down)) path))) --- | pure rewindable migrations -instance Migratory (IsoQ Definition) (IsoQ (Indexed PQ IO ())) where - runMigrations = runMigrations . qmap (qmap (qmap ixDefine)) - -unsafePQ :: (Functor m) => PQ db0 db1 m x -> PQ db0' db1' m x -unsafePQ (PQ pq) = PQ $ fmap (SOP.K . SOP.unK) . pq . SOP.K . SOP.unK - --- | Run migrations. -migrate - :: Migratory def (Indexed PQ IO ()) - => Path (Migration def) db0 db1 - -> PQ db0 db1 IO () -migrate = runIndexed . runMigrations - --- | Run rewindable migrations. -migrateUp - :: Migratory def (IsoQ (Indexed PQ IO ())) - => Path (Migration def) db0 db1 - -> PQ db0 db1 IO () -migrateUp = runIndexed . up . runMigrations - --- | Rewind migrations. -migrateDown - :: Migratory def (IsoQ (Indexed PQ IO ())) - => Path (Migration def) db0 db1 - -> PQ db1 db0 IO () -migrateDown = runIndexed . down . runMigrations - -ixDefine :: Definition db0 db1 -> Indexed PQ IO () db0 db1 -ixDefine = indexedDefine - --- | The `TableType` for a Squeal migration. -type MigrationsTable = - '[ "migrations_unique_name" ::: 'Unique '["name"]] :=> - '[ "name" ::: 'NoDef :=> 'NotNull 'PGtext - , "executed_at" ::: 'Def :=> 'NotNull 'PGtimestamptz - ] - -data MigrationRow = - MigrationRow { name :: Text - , executed_at :: UTCTime } - deriving (GHC.Generic, Show) - -instance SOP.Generic MigrationRow -instance SOP.HasDatatypeInfo MigrationRow - -type MigrationsSchema = '["schema_migrations" ::: 'Table MigrationsTable] -type MigrationsSchemas = Public MigrationsSchema - --- | Creates a `MigrationsTable` if it does not already exist. -createMigrations :: Definition MigrationsSchemas MigrationsSchemas -createMigrations = - createTableIfNotExists #schema_migrations - ( (text & notNullable) `as` #name :* - (timestampWithTimeZone & notNullable & default_ currentTimestamp) - `as` #executed_at ) - ( unique #name `as` #migrations_unique_name ) - --- | Inserts a `Migration` into the `MigrationsTable`, returning --- the time at which it was inserted. -insertMigration :: Statement MigrationsSchemas Text () -insertMigration = Manipulation aParam genericRow $ - insertInto_ #schema_migrations $ - Values_ (Set (param @1) `as` #name :* Default `as` #executed_at) - --- | Deletes a `Migration` from the `MigrationsTable`, returning --- the time at which it was inserted. -deleteMigration :: Statement MigrationsSchemas Text () -deleteMigration = Manipulation aParam genericRow $ - deleteFrom_ #schema_migrations (#name .== param @1) - --- | Selects a `Migration` from the `MigrationsTable`, returning --- the time at which it was inserted. -selectMigration :: Statement MigrationsSchemas Text UTCTime -selectMigration = Query aParam #executed_at $ - select_ #executed_at - $ from (table (#schema_migrations)) - & where_ (#name .== param @1) - -selectMigrations :: Statement MigrationsSchemas () MigrationRow -selectMigrations = query $ select Star (from (table #schema_migrations)) - -{- | `mainMigrate` creates a simple executable -from a connection string and a `Path` of `Migration`s. -} -mainMigrate - :: Migratory p (Indexed PQ IO ()) - => ByteString - -- ^ connection string - -> Path (Migration p) db0 db1 - -- ^ migrations - -> IO () -mainMigrate connectTo migrations = do - command <- getArgs - performCommand command - - where - - performCommand :: [String] -> IO () - performCommand = \case - ["status"] -> withConnection connectTo $ - suppressNotices >> migrateStatus - ["migrate"] -> withConnection connectTo $ - suppressNotices - & pqThen (runIndexed (runMigrations migrations)) - & pqThen migrateStatus - args -> displayUsage args - - migrateStatus :: PQ schema schema IO () - migrateStatus = unsafePQ $ do - runNames <- getRunMigrationNames - let names = qtoList migrationName migrations - unrunNames = names \\ runNames - liftIO $ displayRunned runNames >> displayUnrunned unrunNames - - suppressNotices :: PQ schema schema IO () - suppressNotices = manipulate_ $ - UnsafeManipulation "SET client_min_messages TO WARNING;" - - displayUsage :: [String] -> IO () - displayUsage args = do - putStrLn $ "Invalid command: \"" <> unwords args <> "\". Use:" - putStrLn "migrate to run all available migrations" - putStrLn "rollback to rollback all available migrations" - -{- | `mainMigrateIso` creates a simple executable -from a connection string and a `Path` of `Migration` `IsoQ`s. -} -mainMigrateIso - :: Migratory (IsoQ def) (IsoQ (Indexed PQ IO ())) - => ByteString - -- ^ connection string - -> Path (Migration (IsoQ def)) db0 db1 - -- ^ migrations - -> IO () -mainMigrateIso connectTo migrations = performCommand =<< getArgs - - where - - performCommand :: [String] -> IO () - performCommand = \case - ["status"] -> withConnection connectTo $ - suppressNotices >> migrateStatus - ["migrate"] -> withConnection connectTo $ - suppressNotices - & pqThen (migrateUp migrations) - & pqThen migrateStatus - ["rollback"] -> withConnection connectTo $ - suppressNotices - & pqThen (migrateDown migrations) - & pqThen migrateStatus - args -> displayUsage args - - migrateStatus :: PQ schema schema IO () - migrateStatus = unsafePQ $ do - runNames <- getRunMigrationNames - let names = qtoList migrationName migrations - unrunNames = names \\ runNames - liftIO $ displayRunned runNames >> displayUnrunned unrunNames - - suppressNotices :: PQ schema schema IO () - suppressNotices = manipulate_ $ - UnsafeManipulation "SET client_min_messages TO WARNING;" - - displayUsage :: [String] -> IO () - displayUsage args = do - putStrLn $ "Invalid command: \"" <> unwords args <> "\". Use:" - putStrLn "migrate to run all available migrations" - putStrLn "rollback to rollback all available migrations" - putStrLn "status to display migrations run and migrations left to run" - -getRunMigrationNames :: PQ db0 db0 IO [Text] -getRunMigrationNames = - fmap name <$> - (unsafePQ (define createMigrations - & pqThen (execute selectMigrations)) >>= getRows) - -displayListOfNames :: [Text] -> IO () -displayListOfNames [] = Text.putStrLn " None" -displayListOfNames xs = - let singleName n = Text.putStrLn $ " - " <> n - in traverse_ singleName xs - -displayUnrunned :: [Text] -> IO () -displayUnrunned unrunned = - Text.putStrLn "Migrations left to run:" - >> displayListOfNames unrunned - -displayRunned :: [Text] -> IO () -displayRunned runned = - Text.putStrLn "Migrations already run:" - >> displayListOfNames runned diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Monad.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Session/Monad.hs deleted file mode 100644 index e514abb3..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Monad.hs +++ /dev/null @@ -1,654 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Session.Monad -Description: session monad -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -Run `Squeal.PostgreSQL.Session.Statement`s in the mtl-style -typeclass `MonadPQ`. --} -{-# LANGUAGE - DataKinds - , DefaultSignatures - , FlexibleContexts - , FlexibleInstances - , FunctionalDependencies - , GADTs - , PolyKinds - , MultiParamTypeClasses - , QuantifiedConstraints - , RankNTypes - , TypeApplications - , TypeFamilies - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Session.Monad where - -import Control.Category (Category (..)) -import Control.Monad -import Control.Monad.Morph -import Prelude hiding (id, (.)) - -import Squeal.PostgreSQL.Manipulation -import Squeal.PostgreSQL.Session.Decode -import Squeal.PostgreSQL.Session.Encode -import Squeal.PostgreSQL.Session.Result -import Squeal.PostgreSQL.Session.Statement -import Squeal.PostgreSQL.Query - --- For `MonadPQ` transformer instances -import Control.Monad.Trans.Cont -import Control.Monad.Trans.Identity -import Control.Monad.Trans.Maybe -import Control.Monad.Trans.Reader -import qualified Control.Monad.Trans.State.Lazy as Lazy -import qualified Control.Monad.Trans.State.Strict as Strict -import qualified Control.Monad.Trans.Writer.Lazy as Lazy -import qualified Control.Monad.Trans.Writer.Strict as Strict -import qualified Control.Monad.Trans.RWS.Lazy as Lazy -import qualified Control.Monad.Trans.RWS.Strict as Strict - --- $setup --- >>> import Squeal.PostgreSQL - -{- | `MonadPQ` is an @mtl@ style constraint, similar to -`Control.Monad.State.Class.MonadState`, for using `Database.PostgreSQL.LibPQ` -to run `Statement`s. --} -class Monad pq => MonadPQ db pq | pq -> db where - - {- | - `executeParams` runs a `Statement` which takes out-of-line - `Squeal.PostgreSQL.Expression.Parameter.parameter`s. - - >>> import Data.Int (Int32, Int64) - >>> import Data.Monoid (Sum(Sum)) - >>> :{ - let - sumOf :: Statement db (Int32, Int32) (Sum Int32) - sumOf = query $ values_ $ - ( param @1 @('NotNull 'PGint4) + - param @2 @('NotNull 'PGint4) - ) `as` #getSum - in - withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $ do - result <- executeParams sumOf (2,2) - firstRow result - :} - Just (Sum {getSum = 4}) - -} - executeParams - :: Statement db x y - -- ^ query or manipulation - -> x - -- ^ parameters - -> pq (Result y) - default executeParams - :: (MonadTrans t, MonadPQ db m, pq ~ t m) - => Statement db x y - -- ^ query or manipulation - -> x - -- ^ parameters - -> pq (Result y) - executeParams statement params = lift $ executeParams statement params - - {- | - `executeParams_` runs a returning-free `Statement`. - - >>> type Column = 'NoDef :=> 'NotNull 'PGint4 - >>> type Columns = '["col1" ::: Column, "col2" ::: Column] - >>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] - >>> type DB = Public Schema - >>> import Data.Int(Int32) - >>> :{ - let - insertion :: Statement DB (Int32, Int32) () - insertion = manipulation $ insertInto_ #tab $ Values_ $ - Set (param @1 @('NotNull 'PGint4)) `as` #col1 :* - Set (param @2 @('NotNull 'PGint4)) `as` #col2 - setup :: Definition (Public '[]) DB - setup = createTable #tab - ( notNullable int4 `as` #col1 :* - notNullable int4 `as` #col2 - ) Nil - teardown :: Definition DB (Public '[]) - teardown = dropTable #tab - in - withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $ - define setup - & pqThen (executeParams_ insertion (2,2)) - & pqThen (define teardown) - :} - -} - executeParams_ - :: Statement db x () - -- ^ query or manipulation - -> x - -- ^ parameters - -> pq () - executeParams_ statement params = void $ executeParams statement params - - {- | `execute` runs a parameter-free `Statement`. - - >>> import Data.Int(Int32) - >>> :{ - let - two :: Expr ('NotNull 'PGint4) - two = 2 - twoPlusTwo :: Statement db () (Only Int32) - twoPlusTwo = query $ values_ $ (two + two) `as` #fromOnly - in - withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $ do - result <- execute twoPlusTwo - firstRow result - :} - Just (Only {fromOnly = 4}) - -} - execute - :: Statement db () y - -- ^ query or manipulation - -> pq (Result y) - execute statement = executeParams statement () - - {- | `execute_` runs a parameter-free, returning-free `Statement`. - - >>> :{ - let - silence :: Statement db () () - silence = manipulation $ - UnsafeManipulation "Set client_min_messages TO WARNING" - in - withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $ execute_ silence - :} - -} - execute_ :: Statement db () () -> pq () - execute_ = void . execute - - {- | - `executePrepared` runs a `Statement` on a `Traversable` - container by first preparing the statement, then running the prepared - statement on each element. - - >>> import Data.Int (Int32, Int64) - >>> import Data.Monoid (Sum(Sum)) - >>> :{ - let - sumOf :: Statement db (Int32, Int32) (Sum Int32) - sumOf = query $ values_ $ - ( param @1 @('NotNull 'PGint4) + - param @2 @('NotNull 'PGint4) - ) `as` #getSum - in - withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $ do - results <- executePrepared sumOf [(2,2),(3,3),(4,4)] - traverse firstRow results - :} - [Just (Sum {getSum = 4}),Just (Sum {getSum = 6}),Just (Sum {getSum = 8})] - -} - executePrepared - :: Traversable list - => Statement db x y - -- ^ query or manipulation - -> list x - -- ^ list of parameters - -> pq (list (Result y)) - default executePrepared - :: (MonadTrans t, MonadPQ db m, pq ~ t m) - => Traversable list - => Statement db x y - -- ^ query or manipulation - -> list x - -- ^ list of parameters - -> pq (list (Result y)) - executePrepared statement x = lift $ executePrepared statement x - - {- | - `executePrepared_` runs a returning-free `Statement` on a `Foldable` - container by first preparing the statement, then running the prepared - statement on each element. - - >>> type Column = 'NoDef :=> 'NotNull 'PGint4 - >>> type Columns = '["col1" ::: Column, "col2" ::: Column] - >>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] - >>> type DB = Public Schema - >>> import Data.Int(Int32) - >>> :{ - let - insertion :: Statement DB (Int32, Int32) () - insertion = manipulation $ insertInto_ #tab $ Values_ $ - Set (param @1 @('NotNull 'PGint4)) `as` #col1 :* - Set (param @2 @('NotNull 'PGint4)) `as` #col2 - setup :: Definition (Public '[]) DB - setup = createTable #tab - ( notNullable int4 `as` #col1 :* - notNullable int4 `as` #col2 - ) Nil - teardown :: Definition DB (Public '[]) - teardown = dropTable #tab - in - withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $ - define setup - & pqThen (executePrepared_ insertion [(2,2),(3,3),(4,4)]) - & pqThen (define teardown) - :} - -} - executePrepared_ - :: Foldable list - => Statement db x () - -- ^ query or manipulation - -> list x - -- ^ list of parameters - -> pq () - default executePrepared_ - :: (MonadTrans t, MonadPQ db m, pq ~ t m) - => Foldable list - => Statement db x () - -- ^ query or manipulation - -> list x - -- ^ list of parameters - -> pq () - executePrepared_ statement x = lift $ executePrepared_ statement x - -{- | -`manipulateParams` runs a `Squeal.PostgreSQL.Manipulation.Manipulation`. - ->>> type Column = 'NoDef :=> 'NotNull 'PGint4 ->>> type Columns = '["col1" ::: Column, "col2" ::: Column] ->>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] ->>> type DB = Public Schema ->>> import Control.Monad.IO.Class ->>> import Data.Int(Int32) ->>> :{ -let - insertAdd :: Manipulation_ DB (Int32, Int32) (Only Int32) - insertAdd = insertInto #tab - ( Values_ $ - Set (param @1 @('NotNull 'PGint4)) `as` #col1 :* - Set (param @2 @('NotNull 'PGint4)) `as` #col2 - ) OnConflictDoRaise - ( Returning_ ((#col1 + #col2) `as` #fromOnly) ) - setup :: Definition (Public '[]) DB - setup = createTable #tab - ( notNullable int4 `as` #col1 :* - notNullable int4 `as` #col2 - ) Nil - teardown :: Definition DB (Public '[]) - teardown = dropTable #tab -in - withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $ - define setup - & pqThen - ( do - result <- manipulateParams insertAdd (2::Int32,2::Int32) - Just (Only answer) <- firstRow result - liftIO $ print (answer :: Int32) - ) - & pqThen (define teardown) -:} -4 --} -manipulateParams :: - ( MonadPQ db pq - , GenericParams db params x xs - , GenericRow row y ys - ) => Manipulation '[] db params row - -- ^ `Squeal.PostgreSQL.Manipulation.Insert.insertInto`, - -- `Squeal.PostgreSQL.Manipulation.Update.update`, - -- or `Squeal.PostgreSQL.Manipulation.Delete.deleteFrom`, and friends - -> x -> pq (Result y) -manipulateParams = executeParams . manipulation - -{- | -`manipulateParams_` runs a `Squeal.PostgreSQL.Manipulation.Manipulation`, -for a returning-free statement. - ->>> type Column = 'NoDef :=> 'NotNull 'PGint4 ->>> type Columns = '["col1" ::: Column, "col2" ::: Column] ->>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] ->>> type DB = Public Schema ->>> import Data.Int(Int32) ->>> :{ -let - insertion :: Manipulation_ DB (Int32, Int32) () - insertion = insertInto_ #tab $ Values_ $ - Set (param @1 @('NotNull 'PGint4)) `as` #col1 :* - Set (param @2 @('NotNull 'PGint4)) `as` #col2 - setup :: Definition (Public '[]) DB - setup = createTable #tab - ( notNullable int4 `as` #col1 :* - notNullable int4 `as` #col2 - ) Nil - teardown :: Definition DB (Public '[]) - teardown = dropTable #tab -in - withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $ - define setup - & pqThen (manipulateParams_ insertion (2::Int32,2::Int32)) - & pqThen (define teardown) -:} --} -manipulateParams_ :: - ( MonadPQ db pq - , GenericParams db params x xs - ) => Manipulation '[] db params '[] - -- ^ `Squeal.PostgreSQL.Manipulation.Insert.insertInto_`, - -- `Squeal.PostgreSQL.Manipulation.Update.update_`, - -- or `Squeal.PostgreSQL.Manipulation.Delete.deleteFrom_`, and friends - -> x -> pq () -manipulateParams_ = executeParams_ . manipulation - -{- | -`manipulate` runs a `Squeal.PostgreSQL.Manipulation.Manipulation`, -for a parameter-free statement. - ->>> type Column = 'NoDef :=> 'NotNull 'PGint4 ->>> type Columns = '["col1" ::: Column, "col2" ::: Column] ->>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] ->>> type DB = Public Schema ->>> import Control.Monad.IO.Class ->>> import Data.Int(Int32) ->>> :{ -let - insertTwoPlusTwo :: Manipulation_ DB () (Only Int32) - insertTwoPlusTwo = insertInto #tab - (Values_ $ Set 2 `as` #col1 :* Set 2 `as` #col2) - OnConflictDoRaise - (Returning_ ((#col1 + #col2) `as` #fromOnly)) - setup :: Definition (Public '[]) DB - setup = createTable #tab - ( notNullable int4 `as` #col1 :* - notNullable int4 `as` #col2 - ) Nil - teardown :: Definition DB (Public '[]) - teardown = dropTable #tab -in - withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $ - define setup - & pqThen - ( do - result <- manipulate insertTwoPlusTwo - Just (Only answer) <- firstRow result - liftIO $ print (answer :: Int32) - ) - & pqThen (define teardown) -:} -4 --} -manipulate - :: (MonadPQ db pq, GenericRow row y ys) - => Manipulation '[] db '[] row - -> pq (Result y) -manipulate = execute . manipulation - -{- | -`manipulate_` runs a `Squeal.PostgreSQL.Manipulation.Manipulation`, -for a returning-free, parameter-free statement. - ->>> :{ -let - silence :: Manipulation_ db () () - silence = UnsafeManipulation "Set client_min_messages TO WARNING" -in - withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $ manipulate_ silence -:} --} -manipulate_ - :: MonadPQ db pq - => Manipulation '[] db '[] '[] - -> pq () -manipulate_ = execute_ . manipulation - -{- | -`runQueryParams` runs a `Squeal.PostgreSQL.Query.Query`. - ->>> import Data.Int (Int32, Int64) ->>> import Control.Monad.IO.Class ->>> import Data.Monoid (Sum(Sum)) ->>> :{ -let - sumOf :: Query_ db (Int32, Int32) (Sum Int32) - sumOf = values_ $ - ( param @1 @('NotNull 'PGint4) + - param @2 @('NotNull 'PGint4) - ) `as` #getSum -in - withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $ do - result <- runQueryParams sumOf (2::Int32,2::Int32) - Just (Sum four) <- firstRow result - liftIO $ print (four :: Int32) -:} -4 --} -runQueryParams :: - ( MonadPQ db pq - , GenericParams db params x xs - , GenericRow row y ys - ) => Query '[] '[] db params row - -- ^ `Squeal.PostgreSQL.Query.Select.select` and friends - -> x -> pq (Result y) -runQueryParams = executeParams . query - -{- | -`runQuery` runs a `Squeal.PostgreSQL.Query.Query`, -for a parameter-free statement. - ->>> import Data.Int (Int32, Int64) ->>> import Control.Monad.IO.Class ->>> import Data.Monoid (Sum(Sum)) ->>> :{ -let - twoPlusTwo :: Query_ db () (Sum Int32) - twoPlusTwo = values_ $ (2 + 2) `as` #getSum -in - withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $ do - result <- runQuery twoPlusTwo - Just (Sum four) <- firstRow result - liftIO $ print (four :: Int32) -:} -4 --} -runQuery - :: (MonadPQ db pq, GenericRow row y ys) - => Query '[] '[] db '[] row - -- ^ `Squeal.PostgreSQL.Query.Select.select` and friends - -> pq (Result y) -runQuery = execute . query - -{- | -`traversePrepared` runs a `Squeal.PostgreSQL.Manipulation.Manipulation` -on a `Traversable` container by first preparing the statement, -then running the prepared statement on each element. - ->>> type Column = 'NoDef :=> 'NotNull 'PGint4 ->>> type Columns = '["col1" ::: Column, "col2" ::: Column] ->>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] ->>> type DB = Public Schema ->>> import Control.Monad.IO.Class ->>> import Data.Int(Int32) ->>> :{ -let - insertAdd :: Manipulation_ DB (Int32, Int32) (Only Int32) - insertAdd = insertInto #tab - ( Values_ $ - Set (param @1 @('NotNull 'PGint4)) `as` #col1 :* - Set (param @2 @('NotNull 'PGint4)) `as` #col2 - ) OnConflictDoRaise - ( Returning_ ((#col1 + #col2) `as` #fromOnly) ) - setup :: Definition (Public '[]) DB - setup = createTable #tab - ( notNullable int4 `as` #col1 :* - notNullable int4 `as` #col2 - ) Nil - teardown :: Definition DB (Public '[]) - teardown = dropTable #tab -in - withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $ - define setup - & pqThen - ( do - results <- traversePrepared insertAdd [(2::Int32,2::Int32),(3,3),(4,4)] - answers <- traverse firstRow results - liftIO $ print [answer :: Int32 | Just (Only answer) <- answers] - ) - & pqThen (define teardown) -:} -[4,6,8] --} -traversePrepared - :: ( MonadPQ db pq - , GenericParams db params x xs - , GenericRow row y ys - , Traversable list ) - => Manipulation '[] db params row - -- ^ `Squeal.PostgreSQL.Manipulation.Insert.insertInto`, - -- `Squeal.PostgreSQL.Manipulation.Update.update`, - -- or `Squeal.PostgreSQL.Manipulation.Delete.deleteFrom`, and friends - -> list x -> pq (list (Result y)) -traversePrepared = executePrepared . manipulation - -{- | -`forPrepared` is a flipped `traversePrepared` - ->>> type Column = 'NoDef :=> 'NotNull 'PGint4 ->>> type Columns = '["col1" ::: Column, "col2" ::: Column] ->>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] ->>> type DB = Public Schema ->>> import Control.Monad.IO.Class ->>> import Data.Int(Int32) ->>> :{ -let - insertAdd :: Manipulation_ DB (Int32, Int32) (Only Int32) - insertAdd = insertInto #tab - ( Values_ $ - Set (param @1 @('NotNull 'PGint4)) `as` #col1 :* - Set (param @2 @('NotNull 'PGint4)) `as` #col2 - ) OnConflictDoRaise - ( Returning_ ((#col1 + #col2) `as` #fromOnly) ) - setup :: Definition (Public '[]) DB - setup = createTable #tab - ( notNullable int4 `as` #col1 :* - notNullable int4 `as` #col2 - ) Nil - teardown :: Definition DB (Public '[]) - teardown = dropTable #tab -in - withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $ - define setup - & pqThen - ( do - results <- forPrepared [(2::Int32,2::Int32),(3,3),(4,4)] insertAdd - answers <- traverse firstRow results - liftIO $ print [answer :: Int32 | Just (Only answer) <- answers] - ) - & pqThen (define teardown) -:} -[4,6,8] --} -forPrepared - :: ( MonadPQ db pq - , GenericParams db params x xs - , GenericRow row y ys - , Traversable list ) - => list x - -> Manipulation '[] db params row - -- ^ `Squeal.PostgreSQL.Manipulation.Insert.insertInto`, - -- `Squeal.PostgreSQL.Manipulation.Update.update`, - -- or `Squeal.PostgreSQL.Manipulation.Delete.deleteFrom`, and friends - -> pq (list (Result y)) -forPrepared = flip traversePrepared - -{- | -`traversePrepared_` runs a returning-free -`Squeal.PostgreSQL.Manipulation.Manipulation` on a `Foldable` -container by first preparing the statement, then running the prepared -statement on each element. - ->>> type Column = 'NoDef :=> 'NotNull 'PGint4 ->>> type Columns = '["col1" ::: Column, "col2" ::: Column] ->>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] ->>> type DB = Public Schema ->>> import Data.Int(Int32) ->>> :{ -let - insertion :: Manipulation_ DB (Int32, Int32) () - insertion = insertInto_ #tab $ Values_ $ - Set (param @1 @('NotNull 'PGint4)) `as` #col1 :* - Set (param @2 @('NotNull 'PGint4)) `as` #col2 - setup :: Definition (Public '[]) DB - setup = createTable #tab - ( notNullable int4 `as` #col1 :* - notNullable int4 `as` #col2 - ) Nil - teardown :: Definition DB (Public '[]) - teardown = dropTable #tab -in - withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $ - define setup - & pqThen (traversePrepared_ insertion [(2::Int32,2::Int32),(3,3),(4,4)]) - & pqThen (define teardown) -:} --} -traversePrepared_ - :: ( MonadPQ db pq - , GenericParams db params x xs - , Foldable list ) - => Manipulation '[] db params '[] - -- ^ `Squeal.PostgreSQL.Manipulation.Insert.insertInto_`, - -- `Squeal.PostgreSQL.Manipulation.Update.update_`, - -- or `Squeal.PostgreSQL.Manipulation.Delete.deleteFrom_`, and friends - -> list x -> pq () -traversePrepared_ = executePrepared_ . manipulation - -{- | -`forPrepared_` is a flipped `traversePrepared_` - ->>> type Column = 'NoDef :=> 'NotNull 'PGint4 ->>> type Columns = '["col1" ::: Column, "col2" ::: Column] ->>> type Schema = '["tab" ::: 'Table ('[] :=> Columns)] ->>> type DB = Public Schema ->>> import Data.Int(Int32) ->>> :{ -let - insertion :: Manipulation_ DB (Int32, Int32) () - insertion = insertInto_ #tab $ Values_ $ - Set (param @1 @('NotNull 'PGint4)) `as` #col1 :* - Set (param @2 @('NotNull 'PGint4)) `as` #col2 - setup :: Definition (Public '[]) DB - setup = createTable #tab - ( notNullable int4 `as` #col1 :* - notNullable int4 `as` #col2 - ) Nil - teardown :: Definition DB (Public '[]) - teardown = dropTable #tab -in - withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $ - define setup - & pqThen (forPrepared_ [(2::Int32,2::Int32),(3,3),(4,4)] insertion) - & pqThen (define teardown) -:} --} -forPrepared_ - :: ( MonadPQ db pq - , GenericParams db params x xs - , Foldable list ) - => list x - -> Manipulation '[] db params '[] - -- ^ `Squeal.PostgreSQL.Manipulation.Insert.insertInto_`, - -- `Squeal.PostgreSQL.Manipulation.Update.update_`, - -- or `Squeal.PostgreSQL.Manipulation.Delete.deleteFrom_`, and friends - -> pq () -forPrepared_ = flip traversePrepared_ - -instance MonadPQ db m => MonadPQ db (IdentityT m) -instance MonadPQ db m => MonadPQ db (ReaderT r m) -instance MonadPQ db m => MonadPQ db (Strict.StateT s m) -instance MonadPQ db m => MonadPQ db (Lazy.StateT s m) -instance (Monoid w, MonadPQ db m) => MonadPQ db (Strict.WriterT w m) -instance (Monoid w, MonadPQ db m) => MonadPQ db (Lazy.WriterT w m) -instance MonadPQ db m => MonadPQ db (MaybeT m) -instance MonadPQ db m => MonadPQ db (ExceptT e m) -instance (Monoid w, MonadPQ db m) => MonadPQ db (Strict.RWST r w s m) -instance (Monoid w, MonadPQ db m) => MonadPQ db (Lazy.RWST r w s m) -instance MonadPQ db m => MonadPQ db (ContT r m) diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Oid.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Session/Oid.hs deleted file mode 100644 index b852908a..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Oid.hs +++ /dev/null @@ -1,231 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Session.Oid -Description: object identifiers -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -Object identifiers are used internally by PostgreSQL as -primary keys. They are needed to correctly encode -statement parameters. --} - -{-# LANGUAGE - AllowAmbiguousTypes - , DataKinds - , FlexibleContexts - , FlexibleInstances - , MultiParamTypeClasses - , OverloadedStrings - , PolyKinds - , ScopedTypeVariables - , TypeApplications - , TypeFamilies - , TypeOperators - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Session.Oid - ( -- * Oids - LibPQ.Oid - , OidOf (..) - , OidOfArray (..) - , OidOfNull (..) - , OidOfField (..) - ) where - -import Control.Monad.Catch -import Control.Monad.Reader -import Data.String -import GHC.TypeLits -import PostgreSQL.Binary.Decoding (valueParser, int) - -import qualified Data.ByteString as ByteString -import qualified Database.PostgreSQL.LibPQ as LibPQ -import qualified Generics.SOP as SOP - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Session.Exception -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL - --- | The `LibPQ.Oid` of a `PGType` --- --- >>> :set -XTypeApplications --- >>> conn <- connectdb @'[] "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" --- >>> runReaderT (oidOf @'[] @'PGbool) conn --- Oid 16 --- --- >>> finish conn -class OidOf (db :: SchemasType) (pg :: PGType) where - oidOf :: ReaderT (SOP.K LibPQ.Connection db) IO LibPQ.Oid --- | The `LibPQ.Oid` of an array -class OidOfArray (db :: SchemasType) (pg :: PGType) where - oidOfArray :: ReaderT (SOP.K LibPQ.Connection db) IO LibPQ.Oid -instance OidOfArray db pg => OidOf db ('PGvararray (null pg)) where - oidOf = oidOfArray @db @pg -instance OidOfArray db pg => OidOf db ('PGfixarray dims (null pg)) where - oidOf = oidOfArray @db @pg --- | The `LibPQ.Oid` of a `NullType` -class OidOfNull (db :: SchemasType) (ty :: NullType) where - oidOfNull :: ReaderT (SOP.K LibPQ.Connection db) IO LibPQ.Oid -instance OidOf db pg => OidOfNull db (null pg) where - oidOfNull = oidOf @db @pg --- | The `LibPQ.Oid` of a field -class OidOfField (db :: SchemasType) (field :: (Symbol, NullType)) where - oidOfField :: ReaderT (SOP.K LibPQ.Connection db) IO LibPQ.Oid -instance OidOfNull db ty => OidOfField db (fld ::: ty) where - oidOfField = oidOfNull @db @ty - -instance OidOf db 'PGbool where oidOf = pure $ LibPQ.Oid 16 -instance OidOfArray db 'PGbool where oidOfArray = pure $ LibPQ.Oid 1000 -instance OidOf db 'PGint2 where oidOf = pure $ LibPQ.Oid 21 -instance OidOfArray db 'PGint2 where oidOfArray = pure $ LibPQ.Oid 1005 -instance OidOf db 'PGint4 where oidOf = pure $ LibPQ.Oid 23 -instance OidOfArray db 'PGint4 where oidOfArray = pure $ LibPQ.Oid 1007 -instance OidOf db 'PGint8 where oidOf = pure $ LibPQ.Oid 20 -instance OidOfArray db 'PGint8 where oidOfArray = pure $ LibPQ.Oid 1016 -instance OidOf db 'PGnumeric where oidOf = pure $ LibPQ.Oid 1700 -instance OidOfArray db 'PGnumeric where oidOfArray = pure $ LibPQ.Oid 1231 -instance OidOf db 'PGfloat4 where oidOf = pure $ LibPQ.Oid 700 -instance OidOfArray db 'PGfloat4 where oidOfArray = pure $ LibPQ.Oid 1021 -instance OidOf db 'PGfloat8 where oidOf = pure $ LibPQ.Oid 701 -instance OidOfArray db 'PGfloat8 where oidOfArray = pure $ LibPQ.Oid 1022 -instance OidOf db 'PGmoney where oidOf = pure $ LibPQ.Oid 790 -instance OidOfArray db 'PGmoney where oidOfArray = pure $ LibPQ.Oid 791 -instance OidOf db ('PGchar n) where oidOf = pure $ LibPQ.Oid 18 -instance OidOfArray db ('PGchar n) where oidOfArray = pure $ LibPQ.Oid 1002 -instance OidOf db ('PGvarchar n) where oidOf = pure $ LibPQ.Oid 1043 -instance OidOfArray db ('PGvarchar n) where oidOfArray = pure $ LibPQ.Oid 1015 -instance OidOf db 'PGtext where oidOf = pure $ LibPQ.Oid 25 -instance OidOfArray db 'PGtext where oidOfArray = pure $ LibPQ.Oid 1009 -instance OidOf db 'PGbytea where oidOf = pure $ LibPQ.Oid 17 -instance OidOfArray db 'PGbytea where oidOfArray = pure $ LibPQ.Oid 1001 -instance OidOf db 'PGtimestamp where oidOf = pure $ LibPQ.Oid 1114 -instance OidOfArray db 'PGtimestamp where oidOfArray = pure $ LibPQ.Oid 1115 -instance OidOf db 'PGtimestamptz where oidOf = pure $ LibPQ.Oid 1184 -instance OidOfArray db 'PGtimestamptz where oidOfArray = pure $ LibPQ.Oid 1185 -instance OidOf db 'PGdate where oidOf = pure $ LibPQ.Oid 1082 -instance OidOfArray db 'PGdate where oidOfArray = pure $ LibPQ.Oid 1182 -instance OidOf db 'PGtime where oidOf = pure $ LibPQ.Oid 1083 -instance OidOfArray db 'PGtime where oidOfArray = pure $ LibPQ.Oid 1183 -instance OidOf db 'PGtimetz where oidOf = pure $ LibPQ.Oid 1266 -instance OidOfArray db 'PGtimetz where oidOfArray = pure $ LibPQ.Oid 1270 -instance OidOf db 'PGinterval where oidOf = pure $ LibPQ.Oid 1186 -instance OidOfArray db 'PGinterval where oidOfArray = pure $ LibPQ.Oid 1187 -instance OidOf db 'PGuuid where oidOf = pure $ LibPQ.Oid 2950 -instance OidOfArray db 'PGuuid where oidOfArray = pure $ LibPQ.Oid 2951 -instance OidOf db 'PGinet where oidOf = pure $ LibPQ.Oid 869 -instance OidOfArray db 'PGinet where oidOfArray = pure $ LibPQ.Oid 1041 -instance OidOf db 'PGjson where oidOf = pure $ LibPQ.Oid 114 -instance OidOfArray db 'PGjson where oidOfArray = pure $ LibPQ.Oid 199 -instance OidOf db 'PGjsonb where oidOf = pure $ LibPQ.Oid 3802 -instance OidOfArray db 'PGjsonb where oidOfArray = pure $ LibPQ.Oid 3807 -instance OidOf db 'PGtsvector where oidOf = pure $ LibPQ.Oid 3614 -instance OidOfArray db 'PGtsvector where oidOfArray = pure $ LibPQ.Oid 3643 -instance OidOf db 'PGtsquery where oidOf = pure $ LibPQ.Oid 3615 -instance OidOfArray db 'PGtsquery where oidOfArray = pure $ LibPQ.Oid 3645 -instance OidOf db 'PGoid where oidOf = pure $ LibPQ.Oid 26 -instance OidOfArray db 'PGoid where oidOfArray = pure $ LibPQ.Oid 1028 -instance OidOf db ('PGrange 'PGint4) where oidOf = pure $ LibPQ.Oid 3904 -instance OidOfArray db ('PGrange 'PGint4) where oidOfArray = pure $ LibPQ.Oid 3905 -instance OidOf db ('PGrange 'PGint8) where oidOf = pure $ LibPQ.Oid 3926 -instance OidOfArray db ('PGrange 'PGint8) where oidOfArray = pure $ LibPQ.Oid 3927 -instance OidOf db ('PGrange 'PGnumeric) where oidOf = pure $ LibPQ.Oid 3906 -instance OidOfArray db ('PGrange 'PGnumeric) where oidOfArray = pure $ LibPQ.Oid 3907 -instance OidOf db ('PGrange 'PGtimestamp) where oidOf = pure $ LibPQ.Oid 3908 -instance OidOfArray db ('PGrange 'PGtimestamp) where oidOfArray = pure $ LibPQ.Oid 3909 -instance OidOf db ('PGrange 'PGtimestamptz) where oidOf = pure $ LibPQ.Oid 3910 -instance OidOfArray db ('PGrange 'PGtimestamptz) where oidOfArray = pure $ LibPQ.Oid 3911 -instance OidOf db ('PGrange 'PGdate) where oidOf = pure $ LibPQ.Oid 3912 -instance OidOfArray db ('PGrange 'PGdate) where oidOfArray = pure $ LibPQ.Oid 3913 -instance - ( UserType db ('PGcomposite row) ~ '(sch,td) - , Has sch db schema - , Has td schema ('Typedef ('PGcomposite row)) ) - => OidOf db ('PGcomposite row) where - oidOf = oidOfTypedef (QualifiedAlias @sch @td) -instance - ( UserType db ('PGcomposite row) ~ '(sch,td) - , Has sch db schema - , Has td schema ('Typedef ('PGcomposite row)) ) - => OidOfArray db ('PGcomposite row) where - oidOfArray = oidOfArrayTypedef (QualifiedAlias @sch @td) -instance - ( UserType db ('PGenum labels) ~ '(sch,td) - , Has sch db schema - , Has td schema ('Typedef ('PGenum labels)) ) - => OidOf db ('PGenum labels) where - oidOf = oidOfTypedef (QualifiedAlias @sch @td) -instance - ( UserType db ('PGenum labels) ~ '(sch,td) - , Has sch db schema - , Has td schema ('Typedef ('PGenum labels)) ) - => OidOfArray db ('PGenum labels) where - oidOfArray = oidOfArrayTypedef (QualifiedAlias @sch @td) - -oidOfTypedef - :: (Has sch db schema, Has ty schema pg) - => QualifiedAlias sch ty - -> ReaderT (SOP.K LibPQ.Connection db) IO LibPQ.Oid -oidOfTypedef (_ :: QualifiedAlias sch ty) = ReaderT $ \(SOP.K conn) -> do - resultMaybe <- LibPQ.execParams conn q [] LibPQ.Binary - case resultMaybe of - Nothing -> throwM $ ConnectionException oidErr - Just result -> do - numRows <- LibPQ.ntuples result - when (numRows /= 1) $ throwM $ RowsException oidErr 1 numRows - valueMaybe <- LibPQ.getvalue result 0 0 - case valueMaybe of - Nothing -> throwM $ ConnectionException oidErr - Just value -> case valueParser int value of - Left err -> throwM $ DecodingException oidErr err - Right oid -> return $ LibPQ.Oid oid - where - tyVal = symbolVal (SOP.Proxy @ty) - schVal = symbolVal (SOP.Proxy @sch) - oidErr = "oidOfTypedef " <> fromString (schVal <> "." <> tyVal) - q = ByteString.intercalate " " - [ "SELECT pg_type.oid" - , "FROM pg_type" - , "INNER JOIN pg_namespace" - , "ON pg_type.typnamespace = pg_namespace.oid" - , "WHERE pg_type.typname = " - , "\'" <> fromString tyVal <> "\'" - , "AND pg_namespace.nspname = " - , "\'" <> fromString schVal <> "\'" - , ";" ] - -oidOfArrayTypedef - :: (Has sch db schema, Has ty schema pg) - => QualifiedAlias sch ty - -> ReaderT (SOP.K LibPQ.Connection db) IO LibPQ.Oid -oidOfArrayTypedef (_ :: QualifiedAlias sch ty) = ReaderT $ \(SOP.K conn) -> do - resultMaybe <- LibPQ.execParams conn q [] LibPQ.Binary - case resultMaybe of - Nothing -> throwM $ ConnectionException oidErr - Just result -> do - numRows <- LibPQ.ntuples result - when (numRows /= 1) $ throwM $ RowsException oidErr 1 numRows - valueMaybe <- LibPQ.getvalue result 0 0 - case valueMaybe of - Nothing -> throwM $ ConnectionException oidErr - Just value -> case valueParser int value of - Left err -> throwM $ DecodingException oidErr err - Right oid -> return $ LibPQ.Oid oid - where - tyVal = symbolVal (SOP.Proxy @ty) - schVal = symbolVal (SOP.Proxy @sch) - oidErr = "oidOfArrayTypedef " <> fromString (schVal <> "." <> tyVal) - q = ByteString.intercalate " " - [ "SELECT pg_type.typelem" - , "FROM pg_type" - , "INNER JOIN pg_namespace" - , "ON pg_type.typnamespace = pg_namespace.oid" - , "WHERE pg_type.typname = " - , "\'" <> fromString tyVal <> "\'" - , "AND pg_namespace.nspname = " - , "\'" <> fromString schVal <> "\'" - , ";" ] diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Pool.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Session/Pool.hs deleted file mode 100644 index 6a4b2aaf..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Pool.hs +++ /dev/null @@ -1,130 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Pool -Description: connection pools -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -Connection pools. - -Typical use case would be to create your pool using `createConnectionPool` -and run anything that requires the pool connection with `usingConnectionPool`. - -Here's a simplified example: - ->>> import Squeal.PostgreSQL - ->>> :{ -do - let - qry :: Query_ (Public '[]) () (Only Char) - qry = values_ (inline 'a' `as` #fromOnly) - pool <- createConnectionPool "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" 1 0.5 10 - chr <- usingConnectionPool pool $ do - result <- runQuery qry - Just (Only a) <- firstRow result - return a - destroyConnectionPool pool - putChar chr -:} -a --} - -{-# LANGUAGE - DeriveFunctor - , FlexibleContexts - , FlexibleInstances - , InstanceSigs - , MultiParamTypeClasses - , PolyKinds - , RankNTypes - , ScopedTypeVariables - , TypeFamilies - , TypeInType - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Session.Pool - ( -- * Pool - Pool - , createConnectionPool - , usingConnectionPool - , destroyConnectionPool - ) where - -import Control.Monad.Catch -import Control.Monad.IO.Class -import Data.ByteString -import Data.Time -import Data.Pool - -import Squeal.PostgreSQL.Type.Schema -import Squeal.PostgreSQL.Session (PQ (..)) -import Squeal.PostgreSQL.Session.Connection - --- | Create a striped pool of connections. --- Although the garbage collector will destroy all idle connections when the pool is garbage collected it's recommended to manually `destroyConnectionPool` when you're done with the pool so that the connections are freed up as soon as possible. -createConnectionPool - :: forall (db :: SchemasType) io. MonadIO io - => ByteString - -- ^ The passed string can be empty to use all default parameters, or it can - -- contain one or more parameter settings separated by whitespace. - -- Each parameter setting is in the form keyword = value. Spaces around the equal - -- sign are optional. To write an empty value or a value containing spaces, - -- surround it with single quotes, e.g., keyword = 'a value'. Single quotes and - -- backslashes within the value must be escaped with a backslash, i.e., ' and \. - -> Int - -- ^ The number of stripes (distinct sub-pools) to maintain. The smallest acceptable value is 1. - -> NominalDiffTime - -- ^ Amount of time for which an unused connection is kept open. The smallest acceptable value is 0.5 seconds. - -- The elapsed time before destroying a connection may be a little longer than requested, as the reaper thread wakes at 1-second intervals. - -> Int - -- ^ Maximum number of connections to keep open per stripe. The smallest acceptable value is 1. - -- Requests for connections will block if this limit is reached on a single stripe, even if other stripes have idle connections available. - -> io (Pool (K Connection db)) -createConnectionPool conninfo stripes idle maxResrc = - liftIO $ createPool (connectdb conninfo) finish stripes idle maxResrc - -{-| -Temporarily take a connection from a `Pool`, perform an action with it, -and return it to the pool afterwards. - -If the pool has an idle connection available, it is used immediately. -Otherwise, if the maximum number of connections has not yet been reached, -a new connection is created and used. -If the maximum number of connections has been reached, this function blocks -until a connection becomes available. --} -usingConnectionPool - :: (MonadIO io, MonadMask io) - => Pool (K Connection db) -- ^ pool - -> PQ db db io x -- ^ session - -> io x -usingConnectionPool pool (PQ session) = mask $ \restore -> do - (conn, local) <- liftIO $ takeResource pool - ret <- restore (session conn) `onException` - liftIO (destroyResource pool local conn) - liftIO $ putResource local conn - return $ unK ret - -{- | -Destroy all connections in all stripes in the pool. -Note that this will ignore any exceptions in the destroy function. - -This function is useful when you detect that all connections -in the pool are broken. For example after a database has been -restarted all connections opened before the restart will be broken. -In that case it's better to close those connections so that -`usingConnectionPool` won't take a broken connection from the pool -but will open a new connection instead. - -Another use-case for this function is that when you know you are done -with the pool you can destroy all idle connections immediately -instead of waiting on the garbage collector to destroy them, -thus freeing up those connections sooner. --} -destroyConnectionPool - :: MonadIO io - => Pool (K Connection db) -- ^ pool - -> io () -destroyConnectionPool = liftIO . destroyAllResources diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Result.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Session/Result.hs deleted file mode 100644 index 75bca67d..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Result.hs +++ /dev/null @@ -1,223 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Session.Result -Description: results -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -Get values from a `Result`. --} - -{-# LANGUAGE - FlexibleContexts - , FlexibleInstances - , GADTs - , LambdaCase - , OverloadedStrings - , ScopedTypeVariables - , TypeApplications - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Session.Result - ( Result (..) - , MonadResult (..) - , liftResult - , nextRow - ) where - -import Control.Exception (throw) -import Control.Monad (when, (<=<)) -import Control.Monad.Catch -import Control.Monad.IO.Class -import Data.ByteString (ByteString) -import Data.Text (Text) -import Data.Traversable (for) -import Text.Read (readMaybe) - -import qualified Data.ByteString as ByteString -import qualified Data.ByteString.Char8 as Char8 -import qualified Data.Text.Encoding as Text -import qualified Database.PostgreSQL.LibPQ as LibPQ -import qualified Generics.SOP as SOP - -import Squeal.PostgreSQL.Session.Decode -import Squeal.PostgreSQL.Session.Exception - -{- | `Result`s are generated by executing -`Squeal.PostgreSQL.Session.Statement`s -in a `Squeal.PostgreSQL.Session.Monad.MonadPQ`. - -They contain an underlying `LibPQ.Result` -and a `DecodeRow`. --} -data Result y where - Result - :: SOP.SListI row - => DecodeRow row y - -> LibPQ.Result - -> Result y -instance Functor Result where - fmap f (Result decode result) = Result (fmap f decode) result - -{- | A `MonadResult` operation extracts values -from the `Result` of a `Squeal.PostgreSQL.Session.Monad.MonadPQ` operation. -There is no need to define instances of `MonadResult`. -An instance of `MonadIO` implies an instance of `MonadResult`. -However, the constraint `MonadResult` -does not imply the constraint `MonadIO`. --} -class Monad m => MonadResult m where - -- | Get a row corresponding to a given row number from a `LibPQ.Result`, - -- throwing an exception if the row number is out of bounds. - getRow :: LibPQ.Row -> Result y -> m y - -- | Get all rows from a `LibPQ.Result`. - getRows :: Result y -> m [y] - -- | Get the first row if possible from a `LibPQ.Result`. - firstRow :: Result y -> m (Maybe y) - -- | Returns the number of rows (tuples) in the query result. - ntuples :: Result y -> m LibPQ.Row - -- | Returns the number of columns (fields) in the query result. - nfields :: Result y -> m LibPQ.Column - {- | - Returns the command status tag from the SQL command - that generated the `Result`. - Commonly this is just the name of the command, - but it might include additional data such as the number of rows processed. - -} - cmdStatus :: Result y -> m Text - {- | - Returns the number of rows affected by the SQL command. - This function returns `Just` the number of - rows affected by the SQL statement that generated the `Result`. - This function can only be used following the execution of a - SELECT, CREATE TABLE AS, INSERT, UPDATE, DELETE, MOVE, FETCH, - or COPY statement,or an EXECUTE of a prepared query that - contains an INSERT, UPDATE, or DELETE statement. - If the command that generated the PGresult was anything else, - `cmdTuples` returns `Nothing`. - -} - cmdTuples :: Result y -> m (Maybe LibPQ.Row) - -- | Returns the result status of the command. - resultStatus :: Result y -> m LibPQ.ExecStatus - -- | Check if a `Result`'s status is either `LibPQ.CommandOk` - -- or `LibPQ.TuplesOk` otherwise `throw` a `SQLException`. - okResult :: Result y -> m () - -- | Returns the error message most recently generated by an operation - -- on the connection. - resultErrorMessage :: Result y -> m (Maybe ByteString) - -- | Returns the error code most recently generated by an operation - -- on the connection. - -- - -- https://www.postgresql.org/docs/current/static/errcodes-appendix.html - resultErrorCode :: Result y -> m (Maybe ByteString) - -instance (Monad io, MonadIO io) => MonadResult io where - getRow r (Result decode result) = liftIO $ do - numRows <- LibPQ.ntuples result - numCols <- LibPQ.nfields result - when (numRows < r) $ throw $ RowsException "getRow" r numRows - row' <- traverse (LibPQ.getvalue result r) [0 .. numCols - 1] - case SOP.fromList row' of - Nothing -> throw $ ColumnsException "getRow" numCols - Just row -> case execDecodeRow decode row of - Left parseError -> throw $ DecodingException "getRow" parseError - Right y -> return y - - getRows (Result decode result) = liftIO $ do - numCols <- LibPQ.nfields result - numRows <- LibPQ.ntuples result - for [0 .. numRows - 1] $ \ r -> do - row' <- traverse (LibPQ.getvalue result r) [0 .. numCols - 1] - case SOP.fromList row' of - Nothing -> throw $ ColumnsException "getRows" numCols - Just row -> case execDecodeRow decode row of - Left parseError -> throw $ DecodingException "getRows" parseError - Right y -> return y - - firstRow (Result decode result) = liftIO $ do - numRows <- LibPQ.ntuples result - numCols <- LibPQ.nfields result - if numRows <= 0 then return Nothing else do - row' <- traverse (LibPQ.getvalue result 0) [0 .. numCols - 1] - case SOP.fromList row' of - Nothing -> throw $ ColumnsException "firstRow" numCols - Just row -> case execDecodeRow decode row of - Left parseError -> throw $ DecodingException "firstRow" parseError - Right y -> return $ Just y - - ntuples = liftResult LibPQ.ntuples - - nfields = liftResult LibPQ.nfields - - resultStatus = liftResult LibPQ.resultStatus - - cmdStatus = liftResult (getCmdStatus <=< LibPQ.cmdStatus) - where - getCmdStatus = \case - Nothing -> throwM $ ConnectionException "LibPQ.cmdStatus" - Just bytes -> return $ Text.decodeUtf8 bytes - - cmdTuples = liftResult (getCmdTuples <=< LibPQ.cmdTuples) - where - getCmdTuples = \case - Nothing -> throwM $ ConnectionException "LibPQ.cmdTuples" - Just bytes -> return $ - if ByteString.null bytes - then Nothing - else fromInteger <$> readMaybe (Char8.unpack bytes) - - okResult = liftResult okResult_ - - resultErrorMessage = liftResult LibPQ.resultErrorMessage - - resultErrorCode = liftResult (flip LibPQ.resultErrorField LibPQ.DiagSqlstate) - --- | Intended to be used for unfolding in streaming libraries, `nextRow` --- takes a total number of rows (which can be found with `ntuples`) --- and a `LibPQ.Result` and given a row number if it's too large returns `Nothing`, --- otherwise returning the row along with the next row number. -nextRow - :: MonadIO io - => LibPQ.Row -- ^ total number of rows - -> Result y -- ^ result - -> LibPQ.Row -- ^ row number - -> io (Maybe (LibPQ.Row, y)) -nextRow total (Result decode result) r - = liftIO $ if r >= total then return Nothing else do - numCols <- LibPQ.nfields result - row' <- traverse (LibPQ.getvalue result r) [0 .. numCols - 1] - case SOP.fromList row' of - Nothing -> throw $ ColumnsException "nextRow" numCols - Just row -> case execDecodeRow decode row of - Left parseError -> throw $ DecodingException "nextRow" parseError - Right y -> return $ Just (r+1, y) - -okResult_ :: MonadIO io => LibPQ.Result -> io () -okResult_ result = liftIO $ do - status <- LibPQ.resultStatus result - case status of - LibPQ.CommandOk -> return () - LibPQ.TuplesOk -> return () - _ -> do - stateCodeMaybe <- LibPQ.resultErrorField result LibPQ.DiagSqlstate - case stateCodeMaybe of - Nothing -> throw $ ConnectionException "LibPQ.resultErrorField" - Just stateCode -> do - msgMaybe <- LibPQ.resultErrorMessage result - case msgMaybe of - Nothing -> throw $ ConnectionException "LibPQ.resultErrorMessage" - Just msg -> throw . SQLException $ SQLState status stateCode msg - --- | Lifts actions on results from @LibPQ@. -liftResult - :: MonadIO io - => (LibPQ.Result -> IO x) - -> Result y -> io x -liftResult f (Result _ result) = liftIO $ f result - -execDecodeRow - :: DecodeRow row y - -> SOP.NP (SOP.K (Maybe ByteString)) row - -> Either Text y -execDecodeRow decode = runDecodeRow decode diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Statement.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Session/Statement.hs deleted file mode 100644 index 707e67cb..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Statement.hs +++ /dev/null @@ -1,105 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Session.Statement -Description: statements -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -A top-level `Statement` type wraps a `Squeal.PostgreSQL.Query.Query` -or `Squeal.PostgreSQL.Manipulation.Manipulation` -together with an `EncodeParams` and a `DecodeRow`. --} - -{-# LANGUAGE - DataKinds - , DeriveFunctor - , DeriveFoldable - , DeriveGeneric - , DeriveTraversable - , FlexibleContexts - , GADTs - , RankNTypes -#-} - -module Squeal.PostgreSQL.Session.Statement - ( Statement (..) - , query - , manipulation - ) where - -import Data.Functor.Contravariant -import Data.Profunctor (Profunctor (..)) - -import qualified Generics.SOP as SOP - -import Squeal.PostgreSQL.Manipulation -import Squeal.PostgreSQL.Session.Decode -import Squeal.PostgreSQL.Session.Encode -import Squeal.PostgreSQL.Session.Oid -import Squeal.PostgreSQL.Query -import Squeal.PostgreSQL.Render - --- | A `Statement` consists of a `Squeal.PostgreSQL.Statement.Manipulation` --- or a `Squeal.PostgreSQL.Session.Statement.Query` that can be run --- in a `Squeal.PostgreSQL.Session.Monad.MonadPQ`. -data Statement db x y where - -- | Constructor for a data manipulation language statement - Manipulation - :: (SOP.All (OidOfNull db) params, SOP.SListI row) - => EncodeParams db params x -- ^ encoding of parameters - -> DecodeRow row y -- ^ decoding of returned rows - -> Manipulation '[] db params row - -- ^ `Squeal.PostgreSQL.Manipulation.Insert.insertInto`, - -- `Squeal.PostgreSQL.Manipulation.Update.update`, - -- or `Squeal.PostgreSQL.Manipulation.Delete.deleteFrom`, ... - -> Statement db x y - -- | Constructor for a structured query language statement - Query - :: (SOP.All (OidOfNull db) params, SOP.SListI row) - => EncodeParams db params x -- ^ encoding of parameters - -> DecodeRow row y -- ^ decoding of returned rows - -> Query '[] '[] db params row - -- ^ `Squeal.PostgreSQL.Query.Select.select`, - -- `Squeal.PostgreSQL.Query.Values.values`, ... - -> Statement db x y - -instance Profunctor (Statement db) where - lmap f (Manipulation encode decode q) = - Manipulation (contramap f encode) decode q - lmap f (Query encode decode q) = - Query (contramap f encode) decode q - rmap f (Manipulation encode decode q) = - Manipulation encode (fmap f decode) q - rmap f (Query encode decode q) = - Query encode (fmap f decode) q - dimap f g (Manipulation encode decode q) = - Manipulation (contramap f encode) (fmap g decode) q - dimap f g (Query encode decode q) = - Query (contramap f encode) (fmap g decode) q - -instance Functor (Statement db x) where fmap = rmap - -instance RenderSQL (Statement db x y) where - renderSQL (Manipulation _ _ q) = renderSQL q - renderSQL (Query _ _ q) = renderSQL q - --- | Smart constructor for a structured query language statement -query :: - ( GenericParams db params x xs - , GenericRow row y ys - ) => Query '[] '[] db params row - -- ^ `Squeal.PostgreSQL.Query.Select.select`, - -- `Squeal.PostgreSQL.Query.Values.values`, ... - -> Statement db x y -query = Query genericParams genericRow - --- | Smart constructor for a data manipulation language statement -manipulation :: - ( GenericParams db params x xs - , GenericRow row y ys - ) => Manipulation '[] db params row - -- ^ `Squeal.PostgreSQL.Manipulation.Insert.insertInto`, - -- `Squeal.PostgreSQL.Manipulation.Update.update`, - -- or `Squeal.PostgreSQL.Manipulation.Delete.deleteFrom`, ... - -> Statement db x y -manipulation = Manipulation genericParams genericRow diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Transaction.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Session/Transaction.hs deleted file mode 100644 index f1868563..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Transaction.hs +++ /dev/null @@ -1,156 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Session.Transaction -Description: transaction control language -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -transaction control language --} - -{-# LANGUAGE - MonoLocalBinds - , RankNTypes -#-} - -module Squeal.PostgreSQL.Session.Transaction - ( -- * Transaction - Transaction - , transactionally - , transactionally_ - , transactionallyRetry - , transactionallyRetry_ - , ephemerally - , ephemerally_ - , withSavepoint - -- * Transaction Mode - , TransactionMode (..) - , defaultMode - , longRunningMode - , retryMode - , IsolationLevel (..) - , AccessMode (..) - , DeferrableMode (..) - ) where - -import Control.Monad.Catch -import Data.ByteString - -import Squeal.PostgreSQL.Session.Monad -import Squeal.PostgreSQL.Session.Result -import Squeal.PostgreSQL.Session.Transaction.Unsafe - ( TransactionMode (..) - , defaultMode - , longRunningMode - , retryMode - , IsolationLevel (..) - , AccessMode (..) - , DeferrableMode (..) - ) -import qualified Squeal.PostgreSQL.Session.Transaction.Unsafe as Unsafe - -{- | A type of "safe" `Transaction`s, -do-blocks that permit only -database operations, pure functions, and synchronous exception handling -forbidding arbitrary `IO` operations. - -To permit arbitrary `IO`, - ->>> import qualified Squeal.PostgreSQL.Session.Transaction.Unsafe as Unsafe - -Then use the @Unsafe@ qualified form of the functions below. - -A safe `Transaction` can be run in two ways, - -1) it can be run directly in `IO` because as a - universally quantified type, - @Transaction db x@ permits interpretation in "subtypes" like - @(MonadPQ db m, MonadIO m, MonadCatch m) => m x@ - or - @PQ db db IO x@ - -2) it can be run in a transaction block, using - `transactionally`, `ephemerally`, - or `transactionallyRetry` --} -type Transaction db x = forall m. - ( MonadPQ db m - , MonadResult m - , MonadCatch m - ) => m x - -{- | Run a computation `transactionally`; -first `Unsafe.begin`, -then run the computation, -`onException` `Unsafe.rollback` and rethrow the exception, -otherwise `Unsafe.commit` and `return` the result. --} -transactionally - :: (MonadMask tx, MonadResult tx, MonadPQ db tx) - => TransactionMode - -> Transaction db x -- ^ run inside a transaction - -> tx x -transactionally mode tx = Unsafe.transactionally mode tx - --- | Run a computation `transactionally_`, in `defaultMode`. -transactionally_ - :: (MonadMask tx, MonadResult tx, MonadPQ db tx) - => Transaction db x -- ^ run inside a transaction - -> tx x -transactionally_ tx = Unsafe.transactionally_ tx - -{- | -`transactionallyRetry` a computation; - -* first `Unsafe.begin`, -* then `try` the computation, - - if it raises a serialization failure or deadloack detection, - then `Unsafe.rollback` and restart the transaction, - - if it raises any other exception then `Unsafe.rollback` and rethrow the exception, - - otherwise `Unsafe.commit` and `return` the result. --} -transactionallyRetry - :: (MonadMask tx, MonadResult tx, MonadPQ db tx) - => TransactionMode - -> Transaction db x -- ^ run inside a transaction - -> tx x -transactionallyRetry mode tx = Unsafe.transactionallyRetry mode tx - -{- | `transactionallyRetry` in `retryMode`. -} -transactionallyRetry_ - :: (MonadMask tx, MonadResult tx, MonadPQ db tx) - => Transaction db x -- ^ run inside a transaction - -> tx x -transactionallyRetry_ tx = Unsafe.transactionallyRetry_ tx - -{- | Run a computation `ephemerally`; -Like `transactionally` but always `Unsafe.rollback`, useful in testing. --} -ephemerally - :: (MonadMask tx, MonadResult tx, MonadPQ db tx) - => TransactionMode - -> Transaction db x -- ^ run inside an ephemeral transaction - -> tx x -ephemerally mode tx = Unsafe.ephemerally mode tx - -{- | Run a computation `ephemerally` in `defaultMode`. -} -ephemerally_ - :: (MonadMask tx, MonadResult tx, MonadPQ db tx) - => Transaction db x -- ^ run inside an ephemeral transaction - -> tx x -ephemerally_ tx = Unsafe.ephemerally_ tx - -{- | `withSavepoint`, used in a transaction block, -allows a form of nested transactions, -creating a savepoint, then running a transaction, -rolling back to the savepoint if it returned `Left`, -then releasing the savepoint and returning transaction's result. - -Make sure to run `withSavepoint` in a transaction block, -not directly or you will provoke a SQL exception. --} -withSavepoint - :: ByteString -- ^ savepoint name - -> Transaction db (Either e x) - -> Transaction db (Either e x) -withSavepoint sv tx = Unsafe.withSavepoint sv tx diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Transaction/Unsafe.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Session/Transaction/Unsafe.hs deleted file mode 100644 index c84dcf30..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Session/Transaction/Unsafe.hs +++ /dev/null @@ -1,299 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Session.Transaction.Unsafe -Description: unsafe transaction control language -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -transaction control language permitting arbitrary `IO` --} - -{-# LANGUAGE - DataKinds - , FlexibleContexts - , LambdaCase - , OverloadedStrings - , TypeInType -#-} - -module Squeal.PostgreSQL.Session.Transaction.Unsafe - ( -- * Transaction - transactionally - , transactionally_ - , transactionallyRetry - , transactionallyRetry_ - , ephemerally - , ephemerally_ - , begin - , commit - , rollback - , withSavepoint - -- * Transaction Mode - , TransactionMode (..) - , defaultMode - , retryMode - , longRunningMode - , IsolationLevel (..) - , AccessMode (..) - , DeferrableMode (..) - ) where - -import Control.Monad -import Control.Monad.Catch -import Data.ByteString -import Data.Either - -import Squeal.PostgreSQL.Manipulation -import Squeal.PostgreSQL.Render -import Squeal.PostgreSQL.Session.Exception -import Squeal.PostgreSQL.Session.Monad - -{- | Run a computation `transactionally`; -first `begin`, -then run the computation, -`onException` `rollback` and rethrow the exception, -otherwise `commit` and `return` the result. --} -transactionally - :: (MonadMask tx, MonadPQ db tx) - => TransactionMode - -> tx x -- ^ run inside a transaction - -> tx x -transactionally mode tx = mask $ \restore -> do - manipulate_ $ begin mode - result <- restore tx `onException` manipulate_ rollback - manipulate_ commit - return result - --- | Run a computation `transactionally_`, in `defaultMode`. -transactionally_ - :: (MonadMask tx, MonadPQ db tx) - => tx x -- ^ run inside a transaction - -> tx x -transactionally_ = transactionally defaultMode - -{- | -`transactionallyRetry` a computation; - -* first `begin`, -* then `try` the computation, - - if it raises a serialization failure or deadlock detection, - then `rollback` and restart the transaction, - - if it raises any other exception then `rollback` and rethrow the exception, - - otherwise `commit` and `return` the result. --} -transactionallyRetry - :: (MonadMask tx, MonadPQ db tx) - => TransactionMode - -> tx x -- ^ run inside a transaction - -> tx x -transactionallyRetry mode tx = mask $ \restore -> - loop . try $ do - x <- restore tx - manipulate_ commit - return x - where - loop attempt = do - manipulate_ $ begin mode - attempt >>= \case - Left (SerializationFailure _) -> do - manipulate_ rollback - loop attempt - Left (DeadlockDetected _) -> do - manipulate_ rollback - loop attempt - Left err -> do - manipulate_ rollback - throwM err - Right x -> return x - -{- | `transactionallyRetry` in `retryMode`. -} -transactionallyRetry_ - :: (MonadMask tx, MonadPQ db tx) - => tx x -- ^ run inside a transaction - -> tx x -transactionallyRetry_ = transactionallyRetry retryMode - -{- | Run a computation `ephemerally`; -Like `transactionally` but always `rollback`, useful in testing. --} -ephemerally - :: (MonadMask tx, MonadPQ db tx) - => TransactionMode - -> tx x -- ^ run inside an ephemeral transaction - -> tx x -ephemerally mode tx = mask $ \restore -> do - manipulate_ $ begin mode - result <- restore tx `onException` (manipulate_ rollback) - manipulate_ rollback - return result - -{- | Run a computation `ephemerally` in `defaultMode`. -} -ephemerally_ - :: (MonadMask tx, MonadPQ db tx) - => tx x -- ^ run inside an ephemeral transaction - -> tx x -ephemerally_ = ephemerally defaultMode - --- | @BEGIN@ a transaction. -begin :: TransactionMode -> Manipulation_ db () () -begin mode = UnsafeManipulation $ "BEGIN" <+> renderSQL mode - --- | @COMMIT@ a transaction. -commit :: Manipulation_ db () () -commit = UnsafeManipulation "COMMIT" - --- | @ROLLBACK@ a transaction. -rollback :: Manipulation_ db () () -rollback = UnsafeManipulation "ROLLBACK" - -{- | `withSavepoint`, used in a transaction block, -allows a form of nested transactions, -creating a savepoint, then running a transaction, -rolling back to the savepoint if it returned `Left`, -then releasing the savepoint and returning transaction's result. - -Make sure to run `withSavepoint` in a transaction block, -not directly or you will provoke a SQL exception. --} -withSavepoint - :: MonadPQ db tx - => ByteString -- ^ savepoint name - -> tx (Either e x) - -> tx (Either e x) -withSavepoint savepoint tx = do - let svpt = "SAVEPOINT" <+> savepoint - manipulate_ $ UnsafeManipulation $ svpt - e_x <- tx - when (isLeft e_x) $ - manipulate_ $ UnsafeManipulation $ "ROLLBACK TO" <+> svpt - manipulate_ $ UnsafeManipulation $ "RELEASE" <+> svpt - return e_x - --- | The available transaction characteristics are the transaction `IsolationLevel`, --- the transaction `AccessMode` (`ReadWrite` or `ReadOnly`), and the `DeferrableMode`. -data TransactionMode = TransactionMode - { isolationLevel :: IsolationLevel - , accessMode :: AccessMode - , deferrableMode :: DeferrableMode - } deriving (Show, Eq) - --- | `TransactionMode` with a `ReadCommitted` `IsolationLevel`, --- `ReadWrite` `AccessMode` and `NotDeferrable` `DeferrableMode`. -defaultMode :: TransactionMode -defaultMode = TransactionMode ReadCommitted ReadWrite NotDeferrable - --- | `TransactionMode` with a `Serializable` `IsolationLevel`, --- `ReadWrite` `AccessMode` and `NotDeferrable` `DeferrableMode`, --- appropriate for short-lived queries or manipulations. -retryMode :: TransactionMode -retryMode = TransactionMode Serializable ReadWrite NotDeferrable - --- | `TransactionMode` with a `Serializable` `IsolationLevel`, --- `ReadOnly` `AccessMode` and `Deferrable` `DeferrableMode`. --- This mode is well suited for long-running reports or backups. -longRunningMode :: TransactionMode -longRunningMode = TransactionMode Serializable ReadOnly Deferrable - --- | Render a `TransactionMode`. -instance RenderSQL TransactionMode where - renderSQL mode = - "ISOLATION LEVEL" - <+> renderSQL (isolationLevel mode) - <+> renderSQL (accessMode mode) - <+> renderSQL (deferrableMode mode) - --- | The SQL standard defines four levels of transaction isolation. --- The most strict is `Serializable`, which is defined by the standard in a paragraph --- which says that any concurrent execution of a set of `Serializable` transactions is --- guaranteed to produce the same effect as running them one at a time in some order. --- The other three levels are defined in terms of phenomena, resulting from interaction --- between concurrent transactions, which must not occur at each level. --- The phenomena which are prohibited at various levels are: --- --- __Dirty read__: A transaction reads data written by a concurrent uncommitted transaction. --- --- __Nonrepeatable read__: A transaction re-reads data it has previously read and finds that data --- has been modified by another transaction (that committed since the initial read). --- --- __Phantom read__: A transaction re-executes a query returning a set of rows that satisfy --- a search condition and finds that the set of rows satisfying the condition --- has changed due to another recently-committed transaction. --- --- __Serialization anomaly__: The result of successfully committing a group of transactions is inconsistent --- with all possible orderings of running those transactions one at a time. --- --- In PostgreSQL, you can request any of the four standard transaction --- isolation levels, but internally only three distinct isolation levels are implemented, --- i.e. PostgreSQL's `ReadUncommitted` mode behaves like `ReadCommitted`. --- This is because it is the only sensible way to map the standard isolation levels to --- PostgreSQL's multiversion concurrency control architecture. -data IsolationLevel - = Serializable - -- ^ Dirty read is not possible. - -- Nonrepeatable read is not possible. - -- Phantom read is not possible. - -- Serialization anomaly is not possible. - | RepeatableRead - -- ^ Dirty read is not possible. - -- Nonrepeatable read is not possible. - -- Phantom read is not possible. - -- Serialization anomaly is possible. - | ReadCommitted - -- ^ Dirty read is not possible. - -- Nonrepeatable read is possible. - -- Phantom read is possible. - -- Serialization anomaly is possible. - | ReadUncommitted - -- ^ Dirty read is not possible. - -- Nonrepeatable read is possible. - -- Phantom read is possible. - -- Serialization anomaly is possible. - deriving (Show, Eq) - --- | Render an `IsolationLevel`. -instance RenderSQL IsolationLevel where - renderSQL = \case - Serializable -> "SERIALIZABLE" - ReadCommitted -> "READ COMMITTED" - ReadUncommitted -> "READ UNCOMMITTED" - RepeatableRead -> "REPEATABLE READ" - --- | The transaction access mode determines whether the transaction is `ReadWrite` or `ReadOnly`. --- `ReadWrite` is the default. When a transaction is `ReadOnly`, --- the following SQL commands are disallowed: --- @INSERT@, @UPDATE@, @DELETE@, and @COPY FROM@ --- if the table they would write to is not a temporary table; --- all @CREATE@, @ALTER@, and @DROP@ commands; --- @COMMENT@, @GRANT@, @REVOKE@, @TRUNCATE@; --- and @EXPLAIN ANALYZE@ and @EXECUTE@ if the command they would execute is among those listed. --- This is a high-level notion of `ReadOnly` that does not prevent all writes to disk. -data AccessMode - = ReadWrite - | ReadOnly - deriving (Show, Eq) - --- | Render an `AccessMode`. -instance RenderSQL AccessMode where - renderSQL = \case - ReadWrite -> "READ WRITE" - ReadOnly -> "READ ONLY" - --- | The `Deferrable` transaction property has no effect --- unless the transaction is also `Serializable` and `ReadOnly`. --- When all three of these properties are selected for a transaction, --- the transaction may block when first acquiring its snapshot, --- after which it is able to run without the normal overhead of a --- `Serializable` transaction and without any risk of contributing --- to or being canceled by a serialization failure. --- This `longRunningMode` is well suited for long-running reports or backups. -data DeferrableMode - = Deferrable - | NotDeferrable - deriving (Show, Eq) - --- | Render a `DeferrableMode`. -instance RenderSQL DeferrableMode where - renderSQL = \case - Deferrable -> "DEFERRABLE" - NotDeferrable -> "NOT DEFERRABLE" diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Type.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Type.hs deleted file mode 100644 index a9ec0ea8..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Type.hs +++ /dev/null @@ -1,201 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Type -Description: types -Copyright: (c) Eitan Chatav, 2010 -Maintainer: eitan@morphism.tech -Stability: experimental - -storage newtypes --} -{-# LANGUAGE - AllowAmbiguousTypes - , DeriveAnyClass - , DeriveFoldable - , DeriveFunctor - , DeriveGeneric - , DeriveTraversable - , DerivingStrategies - , DefaultSignatures - , FlexibleContexts - , FlexibleInstances - , FunctionalDependencies - , GADTs - , LambdaCase - , MultiParamTypeClasses - , OverloadedStrings - , ScopedTypeVariables - , TypeApplications - , TypeFamilies - , TypeInType - , TypeOperators - , UndecidableInstances - , UndecidableSuperClasses -#-} - -module Squeal.PostgreSQL.Type - ( -- * Storage newtypes - Money (..) - , Json (..) - , Jsonb (..) - , Composite (..) - , Enumerated (..) - , VarArray (..) - , FixArray (..) - , VarChar, varChar, getVarChar - , FixChar, fixChar, getFixChar - , Only (..) - ) where - -import Data.Proxy -import Data.Int (Int64) -import GHC.TypeLits - -import qualified Data.Text as Strict (Text) -import qualified Data.Text as Strict.Text -import qualified GHC.Generics as GHC -import qualified Generics.SOP as SOP - --- $setup --- >>> import Squeal.PostgreSQL - -{- | The `Money` newtype stores a monetary value in terms -of the number of cents, i.e. @$2,000.20@ would be expressed as -@Money { cents = 200020 }@. - ->>> :kind! PG Money -PG Money :: PGType -= 'PGmoney --} -newtype Money = Money { cents :: Int64 } - deriving stock (Eq, Ord, Show, Read, GHC.Generic) - deriving anyclass (SOP.HasDatatypeInfo, SOP.Generic) - -{- | The `Json` newtype is an indication that the Haskell -type it's applied to should be stored as a -`Squeal.PostgreSQL.Type.Schema.PGjson`. - ->>> :kind! PG (Json [String]) -PG (Json [String]) :: PGType -= 'PGjson --} -newtype Json hask = Json {getJson :: hask} - deriving stock (Eq, Ord, Show, Read, GHC.Generic) - deriving anyclass (SOP.HasDatatypeInfo, SOP.Generic) - -{- | The `Jsonb` newtype is an indication that the Haskell -type it's applied to should be stored as a -`Squeal.PostgreSQL.Type.Schema.PGjsonb`. - ->>> :kind! PG (Jsonb [String]) -PG (Jsonb [String]) :: PGType -= 'PGjsonb --} -newtype Jsonb hask = Jsonb {getJsonb :: hask} - deriving stock (Eq, Ord, Show, Read, GHC.Generic) - deriving anyclass (SOP.HasDatatypeInfo, SOP.Generic) - -{- | The `Composite` newtype is an indication that the Haskell -type it's applied to should be stored as a -`Squeal.PostgreSQL.Type.Schema.PGcomposite`. - ->>> :{ -data Complex = Complex - { real :: Double - , imaginary :: Double - } deriving stock GHC.Generic - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) -:} - ->>> :kind! PG (Composite Complex) -PG (Composite Complex) :: PGType -= 'PGcomposite - '["real" ::: 'NotNull 'PGfloat8, - "imaginary" ::: 'NotNull 'PGfloat8] --} -newtype Composite record = Composite {getComposite :: record} - deriving stock (Eq, Ord, Show, Read, GHC.Generic) - deriving anyclass (SOP.HasDatatypeInfo, SOP.Generic) - -{- | The `Enumerated` newtype is an indication that the Haskell -type it's applied to should be stored as a -`Squeal.PostgreSQL.Type.Schema.PGenum`. - ->>> :kind! PG (Enumerated Ordering) -PG (Enumerated Ordering) :: PGType -= 'PGenum '["LT", "EQ", "GT"] --} -newtype Enumerated enum = Enumerated {getEnumerated :: enum} - deriving stock (Eq, Ord, Show, Read, GHC.Generic) - deriving anyclass (SOP.HasDatatypeInfo, SOP.Generic) - -{- | The `VarArray` newtype is an indication that the Haskell -type it's applied to should be stored as a -`Squeal.PostgreSQL.Type.Schema.PGvararray`. - ->>> import Data.Vector ->>> :kind! PG (VarArray (Vector Double)) -PG (VarArray (Vector Double)) :: PGType -= 'PGvararray ('NotNull 'PGfloat8) --} -newtype VarArray arr - = VarArray {getVarArray :: arr} - deriving stock (Eq, Ord, Show, Read, GHC.Generic) - deriving anyclass (SOP.HasDatatypeInfo, SOP.Generic) - -{- | The `FixArray` newtype is an indication that the Haskell -type it's applied to should be stored as a -`Squeal.PostgreSQL.Type.Schema.PGfixarray`. - ->>> :kind! PG (FixArray ((Double, Double), (Double, Double))) -PG (FixArray ((Double, Double), (Double, Double))) :: PGType -= 'PGfixarray '[2, 2] ('NotNull 'PGfloat8) --} -newtype FixArray arr = FixArray {getFixArray :: arr} - deriving stock (Eq, Ord, Show, Read, GHC.Generic) - deriving anyclass (SOP.HasDatatypeInfo, SOP.Generic) - --- | `Only` is a 1-tuple type, useful for encoding or decoding a singleton -newtype Only x = Only { fromOnly :: x } - deriving (Functor,Foldable,Traversable,Eq,Ord,Read,Show,GHC.Generic) -instance SOP.Generic (Only x) -instance SOP.HasDatatypeInfo (Only x) - -{- | Variable-length text type with limit - ->>> :kind! PG (VarChar 4) -PG (VarChar 4) :: PGType -= 'PGvarchar 4 --} -newtype VarChar (n :: Nat) = VarChar Strict.Text - deriving (Eq,Ord,Read,Show) - --- | Constructor for `VarChar` -varChar :: forall n . KnownNat n => Strict.Text -> Maybe (VarChar n) -varChar t = - if Strict.Text.length t <= fromIntegral (natVal @n Proxy) - then Just $ VarChar t - else Nothing - --- | Access the `Strict.Text` of a `VarChar` -getVarChar :: VarChar n -> Strict.Text -getVarChar (VarChar t) = t - -{- | Fixed-length, blank padded - ->>> :kind! PG (FixChar 4) -PG (FixChar 4) :: PGType -= 'PGchar 4 --} -newtype FixChar (n :: Nat) = FixChar Strict.Text - deriving (Eq,Ord,Read,Show) - --- | Constructor for `FixChar` -fixChar :: forall n . KnownNat n => Strict.Text -> Maybe (FixChar n) -fixChar t = - if Strict.Text.length t == fromIntegral (natVal @n Proxy) - then Just $ FixChar t - else Nothing - --- | Access the `Strict.Text` of a `FixChar` -getFixChar :: FixChar n -> Strict.Text -getFixChar (FixChar t) = t diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Type/Alias.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Type/Alias.hs deleted file mode 100644 index f7bd43da..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Type/Alias.hs +++ /dev/null @@ -1,345 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Type.Alias -Description: aliases -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -This module embeds Postgres's alias system in Haskell in -a typesafe fashion. Thanks to GHC's @OverloadedLabels@ extension, -Squeal can reference aliases by prepending with a @#@. --} - -{-# LANGUAGE - AllowAmbiguousTypes - , ConstraintKinds - , DeriveAnyClass - , DeriveGeneric - , FlexibleContexts - , FlexibleInstances - , FunctionalDependencies - , GADTs - , LambdaCase - , OverloadedStrings - , QuantifiedConstraints - , RankNTypes - , ScopedTypeVariables - , StandaloneDeriving - , TypeApplications - , TypeFamilyDependencies - , TypeInType - , TypeOperators - , UndecidableInstances - , UndecidableSuperClasses -#-} - -module Squeal.PostgreSQL.Type.Alias - ( -- * Aliases - (:::) - , Alias (..) - , IsLabel (..) - , Aliased (As) - , Aliasable (as) - , renderAliased - , mapAliased - , Has - , HasUnique - , HasErr - , HasAll - , HasIn - -- * Error reporting - , LookupFailedError - , PrettyPrintHaystack - , PrettyPrintInfo(..) - , MismatchError - -- * Qualified Aliases - , QualifiedAlias (..) - , IsQualified (..) - -- * Grouping - , Grouping (..) - , GroupedBy - ) where - -import Control.DeepSeq -import Data.ByteString (ByteString) -import Data.String (fromString) -import GHC.Exts (Any, Constraint) -import GHC.OverloadedLabels -import GHC.TypeLits - -import qualified Generics.SOP as SOP -import qualified GHC.Generics as GHC - -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Render - --- $setup --- >>> import Squeal.PostgreSQL - --- | The alias operator `:::` is like a promoted version of `As`, --- a type level pair between an alias and some type. -type (:::) (alias :: Symbol) ty = '(alias,ty) -infixr 6 ::: - - --- | `Grouping` is an auxiliary namespace, created by --- @GROUP BY@ clauses (`Squeal.PostgreSQL.Query.groupBy`), and used --- for typesafe aggregation -data Grouping - = Ungrouped -- ^ no aggregation permitted - | Grouped [(Symbol,Symbol)] -- ^ aggregation required for any column which is not grouped - -{- | A `GroupedBy` constraint indicates that a table qualified column is -a member of the auxiliary namespace created by @GROUP BY@ clauses and thus, -may be called in an output `Squeal.PostgreSQL.Expression.Expression` without aggregating. --} -class (KnownSymbol table, KnownSymbol column) - => GroupedBy table column bys where -instance {-# OVERLAPPING #-} (KnownSymbol table, KnownSymbol column) - => GroupedBy table column ('(table,column) ': bys) -instance {-# OVERLAPPABLE #-} - ( KnownSymbol table - , KnownSymbol column - , GroupedBy table column bys - ) => GroupedBy table column (tabcol ': bys) - --- | `Alias`es are proxies for a type level string or `Symbol` --- and have an `IsLabel` instance so that with @-XOverloadedLabels@ --- --- >>> :set -XOverloadedLabels --- >>> #foobar :: Alias "foobar" --- Alias -data Alias (alias :: Symbol) = Alias - deriving (Eq,GHC.Generic,Ord,Show,NFData) -instance alias1 ~ alias2 => IsLabel alias1 (Alias alias2) where - fromLabel = Alias -instance aliases ~ '[alias] => IsLabel alias (NP Alias aliases) where - fromLabel = fromLabel SOP.:* Nil --- | >>> printSQL (#jimbob :: Alias "jimbob") --- "jimbob" -instance KnownSymbol alias => RenderSQL (Alias alias) where - renderSQL = doubleQuoted . fromString . symbolVal - --- >>> printSQL (#jimbob :* #kandi :: NP Alias '["jimbob", "kandi"]) --- "jimbob", "kandi" -instance SOP.All KnownSymbol aliases => RenderSQL (NP Alias aliases) where - renderSQL - = commaSeparated - . SOP.hcollapse - . SOP.hcmap (SOP.Proxy @KnownSymbol) (SOP.K . renderSQL) - --- | The `As` operator is used to name an expression. `As` is like a demoted --- version of `:::`. --- --- >>> Just "hello" `As` #hi :: Aliased Maybe ("hi" ::: String) --- As (Just "hello") Alias -data Aliased expression aliased where - As - :: KnownSymbol alias - => expression ty - -> Alias alias - -> Aliased expression (alias ::: ty) -deriving instance Show (expression ty) - => Show (Aliased expression (alias ::: ty)) -deriving instance Eq (expression ty) - => Eq (Aliased expression (alias ::: ty)) -deriving instance Ord (expression ty) - => Ord (Aliased expression (alias ::: ty)) -instance (alias0 ~ alias1, alias0 ~ alias2, KnownSymbol alias2) - => IsLabel alias0 (Aliased Alias (alias1 ::: alias2)) where - fromLabel = fromLabel @alias2 `As` fromLabel @alias1 - --- | The `Aliasable` class provides a way to scrap your `Nil`s --- in an `NP` list of `Aliased` expressions. -class KnownSymbol alias => Aliasable alias expression aliased - | aliased -> expression - , aliased -> alias - where as :: expression -> Alias alias -> aliased -instance (KnownSymbol alias, aliased ~ (alias ::: ty)) => Aliasable alias - (expression ty) - (Aliased expression aliased) - where - as = As -instance (KnownSymbol alias, tys ~ '[alias ::: ty]) => Aliasable alias - (expression ty) - (NP (Aliased expression) tys) - where - expression `as` alias = expression `As` alias SOP.:* Nil - --- | >>> let renderMaybe = fromString . maybe "Nothing" (const "Just") --- >>> renderAliased renderMaybe (Just (3::Int) `As` #an_int) --- "Just AS \"an_int\"" -renderAliased - :: (forall ty. expression ty -> ByteString) - -> Aliased expression aliased - -> ByteString -renderAliased render (expression `As` alias) = - render expression <> " AS " <> renderSQL alias - --- | Map a function over an `Aliased` expression. -mapAliased - :: (expr x -> expr y) - -> Aliased expr (alias ::: x) - -> Aliased expr (alias ::: y) -mapAliased f (x `As` alias) = f x `As` alias - --- | @HasUnique alias fields field@ is a constraint that proves that --- @fields@ is a singleton of @alias ::: field@. -type HasUnique alias fields field = fields ~ '[alias ::: field] - --- | @Has alias fields field@ is a constraint that proves that --- @fields@ has a field of @alias ::: field@, inferring @field@ --- from @alias@ and @fields@. -class (KnownSymbol alias) => Has (alias :: Symbol) (fields :: [(Symbol, kind)]) (field :: kind) | alias fields -> field --- having these instances forces 'Has' to inspect 'alias' and 'fields' and thereby fail before delegating to --- 'HasErr', which means 'Has' shows up in error messages instead of 'HasErr' -instance {-# OVERLAPPING #-} (KnownSymbol alias, HasErr (alias ::: field0 ': fields) alias (alias ::: field0 ': fields) field1) - => Has alias (alias ::: field0 ': fields) field1 -instance {-# OVERLAPPABLE #-} (KnownSymbol alias, HasErr (field' ': fields) alias (field' ': fields) field) - => Has alias (field' ': fields) field -instance (KnownSymbol alias, HasErr '[] alias '[] field) - => Has alias '[] field - -{- | 'HasErr' is like `Has` except it also retains the original -list of fields being searched, so that error messages are more -useful. --} -class KnownSymbol alias => - HasErr (allFields :: [(Symbol, kind)]) (alias :: Symbol) (fields :: [(Symbol,kind)]) (field :: kind) - | alias fields -> field where -instance {-# OVERLAPPING #-} (KnownSymbol alias, field0 ~ field1, MismatchError alias allFields field0 field1) - => HasErr allFields alias (alias ::: field0 ': fields) field1 -instance {-# OVERLAPPABLE #-} (KnownSymbol alias, HasErr allFields alias fields field) - => HasErr allFields alias (field' ': fields) field -instance ( KnownSymbol alias - , LookupFailedError alias allFields -- report a nicer error - , field ~ Any -- required to satisfy the fundep - ) => HasErr allFields alias '[] field - --- | @MismatchError@ reports a nicer error with more context when we successfully do a lookup but --- find a different field than we expected. As a type family, it ensures that we only do the (expensive) --- calculation of coming up with our pretty printing information when we actually have a mismatch -type family MismatchError (alias :: Symbol) (fields :: [(Symbol, kind)]) (found :: kind) (expected :: kind) :: Constraint where - MismatchError _ _ found found = () - MismatchError alias fields found expected = MismatchError' (MismatchError' () (DefaultPrettyPrinter fields) alias fields found expected) (PrettyPrintHaystack fields) alias fields found expected - --- | @MismatchError'@ is the workhorse behind @MismatchError@, but taking an additional type as the first argument. We can put another type error --- in there which will only show if @MismatchError'@ is stuck; this allows us to fall back to @DefaultPrettyPrinter@ when a @PrettyPrintHaystack@ instance --- is missing -type family MismatchError' (err :: Constraint) (ppInfo :: PrettyPrintInfo) (alias :: Symbol) (fields :: [(Symbol, kind)]) (found :: kind) (expected :: kind) :: Constraint where - MismatchError' _ ('PrettyPrintInfo needleName haystackName _) alias fields found expected = TypeError - ( 'Text "Type mismatch when looking up " ':<>: needleName ':<>: 'Text " named " ':<>: 'ShowType alias - ':$$: 'Text "in " ':<>: haystackName ':<>: 'Text ":" - -- we don't use a pretty haystack because we want to show the values - ':$$: 'ShowType fields - ':$$: 'Text "" - ':$$: 'Text "Expected: " ':<>: 'ShowType expected - ':$$: 'Text "But found: " ':<>: 'ShowType found - ':$$: 'Text "" - ) - --- | @LookupFailedError@ reports a nicer error when we fail to look up some @needle@ in some @haystack@ -type LookupFailedError needle haystack = LookupFailedError' (LookupFailedError' () (DefaultPrettyPrinter haystack) needle haystack) (PrettyPrintHaystack haystack) needle haystack - --- | @LookupFailedError'@ is the workhorse behind @LookupFailedError@, but taking an additional type as the first argument. We can put another type error --- in there which will only show if @LookupFailedError'@ is stuck; this allows us to fall back to @DefaultPrettyPrinter@ when a @PrettyPrintHaystack@ instance --- is missing -type family LookupFailedError' (fallbackForUnknownKind :: Constraint) (prettyPrintInfo :: PrettyPrintInfo) (needle :: Symbol) (haystack :: [(Symbol, k)]) :: Constraint where - LookupFailedError' _ ('PrettyPrintInfo needleName haystackName prettyHaystack) needle rawHaystack = TypeError - ( 'Text "Could not find " ':<>: needleName ':<>: 'Text " named " ':<>: 'ShowType needle - ':$$: 'Text "in " ':<>: haystackName ':<>: 'Text ":" - ':$$: prettyHaystack - ':$$: 'Text "" - ':$$: 'Text "*Raw " ':<>: haystackName ':<>: 'Text "*:" - ':$$: 'ShowType rawHaystack - ':$$: 'Text "" - ) - --- | @PrettyPrintInfo@ is a data type intended to be used at the type level --- which describes how to pretty print a haystack in our custom errors. The general intention is we use @PrettyPrintHaystack@ --- to define a more specific way of pretty printing our error information for each kind that we care about -data PrettyPrintInfo = PrettyPrintInfo - { _needleName :: ErrorMessage - , _haystackName :: ErrorMessage - , _haystackPrettyPrint :: ErrorMessage - } - --- | 'PrettyPrintHaystack' allows us to use the kind of our haystack to come up --- with nicer errors. It is implemented as an open type family for dependency reasons -type family PrettyPrintHaystack (haystack :: [(Symbol, k)]) :: PrettyPrintInfo - --- | @DefaultPrettyPrinter@ provides a default we can use for kinds that don't provide an instance of @PrettyPrintInfo@, --- although that should generally only be accidental -type family DefaultPrettyPrinter (haystack :: [(Symbol, k)]) :: PrettyPrintInfo where - DefaultPrettyPrinter (haystack :: [(Symbol, k)]) = 'PrettyPrintInfo - ('Text "some kind without a PrettyPrintHaystack instance (" ':<>: 'ShowType k ':<>: 'Text ")") - ('Text "associative list of that kind ([(Symbol, " ':<>: 'ShowType k ':<>: 'Text ")])") - ('ShowType (Sort (MapFst haystack))) - -{-| @HasIn fields (alias ::: field)@ is a constraint that proves that -@fields@ has a field of @alias ::: field@. It is used in @UPDATE@s to -choose which subfields to update. --} -class HasIn fields field where -instance (Has alias fields field) => HasIn fields (alias ::: field) where - --- | `HasAll` extends `Has` to take lists of @aliases@ and @fields@ and infer --- a list of @subfields@. -class - ( SOP.All KnownSymbol aliases - ) => HasAll - (aliases :: [Symbol]) - (fields :: [(Symbol,kind)]) - (subfields :: [(Symbol,kind)]) - | aliases fields -> subfields where -instance {-# OVERLAPPING #-} HasAll '[] fields '[] -instance {-# OVERLAPPABLE #-} - (Has alias fields field, HasAll aliases fields subfields) - => HasAll (alias ': aliases) fields (alias ::: field ': subfields) - --- | Analagous to `IsLabel`, the constraint --- `IsQualified` defines `!` for a column alias qualified --- by a table alias. -class IsQualified qualifier alias expression where - (!) :: Alias qualifier -> Alias alias -> expression - infixl 9 ! -instance IsQualified qualifier alias (Alias qualifier, Alias alias) where - (!) = (,) - -{-| `QualifiedAlias`es enables multi-schema support by allowing a reference -to a `Squeal.PostgreSQL.Type.Schema.Table`, `Squeal.PostgreSQL.Type.Schema.Typedef` -or `Squeal.PostgreSQL.Type.Schema.View` to be qualified by their schemas. By default, -a qualifier of @public@ is provided. - ->>> :{ -let - alias1 :: QualifiedAlias "sch" "tab" - alias1 = #sch ! #tab - alias2 :: QualifiedAlias "public" "vw" - alias2 = #vw -in printSQL alias1 >> printSQL alias2 -:} -"sch"."tab" -"vw" --} -data QualifiedAlias (qualifier :: Symbol) (alias :: Symbol) = QualifiedAlias - deriving (Eq,GHC.Generic,Ord,Show,NFData) -instance (q ~ q', a ~ a') => IsQualified q a (QualifiedAlias q' a') where - _ ! _ = QualifiedAlias -instance (q' ~ "public", a ~ a') => IsLabel a (QualifiedAlias q' a') where - fromLabel = QualifiedAlias -instance (q0 ~ q1, a0 ~ a1, a1 ~ a2, KnownSymbol a2) => - IsQualified q0 a0 (Aliased (QualifiedAlias q1) (a1 ::: a2)) where - _ ! _ = QualifiedAlias `As` Alias -instance (q ~ "public", a0 ~ a1, a1 ~ a2, KnownSymbol a2) => - IsLabel a0 (Aliased (QualifiedAlias q) (a1 ::: a2)) where - fromLabel = QualifiedAlias `As` Alias - -instance (KnownSymbol q, KnownSymbol a) - => RenderSQL (QualifiedAlias q a) where - renderSQL _ = - let - qualifier = renderSQL (Alias @q) - alias = renderSQL (Alias @a) - in - if qualifier == "\"public\"" then alias else qualifier <> "." <> alias diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Type/List.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Type/List.hs deleted file mode 100644 index f3ce37e1..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Type/List.hs +++ /dev/null @@ -1,199 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Type.List -Description: list related types and functions -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -Haskell singly-linked lists are very powerful. This module -provides functionality for type-level lists, heterogeneous -lists and type aligned lists. --} - -{-# LANGUAGE - DataKinds - , FlexibleContexts - , GADTs - , LambdaCase - , MultiParamTypeClasses - , OverloadedStrings - , PolyKinds - , QuantifiedConstraints - , RankNTypes - , ScopedTypeVariables - , TypeApplications - , TypeFamilies - , TypeOperators - , UndecidableInstances -#-} - -module Squeal.PostgreSQL.Type.List - ( -- * Heterogeneous List - SOP.NP (..) - , (*:) - , one - , Join - , disjoin - , Additional (..) - -- * Path - , Path (..) - -- * Type Level List - , Elem - , In - , Length - , MapFst - , Sort - , SubList - , SubsetList - ) where - -import Control.Category.Free -import Data.Function ((&)) -import Data.Kind -import Data.Type.Bool -import GHC.TypeLits - -import Generics.SOP as SOP - --- | `Join` is simply promoted `++` and is used in @JOIN@s in --- `Squeal.PostgreSQL.Query.FromClause`s. -type family Join xs ys where - Join '[] ys = ys - Join (x ': xs) ys = x ': Join xs ys - --- | `disjoin` is a utility function for splitting an `NP` list into pieces. -disjoin - :: forall xs ys expr. SListI xs - => NP expr (Join xs ys) - -> (NP expr xs, NP expr ys) -disjoin = case sList @xs of - SNil -> \ys -> (Nil, ys) - SCons -> \(x :* xsys) -> - case disjoin xsys of (xs,ys) -> (x :* xs, ys) - --- | The `Additional` class is for appending --- type-level list parameterized constructors such as `NP`, --- `Squeal.PostgreSQL.Query.Selection`, and `Squeal.PostgreSQL.Query.FromClause`. -class Additional expr where - also :: expr ys -> expr xs -> expr (Join xs ys) -instance Additional (NP expr) where - also ys = \case - Nil -> ys - x :* xs -> x :* (xs & also ys) - --- | A useful operator for ending an `NP` list of length --- at least 2 without `Nil` -(*:) :: f x -> f y -> NP f '[x,y] -x *: y = x :* y :* Nil -infixl 8 *: - --- | A list of length `one`. -one :: f x -> NP f '[x] -one f = f :* Nil - --- | @Elem@ is a promoted `Data.List.elem`. -type family Elem x xs where - Elem x '[] = 'False - Elem x (x ': _) = 'True - Elem x (_ ': xs) = Elem x xs - --- | @In x xs@ is a constraint that proves that @x@ is in @xs@. -type family In x xs :: Constraint where - In x xs = If (Elem x xs) (() :: Constraint) - (TypeError ('ShowType x ':<>: 'Text " is not in " ':<>: 'ShowType xs)) - -{- | Calculate the `Length` of a type level list - ->>> :kind! Length '[Char,String,Bool,Double] -Length '[Char,String,Bool,Double] :: Nat -= 4 --} -type family Length (xs :: [k]) :: Nat where - Length '[] = 0 - Length (_ : xs) = 1 + Length xs - -{- | `SubList` checks that one type level list is a sublist of another, -with the same ordering. - ->>> :kind! SubList '[1,2,3] '[4,5,6] -SubList '[1,2,3] '[4,5,6] :: Bool -= 'False ->>> :kind! SubList '[1,2,3] '[1,2,3,4] -SubList '[1,2,3] '[1,2,3,4] :: Bool -= 'True ->>> :kind! SubList '[1,2,3] '[0,1,0,2,0,3] -SubList '[1,2,3] '[0,1,0,2,0,3] :: Bool -= 'True ->>> :kind! SubList '[1,2,3] '[3,2,1] -SubList '[1,2,3] '[3,2,1] :: Bool -= 'False --} -type family SubList (xs :: [k]) (ys :: [k]) :: Bool where - SubList '[] ys = 'True - SubList (x ': xs) '[] = 'False - SubList (x ': xs) (x ': ys) = SubList xs ys - SubList (x ': xs) (y ': ys) = SubList (x ': xs) ys - -{- | `SubsetList` checks that one type level list is a subset of another, -regardless of ordering and repeats. - ->>> :kind! SubsetList '[1,2,3] '[4,5,6] -SubsetList '[1,2,3] '[4,5,6] :: Bool -= 'False ->>> :kind! SubsetList '[1,2,3] '[1,2,3,4] -SubsetList '[1,2,3] '[1,2,3,4] :: Bool -= 'True ->>> :kind! SubsetList '[1,2,3] '[0,1,0,2,0,3] -SubsetList '[1,2,3] '[0,1,0,2,0,3] :: Bool -= 'True ->>> :kind! SubsetList '[1,2,3] '[3,2,1] -SubsetList '[1,2,3] '[3,2,1] :: Bool -= 'True ->>> :kind! SubsetList '[1,1,1] '[3,2,1] -SubsetList '[1,1,1] '[3,2,1] :: Bool -= 'True --} -type family SubsetList (xs :: [k]) (ys :: [k]) :: Bool where - SubsetList '[] ys = 'True - SubsetList (x ': xs) ys = Elem x ys && SubsetList xs ys - --- | 'Sort' sorts a type level list of 'Symbol's in ascending lexicographic order -type Sort ls = MergeSort (Twos ls) - --- | 'MergeSort' is the workhorse behind 'Sort' -type family MergeSort (ls :: [[Symbol]]) :: [Symbol] where - MergeSort '[] = '[] - MergeSort '[x] = x - MergeSort ls = MergeSort (FoldMerge ls) - --- | @Two@s splits a type-level list into a list of sorted lists of length 2 (with a singelton list potentially at the end) --- It is required for implementing 'MergeSort' -type family Twos (ls :: [k]) :: [[k]] where - Twos (x ': y ': rs) = Merge '[x] '[y] ': Twos rs - Twos '[x] = '[ '[x]] - Twos '[] = '[] - --- | 'Merge' two sorted lists into one list -type family Merge (ls :: [Symbol]) (rs :: [Symbol]) :: [Symbol] where - Merge '[] r = r - Merge l '[] = l - Merge (l ': ls) (r ': rs) = If (Leq l r) (l ': Merge ls (r ': rs)) (r ': Merge (l ': ls) rs) - --- | 'FoldMerge' folds over a list of sorted lists, merging them into a single sorted list -type family FoldMerge (ls :: [[Symbol]]) :: [[Symbol]] where - FoldMerge (x ': y ': rs) = (Merge x y ': FoldMerge rs) - FoldMerge '[x] = '[x] - FoldMerge '[] = '[] - -type Leq l r = OrderingIsLeq (CmpSymbol l r) - -type family OrderingIsLeq (o :: Ordering) :: Bool where - OrderingIsLeq 'LT = 'True - OrderingIsLeq 'EQ = 'True - OrderingIsLeq 'GT = 'False - --- | 'MapFst' takes the first value of each tuple of a type level list of tuples. Useful for getting --- only the names in associatve lists -type family MapFst (ls :: [(j, k)]) :: [j] where - MapFst ('(j, _) ': rest) = j ': MapFst rest - MapFst '[] = '[] diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Type/PG.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Type/PG.hs deleted file mode 100644 index af29cac3..00000000 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Type/PG.hs +++ /dev/null @@ -1,336 +0,0 @@ -{-| -Module: Squeal.PostgreSQL.Type.PG -Description: embedding of Haskell types into Postgres type system -Copyright: (c) Eitan Chatav, 2010 -Maintainer: eitan@morphism.tech -Stability: experimental - -Provides type families for turning Haskell `Type`s -into corresponding Postgres types. --} -{-# LANGUAGE - AllowAmbiguousTypes - , DeriveAnyClass - , DeriveFoldable - , DeriveFunctor - , DeriveGeneric - , DeriveTraversable - , DerivingStrategies - , DefaultSignatures - , FlexibleContexts - , FlexibleInstances - , FunctionalDependencies - , GADTs - , LambdaCase - , MultiParamTypeClasses - , OverloadedStrings - , ScopedTypeVariables - , TypeApplications - , TypeFamilies - , TypeInType - , TypeOperators - , UndecidableInstances - , UndecidableSuperClasses -#-} - -module Squeal.PostgreSQL.Type.PG - ( -- * PG - IsPG (..) - , NullPG - , TuplePG - , RowPG - -- * Type families - , LabelsPG - , DimPG - , FixPG - , TupleOf - , TupleCodeOf - , RowOf - , ConstructorsOf - , ConstructorNameOf - , ConstructorNamesOf - ) where - -import Data.Aeson (Value) -import Data.Functor.Const (Const) -import Data.Functor.Constant (Constant) -import Data.Kind (Type) -import Data.Int (Int16, Int32, Int64) -import Data.Scientific (Scientific) -import Data.Time (Day, DiffTime, LocalTime, TimeOfDay, TimeZone, UTCTime) -import Data.Vector (Vector) -import Data.UUID.Types (UUID) -import GHC.TypeLits -import Network.IP.Addr (NetAddr, IP) - -import qualified Data.ByteString.Lazy as Lazy (ByteString) -import qualified Data.ByteString as Strict (ByteString) -import qualified Data.Text.Lazy as Lazy (Text) -import qualified Data.Text as Strict (Text) -import qualified Database.PostgreSQL.LibPQ as LibPQ -import qualified Generics.SOP as SOP -import qualified Generics.SOP.Record as SOP -import qualified Generics.SOP.Type.Metadata as Type - -import Squeal.PostgreSQL.Type -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Type.Schema - --- $setup --- >>> import Squeal.PostgreSQL --- >>> import Data.Text (Text) --- >>> import qualified GHC.Generics as GHC - -{- | The `PG` type family embeds a subset of Haskell types -as Postgres types. As an open type family, `PG` is extensible. - ->>> :kind! PG LocalTime -PG LocalTime :: PGType -= 'PGtimestamp - -The preferred way to generate `PG`s of your own type is through -generalized newtype deriving or via deriving. - ->>> newtype UserId = UserId {getUserId :: UUID} deriving newtype IsPG - ->>> :kind! PG UserId -PG UserId :: PGType -= 'PGuuid - ->>> :{ -data Answer = Yes | No - deriving stock GHC.Generic - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) - deriving IsPG via Enumerated Answer -:} - ->>> :kind! PG Answer -PG Answer :: PGType -= 'PGenum '["Yes", "No"] - ->>> :{ -data Complex = Complex {real :: Double, imaginary :: Double} - deriving stock GHC.Generic - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) - deriving IsPG via Composite Complex -:} - ->>> :kind! PG Complex -PG Complex :: PGType -= 'PGcomposite - '["real" ::: 'NotNull 'PGfloat8, - "imaginary" ::: 'NotNull 'PGfloat8] --} -class IsPG (hask :: Type) where type PG hask :: PGType --- | `PGbool` -instance IsPG Bool where type PG Bool = 'PGbool --- | `PGint2` -instance IsPG Int16 where type PG Int16 = 'PGint2 --- | `PGint4` -instance IsPG Int32 where type PG Int32 = 'PGint4 --- | `PGint8` -instance IsPG Int64 where type PG Int64 = 'PGint8 --- | `PGint2` -instance IsPG LibPQ.Oid where type PG LibPQ.Oid = 'PGoid --- | `PGnumeric` -instance IsPG Scientific where type PG Scientific = 'PGnumeric --- | `PGfloat4` -instance IsPG Float where type PG Float = 'PGfloat4 --- | `PGfloat8` -instance IsPG Double where type PG Double = 'PGfloat8 --- | `PGchar` @1@ -instance IsPG Char where type PG Char = 'PGchar 1 --- | `PGtext` -instance IsPG Strict.Text where type PG Strict.Text = 'PGtext --- | `PGtext` -instance IsPG Lazy.Text where type PG Lazy.Text = 'PGtext --- | `PGtext` -instance IsPG String where type PG String = 'PGtext --- | `PGbytea` -instance IsPG Strict.ByteString where type PG Strict.ByteString = 'PGbytea --- | `PGbytea` -instance IsPG Lazy.ByteString where type PG Lazy.ByteString = 'PGbytea --- | `PGtimestamp` -instance IsPG LocalTime where type PG LocalTime = 'PGtimestamp --- | `PGtimestamptz` -instance IsPG UTCTime where type PG UTCTime = 'PGtimestamptz --- | `PGdate` -instance IsPG Day where type PG Day = 'PGdate --- | `PGtime` -instance IsPG TimeOfDay where type PG TimeOfDay = 'PGtime --- | `PGtimetz` -instance IsPG (TimeOfDay, TimeZone) where type PG (TimeOfDay, TimeZone) = 'PGtimetz --- | `PGinterval` -instance IsPG DiffTime where type PG DiffTime = 'PGinterval --- | `PGuuid` -instance IsPG UUID where type PG UUID = 'PGuuid --- | `PGinet` -instance IsPG (NetAddr IP) where type PG (NetAddr IP) = 'PGinet --- | `PGjson` -instance IsPG Value where type PG Value = 'PGjson --- | `PGvarchar` -instance IsPG (VarChar n) where type PG (VarChar n) = 'PGvarchar n --- | `PGvarchar` -instance IsPG (FixChar n) where type PG (FixChar n) = 'PGchar n --- | `PG hask` -instance IsPG hask => IsPG (Const hask tag) where type PG (Const hask tag) = PG hask --- | `PG hask` -instance IsPG hask => IsPG (SOP.K hask tag) where type PG (SOP.K hask tag) = PG hask --- | `PG hask` -instance IsPG hask => IsPG (Constant hask tag) where type PG (Constant hask tag) = PG hask - --- | `PGmoney` -instance IsPG Money where type PG Money = 'PGmoney --- | `PGjson` -instance IsPG (Json hask) where type PG (Json hask) = 'PGjson --- | `PGjsonb` -instance IsPG (Jsonb hask) where type PG (Jsonb hask) = 'PGjsonb --- | `PGcomposite` @(@`RowPG` @hask)@ -instance IsPG (Composite hask) where - type PG (Composite hask) = 'PGcomposite (RowPG hask) --- | `PGenum` @(@`LabelsPG` @hask)@ -instance IsPG (Enumerated hask) where - type PG (Enumerated hask) = 'PGenum (LabelsPG hask) --- | `PGvararray` @(@`NullPG` @x)@ -instance IsPG (VarArray (Vector x)) where - type PG (VarArray (Vector x)) = 'PGvararray (NullPG x) --- | `PGvararray` @(@`NullPG` @x)@ -instance IsPG (VarArray [x]) where - type PG (VarArray [x]) = 'PGvararray (NullPG x) --- | `PGfixarray` @(@`DimPG` @hask) (@`FixPG` @hask)@ -instance IsPG (FixArray hask) where - type PG (FixArray hask) = 'PGfixarray (DimPG hask) (FixPG hask) - -{-| The `LabelsPG` type family calculates the constructors of a -Haskell enum type. - ->>> data Schwarma = Beef | Lamb | Chicken deriving GHC.Generic ->>> instance SOP.Generic Schwarma ->>> instance SOP.HasDatatypeInfo Schwarma ->>> :kind! LabelsPG Schwarma -LabelsPG Schwarma :: [Type.ConstructorName] -= '["Beef", "Lamb", "Chicken"] --} -type family LabelsPG (hask :: Type) :: [Type.ConstructorName] where - LabelsPG hask = - ConstructorNamesOf (ConstructorsOf (SOP.DatatypeInfoOf hask)) - -{- | -`RowPG` turns a Haskell `Type` into a `RowType`. - -`RowPG` may be applied to normal Haskell record types provided they -have `SOP.Generic` and `SOP.HasDatatypeInfo` instances; - ->>> data Person = Person { name :: Strict.Text, age :: Int32 } deriving GHC.Generic ->>> instance SOP.Generic Person ->>> instance SOP.HasDatatypeInfo Person ->>> :kind! RowPG Person -RowPG Person :: [(Symbol, NullType)] -= '["name" ::: 'NotNull 'PGtext, "age" ::: 'NotNull 'PGint4] --} -type family RowPG (hask :: Type) :: RowType where - RowPG hask = RowOf (SOP.RecordCodeOf hask) - --- | `RowOf` applies `NullPG` to the fields of a list. -type family RowOf (record :: [(Symbol, Type)]) :: RowType where - RowOf (col ::: ty ': record) = col ::: NullPG ty ': RowOf record - RowOf '[] = '[] - -{- | `NullPG` turns a Haskell type into a `NullType`. - ->>> :kind! NullPG Double -NullPG Double :: NullType -= 'NotNull 'PGfloat8 ->>> :kind! NullPG (Maybe Double) -NullPG (Maybe Double) :: NullType -= 'Null 'PGfloat8 --} -type family NullPG (hask :: Type) :: NullType where - NullPG (Maybe hask) = 'Null (PG hask) - NullPG hask = 'NotNull (PG hask) - -{- | `TuplePG` turns a Haskell tuple type (including record types) into -the corresponding list of `NullType`s. - ->>> :kind! TuplePG (Double, Maybe Char) -TuplePG (Double, Maybe Char) :: [NullType] -= '[ 'NotNull 'PGfloat8, 'Null ('PGchar 1)] --} -type family TuplePG (hask :: Type) :: [NullType] where - TuplePG hask = TupleOf (TupleCodeOf hask (SOP.Code hask)) - --- | `TupleOf` turns a list of Haskell `Type`s into a list of `NullType`s. -type family TupleOf (tuple :: [Type]) :: [NullType] where - TupleOf (hask ': tuple) = NullPG hask ': TupleOf tuple - TupleOf '[] = '[] - --- | `TupleCodeOf` takes the `SOP.Code` of a haskell `Type` --- and if it's a simple product returns it, otherwise giving a `TypeError`. -type family TupleCodeOf (hask :: Type) (code :: [[Type]]) :: [Type] where - TupleCodeOf hask '[tuple] = tuple - TupleCodeOf hask '[] = - TypeError - ( 'Text "The type `" ':<>: 'ShowType hask ':<>: 'Text "' is not a tuple type." - ':$$: 'Text "It is a void type with no constructors." - ) - TupleCodeOf hask (_ ': _ ': _) = - TypeError - ( 'Text "The type `" ':<>: 'ShowType hask ':<>: 'Text "' is not a tuple type." - ':$$: 'Text "It is a sum type with more than one constructor." - ) - --- | Calculates constructors of a datatype. -type family ConstructorsOf (datatype :: Type.DatatypeInfo) - :: [Type.ConstructorInfo] where - ConstructorsOf ('Type.ADT _module _datatype constructors _strictness) = - constructors - ConstructorsOf ('Type.Newtype _module _datatype constructor) = - '[constructor] - --- | Calculates the name of a nullary constructor, otherwise --- generates a type error. -type family ConstructorNameOf (constructor :: Type.ConstructorInfo) - :: Type.ConstructorName where - ConstructorNameOf ('Type.Constructor name) = name - ConstructorNameOf ('Type.Infix name _assoc _fix) = TypeError - ('Text "ConstructorNameOf error: non-nullary constructor " - ':<>: 'Text name) - ConstructorNameOf ('Type.Record name _fields) = TypeError - ('Text "ConstructorNameOf error: non-nullary constructor " - ':<>: 'Text name) - --- | Calculate the names of nullary constructors. -type family ConstructorNamesOf (constructors :: [Type.ConstructorInfo]) - :: [Type.ConstructorName] where - ConstructorNamesOf '[] = '[] - ConstructorNamesOf (constructor ': constructors) = - ConstructorNameOf constructor ': ConstructorNamesOf constructors - --- | `DimPG` turns Haskell nested homogeneous tuples into a list of lengths, --- up to a depth of 10 for each dimension. -type family DimPG (hask :: Type) :: [Nat] where - DimPG (x,x) = 2 ': DimPG x - DimPG (x,x,x) = 3 ': DimPG x - DimPG (x,x,x,x) = 4 ': DimPG x - DimPG (x,x,x,x,x) = 5 ': DimPG x - DimPG (x,x,x,x,x,x) = 6 ': DimPG x - DimPG (x,x,x,x,x,x,x) = 7 ': DimPG x - DimPG (x,x,x,x,x,x,x,x) = 8 ': DimPG x - DimPG (x,x,x,x,x,x,x,x,x) = 9 ': DimPG x - DimPG (x,x,x,x,x,x,x,x,x,x) = 10 ': DimPG x - DimPG x = '[] - --- | `FixPG` extracts `NullPG` of the base type of nested homogeneous tuples, --- up to a depth of 10 for each dimension. -type family FixPG (hask :: Type) :: NullType where - FixPG (x,x) = FixPG x - FixPG (x,x,x) = FixPG x - FixPG (x,x,x,x) = FixPG x - FixPG (x,x,x,x,x) = FixPG x - FixPG (x,x,x,x,x,x) = FixPG x - FixPG (x,x,x,x,x,x,x) = FixPG x - FixPG (x,x,x,x,x,x,x,x) = FixPG x - FixPG (x,x,x,x,x,x,x,x,x) = FixPG x - FixPG (x,x,x,x,x,x,x,x,x,x) = FixPG x - FixPG (x,x,x,x,x,x,x,x,x,x,x) = FixPG x - FixPG x = NullPG x diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Type/Schema.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Type/Schema.hs index 681acfdf..8431aa39 100644 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Type/Schema.hs +++ b/squeal-postgresql/src/Squeal/PostgreSQL/Type/Schema.hs @@ -36,102 +36,11 @@ It also defines useful type families to operate on these. module Squeal.PostgreSQL.Type.Schema ( -- * Postgres Type PGType (..) - , NullType (..) - , RowType - , FromType - -- * Schema Type - , ColumnType - , ColumnsType - , TableType - , SchemumType (..) - , IndexType (..) - , FunctionType - , ReturnsType (..) - , SchemaType - , SchemasType - , PrettyPrintPartitionedSchema - , Public - , PartitionedSchema(..) - , PartitionSchema - , SchemaFunctions - , SchemaIndexes - , SchemaProcedures - , SchemaTables - , SchemaTypes - , SchemaUnsafes - , SchemaViews - -- * Database Subsets - , SubDB - , SubsetDB - , ElemDB - -- * Constraint - , (:=>) - , Optionality (..) - , TableConstraint (..) - , TableConstraints - , Uniquely - -- * Enumerated Label - , IsPGlabel (..) - , PGlabel (..) - -- * Data Definition - , Create - , CreateIfNotExists - , CreateOrReplace - , Drop - , DropSchemum - , DropIfExists - , DropSchemumIfExists - , Alter - , AlterIfExists - , Rename - , RenameIfExists - , SetSchema - , ConstraintInvolves - , DropIfConstraintsInvolve - -- * Type Classification - , PGNum - , PGIntegral - , PGFloating - , PGJsonType - , PGJsonKey - , SamePGType - , AllNotNull - , NotAllNull - -- * Nullification - , NullifyType - , NullifyRow - , NullifyFrom - -- * Table Conversion - , TableToColumns - , ColumnsToRow - , TableToRow - -- * Updatable - , Updatable - , AllUnique - , IsNotElem - -- * User Types - , UserType - , UserTypeName - , UserTypeNamespace ) where -import Control.Category -import Data.Kind -import Data.Monoid hiding (All) -import Data.Type.Bool -import Generics.SOP -import GHC.TypeLits -import Prelude hiding (id, (.)) - -import Squeal.PostgreSQL.Type.Alias -import Squeal.PostgreSQL.Type.List -import Squeal.PostgreSQL.Render - --- $setup --- >>> import Squeal.PostgreSQL - -- | `PGType` is the promoted datakind of PostgreSQL types. -- +-- >>> :set -XDataKinds -- >>> :kind 'PGbool -- 'PGbool :: PGType data PGType @@ -140,634 +49,3 @@ data PGType | PGint4 -- ^ signed four-byte integer | PGint8 -- ^ signed eight-byte integer | PGnumeric -- ^ arbitrary precision numeric type - | PGfloat4 -- ^ single precision floating-point number (4 bytes) - | PGfloat8 -- ^ double precision floating-point number (8 bytes) - | PGmoney -- ^ currency amount - | PGchar Nat -- ^ fixed-length character string - | PGvarchar Nat -- ^ variable-length character string - | PGtext -- ^ variable-length character string - | PGbytea -- ^ binary data ("byte array") - | PGtimestamp -- ^ date and time (no time zone) - | PGtimestamptz -- ^ date and time, including time zone - | PGdate -- ^ calendar date (year, month, day) - | PGtime -- ^ time of day (no time zone) - | PGtimetz -- ^ time of day, including time zone - | PGinterval -- ^ time span - | PGuuid -- ^ universally unique identifier - | PGinet -- ^ IPv4 or IPv6 host address - | PGjson -- ^ textual JSON data - | PGjsonb -- ^ binary JSON data, decomposed - | PGvararray NullType -- ^ variable length array - | PGfixarray [Nat] NullType -- ^ fixed length array - | PGenum [Symbol] -- ^ enumerated (enum) types are data types that comprise a static, ordered set of values. - | PGcomposite RowType -- ^ a composite type represents the structure of a row or record; it is essentially just a list of field names and their data types. - | PGtsvector -- ^ A tsvector value is a sorted list of distinct lexemes, which are words that have been normalized to merge different variants of the same word. - | PGtsquery -- ^ A tsquery value stores lexemes that are to be searched for. - | PGoid -- ^ Object identifiers (OIDs) are used internally by PostgreSQL as primary keys for various system tables. - | PGrange PGType -- ^ Range types are data types representing a range of values of some element type (called the range's subtype). - | UnsafePGType Symbol -- ^ an escape hatch for unsupported PostgreSQL types - --- | `NullType` encodes the potential presence or definite absence of a --- @NULL@ allowing operations which are sensitive to such to be well typed. --- --- >>> :kind 'Null 'PGint4 --- 'Null 'PGint4 :: NullType --- >>> :kind 'NotNull ('PGvarchar 50) --- 'NotNull ('PGvarchar 50) :: NullType -data NullType - = Null PGType -- ^ @NULL@ may be present - | NotNull PGType -- ^ @NULL@ is absent - --- | The constraint operator, `:=>` is a type level pair --- between a "constraint" and some type, for use in pairing --- an `Optionality` with a `NullType` to produce a `ColumnType` --- or a `TableConstraints` and a `ColumnsType` to produce a `TableType`. -type (:=>) constraint ty = '(constraint,ty) -infixr 7 :=> - --- | `Optionality` encodes the availability of @DEFAULT@ for inserts and updates. --- A column can be assigned a default value. --- A data `Squeal.PostgreSQL.Manipulations.Manipulation` command can also --- request explicitly that a column be set to its default value, --- without having to know what that value is. -data Optionality - = Def -- ^ @DEFAULT@ is available for inserts and updates - | NoDef -- ^ @DEFAULT@ is unavailable for inserts and updates - --- | `ColumnType` encodes the allowance of @DEFAULT@ and @NULL@ and the --- base `PGType` for a column. --- --- >>> :set -XTypeFamilies -XTypeInType --- >>> import GHC.TypeLits --- >>> type family IdColumn :: ColumnType where IdColumn = 'Def :=> 'NotNull 'PGint4 --- >>> type family EmailColumn :: ColumnType where EmailColumn = 'NoDef :=> 'Null 'PGtext -type ColumnType = (Optionality,NullType) - --- | `ColumnsType` is a row of `ColumnType`s. --- --- >>> :{ --- type family UsersColumns :: ColumnsType where --- UsersColumns = --- '[ "name" ::: 'NoDef :=> 'NotNull 'PGtext --- , "id" ::: 'Def :=> 'NotNull 'PGint4 --- ] --- :} -type ColumnsType = [(Symbol,ColumnType)] - -type instance PrettyPrintHaystack (haystack :: ColumnsType) = - 'PrettyPrintInfo ('Text "column definition (ColumnType)") ('Text "table (ColumnsType)") ('ShowType (Sort (MapFst haystack))) - --- | `TableConstraint` encodes various forms of data constraints --- of columns in a table. --- `TableConstraint`s give you as much control over the data in your tables --- as you wish. If a user attempts to store data in a column that would --- violate a constraint, an error is raised. This applies --- even if the value came from the default value definition. -data TableConstraint - = Check [Symbol] - | Unique [Symbol] - | PrimaryKey [Symbol] - | ForeignKey [Symbol] Symbol Symbol [Symbol] - -{- | A `TableConstraints` is a row of `TableConstraint`s. - ->>> :{ -type family UsersConstraints :: TableConstraints where - UsersConstraints = '[ "pk_users" ::: 'PrimaryKey '["id"] ] -:} --} -type TableConstraints = [(Symbol,TableConstraint)] - -type instance PrettyPrintHaystack (haystack :: TableConstraints) = - 'PrettyPrintInfo ('Text "constraint (TableConstraint)") ('Text "table (TableConstraints)") ('ShowType (Sort (MapFst haystack))) - --- | A `ForeignKey` must reference columns that either are --- a `PrimaryKey` or form a `Unique` constraint. -type family Uniquely - (key :: [Symbol]) - (constraints :: TableConstraints) :: Constraint where - Uniquely key (uq ::: 'Unique key ': constraints) = () - Uniquely key (pk ::: 'PrimaryKey key ': constraints) = () - Uniquely key (_ ': constraints) = Uniquely key constraints - --- | `TableType` encodes a row of constraints on a table as well as the types --- of its columns. --- --- >>> :{ --- type family UsersTable :: TableType where --- UsersTable = --- '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=> --- '[ "id" ::: 'Def :=> 'NotNull 'PGint4 --- , "name" ::: 'NoDef :=> 'NotNull 'PGtext --- ] --- :} -type TableType = (TableConstraints,ColumnsType) - -{- | A `RowType` is a row of `NullType`s. They correspond to Haskell -record types by means of `Squeal.PostgreSQL.Binary.RowPG` and are used in many places. - ->>> :{ -type family PersonRow :: RowType where - PersonRow = - '[ "name" ::: 'NotNull 'PGtext - , "age" ::: 'NotNull 'PGint4 - , "dateOfBirth" ::: 'Null 'PGdate - ] -:} --} -type RowType = [(Symbol,NullType)] - -type instance PrettyPrintHaystack (haystack :: RowType) = - 'PrettyPrintInfo ('Text "column (NullType)") ('Text "row (RowType)") ('ShowType (Sort (MapFst haystack))) - -{- | `FromType` is a row of `RowType`s. It can be thought of as -a product, or horizontal gluing and is used in `Squeal.PostgreSQL.Query.From.FromClause`s -and `Squeal.PostgreSQL.Query.Table.TableExpression`s. --} -type FromType = [(Symbol,RowType)] - -type instance PrettyPrintHaystack (haystack :: FromType) = - 'PrettyPrintInfo ('Text "row (RowType)") ('Text "from clause (FromType)") ('ShowType (Sort (MapFst haystack))) - --- | `ColumnsToRow` removes column constraints. -type family ColumnsToRow (columns :: ColumnsType) :: RowType where - ColumnsToRow (column ::: _ :=> ty ': columns) = - column ::: ty ': ColumnsToRow columns - ColumnsToRow '[] = '[] - --- | `TableToColumns` removes table constraints. -type family TableToColumns (table :: TableType) :: ColumnsType where - TableToColumns (constraints :=> columns) = columns - --- | Convert a table to a row type. -type family TableToRow (table :: TableType) :: RowType where - TableToRow tab = ColumnsToRow (TableToColumns tab) - --- | Numeric Postgres types. -type PGNum = - '[ 'PGint2, 'PGint4, 'PGint8, 'PGnumeric, 'PGfloat4, 'PGfloat8] - --- | Floating Postgres types. -type PGFloating = '[ 'PGfloat4, 'PGfloat8, 'PGnumeric] - --- | Integral Postgres types. -type PGIntegral = '[ 'PGint2, 'PGint4, 'PGint8] - --- | Equality constraint on the underlying `PGType` of two columns. -class SamePGType - (ty0 :: (Symbol,ColumnType)) (ty1 :: (Symbol,ColumnType)) where -instance ty0 ~ ty1 => SamePGType - (alias0 ::: def0 :=> null0 ty0) - (alias1 ::: def1 :=> null1 ty1) - --- | `AllNotNull` is a constraint that proves a `ColumnsType` has no @NULL@s. -type family AllNotNull (columns :: ColumnsType) :: Constraint where - AllNotNull (_ ::: _ :=> 'NotNull _ ': columns) = AllNotNull columns - AllNotNull '[] = () - --- | `NotAllNull` is a constraint that proves a `ColumnsType` has some --- @NOT NULL@. -type family NotAllNull (columns :: ColumnsType) :: Constraint where - NotAllNull (_ ::: _ :=> 'NotNull _ ': _) = () - NotAllNull (_ ::: _ :=> 'Null _ ': columns) = NotAllNull columns - --- | `NullifyType` is an idempotent that nullifies a `NullType`. -type family NullifyType (ty :: NullType) :: NullType where - NullifyType (null ty) = 'Null ty - --- | `NullifyRow` is an idempotent that nullifies a `RowType`. -type family NullifyRow (columns :: RowType) :: RowType where - NullifyRow (column ::: ty ': columns) = - column ::: NullifyType ty ': NullifyRow columns - NullifyRow '[] = '[] - --- | `NullifyFrom` is an idempotent that nullifies a `FromType` --- used to nullify the left or right hand side of an outer join --- in a `Squeal.PostgreSQL.Query.From.FromClause`. -type family NullifyFrom (tables :: FromType) :: FromType where - NullifyFrom (table ::: columns ': tables) = - table ::: NullifyRow columns ': NullifyFrom tables - NullifyFrom '[] = '[] - --- | @Create alias x xs@ adds @alias ::: x@ to the end of @xs@ and is used in --- `Squeal.PostgreSQL.Definition.Table.createTable` statements and in @ALTER TABLE@ --- `Squeal.PostgreSQL.Definition.Table.addColumn`. -type family Create alias x xs where - Create alias x '[] = '[alias ::: x] - Create alias x (alias ::: y ': xs) = TypeError - ('Text "Create: alias " - ':<>: 'ShowType alias - ':<>: 'Text "already exists") - Create alias y (x ': xs) = x ': Create alias y xs - -{-| Similar to `Create` but no error on pre-existence-} -type family CreateIfNotExists alias x xs where - CreateIfNotExists alias x '[] = '[alias ::: x] - CreateIfNotExists alias x (alias ::: y ': xs) = alias ::: y ': xs - CreateIfNotExists alias y (x ': xs) = x ': CreateIfNotExists alias y xs - -{-| Similar to `Create` but used to replace values -with the same type.-} -type family CreateOrReplace alias x xs where - CreateOrReplace alias x '[] = '[alias ::: x] - CreateOrReplace alias x (alias ::: x ': xs) = alias ::: x ': xs - CreateOrReplace alias x (alias ::: y ': xs) = TypeError - ('Text "CreateOrReplace: expected type " - ':<>: 'ShowType x - ':<>: 'Text " but alias " - ':<>: 'ShowType alias - ':<>: 'Text " has type " - ':<>: 'ShowType y) - CreateOrReplace alias y (x ': xs) = x ': CreateOrReplace alias y xs - --- | @Drop alias xs@ removes the type associated with @alias@ in @xs@ --- and is used in `Squeal.PostgreSQL.Definition.dropTable` statements --- and in @ALTER TABLE@ `Squeal.PostgreSQL.Definition.dropColumn` statements. -type family Drop alias xs where - Drop alias '[] = TypeError - ('Text "Drop: alias " - ':<>: 'ShowType alias - ':<>: 'Text " does not exist" ) - Drop alias (alias ::: x ': xs) = xs - Drop alias (x ': xs) = x ': Drop alias xs - --- | Drop a particular flavor of schemum type -type family DropSchemum alias sch xs where - DropSchemum alias sch '[] = TypeError - ('Text "DropSchemum: alias " - ':<>: 'ShowType alias - ':<>: 'Text " does not exist" ) - DropSchemum alias sch (alias ::: sch x ': xs) = xs - DropSchemum alias sch0 (alias ::: sch1 x ': xs) = TypeError - ('Text "DropSchemum: expected schemum " - ':<>: 'ShowType sch0 - ':<>: 'Text " but alias " - ':<>: 'ShowType alias - ':<>: 'Text " has schemum " - ':<>: 'ShowType sch1) - DropSchemum alias sch (x ': xs) = x ': DropSchemum alias sch xs - --- | Similar to `Drop` but no error on non-existence -type family DropIfExists alias xs where - DropIfExists alias '[] = '[] - DropIfExists alias (alias ::: x ': xs) = xs - DropIfExists alias (x ': xs) = x ': DropIfExists alias xs - --- | Similar to `DropSchemum` but no error on non-existence -type family DropSchemumIfExists alias sch xs where - DropSchemumIfExists alias sch '[] = '[] - DropSchemumIfExists alias sch (alias ::: sch x ': xs) = xs - DropSchemumIfExists alias sch0 (alias ::: sch1 x ': xs) = TypeError - ('Text "DropSchemumIfExists: expected schemum " - ':<>: 'ShowType sch1 - ':<>: 'Text " but alias " - ':<>: 'ShowType alias - ':<>: 'Text " has schemum " - ':<>: 'ShowType sch0) - DropSchemumIfExists alias sch (x ': xs) = x ': DropSchemumIfExists alias sch xs - --- | @Alter alias x xs@ replaces the type associated with an @alias@ in @xs@ --- with the type @x@ and is used in `Squeal.PostgreSQL.Definition.alterTable` --- and `Squeal.PostgreSQL.Definition.alterColumn`. -type family Alter alias x xs where - Alter alias x '[] = TypeError - ('Text "Alter: alias " - ':<>: 'ShowType alias - ':<>: 'Text " does not exist" ) - Alter alias x1 (alias ::: x0 ': xs) = alias ::: x1 ': xs - Alter alias x1 (x0 ': xs) = x0 ': Alter alias x1 xs - --- | Similar to `Alter` but no error on non-existence -type family AlterIfExists alias x xs where - AlterIfExists alias x '[] = '[] - AlterIfExists alias x1 (alias ::: x0 ': xs) = alias ::: x1 ': xs - AlterIfExists alias x1 (x0 ': xs) = x0 ': AlterIfExists alias x1 xs - --- | @Rename alias0 alias1 xs@ replaces the alias @alias0@ by @alias1@ in @xs@ --- and is used in `Squeal.PostgreSQL.Definition.alterTableRename` and --- `Squeal.PostgreSQL.Definition.renameColumn`. -type family Rename alias0 alias1 xs where - Rename alias0 alias1 '[] = TypeError - ('Text "Rename: alias " - ':<>: 'ShowType alias0 - ':<>: 'Text " does not exist" ) - Rename alias0 alias1 ((alias0 ::: x0) ': xs) = (alias1 ::: x0) ': xs - Rename alias0 alias1 (x ': xs) = x ': Rename alias0 alias1 xs - --- | Similar to `Rename` but no error on non-existence -type family RenameIfExists alias0 alias1 xs where - RenameIfExists alias x '[] = '[] - RenameIfExists alias0 alias1 ((alias0 ::: x0) ': xs) = (alias1 ::: x0) ': xs - RenameIfExists alias0 alias1 (x ': xs) = x ': RenameIfExists alias0 alias1 xs - --- | Move an object from one schema to another -type family SetSchema sch0 sch1 schema0 schema1 obj srt ty db where - SetSchema sch0 sch1 schema0 schema1 obj srt ty db = Alter sch1 - (Create obj (srt ty) schema1) - (Alter sch0 (DropSchemum obj srt schema0) db) - -{- | `SubDB` checks that one `SchemasType` is a sublist of another, -with the same ordering. - ->>> :kind! SubDB '["a" ::: '["b" ::: 'View '[]]] '["a" ::: '["b" ::: 'View '[], "c" ::: 'Typedef 'PGint4]] -SubDB '["a" ::: '["b" ::: 'View '[]]] '["a" ::: '["b" ::: 'View '[], "c" ::: 'Typedef 'PGint4]] :: Bool -= 'True --} -type family SubDB (db0 :: SchemasType) (db1 :: SchemasType) :: Bool where - SubDB '[] db1 = 'True - SubDB (sch ': db0) '[] = 'False - SubDB (sch ::: schema0 ': db0) (sch ::: schema1 ': db1) = - If (SubList schema0 schema1) - (SubDB db0 db1) - (SubDB (sch ::: schema0 ': db0) db1) - SubDB db0 (sch1 ': db1) = SubDB db0 db1 - -{- | `SubsetDB` checks that one `SchemasType` is a subset of another, -regardless of ordering. - ->>> :kind! SubsetDB '["a" ::: '["d" ::: 'Typedef 'PGint2, "b" ::: 'View '[]]] '["a" ::: '["b" ::: 'View '[], "c" ::: 'Typedef 'PGint4, "d" ::: 'Typedef 'PGint2]] -SubsetDB '["a" ::: '["d" ::: 'Typedef 'PGint2, "b" ::: 'View '[]]] '["a" ::: '["b" ::: 'View '[], "c" ::: 'Typedef 'PGint4, "d" ::: 'Typedef 'PGint2]] :: Bool -= 'True --} -type family SubsetDB (db0 :: SchemasType) (db1 :: SchemasType) :: Bool where - SubsetDB '[] db1 = 'True - SubsetDB (sch ': db0) db1 = ElemDB sch db1 && SubsetDB db0 db1 - -{- | `ElemDB` checks that a schema may be found as a subset of another in a database, -regardless of ordering. --} -type family ElemDB (sch :: (Symbol, SchemaType)) (db :: SchemasType) :: Bool where - ElemDB sch '[] = 'False - ElemDB (sch ::: schema0) (sch ::: schema1 ': _) = SubsetList schema0 schema1 - ElemDB sch (_ ': schs) = ElemDB sch schs - --- | Check if a `TableConstraint` involves a column -type family ConstraintInvolves column constraint where - ConstraintInvolves column ('Check columns) = column `Elem` columns - ConstraintInvolves column ('Unique columns) = column `Elem` columns - ConstraintInvolves column ('PrimaryKey columns) = column `Elem` columns - ConstraintInvolves column ('ForeignKey columns sch tab refcolumns) - = column `Elem` columns - --- | Drop all `TableConstraint`s that involve a column -type family DropIfConstraintsInvolve column constraints where - DropIfConstraintsInvolve column '[] = '[] - DropIfConstraintsInvolve column (alias ::: constraint ': constraints) - = If (ConstraintInvolves column constraint) - (DropIfConstraintsInvolve column constraints) - (alias ::: constraint ': DropIfConstraintsInvolve column constraints) - --- | A `SchemumType` is a user-created type, like a `Table`, --- `View` or `Typedef`. -data SchemumType - = Table TableType - | View RowType - | Typedef PGType - | Index IndexType - | Function FunctionType - | Procedure [NullType] - | UnsafeSchemum Symbol - -{- | Use `:=>` to pair the parameter types with the return -type of a function. - ->>> :{ -type family Fn :: FunctionType where - Fn = '[ 'NotNull 'PGint4] :=> 'Returns ('NotNull 'PGint4) -:} --} -type FunctionType = ([NullType], ReturnsType) - -{- | -PostgreSQL provides several index types: -B-tree, Hash, GiST, SP-GiST, GIN and BRIN. -Each index type uses a different algorithm -that is best suited to different types of queries. --} -data IndexType - = Btree - -- ^ B-trees can handle equality and range queries on data - -- that can be sorted into some ordering. - | Hash - -- ^ Hash indexes can only handle simple equality comparisons. - | Gist - -- ^ GiST indexes are not a single kind of index, - -- but rather an infrastructure within which many different - -- indexing strategies can be implemented. - | Spgist - -- ^ SP-GiST indexes, like GiST indexes, - -- offer an infrastructure that supports various kinds of searches. - | Gin - -- ^ GIN indexes are “inverted indexes” which are appropriate for - -- data values that contain multiple component values, such as arrays. - | Brin - -- ^ BRIN indexes (a shorthand for Block Range INdexes) store summaries - -- about the values stored in consecutive physical block ranges of a table. - -{- | Return type of a function-} -data ReturnsType - = Returns NullType -- ^ function - | ReturnsTable RowType -- ^ set returning function - -{- | A schema of a database consists of a list of aliased, -user-defined `SchemumType`s. - ->>> :{ -type family Schema :: SchemaType where - Schema = - '[ "users" ::: 'Table ( - '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=> - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "name" ::: 'NoDef :=> 'NotNull 'PGtext - ]) - , "emails" ::: 'Table ( - '[ "pk_emails" ::: 'PrimaryKey '["id"] - , "fk_user_id" ::: 'ForeignKey '["user_id"] "public" "users" '["id"] - ] :=> - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4 - , "email" ::: 'NoDef :=> 'Null 'PGtext - ]) - ] -:} --} -type SchemaType = [(Symbol,SchemumType)] - --- | A @PartitionedSchema@ is a @SchemaType@ where each constructor of @SchemumType@ has --- been separated into its own list -data PartitionedSchema = PartitionedSchema - { _tables :: [(Symbol, TableType)] - , _views :: [(Symbol, RowType)] - , _types :: [(Symbol, PGType)] - , _indexes :: [(Symbol, IndexType)] - , _functions :: [(Symbol, FunctionType)] - , _procedures :: [(Symbol, [NullType])] - , _unsafes :: [(Symbol, Symbol)] - } - --- | @PartitionSchema@ partitions a @SchemaType@ into a @PartitionedSchema@ -type PartitionSchema schema = PartitionSchema' schema ('PartitionedSchema '[] '[] '[] '[] '[] '[] '[]) - -type family PartitionSchema' (remaining :: SchemaType) (acc :: PartitionedSchema) :: PartitionedSchema where - PartitionSchema' '[] ps = ps - PartitionSchema' ('(s, 'Table table) ': rest) ('PartitionedSchema tables views types indexes functions procedures unsafe) - = PartitionSchema' rest ('PartitionedSchema ('(s, table) ': tables) views types indexes functions procedures unsafe) - PartitionSchema' ('(s, 'View view) ': rest) ('PartitionedSchema tables views types indexes functions procedures unsafe) - = PartitionSchema' rest ('PartitionedSchema tables ('(s, view) ': views) types indexes functions procedures unsafe) - PartitionSchema' ('(s, 'Typedef typ) ': rest) ('PartitionedSchema tables views types indexes functions procedures unsafe) - = PartitionSchema' rest ('PartitionedSchema tables views ('(s, typ) ': types) indexes functions procedures unsafe) - PartitionSchema' ('(s, 'Index ix) ': rest) ('PartitionedSchema tables views types indexes functions procedures unsafe) - = PartitionSchema' rest ('PartitionedSchema tables views types ('(s, ix) ': indexes) functions procedures unsafe) - PartitionSchema' ('(s, 'Function f) ': rest) ('PartitionedSchema tables views types indexes functions procedures unsafe) - = PartitionSchema' rest ('PartitionedSchema tables views types indexes ('(s, f) ': functions) procedures unsafe) - PartitionSchema' ('(s, 'Procedure p) ': rest) ('PartitionedSchema tables views types indexes functions procedures unsafe) - = PartitionSchema' rest ('PartitionedSchema tables views types indexes functions ('(s, p) ': procedures) unsafe) - PartitionSchema' ('(s, 'UnsafeSchemum u) ': rest) ('PartitionedSchema tables views types indexes functions procedures unsafe) - = PartitionSchema' rest ('PartitionedSchema tables views types indexes functions procedures ('(s, u) ': unsafe)) - --- | Get the tables from a @PartitionedSchema@ -type family SchemaTables (schema :: PartitionedSchema) :: [(Symbol, TableType)] where - SchemaTables ('PartitionedSchema tables _ _ _ _ _ _) = tables --- | Get the views from a @PartitionedSchema@ -type family SchemaViews (schema :: PartitionedSchema) :: [(Symbol, RowType)] where - SchemaViews ('PartitionedSchema _ views _ _ _ _ _) = views --- | Get the typedefs from a @PartitionedSchema@ -type family SchemaTypes (schema :: PartitionedSchema) :: [(Symbol, PGType)] where - SchemaTypes ('PartitionedSchema _ _ types _ _ _ _) = types --- | Get the indexes from a @PartitionedSchema@ -type family SchemaIndexes (schema :: PartitionedSchema) :: [(Symbol, IndexType)] where - SchemaIndexes ('PartitionedSchema _ _ _ indexes _ _ _) = indexes --- | Get the functions from a @PartitionedSchema@ -type family SchemaFunctions (schema :: PartitionedSchema) :: [(Symbol, FunctionType)] where - SchemaFunctions ('PartitionedSchema _ _ _ _ functions _ _) = functions --- | Get the procedured from a @PartitionedSchema@ -type family SchemaProcedures (schema :: PartitionedSchema) :: [(Symbol, [NullType])] where - SchemaProcedures ('PartitionedSchema _ _ _ _ _ procedures _) = procedures --- | Get the unsafe schema types from a @PartitionedSchema@ -type family SchemaUnsafes (schema :: PartitionedSchema) :: [(Symbol, Symbol)] where - SchemaUnsafes ('PartitionedSchema _ _ _ _ _ _ unsafes) = unsafes - --- | @PrettyPrintPartitionedSchema@ makes a nice @ErrorMessage@ showing a @PartitionedSchema@, --- only including the names of the things in it and not the values. Additionally, empty --- fields are omitted -type family PrettyPrintPartitionedSchema (schema :: PartitionedSchema) :: ErrorMessage where - PrettyPrintPartitionedSchema schema = IntersperseNewlines (FilterNonEmpty - [ FieldIfNonEmpty "Tables" (SchemaTables schema) - , FieldIfNonEmpty "Views" (SchemaViews schema) - , FieldIfNonEmpty "Types" (SchemaTypes schema) - , FieldIfNonEmpty "Indexes" (SchemaIndexes schema) - , FieldIfNonEmpty "Functions" (SchemaFunctions schema) - , FieldIfNonEmpty "Procedures" (SchemaProcedures schema) - , FieldIfNonEmpty "Unsafe schema items" (SchemaUnsafes schema) - ]) - -type family FieldIfNonEmpty (fieldName :: Symbol) (value :: [(Symbol, k)]) :: ErrorMessage where - FieldIfNonEmpty _ '[] = 'Text "" - FieldIfNonEmpty n xs = 'Text " " ':<>: 'Text n ':<>: 'Text ":" ':$$: 'Text " " ':<>: 'ShowType (Sort (MapFst xs)) - -type family FilterNonEmpty (ls :: [ErrorMessage]) :: [ErrorMessage] where - FilterNonEmpty ('Text "" ': rest) = FilterNonEmpty rest - FilterNonEmpty (x ': rest) = x ': FilterNonEmpty rest - FilterNonEmpty '[] = '[] - -type family IntersperseNewlines (ls :: [ErrorMessage]) :: ErrorMessage where - IntersperseNewlines (x ': y ': '[]) = x ':$$: y - IntersperseNewlines (x ': xs) = x ':$$: IntersperseNewlines xs - IntersperseNewlines '[] = 'Text "" - -type instance PrettyPrintHaystack (haystack :: SchemaType) = - 'PrettyPrintInfo ('Text "table, view, typedef, index, function, or procedure (SchemumType)") ('Text "schema (SchemaType)") - ( PrettyPrintPartitionedSchema (PartitionSchema haystack) - ) - -{- | -A database contains one or more named schemas, which in turn contain tables. -The same object name can be used in different schemas without conflict; -for example, both schema1 and myschema can contain tables named mytable. -Unlike databases, schemas are not rigidly separated: -a user can access objects in any of the schemas in the database they are connected to, -if they have privileges to do so. - -There are several reasons why one might want to use schemas: - - * To allow many users to use one database without interfering with each other. - * To organize database objects into logical groups to make them more manageable. - * Third-party applications can be put into separate schemas - so they do not collide with the names of other objects. --} -type SchemasType = [(Symbol,SchemaType)] - -type instance PrettyPrintHaystack (haystack :: SchemasType) = - 'PrettyPrintInfo ('Text "schema (SchemaType)") ('Text "database (SchemasType)") ('Text " " ':<>: 'ShowType (Sort (MapFst haystack))) - --- | A type family to use for a single schema database. -type family Public (schema :: SchemaType) :: SchemasType - where Public schema = '["public" ::: schema] - --- | `IsPGlabel` looks very much like the `IsLabel` class. Whereas --- the overloaded label, `fromLabel` is used for column references, --- `label`s are used for enum terms. A `label` is called with --- type application like `label` @"beef". -class IsPGlabel (label :: Symbol) expr where label :: expr -instance label ~ label1 - => IsPGlabel label (PGlabel label1) where label = PGlabel -instance labels ~ '[label] - => IsPGlabel label (NP PGlabel labels) where label = PGlabel :* Nil -instance IsPGlabel label (y -> K y label) where label = K -instance IsPGlabel label (y -> NP (K y) '[label]) where label y = K y :* Nil --- | A `PGlabel` unit type with an `IsPGlabel` instance -data PGlabel (label :: Symbol) = PGlabel -instance KnownSymbol label => RenderSQL (PGlabel label) where - renderSQL _ = "\'" <> renderSymbol @label <> "\'" -instance All KnownSymbol labels => RenderSQL (NP PGlabel labels) where - renderSQL - = commaSeparated - . hcollapse - . hcmap (Proxy @KnownSymbol) (K . renderSQL) - --- | Is a type a valid JSON key? -type PGJsonKey = '[ 'PGint2, 'PGint4, 'PGtext ] - --- | Is a type a valid JSON type? -type PGJsonType = '[ 'PGjson, 'PGjsonb ] - --- | Utility class for `AllUnique` to provide nicer error messages. -class IsNotElem x isElem where -instance IsNotElem x 'False where -instance (TypeError ( 'Text "Cannot assign to " - ':<>: 'ShowType alias - ':<>: 'Text " more than once")) - => IsNotElem '(alias, a) 'True where - --- | No elem of @xs@ appears more than once, in the context of assignment. -class AllUnique (xs :: [(Symbol, a)]) where -instance AllUnique '[] where -instance (IsNotElem x (Elem x xs), AllUnique xs) => AllUnique (x ': xs) where - --- | Updatable lists of columns -type Updatable table columns = - ( All (HasIn (TableToColumns table)) columns - , AllUnique columns - , SListI (TableToColumns table) ) - --- | Calculate the name of a user defined type. -type family UserTypeName (schema :: SchemaType) (ty :: PGType) where - UserTypeName '[] ty = 'Nothing - UserTypeName (td ::: 'Typedef ty ': _) ty = 'Just td - UserTypeName (_ ': schema) ty = UserTypeName schema ty - --- | Helper to calculate the schema of a user defined type. -type family UserTypeNamespace - (sch :: Symbol) - (td :: Maybe Symbol) - (schemas :: SchemasType) - (ty :: PGType) where - UserTypeNamespace sch 'Nothing schemas ty = UserType schemas ty - UserTypeNamespace sch ('Just td) schemas ty = '(sch, td) - --- | Calculate the schema and name of a user defined type. -type family UserType (db :: SchemasType) (ty :: PGType) where - UserType '[] ty = TypeError - ('Text "No such user type: " ':<>: 'ShowType ty) - UserType (sch ::: schema ': schemas) ty = - UserTypeNamespace sch (UserTypeName schema ty) schemas ty From df10c5e0078c821256d456febd66d0af27da7d48 Mon Sep 17 00:00:00 2001 From: Eitan Chatav Date: Wed, 26 Jan 2022 11:58:34 -0800 Subject: [PATCH 14/18] third pass minimization --- squeal-postgresql/squeal-postgresql.cabal | 28 +-------------- .../src/Squeal/PostgreSQL/Type/Schema.hs | 35 ------------------- 2 files changed, 1 insertion(+), 62 deletions(-) diff --git a/squeal-postgresql/squeal-postgresql.cabal b/squeal-postgresql/squeal-postgresql.cabal index f0235bbb..d58845a4 100644 --- a/squeal-postgresql/squeal-postgresql.cabal +++ b/squeal-postgresql/squeal-postgresql.cabal @@ -24,33 +24,7 @@ library default-language: Haskell2010 ghc-options: -Wall build-depends: - aeson >= 1.4.7.1 - , base >= 4.12.0.0 && < 5.0 - , binary >= 0.8.7.0 - , binary-parser >= 0.5.5 - , bytestring >= 0.10.10.0 - , bytestring-strict-builder >= 0.4.5.3 - , deepseq >= 1.4.4.0 - , exceptions >= 0.10.3 - , free-categories >= 0.2.0.0 - , generics-sop >= 0.5.1.0 - , mmorph >= 1.1.3 - , monad-control >= 1.0.2.3 - , mtl >= 2.2.2 - , network-ip >= 0.3.0.3 - , postgresql-binary >= 0.12.2 - , postgresql-libpq >= 0.9.4.2 - , profunctors >= 5.5.2 - , records-sop >= 0.1.0.3 - , resource-pool >= 0.2.3.2 - , scientific >= 0.3.6.2 - , text >= 1.2.3.2 - , time >= 1.9.3 - , transformers >= 0.5.6.2 - , transformers-base >= 0.4.5.2 - , unliftio >= 0.2.12.1 - , uuid-types >= 1.0.3 - , vector >= 0.12.1.2 + base >= 4.12.0.0 && < 5.0 test-suite doctest default-language: Haskell2010 diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Type/Schema.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Type/Schema.hs index 8431aa39..9872b1a0 100644 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Type/Schema.hs +++ b/squeal-postgresql/src/Squeal/PostgreSQL/Type/Schema.hs @@ -1,38 +1,3 @@ -{-| -Module: Squeal.PostgreSQL.Type.Schema -Description: Postgres type system -Copyright: (c) Eitan Chatav, 2019 -Maintainer: eitan@morphism.tech -Stability: experimental - -Provides a type-level DSL for kinds of Postgres types, -tables, schema, constraints, and more. -It also defines useful type families to operate on these. --} - -{-# LANGUAGE - AllowAmbiguousTypes - , ConstraintKinds - , DeriveAnyClass - , DeriveGeneric - , FlexibleContexts - , FlexibleInstances - , FunctionalDependencies - , GADTs - , LambdaCase - , OverloadedStrings - , QuantifiedConstraints - , RankNTypes - , ScopedTypeVariables - , StandaloneDeriving - , TypeApplications - , TypeFamilyDependencies - , TypeInType - , TypeOperators - , UndecidableInstances - , UndecidableSuperClasses -#-} - module Squeal.PostgreSQL.Type.Schema ( -- * Postgres Type PGType (..) From 496d22979197d725855591a0003930271e2ad4aa Mon Sep 17 00:00:00 2001 From: Eitan Chatav Date: Wed, 26 Jan 2022 12:00:52 -0800 Subject: [PATCH 15/18] add back GHC 8.8 and 8.10 stack yamls --- stack-ghc8_10.yaml | 3 +++ stack-ghc8_8.yaml | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 stack-ghc8_10.yaml create mode 100644 stack-ghc8_8.yaml diff --git a/stack-ghc8_10.yaml b/stack-ghc8_10.yaml new file mode 100644 index 00000000..3561eb09 --- /dev/null +++ b/stack-ghc8_10.yaml @@ -0,0 +1,3 @@ +resolver: lts-18.20 +packages: +- squeal-postgresql diff --git a/stack-ghc8_8.yaml b/stack-ghc8_8.yaml new file mode 100644 index 00000000..3561eb09 --- /dev/null +++ b/stack-ghc8_8.yaml @@ -0,0 +1,3 @@ +resolver: lts-18.20 +packages: +- squeal-postgresql From 0674463919be67f826211e9ea18e62d3f6dc77b1 Mon Sep 17 00:00:00 2001 From: Eitan Chatav Date: Wed, 26 Jan 2022 12:07:37 -0800 Subject: [PATCH 16/18] remove handbook --- squeal-core-concepts-handbook.md | 980 ------------------ .../isqualified-alias.png | Bin 12057 -> 0 bytes .../isqualified-can-be-np.png | Bin 39024 -> 0 bytes .../isqualified-intro.png | Bin 57879 -> 0 bytes .../isqualified-join-constraint.png | Bin 31136 -> 0 bytes 5 files changed, 980 deletions(-) delete mode 100644 squeal-core-concepts-handbook.md delete mode 100644 squeal-core-concepts-handbook/isqualified-alias.png delete mode 100644 squeal-core-concepts-handbook/isqualified-can-be-np.png delete mode 100644 squeal-core-concepts-handbook/isqualified-intro.png delete mode 100644 squeal-core-concepts-handbook/isqualified-join-constraint.png diff --git a/squeal-core-concepts-handbook.md b/squeal-core-concepts-handbook.md deleted file mode 100644 index 4c337501..00000000 --- a/squeal-core-concepts-handbook.md +++ /dev/null @@ -1,980 +0,0 @@ -# Squeal Core Concepts Handbook - -This handbook isn't intended as a comprehensive reference to Squeal; that's what -the Haddock is for. Instead, this is meant to give you an understanding of the -fundamental parts of Squeal: its core types and typeclasses, as well as its -heavy use of phantom types. Once you understand these, which are the most -complicated part of learning to use Squeal, you should have a solid base to -figure out everything else. - -At its core, you can view Squeal as a small group of easy-to-understand types -(`Query`, `Manipulation`, `Statement`, `Expression`, `FromClause`, and -`TableExpression`) that have hard-to-understand type parameters (`Expression -grouping lat with db params from ty`). The former map to your existing -understanding of SQL in a fairly obvious way; the latter make sure that your -queries are actually valid. - -We can get our first, most basic understanding of Squeal by ignoring the type -parameters entirely and looking specifically at those core types. - -## Squeal's Core Types - -Imagine, if you would, a world where we only had those six types above, with no -type parameters attached to them. - -That would be possible, right? And you can see how they could fit together to -form larger queries. That gives us the composability we wanted. - -[`Query`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Query.html#t:Query) and [`Manipulation`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Manipulation.html#t:Manipulation) -line up cleanly with what we expect a SQL query to look like, so let's start by -looking at those two types. - -`Query`s represent mainly SQL **SELECT**. `Manipulation`s represent **UPDATE**, -**INSERT INTO**, and **DELETE FROM**. - -```haskell -query = select - (List $ #u ! #name `as` #userName :* #e ! #email `as` #userEmail) - ( from (table (#users `as` #u) - & innerJoin (table (#emails `as` #e)) - (#u ! #id .== #e ! #user_id)) ) -``` - -`Query`s have two parts, the description of the columns selected, and the -`TableExpression` that describes where to select those columns from. - -```text -query = select - - (List $ #u ! #name `as` #userName } the column selection - :* #e ! #email `as` #userEmail) } - - ( from (table (#users `as` #u) } - & innerJoin (table (#emails `as` #e)) } the TableExpression - (#u ! #id .== #e ! #user_id)) ) } -``` - -A [`TableExpression`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Query-Table.html#t:TableExpression) -is generated from a [`FromClause`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Query-From.html#t:FromClause), -which only describes the joins. You convert a `FromClause` to a `TableExpression` using `from`. Once you -have a TableExpression, that's where functions like [`where_`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Query-Table.html#v:where_) -and [`having`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Query-Table.html#v:having) -let you restrict the rows you get back. - -```text -( from } - ( table (#users `as` #u) } } - & innerJoin } a FromClause } the whole thing is - (table (#emails `as` #e)) } } a TableExpression - (#u ! #id .== #e ! #user_id)) } } -& where_ ... } -) } -``` - -Manipulations aren't particularly different from Querys. The one thing that -should be pointed out is that all of them specify a table using an `Aliased -(QualifiedAlias sch) (tab ::: tab0)`, and that UPDATE and DELETE have a -[`UsingClause`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Manipulation.html#t:UsingClause) -instead of a `TableExpression` to specify additional joins. `UsingClause` specifically -takes a `FromClause`, to prevent you from doing things like putting `WHERE` or `HAVING` on them. - -```haskell -manip = deleteFrom - #users - (Using (table #emails)) - (#users ! #id .== #emails ! #user_id) - Returning_ Nil -``` - -Once you get into specifying the actual bits of data you care about, you're -primarily concerned with generating values of type [`Expression`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Expression.html#t:Expression). -We're still ignoring most of the type parameters in Squeal's types, but in the case of -Expression the very last one, `ty`, is of interest to us. - -```haskell --- imagine a stripped-down version of Expression -data Expr ty - --- this is for teaching purposes; will not typecheck -foo :: Expr ('NotNull 'PGint4) -foo = #table ! #field1 -``` - -You'll construct Expressions (and `Condition`, which is just an alias for -`Expression (null 'PGbool)`) everywhere. - -The one last piece of the puzzle that we haven't explained yet: how do you -specify which columns in a Query that you're returning? How do you specify -which columns you're updating in an UPDATE Manipulation? The key here is the -[`NP`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Type-List.html#t:NP) -type, which Squeal reexports from `generics-sop`. You'll see this type in a -lot of different places. It will also confuse the hell out of you the first time -you see it in documentation. - -```haskell -selectedColumns :: NP '[ "userName" ::: 'NotNull 'PGtext, "userEmail" ::: 'Null 'PGtext ] -selectedColumns = - #users ! #name `as` #userName - :* #emails ! #email `as` #userEmail -``` - -It's a way for Squeal to have heterogeneous lists that also typecheck against the -DB schema. We'll talk about how that typechecking works later, once we -understand Squeal's type parameters. From a practical perspective, mainly -you need to know that you construct them using `(:*)`. - -All of the types we've talked about so far live in "Postgres-land;" they only -know about Postgres types, and have no knowledge of how to translate those types -into actual in-memory Haskell data. That translation lives inside the -[`Statement`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Type-List.html#t:NP) -type, which lives one level above Query and Manipulation. Both -Query and Manipulation can be converted to Statement by providing an encoder to -convert the query's params from Haskell types to Postgres types, and a decoder -to do the opposite for the query's results. We'll look at encoding and decoding -to/from PG later; just remember that conceptually the Statement type is there to -combine a raw query with an associated Haskell <=> PG codec. - -Let's put all this together in a concrete example. Say we had the following -typical query: - -```haskell -getUsers :: Statement DB () User -getUsers = query $ select - (List $ #u ! #name `as` #userName :* #e ! #email `as` #userEmail) - ( from (table (#users `as` #u) - & innerJoin (table (#emails `as` #e)) - (#u ! #id .== #e ! #user_id)) ) -``` - -Since all this is is an expression built out of the smaller types we've looked -at above, we can expand this out into its constituent parts and see just -how everything fits together. Ignore the amount of noise in the type -parameters; focus on the type heads like Query, Expression etc. that we -talked about. - -```haskell -getUsersQuery :: Query with lat DB params '[ "userName" ::: 'NotNull 'PGtext, "userEmail" ::: 'Null 'PGtext ] -getUsersQuery = - select - (List getUsersSelection) - ( from (table (#users `as` #u) - & innerJoin (table (#emails `as` #e)) - (#u ! #id .== #e ! #user_id)) ) - -getUsersSelection :: NP (Aliased (Expression 'Ungrouped with lat DB params - '[ "u" ::: '[ "id" ::: 'NotNull 'PGint4 - , "name" ::: 'NotNull 'PGtext - ] - , "e" ::: '[ "id" ::: 'NotNull 'PGint4 - , "user_id" ::: 'NotNull 'PGint4 - , "email" ::: 'Null 'PGtext - ] - ])) - '[ "userName" ::: 'NotNull 'PGtext, "userEmail" ::: 'Null 'PGtext ] -getUsersSelection = - #u ! #name `as` #userName :* #e ! #email `as` #userEmail - -getUsersFrom :: TableExpression 'Ungrouped lat with DB params - '[ "u" ::: '[ "id" ::: 'NotNull 'PGint4 - , "name" ::: 'NotNull 'PGtext - ] - , "e" ::: '[ "id" ::: 'NotNull 'PGint4 - , "user_id" ::: 'NotNull 'PGint4 - , "email" ::: 'Null 'PGtext - ] - ] -getUsersFrom = - ( from (table (#users `as` #u) - & innerJoin (table (#emails `as` #e)) - getUsersJoinExpr)) - -getUsersJoinExpr :: Expression 'Ungrouped lat with DB params - (Join - '[ "u" ::: '[ "id" ::: 'NotNull 'PGint4 - , "name" ::: 'NotNull 'PGtext - ] - ] - '[ "e" ::: '[ "id" ::: 'NotNull 'PGint4 - , "user_id" ::: 'NotNull 'PGint4 - , "email" ::: 'Null 'PGtext - ] - ]) - ('Null 'PGbool) -getUsersJoinExpr = - #u ! #id .== #e ! #user_id -``` - -These definitions can be loaded into the REPL. You can try playing around with -them if you'd like to get a feel for how the types work. You'll need the -following DB schema type in scope: - -```haskell -type UsersColumns = - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "name" ::: 'NoDef :=> 'NotNull 'PGtext - ] - -type UsersConstraints = '[ "pk_users" ::: 'PrimaryKey '["id"] ] - -type EmailsColumns = - '[ "id" ::: 'Def :=> 'NotNull 'PGint4 - , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4 - , "email" ::: 'NoDef :=> 'Null 'PGtext ] - -type EmailsConstraints = - '[ "pk_emails" ::: 'PrimaryKey '["id"] - , "fk_user_id" ::: 'ForeignKey '["user_id"] "public" "users" '["id"] - ] - -type Schema = - '[ "users" ::: 'Table (UsersConstraints :=> UsersColumns) - , "emails" ::: 'Table (EmailsConstraints :=> EmailsColumns) - ] - -type DB = Public Schema -``` - -## Where's the polymorphism? Actually constructing Expressions - -So far we've been using `(!)` and `as` without really understanding them; we -just use them as if they're equivalent to SQL `.` and `AS`, and assuming that -they'll do the right thing. - -But what's the actual type of this fragment? - -```haskell -#some_table ! #yay -``` - -When we were writing simple queries, we didn't particularly have to care about -what type this is. But as we write more complicated queries and start to think -about making our queries more modular, we need to be able to express the types -of values like this. - -For instance, depending on where you use it, it might be an Expression: - -```haskell -foo :: Expression 'Ungrouped lat with db '[] '[ "some_table" ::: '[ "yay" ::: 'NotNull 'PGint4 ] ] ('NotNull 'PGint4) -foo = #some_table ! #yay -``` - -But it can also be an NP: - -```haskell -bar :: NP (Expression 'Ungrouped lat with db '[] '[ "some_table" ::: '[ "yay" ::: 'NotNull 'PGint4 ] ]) '[ 'NotNull 'PGint4 ] -bar = #some_table ! #yay -``` - -It can even become a table name: - -```haskell -baz :: Aliased (QualifiedAlias "some_table") ("yay" ::: "yay") -baz = #some_table ! #yay - -fromClause :: FromClause lat with - '[ "some_table" ::: - '[ "yay" ::: 'Table - ('[] :=> '[ "a" ::: 'NoDef :=> 'NotNull 'PGint4 ]) - ] - ] - '[] - '[ "yay" ::: '[ "a" ::: 'NotNull 'PGint4 ] ] -fromClause = table baz -``` - -The key here is that both `(!)` and `as` are polymorphic in their return -type. This polymorphism is key to making Squeal convenient to write, but it can -be very confusing when first starting to use the library, as it makes it hard to -understand how to construct a certain Squeal type. This misunderstanding seems -like a consequence of how we usually use polymorphic returns. Usually the -polymorphic return is on a function that does some kind of conversion or -processing; canonical examples are things like `read` from Base, or `fromJSON` -from Aeson. In Squeal, that type conversion happens when you *name* things. It -accomplishes the goal of embedded SQL in Haskell in a relatively easy-to-read -way, but it takes some getting used to. - -What `(!)` can return is defined by the typeclass [`IsQualified`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Type-Alias.html#t:IsQualified); -similarly, `as` uses a typeclass called [`Aliasable`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Type-Alias.html#t:Aliasable). -If you squint through the mess of type parameter noise, you can see that this -is how certain types are constructed (for instance, our NP lists of expressions -in select lists, and expressions themselves): - -![IsQualified/Aliasable allows us to construct many different types with labels](squeal-core-concepts-handbook/isqualified-intro.png) - -and thus, how you can use `(!)` and `as` in so many places. - -This is especially important around uses of NP and update lists; a common -mistake is writing something like this while doing SELECTs: - -```haskell -select - (List $ - #table ! #field1 - :* #table ! #field2 - :* Nil) - (from ...) -``` - -But this final `Nil` is actually unnecessary! The NP cons operator `:*` takes -another NP as its second argument, which is why you might think you need Nil to -terminate the NP list. But if you look at the instances of `IsQualified`, it can -polymorphically return an NP already! - -![IsQualified allows us to directly create a value of type NP](squeal-core-concepts-handbook/isqualified-can-be-np.png) - -So we can simplify our definition a bit: - -```haskell --- this typechecks -select - (List $ - -- note that these two lines have different types - (#table ! #field1 :: Aliased (Expression _ _ _ _ _) _) - :* (#table ! #field2 :: NP (Aliased (Expression _ _ _ _ _) _))) - (from ...) -``` - -We'll see later that this is also Squeal's preferred way to handle things like -CTEs. - -What does all this mean for us? It gives us the type signatures we'd need when -we want to, say, have a common query that's parameterized by a table name, or a -query that needs to be parameterized by which column to run a WHERE on. For -instance, for a table name as a parameter, we'd want an `Aliased (QualifiedAlias -sch) tab`, since that's the parameter for `table`. We can then be assured that -callers can construct values of this type, since we can see an instance for -`IsQualified` for it. - -![Table names can be constructed through IsQualified as well](squeal-core-concepts-handbook/isqualified-alias.png) - -Following the typeclasses, we see that we can do something similar for -Expression, since there are instances for `IsQualified` and `Aliasable` for -generating Expressions as well. - -If you wanted to be completely general and allow a Squeal identifier to be used -in, say, both an expression and table name position, you could use the -IsQualified and Aliasable constraints directly. In general this doesn't seem to -be very useful though. - -One last thing is that while the return type of `(!)` is polymorphic, its -arguments are not; it takes two [`Alias`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Type-Alias.html#t:Alias)es, which are essentially renamed -`Proxy`'s. Since you can also generate Aliases from labels like `#table`, this -provides another avenue you can use to make your queries more general and -modular; take in Aliases as parameters and use `(!)` to convert to the type you -need on-demand. - -## Squeal's type parameters - -It's finally time to talk about the heart and soul of Squeal, what makes it -tick: the plethora of type parameters on all of its core types. - -This is where Squeal handles the heavy lifting of ensuring that all of your -queries are well-formed, that they reference columns that actually exist, that -your SQL comparisons are well-typed, that the columns that you insert as DEFAULT -actually have default values, and so on. - -To refresh your memory, here's the full type signature of `Expression`, in all of -its phantomly-typed glory: - -```haskell -newtype Expression grp lat with db params from ty -``` - -One quick note before we dive into these. Throughout Squeal you'll see the -type operator `(:::)`. For instance, Squeal uses type-level specifications of -table columns that look like so: - -```haskell -type Columns = - '[ "col1" ::: 'NotNull 'PGbool - , "col2" ::: 'NotNull 'PGint4 - ] -``` - -In the Squeal ecosystem, this essentially means "type of." Note that it's *3* -colons, not 2 like for normal type signatures. Underneath the hood, `(:::)` is -just an alias for type-level tuples, so the above type is equivalent to the -following: - -```haskell -type Columns = - '[ '("col1", 'NotNull 'PGbool) - , '("col2", 'NotNull 'PGint4) - ] -``` - -With that out of the way, let's go through Squeal's type parameters one-by-one, -shall we? - -### `grp` - -In SQL, an expression is either a "normal" value or an aggregate value. You probably -already understand this intuitively, even if you never put a name on it. For -instance, let's say you wrote a query like the following: - -```SQL -CREATE TABLE foo - ( user_id INT4 NOT NULL - , value INT4 NOT NULL - ); - -SELECT user_id, SUM(value) - FROM foo - GROUP BY user_id; -``` - -In the select list of the above query, `user_id` and `SUM(value)` are both -"aggregate" values; **user_id** because it's included in the GROUP BY, and -**SUM(value)** since it's directly calling an aggregate function. - -If we don't call any aggregating functions or do a GROUP BY, we have "normal" -values instead: - -```SQL -SELECT user_id, value - FROM foo; -``` - -It's important to understand the distinction, because you can't mix normal and -aggregate values in the same query: - -```SQL -SELECT user_id, SUM(value) - FROM foo; - --- ERROR: column "foo.user_id" must appear in the GROUP BY clause or be used in an aggregate function --- LINE 1: SELECT user_id, SUM(value) -``` - -Postgres barfs, because SUM constrains it to only produce one row, but then what -should the `user_id` of that row be? The complicated part is that an identifier -like `user_id` could be either a normal or an aggregate value, depending on the -surrounding context. So Squeal is obligated to keep track of whether an -expression was generated from an aggregate function or is part of the GROUP BY, -which it does using the `grp` type parameter on an Expression. - -It can either be `'Ungrouped`, meaning a normal value, or `'Grouped bys`, which -indicates that Expression is valid in any context where the columns `bys` are -grouped. - -For instance, selecting `#table ! #field1` in an aggregated context -would want a type like `Expression ('Grouped '[ "table" ::: "field1" ]) ...`, -indicating that this selection isn't valid without that field grouped. Calling -a function like Squeal's [`count`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Expression-Aggregate.html#v:count) -would return a type like `Expression ('Grouped bys) ...`, where the type variable -being abstract means that it doesn't care *what* columns the query is grouped by, -just that it's grouped in *some* way. - -However, if you use an aggregate function, the query *does* have to be grouped, -which can cause a confusing error the first time you see it. If you're used -to writing things like `SELECT COUNT(id) FROM ...` in SQL, you might try to -naively translate this into Squeal like so: - -```haskell -foo :: Query lat with - '[ "public" ::: - '[ "table" ::: 'Table - ('[] :=> '[ "id" ::: 'NoDef :=> 'NotNull 'PGint4 ]) - ] - ] - '[] - '[ "a" ::: 'NotNull 'PGint8 ] -foo = select - (count (All $ #table ! #id) `as` #a) - (from (table #table)) -``` - -Which will promptly fail with a type error: - -```haskell - • No instance for (Aggregate AggregateArg (Expression 'Ungrouped)) - arising from a use of ‘count’ - • In the first argument of ‘as’, namely - ‘count (All $ #table ! #id)’ - In the first argument of ‘select’, namely - ‘(count (All $ #table ! #id) `as` #a)’ - In the expression: - select (count (All $ #table ! #id) `as` #a) (from (table #table)) -``` - -What this is *essentially* telling you is that you forgot to group your query, -which you need to do **even if you're running your aggregate on all rows**. We -do this by explicitly telling Squeal to group on no columns, which is one of the -few legitimate uses of `Nil`: - -```haskell -foo :: Query lat with - '[ "public" ::: - '[ "table" ::: 'Table - ('[] :=> '[ "id" ::: 'NoDef :=> 'NotNull 'PGint4 ]) - ] - ] - '[] - '[ "a" ::: 'NotNull 'PGint8 ] -foo = select - (count (All $ #table ! #id) `as` #a) - ( from (table #table) - & groupBy Nil -- group by nothing! - ) -``` - -You will mess this up at least 2 times. - -### `from` - -When a column is specified in the selection list, how do you know when it's -valid? This may seem like a silly question, but consider the following -(stupid) query: - -```SQL -SELECT bar.id FROM foo; -``` - -No one would write a query like this. Syntactically it seems fine. But it's -nonsense, because we haven't pulled in the `bar` table to our query! And -Postgres agrees with us. - -``` -ERROR: missing FROM-clause entry for table "bar" -LINE 1: SELECT bar.id -``` - -Every query creates a *scope* for identifiers, separate from the database scope -of table names and schemas, and each join adds a new name to this scope. This is -plain to see if we use AS to rename a table that we pull in. - -```haskell -SELECT f.user_id FROM foo AS f; --- ok - -SELECT foo.user_id FROM foo AS f; --- ERROR: invalid reference to FROM-clause entry for table "foo" --- LINE 1: SELECT foo.user_id FROM foo AS f; -``` - -The name `f` is only in scope for this query. Even when you *don't* specify -an alias in the SQL, it works as if you had specified the alias as the table's -name, which we can see since the latter query gets rejected by Postgres. - -In order to figure out whether identifiers like `#table ! #field` are valid, -Squeal needs to track this scope as well. It does so using the `from` type -variable. - -In fact, this type variable is all you need to write standalone expressions. - -```haskell --- doesn't typecheck -badExpr :: Expression 'Ungrouped '[] with db params from ('NotNull 'PGint4) -badExpr = #table ! #field - --- does typecheck -goodExpr :: Expression 'Ungrouped '[] with db params - '[ "table" ::: '[ "field" ::: 'NotNull 'PGint4 ] ] - ('NotNull 'PGint4) -goodExpr = #table ! #field -``` - -Whenever you create an expression by referring to a column by name, it's this -scope inside the `from` type variable that Squeal checks to ensure that the -reference is valid. If you're getting `HasErr` constraint violations, it's -likely that the contents of this variable aren't in the right form. - -The only way to add things to `from` is by using joins. If you look at the type -signature of the various join functions that Squeal provides, they take an -existing `FromClause` and add an extra table to it. - -```haskell -table - :: (Has sch db schema, Has tab schema (Table table)) - => Aliased (QualifiedAlias sch) (alias ::: tab) - -> FromClause lat with db params '[alias ::: TableToRow table] - -- `table` (and `view`, `common`, `subquery`) produce a FromClause - -- containing a single set of columns... - -innerJoin - :: FromClause lat with db params right - -> Condition Ungrouped lat with db params (Join left right) - -> FromClause lat with db params left - -> FromClause lat with db params (Join left right) - -- ...which the join functions append to the tables already in `from` -``` - -For a more complicated query, here's what the value of `from` may end up -looking like: - -```haskell -type QueryFrom = - '[ "table1" ::: - '[ "id" ::: 'NotNull 'PGuuid - , "field1" ::: 'Null 'PGint4 - , "field2" ::: 'NotNull 'PGtext - ] - , "table2" ::: - '[ "id" ::: 'NotNull 'PGuuid - , "field1" ::: 'NotNull 'PGbool - ] - , "table3" ::: - '[ "id" ::: 'NotNull 'PGuuid - , "table2_id" ::: 'NotNull 'PGuuid - , "created_at" ::: 'NotNull 'PGtimestamptz - ] - ] - -type DB = -- ... some schema with the appropriate tables ... - -froms :: FromClause lat with DB params QueryFrom -froms = - ( table (#table1 `as` #table1) - & innerJoin (table (#table2 `as` #table2)) - (#table2 ! #id .== #table1 ! #id) - & innerJoin (table #table3) - (#table3 ! #table2_id .== #table2 ! #id) - ) -``` - -Note that the order of tables in `from` needs to be the same order in which they -were joined. - -Explicitly providing a type alias for which tables a query has access to -like this is often useful, especially when you want to allow callers of your -query to pass in custom expressions. For instance, if you wanted to allow -callers to pass in a filtering condition, you could do that by ensuring that -the `from` in the passed-in Expression is valid for the tables that the query -joins on. - -```haskell -parameterized :: - Expression 'Ungrouped lat with db params QueryFrom ('NotNull 'PGbool) - -> Query lat with db params '[ "a" ::: 'NotNull 'PGint4 ] -parameterized cond = - select - (#table1 ! #field1) - ( from froms - & where_ cond - ) -``` - -Then, as you'd expect, any callsite could use any columns specified in that -query scope, while rejecting any references that aren't in scope. - -```haskell --- these typecheck -qry1 = parameterized (#table1 ! #field2 .== "mobile") -qry2 = parameterized (#table2 ! #field1) - --- these don't -qry3 = parameterized (#table4 ! #id .== "SimSpace") -qry4 = parameterized (#table1 ! #nonfield) -``` - -Note the "flow" of type-level data going on here: we start off with existing -table definitions in `db`, which get pulled into the value of `from` by using -`innerJoin`, `leftOuterJoin`, etc. Once `from` has the right type-level value, -now expressions like `#table1 ! #field2` will satisfy their `IsQualified` -constraint and typecheck. - -### `lat` - -Remember when I said that `from` was the only place that Squeal looked when -it was checking whether column references were valid? That was a slight lie. -Squeal actually checks one other place, the `lat` type param. - -You can see this in the various IsQualified instances for Expression, Aliased Expression, -etc. - -![IsQualified looks in both lat and from for scoping expressions](squeal-core-concepts-handbook/isqualified-join-constraint.png) - -Other than joins, there's one other place where identifiers can come into scope -for a query: a Postgres-specific feature called lateral joins. If you've used -Microsoft SQL Server before, you might know these as CROSS APPLYs, but if not, -it's totally understandable if you've never seen a lateral join before. - -To understand what a lateral join does, it's helpful to compare it to a normal -subquery join. When you do a normal subquery join, the subquery is a completely -different query from the one it's being joined into, which means that the subquery -can't access anything that's in scope from the parent query. - -```SQL -SELECT * - FROM some_table st - JOIN (SELECT * FROM some_other_table sot WHERE st.id = sot.table_id) - -- error: st is not in scope in the subquery -``` - -But sometimes having those values in scope would be extremely useful. Say you -have a table for your users, and another table containing login events. - -```SQL -CREATE TABLE "user" - ( id SERIAL PRIMARY KEY NOT NULL - , username TEXT NOT NULL - , email TEXT NOT NULL - , created_at TIMESTAMPTZ NOT NULL - ); - -CREATE TABLE "login_event" - ( user_id INT4 NOT NULL REFERENCES "user" (id) - , source TEXT NOT NULL -- web|mobile|embed - , timestamp TIMESTAMPTZ NOT NULL - ); -``` - -How would you write a query to get, for each user, the latest login, including -all the username/email information, and the source information? It's possible -using normal subqueries, albeit somewhat painful to write. If you don't believe -me, stop reading and try writing a raw SQL query to get this data. - -Lateral joins give you a more convenient way to write "dependent" queries like -this. They act more like a "foreach" loop: Postgres will run the lateral -subquery for each row being joined to, and calculate separate sets of rows to -join with. - -```SQL -SELECT u.id, u.username, u.email, u.created_at, - le.source, le.timestamp - FROM public.user AS u - INNER JOIN LATERAL - (SELECT le.source, le.timestamp - FROM public.login_event AS le - WHERE le.user_id = u.id - ORDER BY le.timestamp DESC - LIMIT 1) AS le - ON TRUE; -``` - -Helpful, right? But since it's another way in which identifiers can come into -scope for a query, Squeal needs a way to represent it. - -Structurally, `lat` looks exactly like `from`; it's a mapping of table -names to lists of columns and their types. You'll never have to worry about it -unless you're using lateral joins; Squeal provides explicit lateral versions -of all the normal join functions which are there if you need them. Since most -of the time you won't be using these, well... - -### `db` - -We saw in the discussion of `from` how that type parameter gets populated by -usages of functions like `innerJoin`, `leftOuterJoin`, and so on. But the -table types that get joined on have to *come* from `db`, which we can see -happening in the type signatures for `table` and `view`: - -```haskell -table - :: (Has sch db schema, Has tab schema (Table table)) - => Aliased (QualifiedAlias sch) (alias ::: tab) - -> FromClause lat with db params '[alias ::: TableToRow table] - -view - :: (Has sch db schema, Has vw schema (View view)) - => Aliased (QualifiedAlias sch) (alias ::: vw) - -> FromClause lat with db params '[alias ::: view] -``` - -It can be a little easier to see what's going on here if we have an explicit -DB type. Let's define one. - -```haskell -type DB = '[ "public" ::: Schema ] - -type Schema = '[ "users" ::: Table UsersTable ] - -type UsersTable = - '[] :=> '[ "id" ::: 'NotNull 'PGuuid ] - --- using our new type -table (#public ! #users) -``` - -Walk through the constraints being discharged: the first `Has sch db schema` -becomes `Has "public" DB schema`, checking whether the "public" schema exists -within our database type. The second `Has` becomes `Has "users" schema (Table table)`, -checking whether there's a correctly named table within the schema found by -the first constraint. Once those constraints are discharged, the compiler -is happy to give us a `FromClause` which we can use in our joins and queries, -thus bringing columns into scope. - -That's all `db` is: a type-level tree structure representing your entire -database schema, such that downstream code can figure out what tables and -columns exists, and thus, whether your queries are reasonable. You can think -of it as the source of truth about column types that everything starts from. - -In practice most of the compilation errors you encounter won't be caused by your -`db` type. It's not usually a go-to type parameter to put constraints on for -abstraction, either. Squeal does not provide any functionality for keeping -your top-level DB type definition in sync with your actual Postgres schema, -however. Ensuring that your DB type reflects reality is your responsibility. - -### `with` - -You've probably seen SQL queries of the form `WITH AS SELECT ...`, -like a let-binding but for a subquery. Squeal has support for these sorts of -queries as well, using the [`with`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Query-With.html#v:with) function. - -```haskell -qry :: Query lat with DB params '[ "a" ::: 'NotNull 'PGbool ] -qry = with - (select (true `as` #a) (from (table #users)) `as` #cte) - (select Star - (from (common #cte))) -- note the use of `common` to bring the CTE into scope - -- for this query -``` - -Since these subqueries are scoped to each individual query, Squeal needs some -way of keeping track of them so that it can check that usages of `common` are -valid. As you might guess, that storage is happening in the `with` type -parameter. - -```haskell -with - ( select (true `as` #a) (from (table #users)) `as` #subq1 - :>> select (false `as`#b) (from (table #users)) `as` #subq2 - ) - - --- :: Query ... with ... --- where `with` = '[ "subq1" ::: '[ "a" ::: 'NotNull 'PGbool ], "subq2" ::: '[ "b" ::: 'NotNull 'PGbool ] ] -``` - -Note that when passing the CTEs to `with`, we use `(:>>)` to construct the list. -When constructing select lists and such, we were using `(:*)`, but here the -argument has a different type. Instead of a value of type `NP`, `with` takes in -a value of type [`Path`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Type-List.html#t:Path). -The practical difference is that subqueries further in the list can use subqueries -earlier in the list. Just remember that you need to use this pointier list -constructor when using CTEs. - -In theory `with` can be a useful point of abstraction, by specifying that some -query needs a subquery of some other type in scope. In practice you can -usually just pass that subquery as a Haskell parameter. - -One small trick about using CTEs in Squeal: `with` requires its subqueries to -all be the same type, either all `Query`'s or all `Manipulation`s. That's -a little annoying, since it seems like you couldn't, say, run an update and -then do a query on the updated rows; you'd have a `Manipulation` and a `Query` -in the same CTE expression. But Squeal provides a convenient function -[`queryStatement`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Manipulation.html#v:queryStatement) -to convert from a `Query` to a `Manipulation` (since any query is just a manipulation -that touches no rows). That'll allow you to implement cases like these. - -A caution about the queries in a CTE block: Squeal represents them as a -sequential path, but [Postgres actually runs them concurrently, so their order is -unpredictable.](https://www.postgresql.org/docs/12/queries-with.html#QUERIES-WITH-MODIFYING) -So you may not be able to rely on them to, say, run an update statement and a query in the order -they're written. You can resolve this by having one statement in the query -depend on a value returned by another one. - -### `params` - -When generating queries, it's possible to write normal Haskell functions that -take in values and inline those values into your queries. However, Squeal also -provides a built-in hole for parameters, which you use through the `param` -function instead of inlining them. - -```haskell -qry - :: params ~ '[ 'NotNull 'PGint4, 'NotNull 'PGtext ] - => Query lat with DB params '[ "email" ::: 'Null 'PGtext ] -qry = select - (#e ! #email) - ( from (table (#users `as` #u) - & innerJoin (table (#emails `as` #e)) - (#e ! #user_id .== #u ! #id)) - & where_ (#u ! #id .== param @1) -- specify which param with TypeApplications - & where_ (#u ! #name .== param @2) -- note that the params are 1-indexed - ) -``` - -The main reason to specify the `params` variable and pass values in this way -is to make some common value available to all the components of a query, without -needing to explicitly pass it around. For instance, when writing a query with -CTEs, you'll probably have some common ID that all parts of the query need. - -```haskell -qry - :: params ~ '[ 'NotNull 'PGuuid ] - => Query lat with DB params ... -qry = with - ( ... -- all CTE definitions here can make use of the UUID param - ) - ... -- as can the main query -``` - -Another distinction between inlining and parameters is that parameters are -supplied to the DB using Postgres' binary format, and thus aren't susceptible -to SQL injection attacks. Inlined values are, as the name implies, directly -inlined in the SQL string that gets sent to the database, and thus quite a bit -more dangerous. - -Because of this, you should prefer passing data into a query using parameters -whenever possible. - -There isn't much else to say about the `params` type variable; this is all it's -used for. - -### `ty` - -Nothing special here, it's just the Postgres type and nullity of the expression. - -You can see the possible nullities on the constructors of [`NullType`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Type-Schema.html#t:NullType), -and the possible types on the constructors of [`PGType`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Type-Schema.html#t:PGType). - -## Encoding and decoding - -Despite everything we've looked at, we still don't know how to bridge the gap -between Haskell-land and Postgres-land. How do we get the data from the database -into a form that our program can understand? - -As mentioned earlier, the type that bridges this gap is `Statement`, as it -combines some query with encoders for parameters, and decoders for PG rows. - -Squeal provides the [`query`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Session-Statement.html#v:query) -and [`manipulation`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Session-Statement.html#v:manipulation) -functions for converting Query's and Manipulations into Statement, but note -that these rely on Squeal's generic encoding/decoding, which takes away control -of the translation. Sometimes it works, but most of the time you're not doing -1-to-1 translation between a record type and rows with *exactly* the same number -of columns, where the column names are *exactly* the same as the record fields. -For those cases, it's better to write your codecs manually. - -My recommendation is to use the constructors of [`Statement`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Session-Statement.html#t:Statement) -directly: `Query` and `Manipulation`. You can see that these take in -arguments of type [`EncodeParams`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Session-Encode.html#t:EncodeParams) -and [`DecodeRow`](https://hackage.haskell.org/package/squeal-postgresql-0.7.0.1/docs/Squeal-PostgreSQL-Session-Decode.html#t:DecodeRow). - -For `EncodeParams`, you need to turn the parameter type into a list of functions -for pulling the individual values using `(.*)` and `(*.)`. `(*.)` is for the -very last element of the list, `(.*)` is for everything else. - -```haskell -data SomeType = SomeType { a :: Bool, b :: Int32, c :: Int64 } - --- Notice how we don't actually take in a value of SomeType, --- we just create a list of accessor functions -enc :: EncodeParams db '[ 'NotNull 'PGbool, 'NotNull 'PGint4, 'NotNull 'PGint8 ] SomeType -enc = a .* b *. c -``` - -You will mix up these operators and get a confusing type error at least once. - -`DecodeRow` implements both `Monad` and `IsLabel`, which lets you construct -decoders like the following: - -```haskell --- Note how we can decode from a row with completely different --- column names from our record type -dec :: DecodeRow - '[ "a_bool" ::: 'NotNull 'PGbool - , "a_4byteint" ::: 'NotNull 'PGint4 - , "an_8byteint" ::: 'NotNull 'PGint8 - ] - SomeType -dec = do - a <- #a_bool - b <- #a_4byteint - c <- #an_8byteint - pure $ SomeType { a = a, b = b, c = c } -``` - -Since it implements `Monad`, you can implement complicated conditional parsers, -key off of certain columns being null or non-null to parse further columns, -construct intermediate data structures from groups of fields, etc. diff --git a/squeal-core-concepts-handbook/isqualified-alias.png b/squeal-core-concepts-handbook/isqualified-alias.png deleted file mode 100644 index 8d21114861181ec79a1fab6db223e1d5b550af29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12057 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 Vrjxd(K#wYtxUjTP(bw<4{vQXjIf?)P diff --git a/squeal-core-concepts-handbook/isqualified-can-be-np.png b/squeal-core-concepts-handbook/isqualified-can-be-np.png deleted file mode 100644 index 6788eb62b4a56a954bade7d58c2a4961bfc1770c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39024 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 z 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 diff --git a/squeal-core-concepts-handbook/isqualified-intro.png b/squeal-core-concepts-handbook/isqualified-intro.png deleted file mode 100644 index 004d26e622c2662a9351dc85bada86818ccda161..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57879 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

yXj8Y^mt-_kBLGEf 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 CKd7$& From f63e9b4f135aed5954c7b7521225006f8b561d30 Mon Sep 17 00:00:00 2001 From: Eitan Chatav Date: Wed, 26 Jan 2022 12:17:11 -0800 Subject: [PATCH 17/18] fix haddock --- squeal-postgresql/squeal-postgresql.cabal | 1 - .../src/Squeal/PostgreSQL/Type/Schema.hs | 12 ++++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/squeal-postgresql/squeal-postgresql.cabal b/squeal-postgresql/squeal-postgresql.cabal index d58845a4..0efa424e 100644 --- a/squeal-postgresql/squeal-postgresql.cabal +++ b/squeal-postgresql/squeal-postgresql.cabal @@ -11,7 +11,6 @@ copyright: Copyright (c) 2021 Morphism, LLC category: Database build-type: Simple cabal-version: >=1.18 -extra-doc-files: README.md source-repository head type: git diff --git a/squeal-postgresql/src/Squeal/PostgreSQL/Type/Schema.hs b/squeal-postgresql/src/Squeal/PostgreSQL/Type/Schema.hs index 9872b1a0..260ef86a 100644 --- a/squeal-postgresql/src/Squeal/PostgreSQL/Type/Schema.hs +++ b/squeal-postgresql/src/Squeal/PostgreSQL/Type/Schema.hs @@ -1,3 +1,15 @@ +{-| +Module: Squeal.PostgreSQL.Type.Schema +Description: Postgres type system +Copyright: (c) Eitan Chatav, 2019 +Maintainer: eitan@morphism.tech +Stability: experimental + +Provides a type-level DSL for kinds of Postgres types, +tables, schema, constraints, and more. +It also defines useful type families to operate on these. +-} + module Squeal.PostgreSQL.Type.Schema ( -- * Postgres Type PGType (..) From 9688a12506e9b384b6f7b697731ad46c981612ff Mon Sep 17 00:00:00 2001 From: Eitan Chatav Date: Thu, 21 Apr 2022 18:33:00 -0700 Subject: [PATCH 18/18] try adding dependency --- squeal-postgresql/squeal-postgresql.cabal | 1 + 1 file changed, 1 insertion(+) diff --git a/squeal-postgresql/squeal-postgresql.cabal b/squeal-postgresql/squeal-postgresql.cabal index 0efa424e..221f9310 100644 --- a/squeal-postgresql/squeal-postgresql.cabal +++ b/squeal-postgresql/squeal-postgresql.cabal @@ -34,3 +34,4 @@ test-suite doctest build-depends: base >= 4.12.0.0 && < 5.0 , doctest >= 0.16.3 + , squeal-postgresql