on master help sign in
Add upsert support and allow arbitrary queries in INSERT, UPDATE and DELETE (#85)

This PR makes several changes to our "manipulation" functions (`insert`, `update`, `delete`). Firstly, we now support `ON CONFLICT DO UPDATE`, aka "upsert". Secondly, we now allow the insertion of arbitrary queries (not just static `VALUES`). `values` recovers the old behaviour. Thirdly, our `Update` and `Delete` statements now support `FROM` and `USING` clauses respectively, allowing joining against other tables. Fourthly, `Returning` is now an `Applicative`, which means you can say `returning = pure ()` if you don't care about the number of rows affected. In terms of generating the SQL to implement these features, it was unfortunately significantly less work to roll our own here than to add this upstream to Opaleye proper, because it would have required more refactoring than I felt comfortable doing.

commit5d2b9e4cdaa3ebcdaf49a66f39db0913df020e44 authorShane <shane.obrien@circuithub.com> dateThu 15 Jul 2021, 00:30 (5 years ago) parentb44ea81
21 files changed+720−316
docs/concepts/insert.rst+37−10 rel8.cabal+6 src/Rel8.hs+9 src/Rel8/Query/SQL.hs+5−59 src/Rel8/Schema/Name.hs+12 src/Rel8/Schema/Table.hs+17 src/Rel8/Statement/Delete.hs+34−40 src/Rel8/Statement/Insert.hs+46−65 src/Rel8/Statement/OnConflict.hs+106 src/Rel8/Statement/Returning.hs+121−10 src/Rel8/Statement/SQL.hs+29 src/Rel8/Statement/Select.hs+77−20 src/Rel8/Statement/Set.hs+33 src/Rel8/Statement/Update.hs+38−48 src/Rel8/Statement/Using.hs+36 src/Rel8/Statement/View.hs+20−20 src/Rel8/Statement/Where.hs+31 src/Rel8/Table/Name.hs+3−17 src/Rel8/Table/Opaleye.hs+36−4 src/Rel8/Table/Projection.hs+4−4 tests/Main.hs+20−19
docs/concepts/insert.rst+37−10
@@ -21,9 +21,16 @@ using ``delete``. ``Delete`` takes:
2121``from``
2222 The ``TableSchema`` for the table to delete rows from.
2323
24+``using``
25+ This is a simple ``Query`` that forms the ``USING`` clause of the ``DELETE``
26+ statement. This can be used to join against other tables, and the results
27+ can be referenced in the ``deleteWhere`` parameter. For simple ``DELETE``\s
28+ where you don't need to do this, you can set ``using = pure ()``.
29+
2430``deleteWhere``
2531 The ``WHERE`` clause of the ``DELETE`` statement. This is a function that
26− takes a single ``Expr`` table as input.
32+ takes two inputs: the result of the ``using`` query, and the current value
33+ of the row.
2734
2835``returning``
2936 What to return - see :ref:`returning`.
@@ -37,16 +44,23 @@ using ``update``. ``Update`` takes:
3744``target``
3845 The ``TableSchema`` for the table to update rows in.
3946
40−``updateWhere``
41− The ``WHERE`` clause of the ``UPDATE`` statement. This is a function that
42− takes a single ``Expr`` table as input.
47+``from``
48+ This is a simple ``Query`` that forms the ``FROM`` clause of the ``UPDATE``
49+ statement. This can be used to join against other tables, and the results
50+ can be referenced in the ``set`` and ``updateWhere`` parameters. For simple
51+ ``UPDATE``\s where you don't need to do this, you can set ``from = pure ()``.
4352
4453``set``
4554 A row to row transformation function, indicating how to update selected rows.
4655 This function takes rows of the same shape as ``target`` but in the ``Expr``
4756 context. One way to write this function is to use record update syntax::
4857
49− set = \row -> row { rowName = "new name" }
58+ set = \from row -> row { rowName = "new name" }
59+
60+``updateWhere``
61+ The ``WHERE`` clause of the ``UPDATE`` statement. This is a function that
62+ takes two inputs: the result of the ``from`` query, and the current value of
63+ the row.
5064
5165``returning``
5266 What to return - see :ref:`returning`.
@@ -64,11 +78,11 @@ using ``insert``. ``Insert`` takes:
6478 The rows to insert. These are the same as ``into``, but in the ``Expr``
6579 context. You can construct rows from their individual fields::
6680
67− rows = [ MyTable { myTableA = lit "A", myTableB = lit 42 }
81+ rows = values [ MyTable { myTableA = lit "A", myTableB = lit 42 }
6882
6983 or you can use ``lit`` on a table value in the ``Result`` context::
7084
71− rows = [ lit MyTable { myTableA = "A", myTableB = 42 }
85+ rows = values [ lit MyTable { myTableA = "A", myTableB = 42 }
7286
7387``onConflict``
7488 What should happen if an insert clashes with rows that already exist. This
@@ -80,6 +94,10 @@ using ``insert``. ``Insert`` takes:
8094 ``DoNothing``
8195 PostgreSQL should not insert the duplicate rows.
8296
97+ ``DoUpdate``
98+ PostgreSQL should instead try to update any existing rows that conflict
99+ with rows proposed for insertion.
100+
83101``returning``
84102 What to return - see :ref:`returning`.
85103
@@ -99,11 +117,20 @@ For example, if we are inserting orders, we might want the order ids returned::
99117
100118 insert Insert
101119 { into = orderSchema
102− , rows = [ order ]
120+ , rows = values [ order ]
103121 , onConflict = Abort
104122 , returning = Projection orderId
105123 }
106124
125+If we don't want to return anything, we can use ``pure ()``::
126+
127+ insert Insert
128+ { into = orderSchema
129+ , rows = values [ order ]
130+ , onConflict = Abort
131+ , returning = pure ()
132+ }
133+
107134Default values
108135--------------
109136
@@ -119,7 +146,7 @@ construct the ``DEFAULT`` expression::
119146
120147 insert Insert
121148 { into = orderSchema
122− , rows = [ Order { orderId = unsafeDefault, ... } ]
149+ , rows = values [ Order { orderId = unsafeDefault, ... } ]
123150 , onConflict = Abort
124151 , returning = Projection orderId
125152 }
@@ -148,7 +175,7 @@ them in Rel8, rather than in your database schema.
148175
149176 insert Insert
150177 { into = orderSchema
151− , rows = [ Order { orderId = nextval "order_id_seq", ... } ]
178+ , rows = values [ Order { orderId = nextval "order_id_seq", ... } ]
152179 , onConflict = Abort
153180 , returning = Projection orderId
154181 }
rel8.cabal+6
@@ -27,6 +27,7 @@ library
2727 , contravariant
2828 , hasql ^>= 1.4.5.1
2929 , opaleye ^>= 0.7.3.0
30+ , pretty
3031 , profunctors
3132 , scientific
3233 , semialign
@@ -138,10 +139,15 @@ library
138139
139140 Rel8.Statement.Delete
140141 Rel8.Statement.Insert
142+ Rel8.Statement.OnConflict
141143 Rel8.Statement.Returning
142144 Rel8.Statement.Select
145+ Rel8.Statement.Set
146+ Rel8.Statement.SQL
143147 Rel8.Statement.Update
148+ Rel8.Statement.Using
144149 Rel8.Statement.View
150+ Rel8.Statement.Where
145151
146152 Rel8.Table
147153 Rel8.Table.ADT
src/Rel8.hs+9
@@ -244,6 +244,9 @@ module Rel8
244244 , nullsFirst
245245 , nullsLast
246246
247+ -- ** Bindings
248+ , rebind
249+
247250 -- * IO
248251 , Serializable
249252 , ToExprs
@@ -258,16 +261,20 @@ module Rel8
258261 -- ** @INSERT@
259262 , Insert(..)
260263 , OnConflict(..)
264+ , Upsert(..)
261265 , insert
262266 , unsafeDefault
267+ , showInsert
263268
264269 -- ** @DELETE@
265270 , Delete(..)
266271 , delete
272+ , showDelete
267273
268274 -- ** @UPDATE@
269275 , Update(..)
270276 , update
277+ , showUpdate
271278
272279 -- ** @.. RETURNING@
273280 , Returning(..)
@@ -332,8 +339,10 @@ import Rel8.Schema.Result ( Result )
332339import Rel8.Schema.Table
333340import Rel8.Statement.Delete
334341import Rel8.Statement.Insert
342+import Rel8.Statement.OnConflict
335343import Rel8.Statement.Returning
336344import Rel8.Statement.Select
345+import Rel8.Statement.SQL
337346import Rel8.Statement.Update
338347import Rel8.Statement.View
339348import Rel8.Table
src/Rel8/Query/SQL.hs+5−59
@@ -1,75 +1,21 @@
11{-# language FlexibleContexts #-}
2−{-# language TypeFamilies #-}
3−{-# language ViewPatterns #-}
2+{-# language MonoLocalBinds #-}
43
54module Rel8.Query.SQL
65 ( showQuery
7− , sqlForQuery, sqlForQueryWithNames
86 )
97where
108
119-- base
12−import Data.Foldable ( fold )
13−import Data.Functor.Const ( Const( Const ), getConst )
14−import Data.Void ( Void )
1510import Prelude
1611
17−-- opaleye
18−import qualified Opaleye.Internal.HaskellDB.Sql as Opaleye
19−import qualified Opaleye.Internal.PrimQuery as Opaleye
20−import qualified Opaleye.Internal.Print as Opaleye
21−import qualified Opaleye.Internal.Optimize as Opaleye
22−import qualified Opaleye.Internal.QueryArr as Opaleye hiding ( Select )
23−import qualified Opaleye.Internal.Sql as Opaleye
24−
2512-- rel8
2613import Rel8.Expr ( Expr )
27−import Rel8.Expr.Opaleye ( toPrimExpr )
2814import Rel8.Query ( Query )
29−import Rel8.Query.Opaleye ( toOpaleye )
30−import Rel8.Schema.Name ( Name( Name ), Selects )
31−import Rel8.Schema.HTable ( htabulateA, hfield )
32−import Rel8.Table ( Table, toColumns )
33−import Rel8.Table.Cols ( toCols )
34−import Rel8.Table.Name ( namesFromLabels )
35−import Rel8.Table.Opaleye ( castTable )
15+import Rel8.Statement.Select ( ppSelect )
16+import Rel8.Table ( Table )
3617
3718
38−-- | Convert a query to a 'String' containing the query as a @SELECT@
39−-- statement.
19+-- | Convert a 'Query' to a 'String' containing a @SELECT@ statement.
4020showQuery :: Table Expr a => Query a -> String
41−showQuery = fold . sqlForQuery
42−
43−
44−sqlForQuery :: Table Expr a
45− => Query a -> Maybe String
46−sqlForQuery = sqlForQueryWithNames namesFromLabels . fmap toCols
47−
48−
49−sqlForQueryWithNames :: Selects names exprs
50− => names -> Query exprs -> Maybe String
51−sqlForQueryWithNames names query =
52− show . Opaleye.ppSql . selectFrom names exprs <$> optimize primQuery
53− where
54− (exprs, primQuery, _) =
55− Opaleye.runSimpleQueryArrStart (toOpaleye query) ()
56−
57−
58−optimize :: Opaleye.PrimQuery' a -> Maybe (Opaleye.PrimQuery' Void)
59−optimize = Opaleye.removeEmpty . Opaleye.optimize
60−
61−
62−selectFrom :: Selects names exprs
63− => names -> exprs -> Opaleye.PrimQuery' Void -> Opaleye.Select
64−selectFrom (toColumns -> names) (toColumns . castTable -> exprs) query =
65− Opaleye.SelectFrom $ Opaleye.newSelect
66− { Opaleye.attrs = Opaleye.SelectAttrs attributes
67− , Opaleye.tables = Opaleye.oneTable select
68− }
69− where
70− select = Opaleye.foldPrimQuery Opaleye.sqlQueryGenerator query
71− attributes = getConst $ htabulateA $ \field -> case hfield names field of
72− Name name -> case hfield exprs field of
73− expr -> Const (pure (makeAttr name (toPrimExpr expr)))
74− makeAttr label expr =
75− (Opaleye.sqlExpr expr, Just (Opaleye.SqlColumn label))
21+showQuery = foldMap show . ppSelect
src/Rel8/Schema/Name.hs+12
@@ -13,6 +13,7 @@
1313module Rel8.Schema.Name
1414 ( Name(..)
1515 , Selects
16+ , ppColumn
1617 )
1718where
1819
@@ -22,6 +23,13 @@ import Data.Kind ( Constraint, Type )
2223import Data.String ( IsString )
2324import Prelude
2425
26+-- opaleye
27+import qualified Opaleye.Internal.HaskellDB.Sql as Opaleye
28+import qualified Opaleye.Internal.HaskellDB.Sql.Print as Opaleye
29+
30+-- pretty
31+import Text.PrettyPrint ( Doc )
32+
2533-- rel8
2634import Rel8.Expr ( Expr )
2735import qualified Rel8.Schema.Kind as K
@@ -63,3 +71,7 @@ instance Sql DBType a => Table Name (Name a) where
6371type Selects :: Type -> Type -> Constraint
6472class Transposes Name Expr names exprs => Selects names exprs
6573instance Transposes Name Expr names exprs => Selects names exprs
74+
75+
76+ppColumn :: String -> Doc
77+ppColumn = Opaleye.ppSqlExpr . Opaleye.ColumnSqlExpr . Opaleye.SqlColumn
src/Rel8/Schema/Table.hs+17
@@ -1,14 +1,24 @@
11{-# language DeriveFunctor #-}
22{-# language DerivingStrategies #-}
3+{-# language DisambiguateRecordFields #-}
4+{-# language NamedFieldPuns #-}
35
46module Rel8.Schema.Table
57 ( TableSchema(..)
8+ , ppTable
69 )
710where
811
912-- base
1013import Prelude
1114
15+-- opaleye
16+import qualified Opaleye.Internal.HaskellDB.Sql as Opaleye
17+import qualified Opaleye.Internal.HaskellDB.Sql.Print as Opaleye
18+
19+-- pretty
20+import Text.PrettyPrint ( Doc )
21+
1222
1323-- | The schema for a table. This is used to specify the name and schema that a
1424-- table belongs to (the @FROM@ part of a SQL query), along with the schema of
@@ -27,3 +37,10 @@ data TableSchema names = TableSchema
2737 -- data type here, parameterized by the 'Rel8.ColumnSchema.ColumnSchema' functor.
2838 }
2939 deriving stock Functor
40+
41+
42+ppTable :: TableSchema a -> Doc
43+ppTable TableSchema {name, schema} = Opaleye.ppTable Opaleye.SqlTable
44+ { sqlTableSchemaName = schema
45+ , sqlTableName = name
46+ }
src/Rel8/Statement/Delete.hs+34−40
@@ -1,13 +1,15 @@
11{-# language DuplicateRecordFields #-}
22{-# language GADTs #-}
33{-# language NamedFieldPuns #-}
4−{-# language ScopedTypeVariables #-}
4+{-# language RankNTypes #-}
5+{-# language RecordWildCards #-}
56{-# language StandaloneKindSignatures #-}
6−{-# language TypeApplications #-}
7+{-# language StrictData #-}
78
89module Rel8.Statement.Delete
910 ( Delete(..)
1011 , delete
12+ , ppDelete
1113 )
1214where
1315
@@ -18,23 +20,24 @@ import Prelude
1820
1921-- hasql
2022import Hasql.Connection ( Connection )
21−import qualified Hasql.Decoders as Hasql
2223import qualified Hasql.Encoders as Hasql
2324import qualified Hasql.Session as Hasql
2425import qualified Hasql.Statement as Hasql
2526
26−-- opaleye
27−import qualified Opaleye.Internal.Manipulation as Opaleye
27+-- pretty
28+import Text.PrettyPrint ( Doc, (<+>), ($$), text )
2829
2930-- rel8
3031import Rel8.Expr ( Expr )
31−import Rel8.Expr.Opaleye ( toColumn, toPrimExpr )
32+import Rel8.Query ( Query )
3233import Rel8.Schema.Name ( Selects )
33−import Rel8.Schema.Table ( TableSchema )
34−import Rel8.Statement.Returning ( Returning( NumberOfRowsAffected, Projection ) )
35−import Rel8.Table.Cols ( fromCols, toCols )
36−import Rel8.Table.Opaleye ( castTable, table, unpackspec )
37−import Rel8.Table.Serialize ( Serializable, parse )
34+import Rel8.Schema.Table ( TableSchema, ppTable )
35+import Rel8.Statement.Returning
36+ ( Returning
37+ , decodeReturning, emptyReturning, ppReturning
38+ )
39+import Rel8.Statement.Using ( ppUsing )
40+import Rel8.Statement.Where ( ppWhere )
3841
3942-- text
4043import qualified Data.Text as Text
@@ -47,7 +50,10 @@ data Delete a where
4750 Delete :: Selects names exprs =>
4851 { from :: TableSchema names
4952 -- ^ Which table to delete from.
50− , deleteWhere :: exprs -> Expr Bool
53+ , using :: Query using
54+ -- ^ @USING@ clause — this can be used to join against other tables,
55+ -- and its results can be referenced in the @WHERE@ clause
56+ , deleteWhere :: using -> exprs -> Expr Bool
5157 -- ^ Which rows should be selected for deletion.
5258 , returning :: Returning names a
5359 -- ^ What to return from the @DELETE@ statement.
@@ -55,38 +61,26 @@ data Delete a where
5561 -> Delete a
5662
5763
58−-- | Run a @DELETE@ statement.
59−delete :: Connection -> Delete a -> IO a
60−delete c Delete {from, deleteWhere, returning} =
61− case returning of
62− NumberOfRowsAffected -> Hasql.run session c >>= either throwIO pure
63− where
64− session = Hasql.statement () statement
65− statement = Hasql.Statement bytes params decode prepare
66− bytes = encodeUtf8 $ Text.pack sql
67− params = Hasql.noParams
68− decode = Hasql.rowsAffected
69− prepare = False
70− sql = Opaleye.arrangeDeleteSql from' where'
71− where
72− from' = table $ toCols <$> from
73− where' = toColumn . toPrimExpr . deleteWhere . fromCols
64+ppDelete :: Delete a -> Maybe Doc
65+ppDelete Delete {..} = do
66+ (usingDoc, i) <- ppUsing using
67+ pure $ text "DELETE FROM" <+> ppTable from
68+ $$ usingDoc
69+ $$ ppWhere from (deleteWhere i)
70+ $$ ppReturning from returning
7471
75− Projection project -> Hasql.run session c >>= either throwIO pure
72+
73+-- | Run a 'Delete' statement.
74+delete :: Connection -> Delete a -> IO a
75+delete connection d@Delete {returning} =
76+ case show <$> ppDelete d of
77+ Nothing -> pure (emptyReturning returning)
78+ Just sql ->
79+ Hasql.run session connection >>= either throwIO pure
7680 where
7781 session = Hasql.statement () statement
7882 statement = Hasql.Statement bytes params decode prepare
7983 bytes = encodeUtf8 $ Text.pack sql
8084 params = Hasql.noParams
81− decode = decoder project
85+ decode = decodeReturning returning
8286 prepare = False
83− sql =
84− Opaleye.arrangeDeleteReturningSql unpackspec from' where' project'
85− where
86− from' = table $ toCols <$> from
87− where' = toColumn . toPrimExpr . deleteWhere . fromCols
88− project' = castTable . toCols . project . fromCols
89− where
90− decoder :: forall exprs projection a. Serializable projection a
91− => (exprs -> projection) -> Hasql.Result [a]
92− decoder _ = Hasql.rowList (parse @projection @a)
src/Rel8/Statement/Insert.hs+46−65
@@ -1,64 +1,65 @@
11{-# language DuplicateRecordFields #-}
2+{-# language FlexibleContexts #-}
23{-# language GADTs #-}
34{-# language NamedFieldPuns #-}
4−{-# language ScopedTypeVariables #-}
5+{-# language RecordWildCards #-}
56{-# language StandaloneKindSignatures #-}
6−{-# language TypeApplications #-}
7+{-# language StrictData #-}
78
89module Rel8.Statement.Insert
910 ( Insert(..)
10− , OnConflict(..)
1111 , insert
12+ , ppInsert
13+ , ppInto
1214 )
1315where
1416
1517-- base
1618import Control.Exception ( throwIO )
17−import Data.List.NonEmpty ( NonEmpty( (:|) ) )
19+import Data.Foldable ( toList )
1820import Data.Kind ( Type )
1921import Prelude
2022
2123-- hasql
2224import Hasql.Connection ( Connection )
23−import qualified Hasql.Decoders as Hasql
2425import qualified Hasql.Encoders as Hasql
2526import qualified Hasql.Session as Hasql
2627import qualified Hasql.Statement as Hasql
2728
2829-- opaleye
29−import qualified Opaleye.Internal.Manipulation as Opaleye
30−import qualified Opaleye.Manipulation as Opaleye
30+import qualified Opaleye.Internal.HaskellDB.Sql.Print as Opaleye
31+
32+-- pretty
33+import Text.PrettyPrint ( Doc, (<+>), ($$), parens, text )
3134
3235-- rel8
33−import Rel8.Schema.Name ( Selects )
34−import Rel8.Schema.Table ( TableSchema )
35−import Rel8.Statement.Returning ( Returning( Projection, NumberOfRowsAffected ) )
36−import Rel8.Table.Cols ( fromCols, toCols )
37−import Rel8.Table.Opaleye ( castTable, table, unpackspec )
38−import Rel8.Table.Serialize ( Serializable, parse )
36+import Rel8.Query ( Query )
37+import Rel8.Schema.Name ( Name, Selects, ppColumn )
38+import Rel8.Schema.Table ( TableSchema(..), ppTable )
39+import Rel8.Statement.OnConflict ( OnConflict, ppOnConflict )
40+import Rel8.Statement.Returning
41+ ( Returning
42+ , decodeReturning, emptyReturning, ppReturning
43+ )
44+import Rel8.Statement.Select ( ppSelect )
45+import Rel8.Table ( Table )
46+import Rel8.Table.Name ( showNames )
3947
4048-- text
4149import qualified Data.Text as Text ( pack )
4250import Data.Text.Encoding ( encodeUtf8 )
4351
4452
45−-- | @OnConflict@ allows you to add an @ON CONFLICT@ clause to an @INSERT@
46−-- statement.
47−type OnConflict :: Type
48−data OnConflict
49− = Abort -- ^ @ON CONFLICT ABORT@
50− | DoNothing -- ^ @ON CONFLICT DO NOTHING@
51−
52−
5353-- | The constituent parts of a SQL @INSERT@ statement.
5454type Insert :: Type -> Type
5555data Insert a where
5656 Insert :: Selects names exprs =>
5757 { into :: TableSchema names
5858 -- ^ Which table to insert into.
59− , rows :: [exprs]
60− -- ^ The rows to insert.
61− , onConflict :: OnConflict
59+ , rows :: Query exprs
60+ -- ^ The rows to insert. This can be an arbitrary query — use
61+ -- 'Rel8.values' insert a static list of rows.
62+ , onConflict :: OnConflict names
6263 -- ^ What to do if the inserted rows conflict with data already in the
6364 -- table.
6465 , returning :: Returning names a
@@ -67,52 +68,32 @@ data Insert a where
6768 -> Insert a
6869
6970
70−-- | Run an @INSERT@ statement
71−insert :: Connection -> Insert a -> IO a
72−insert c Insert {into, rows, onConflict, returning} =
73− case (rows, returning) of
74− ([], NumberOfRowsAffected) -> pure 0
75− ([], Projection _) -> pure []
71+ppInsert :: Insert a -> Maybe Doc
72+ppInsert Insert {..} = do
73+ rows' <- ppSelect rows
74+ pure $ text "INSERT INTO" <+> ppInto into
75+ $$ rows'
76+ $$ ppOnConflict into onConflict
77+ $$ ppReturning into returning
7678
77− (x:xs, NumberOfRowsAffected) -> Hasql.run session c >>= either throwIO pure
78− where
79− session = Hasql.statement () statement
80− statement = Hasql.Statement bytes params decode prepare
81− bytes = encodeUtf8 $ Text.pack sql
82− params = Hasql.noParams
83− decode = Hasql.rowsAffected
84− prepare = False
85− sql = Opaleye.arrangeInsertManySql into' rows' onConflict'
86− where
87− into' = table $ toCols <$> into
88− rows' = toCols <$> x :| xs
8979
90− (x:xs, Projection project) -> Hasql.run session c >>= either throwIO pure
80+ppInto :: Table Name a => TableSchema a -> Doc
81+ppInto table@TableSchema {columns} =
82+ ppTable table <+>
83+ parens (Opaleye.commaV ppColumn (toList (showNames columns)))
84+
85+
86+-- | Run an 'Insert' statement.
87+insert :: Connection -> Insert a -> IO a
88+insert connection i@Insert {returning} =
89+ case show <$> ppInsert i of
90+ Nothing -> pure (emptyReturning returning)
91+ Just sql ->
92+ Hasql.run session connection >>= either throwIO pure
9193 where
9294 session = Hasql.statement () statement
9395 statement = Hasql.Statement bytes params decode prepare
9496 bytes = encodeUtf8 $ Text.pack sql
9597 params = Hasql.noParams
96− decode = decoder project
98+ decode = decodeReturning returning
9799 prepare = False
98− sql =
99− Opaleye.arrangeInsertManyReturningSql
100− unpackspec
101− into'
102− rows'
103− project'
104− onConflict'
105− where
106− into' = table $ toCols <$> into
107− rows' = toCols <$> x :| xs
108− project' = castTable . toCols . project . fromCols
109−
110− where
111− onConflict' =
112− case onConflict of
113− DoNothing -> Just Opaleye.DoNothing
114− Abort -> Nothing
115−
116− decoder :: forall exprs projection a. Serializable projection a
117− => (exprs -> projection) -> Hasql.Result [a]
118− decoder _ = Hasql.rowList (parse @projection @a)
src/Rel8/Statement/OnConflict.hsnew file+106
@@ -0,0 +1,106 @@
1+{-# language DuplicateRecordFields #-}
2+{-# language FlexibleContexts #-}
3+{-# language GADTs #-}
4+{-# language LambdaCase #-}
5+{-# language NamedFieldPuns #-}
6+{-# language RecordWildCards #-}
7+{-# language StandaloneKindSignatures #-}
8+{-# language StrictData #-}
9+
10+module Rel8.Statement.OnConflict
11+ ( OnConflict(..)
12+ , Upsert(..)
13+ , ppOnConflict
14+ )
15+where
16+
17+-- base
18+import Data.Foldable ( toList )
19+import Data.Kind ( Type )
20+import Prelude
21+
22+-- opaleye
23+import qualified Opaleye.Internal.HaskellDB.Sql.Print as Opaleye
24+
25+-- pretty
26+import Text.PrettyPrint ( Doc, (<+>), ($$), parens, text )
27+
28+-- rel8
29+import Rel8.Expr ( Expr )
30+import Rel8.Schema.Name ( Name, Selects, ppColumn )
31+import Rel8.Schema.Table ( TableSchema(..) )
32+import Rel8.Statement.Set ( ppSet )
33+import Rel8.Statement.Where ( ppWhere )
34+import Rel8.Table ( Table, toColumns )
35+import Rel8.Table.Cols ( Cols( Cols ) )
36+import Rel8.Table.Name ( showNames )
37+import Rel8.Table.Opaleye ( attributes )
38+import Rel8.Table.Projection ( Projecting, Projection, apply )
39+
40+
41+-- | 'OnConflict' represents the @ON CONFLICT@ clause of an @INSERT@
42+-- statement. This specifies what ought to happen when one or more of the
43+-- rows proposed for insertion conflict with an existing row in the table.
44+type OnConflict :: Type -> Type
45+data OnConflict names
46+ = Abort
47+ -- ^ Abort the transaction if there are conflicting rows (Postgres' default)
48+ | DoNothing
49+ -- ^ @ON CONFLICT DO NOTHING@
50+ | DoUpdate (Upsert names)
51+ -- ^ @ON CONFLICT DO UPDATE@
52+
53+
54+-- | The @ON CONFLICT (...) DO UPDATE@ clause of an @INSERT@ statement, also
55+-- known as \"upsert\".
56+--
57+-- When an existing row conflicts with a row proposed for insertion,
58+-- @ON CONFLICT DO UPDATE@ allows you to instead update this existing row. The
59+-- conflicting row proposed for insertion is then \"excluded\", but its values
60+-- can still be referenced from the @SET@ and @WHERE@ clauses of the @UPDATE@
61+-- statement.
62+--
63+-- Upsert in Postgres requires an explicit set of \"conflict targets\" — the
64+-- set of columns comprising the @UNIQUE@ index from conflicts with which we
65+-- would like to recover.
66+type Upsert :: Type -> Type
67+data Upsert names where
68+ Upsert :: (Selects names exprs, Projecting names index, excluded ~ exprs) =>
69+ { index :: Projection names index
70+ -- ^ The set of conflict targets, projected from the set of columns for
71+ -- the whole table
72+ , set :: excluded -> exprs -> exprs
73+ -- ^ How to update each selected row.
74+ , updateWhere :: excluded -> exprs -> Expr Bool
75+ -- ^ Which rows to select for update.
76+ }
77+ -> Upsert names
78+
79+
80+ppOnConflict :: TableSchema names -> OnConflict names -> Doc
81+ppOnConflict schema = \case
82+ Abort -> mempty
83+ DoNothing -> text "ON CONFLICT DO NOTHING"
84+ DoUpdate upsert -> ppUpsert schema upsert
85+
86+
87+ppUpsert :: TableSchema names -> Upsert names -> Doc
88+ppUpsert schema@TableSchema {columns} Upsert {..} =
89+ text "ON CONFLICT" <+>
90+ ppIndex schema index <+>
91+ text "DO UPDATE" $$
92+ ppSet schema (set excluded) $$
93+ ppWhere schema (updateWhere excluded)
94+ where
95+ excluded = attributes TableSchema
96+ { schema = Nothing
97+ , name = "excluded"
98+ , columns
99+ }
100+
101+
102+ppIndex :: (Table Name names, Projecting names index)
103+ => TableSchema names -> Projection names index -> Doc
104+ppIndex TableSchema {columns} index =
105+ parens $ Opaleye.commaV ppColumn $ toList $
106+ showNames $ Cols $ apply index $ toColumns columns
src/Rel8/Statement/Returning.hs+121−10
@@ -1,29 +1,140 @@
11{-# language GADTs #-}
2+{-# language LambdaCase #-}
3+{-# language NamedFieldPuns #-}
4+{-# language RankNTypes #-}
5+{-# language ScopedTypeVariables #-}
26{-# language StandaloneKindSignatures #-}
7+{-# language StrictData #-}
8+{-# language TypeApplications #-}
39
410module Rel8.Statement.Returning
5− ( Returning(..)
11+ ( Returning( NumberOfRowsAffected, Projection )
12+
13+ , decodeReturning
14+ , emptyReturning
15+ , ppReturning
616 )
717where
818
919-- base
20+import Control.Applicative ( liftA2 )
21+import Data.Foldable ( toList )
1022import Data.Int ( Int64 )
1123import Data.Kind ( Type )
12−import Prelude ()
24+import Data.List.NonEmpty ( NonEmpty )
25+import Prelude
26+
27+-- hasql
28+import qualified Hasql.Decoders as Hasql
29+
30+-- opaleye
31+import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye
32+import qualified Opaleye.Internal.HaskellDB.Sql.Print as Opaleye
33+import qualified Opaleye.Internal.Sql as Opaleye
34+
35+-- pretty
36+import Text.PrettyPrint ( Doc, (<+>), text )
1337
1438-- rel8
1539import Rel8.Schema.Name ( Selects )
16−import Rel8.Table.Serialize ( Serializable )
40+import Rel8.Schema.Table ( TableSchema(..) )
41+import Rel8.Table.Opaleye ( castTable, exprs, view )
42+import Rel8.Table.Serialize ( Serializable, parse )
43+
44+-- semigropuoids
45+import Data.Functor.Apply ( Apply, (<.>) )
1746
1847
19−-- | @INSERT@, @UPDATE@ and @DELETE@ all support returning either the number of
20−-- rows affected, or the actual rows modified. 'Projection' allows you to
21−-- project out of these returned rows, which can be useful if you want to log
22−-- exactly which rows were deleted, or to view a generated id (for example, if
23−-- using a column with an autoincrementing counter as a default value).
48+-- | 'Rel8.Insert', 'Rel8.Update' and 'Rel8.Delete' all support returning either
49+-- the number of rows affected, or the actual rows modified.
2450type Returning :: Type -> Type -> Type
2551data Returning names a where
52+ Pure :: a -> Returning names a
53+ Ap :: Returning names (a -> b) -> Returning names a -> Returning names b
54+
55+ -- | Return the number of rows affected.
2656 NumberOfRowsAffected :: Returning names Int64
27− Projection :: (Selects names exprs, Serializable projection a)
28− => (exprs -> projection)
57+
58+ -- | 'Projection' allows you to project out of the affected rows, which can
59+ -- be useful if you want to log exactly which rows were deleted, or to view
60+ -- a generated id (for example, if using a column with an autoincrementing
61+ -- counter via 'Rel8.nextval').
62+ Projection :: (Selects names exprs, Serializable returning a)
63+ => (exprs -> returning)
2964 -> Returning names [a]
65+
66+
67+instance Functor (Returning names) where
68+ fmap f = \case
69+ Pure a -> Pure (f a)
70+ Ap g a -> Ap (fmap (f .) g) a
71+ m -> Ap (Pure f) m
72+
73+
74+instance Apply (Returning names) where
75+ (<.>) = Ap
76+
77+
78+instance Applicative (Returning names) where
79+ pure = Pure
80+ (<*>) = Ap
81+
82+
83+projections :: ()
84+ => TableSchema names -> Returning names a -> Maybe (NonEmpty Opaleye.PrimExpr)
85+projections schema@TableSchema {columns} = \case
86+ Pure _ -> Nothing
87+ Ap f a -> projections schema f <> projections schema a
88+ NumberOfRowsAffected -> Nothing
89+ Projection f -> Just (exprs (castTable (f (view columns))))
90+
91+
92+runReturning :: ()
93+ => ((Int64 -> a) -> r)
94+ -> (forall x. Hasql.Row x -> ([x] -> a) -> r)
95+ -> Returning names a
96+ -> r
97+runReturning rowCount rowList = \case
98+ Pure a -> rowCount (const a)
99+ Ap fs as ->
100+ runReturning
101+ (\withCount ->
102+ runReturning
103+ (\withCount' -> rowCount (withCount <*> withCount'))
104+ (\decoder -> rowList decoder . liftA2 withCount length64)
105+ as)
106+ (\decoder withRows ->
107+ runReturning
108+ (\withCount -> rowList decoder $ withRows <*> withCount . length64)
109+ (\decoder' withRows' ->
110+ rowList (liftA2 (,) decoder decoder') $
111+ withRows <$> fmap fst <*> withRows' . fmap snd)
112+ as)
113+ fs
114+ NumberOfRowsAffected -> rowCount id
115+ Projection (_ :: exprs -> returning) -> rowList decoder' id
116+ where
117+ decoder' = parse @returning
118+ where
119+ length64 :: Foldable f => f x -> Int64
120+ length64 = fromIntegral . length
121+
122+
123+decodeReturning :: Returning names a -> Hasql.Result a
124+decodeReturning = runReturning
125+ (<$> Hasql.rowsAffected)
126+ (\decoder withRows -> withRows <$> Hasql.rowList decoder)
127+
128+
129+emptyReturning :: Returning names a -> a
130+emptyReturning =
131+ runReturning (\withCount -> withCount 0) (\_ withRows -> withRows [])
132+
133+
134+ppReturning :: TableSchema names -> Returning names a -> Doc
135+ppReturning schema returning = case projections schema returning of
136+ Nothing -> mempty
137+ Just columns ->
138+ text "RETURNING" <+> Opaleye.commaV Opaleye.ppSqlExpr (toList sqlExprs)
139+ where
140+ sqlExprs = Opaleye.sqlExpr <$> columns
src/Rel8/Statement/SQL.hsnew file+29
@@ -0,0 +1,29 @@
1+module Rel8.Statement.SQL
2+ ( showDelete
3+ , showInsert
4+ , showUpdate
5+ )
6+where
7+
8+-- base
9+import Prelude
10+
11+-- rel8
12+import Rel8.Statement.Delete ( Delete, ppDelete )
13+import Rel8.Statement.Insert ( Insert, ppInsert )
14+import Rel8.Statement.Update ( Update, ppUpdate )
15+
16+
17+-- | Convert a 'Delete' to a 'String' containing a @DELETE@ statement.
18+showDelete :: Delete a -> String
19+showDelete = foldMap show . ppDelete
20+
21+
22+-- | Convert an 'Insert' to a 'String' containing an @INSERT@ statement.
23+showInsert :: Insert a -> String
24+showInsert = foldMap show . ppInsert
25+
26+
27+-- | Convert an 'Update' to a 'String' containing an @UPDATE@ statement.
28+showUpdate :: Update a -> String
29+showUpdate = foldMap show . ppUpdate
src/Rel8/Statement/Select.hs+77−20
@@ -1,15 +1,22 @@
1+{-# language DeriveTraversable #-}
2+{-# language DerivingStrategies #-}
3+{-# language FlexibleContexts #-}
14{-# language MonoLocalBinds #-}
25{-# language ScopedTypeVariables #-}
36{-# language TypeApplications #-}
47
58module Rel8.Statement.Select
69 ( select
7− , selectWithNames
10+ , ppSelect
11+
12+ , Optimized(..)
13+ , ppPrimSelect
814 )
915where
1016
1117-- base
1218import Control.Exception ( throwIO )
19+import Data.Void ( Void )
1320import Prelude
1421
1522-- hasql
@@ -19,10 +26,26 @@ import qualified Hasql.Encoders as Hasql
1926import qualified Hasql.Session as Hasql
2027import qualified Hasql.Statement as Hasql
2128
29+-- opaleye
30+import qualified Opaleye.Internal.HaskellDB.Sql as Opaleye
31+import qualified Opaleye.Internal.PrimQuery as Opaleye
32+import qualified Opaleye.Internal.Print as Opaleye
33+import qualified Opaleye.Internal.Optimize as Opaleye
34+import qualified Opaleye.Internal.QueryArr as Opaleye hiding ( Select )
35+import qualified Opaleye.Internal.Sql as Opaleye
36+
37+-- pretty
38+import Text.PrettyPrint ( Doc )
39+
2240-- rel8
41+import Rel8.Expr ( Expr )
2342import Rel8.Query ( Query )
24−import Rel8.Query.SQL ( sqlForQuery, sqlForQueryWithNames )
43+import Rel8.Query.Opaleye ( toOpaleye )
2544import Rel8.Schema.Name ( Selects )
45+import Rel8.Table ( Table )
46+import Rel8.Table.Cols ( toCols )
47+import Rel8.Table.Name ( namesFromLabels )
48+import Rel8.Table.Opaleye ( castTable, exprsWithNames )
2649import Rel8.Table.Serialize ( Serializable, parse )
2750
2851-- text
@@ -30,12 +53,12 @@ import qualified Data.Text as Text
3053import Data.Text.Encoding ( encodeUtf8 )
3154
3255
33−-- | Run a @SELECT@ query, returning all rows.
56+-- | Run a @SELECT@ statement, returning all rows.
3457select :: forall exprs a. Serializable exprs a
3558 => Connection -> Query exprs -> IO [a]
36−select c query = case sqlForQuery query of
59+select c query = case ppSelect query of
3760 Nothing -> pure []
38− Just sql -> Hasql.run session c >>= either throwIO pure
61+ Just doc -> Hasql.run session c >>= either throwIO pure
3962 where
4063 session = Hasql.statement () statement
4164 statement = Hasql.Statement bytes params decode prepare
@@ -43,20 +66,54 @@ select c query = case sqlForQuery query of
4366 params = Hasql.noParams
4467 decode = Hasql.rowList (parse @exprs @a)
4568 prepare = False
69+ sql = show doc
4670
4771
48−selectWithNames :: forall exprs a names.
49− ( Selects names exprs
50− , Serializable exprs a
51− )
52− => Connection -> names -> Query exprs -> IO [a]
53−selectWithNames c names query = case sqlForQueryWithNames names query of
54− Nothing -> pure []
55− Just sql -> Hasql.run session c >>= either throwIO pure
56− where
57− session = Hasql.statement () statement
58− statement = Hasql.Statement bytes params decode prepare
59− bytes = encodeUtf8 (Text.pack sql)
60− params = Hasql.noParams
61− decode = Hasql.rowList (parse @exprs @a)
62− prepare = False
72+ppSelect :: Table Expr a => Query a -> Maybe Doc
73+ppSelect query = do
74+ primQuery' <- case optimize primQuery of
75+ Empty -> Nothing
76+ Unit -> Just Opaleye.Unit
77+ Optimized primQuery' -> Just primQuery'
78+ pure $ Opaleye.ppSql $ primSelectWith names (toCols exprs) primQuery'
79+ where
80+ names = namesFromLabels
81+ (exprs, primQuery, _) =
82+ Opaleye.runSimpleQueryArrStart (toOpaleye query) ()
83+
84+
85+ppPrimSelect :: Query a -> (Optimized Doc, a)
86+ppPrimSelect query =
87+ (Opaleye.ppSql . primSelect <$> optimize primQuery, a)
88+ where
89+ (a, primQuery, _) =
90+ Opaleye.runSimpleQueryArrStart (toOpaleye query) ()
91+
92+
93+data Optimized a = Empty | Unit | Optimized a
94+ deriving stock (Functor, Foldable, Traversable)
95+
96+
97+optimize :: Opaleye.PrimQuery' a -> Optimized (Opaleye.PrimQuery' Void)
98+optimize query = case Opaleye.removeEmpty (Opaleye.optimize query) of
99+ Nothing -> Empty
100+ Just Opaleye.Unit -> Unit
101+ Just query' -> Optimized query'
102+
103+
104+primSelect :: Opaleye.PrimQuery' Void -> Opaleye.Select
105+primSelect = Opaleye.foldPrimQuery Opaleye.sqlQueryGenerator
106+
107+
108+primSelectWith :: Selects names exprs
109+ => names -> exprs -> Opaleye.PrimQuery' Void -> Opaleye.Select
110+primSelectWith names exprs query =
111+ Opaleye.SelectFrom $ Opaleye.newSelect
112+ { Opaleye.attrs = Opaleye.SelectAttrs attrs
113+ , Opaleye.tables = Opaleye.oneTable (primSelect query)
114+ }
115+ where
116+ attrs = makeAttr <$> exprsWithNames names (castTable exprs)
117+ where
118+ makeAttr (label, expr) =
119+ (Opaleye.sqlExpr expr, Just (Opaleye.SqlColumn label))
src/Rel8/Statement/Set.hsnew file+33
@@ -0,0 +1,33 @@
1+{-# language MonoLocalBinds #-}
2+{-# language NamedFieldPuns #-}
3+
4+module Rel8.Statement.Set
5+ ( ppSet
6+ )
7+where
8+
9+-- base
10+import Data.Foldable ( toList )
11+import Prelude ()
12+
13+-- opaleye
14+import qualified Opaleye.Internal.HaskellDB.Sql.Print as Opaleye
15+import qualified Opaleye.Internal.Sql as Opaleye
16+
17+-- pretty
18+import Text.PrettyPrint ( Doc, (<+>), equals, text )
19+
20+-- rel8
21+import Rel8.Schema.Name ( Selects, ppColumn )
22+import Rel8.Schema.Table ( TableSchema(..) )
23+import Rel8.Table.Opaleye ( attributes, exprsWithNames )
24+
25+
26+ppSet :: Selects names exprs
27+ => TableSchema names -> (exprs -> exprs) -> Doc
28+ppSet schema@TableSchema {columns} f =
29+ text "SET" <+> Opaleye.commaV ppAssign (toList assigns)
30+ where
31+ assigns = exprsWithNames columns (f (attributes schema))
32+ ppAssign (column, expr) =
33+ ppColumn column <+> equals <+> Opaleye.ppSqlExpr (Opaleye.sqlExpr expr)
src/Rel8/Statement/Update.hs+38−48
@@ -1,12 +1,14 @@
1+{-# language DuplicateRecordFields #-}
12{-# language GADTs #-}
23{-# language NamedFieldPuns #-}
3−{-# language ScopedTypeVariables #-}
4+{-# language RecordWildCards #-}
45{-# language StandaloneKindSignatures #-}
5−{-# language TypeApplications #-}
6+{-# language StrictData #-}
67
78module Rel8.Statement.Update
89 ( Update(..)
910 , update
11+ , ppUpdate
1012 )
1113where
1214
@@ -17,23 +19,25 @@ import Prelude
1719
1820-- hasql
1921import Hasql.Connection ( Connection )
20−import qualified Hasql.Decoders as Hasql
2122import qualified Hasql.Encoders as Hasql
2223import qualified Hasql.Session as Hasql
2324import qualified Hasql.Statement as Hasql
2425
25−-- opaleye
26−import qualified Opaleye.Internal.Manipulation as Opaleye
26+-- pretty
27+import Text.PrettyPrint ( Doc, (<+>), ($$), text )
2728
2829-- rel8
2930import Rel8.Expr ( Expr )
30−import Rel8.Expr.Opaleye ( toColumn, toPrimExpr )
31+import Rel8.Query ( Query )
3132import Rel8.Schema.Name ( Selects )
32−import Rel8.Schema.Table ( TableSchema )
33−import Rel8.Statement.Returning ( Returning( Projection, NumberOfRowsAffected ) )
34−import Rel8.Table.Cols ( fromCols, toCols )
35−import Rel8.Table.Opaleye ( castTable, table, unpackspec )
36−import Rel8.Table.Serialize ( Serializable, parse )
33+import Rel8.Schema.Table ( TableSchema(..), ppTable )
34+import Rel8.Statement.Returning
35+ ( Returning
36+ , decodeReturning, emptyReturning, ppReturning
37+ )
38+import Rel8.Statement.Set ( ppSet )
39+import Rel8.Statement.Using ( ppFrom )
40+import Rel8.Statement.Where ( ppWhere )
3741
3842-- text
3943import qualified Data.Text as Text
@@ -46,9 +50,12 @@ data Update a where
4650 Update :: Selects names exprs =>
4751 { target :: TableSchema names
4852 -- ^ Which table to update.
49− , set :: exprs -> exprs
53+ , from :: Query from
54+ -- ^ @FROM@ clause — this can be used to join against other tables,
55+ -- and its results can be referenced in the @SET@ and @WHERE@ clauses.
56+ , set :: from -> exprs -> exprs
5057 -- ^ How to update each selected row.
51− , updateWhere :: exprs -> Expr Bool
58+ , updateWhere :: from -> exprs -> Expr Bool
5259 -- ^ Which rows to select for update.
5360 , returning :: Returning names a
5461 -- ^ What to return from the @UPDATE@ statement.
@@ -56,46 +63,29 @@ data Update a where
5663 -> Update a
5764
5865
66+ppUpdate :: Update a -> Maybe Doc
67+ppUpdate Update {..} = do
68+ (fromDoc, i) <- ppFrom from
69+ pure $
70+ text "UPDATE" <+>
71+ ppTable target $$
72+ ppSet target (set i) $$
73+ fromDoc $$
74+ ppWhere target (updateWhere i) $$
75+ ppReturning target returning
76+
77+
5978-- | Run an @UPDATE@ statement.
6079update :: Connection -> Update a -> IO a
61−update c Update {target, set, updateWhere, returning} =
62− case returning of
63− NumberOfRowsAffected -> Hasql.run session c >>= either throwIO pure
80+update connection u@Update {returning} =
81+ case show <$> ppUpdate u of
82+ Nothing -> pure (emptyReturning returning)
83+ Just sql ->
84+ Hasql.run session connection >>= either throwIO pure
6485 where
6586 session = Hasql.statement () statement
6687 statement = Hasql.Statement bytes params decode prepare
6788 bytes = encodeUtf8 $ Text.pack sql
6889 params = Hasql.noParams
69− decode = Hasql.rowsAffected
90+ decode = decodeReturning returning
7091 prepare = False
71− sql = Opaleye.arrangeUpdateSql target' set' where'
72− where
73− target' = table $ toCols <$> target
74− set' = toCols . set . fromCols
75− where' = toColumn . toPrimExpr . updateWhere . fromCols
76−
77− Projection project -> Hasql.run session c >>= either throwIO pure
78− where
79− session = Hasql.statement () statement
80− statement = Hasql.Statement bytes params decode prepare
81− bytes = encodeUtf8 $ Text.pack sql
82− params = Hasql.noParams
83− decode = decoder project
84− prepare = False
85− sql =
86− Opaleye.arrangeUpdateReturningSql
87− unpackspec
88− target'
89− set'
90− where'
91− project'
92− where
93− target' = table $ toCols <$> target
94− set' = toCols . set . fromCols
95− where' = toColumn . toPrimExpr . updateWhere . fromCols
96− project' = castTable . toCols . project . fromCols
97−
98− where
99− decoder :: forall exprs projection a. Serializable projection a
100− => (exprs -> projection) -> Hasql.Result [a]
101− decoder _ = Hasql.rowList (parse @projection @a)
src/Rel8/Statement/Using.hsnew file+36
@@ -0,0 +1,36 @@
1+module Rel8.Statement.Using
2+ ( ppFrom
3+ , ppUsing
4+ )
5+where
6+
7+-- base
8+import Prelude
9+
10+-- pretty
11+import Text.PrettyPrint ( Doc, (<+>), parens, text )
12+
13+-- rel8
14+import Rel8.Query ( Query )
15+import Rel8.Schema.Table ( TableSchema(..), ppTable )
16+import Rel8.Statement.Select ( Optimized(..), ppPrimSelect )
17+
18+
19+ppFrom :: Query a -> Maybe (Doc, a)
20+ppFrom = ppJoin "FROM"
21+
22+
23+ppUsing :: Query a -> Maybe (Doc, a)
24+ppUsing = ppJoin "USING"
25+
26+
27+ppJoin :: String -> Query a -> Maybe (Doc, a)
28+ppJoin clause join = do
29+ doc <- case ofrom of
30+ Empty -> Nothing
31+ Unit -> Just mempty
32+ Optimized doc -> Just $ text clause <+> parens doc <+> ppTable alias
33+ pure (doc, a)
34+ where
35+ alias = TableSchema {name = "T1", schema = Nothing, columns = ()}
36+ (ofrom, a) = ppPrimSelect join
src/Rel8/Statement/View.hs+20−20
@@ -8,7 +8,6 @@ where
88
99-- base
1010import Control.Exception ( throwIO )
11−import Control.Monad ( (>=>) )
1211import Data.Foldable ( fold )
1312import Data.Maybe ( fromMaybe )
1413import Prelude
@@ -22,11 +21,15 @@ import qualified Hasql.Statement as Hasql
2221
2322-- rel8
2423import Rel8.Query ( Query )
25−import Rel8.Query.SQL ( sqlForQueryWithNames )
2624import Rel8.Schema.Name ( Selects )
27−import Rel8.Schema.Table ( TableSchema( TableSchema ) )
25+import Rel8.Schema.Table ( TableSchema )
26+import Rel8.Statement.Insert ( ppInto )
27+import Rel8.Statement.Select ( ppSelect )
2828import Rel8.Table.Alternative ( emptyTable )
2929
30+-- pretty
31+import Text.PrettyPrint ( Doc, (<+>), ($$), text )
32+
3033-- text
3134import qualified Data.Text as Text
3235import Data.Text.Encoding ( encodeUtf8 )
@@ -36,9 +39,9 @@ import Data.Text.Encoding ( encodeUtf8 )
3639-- statement that will save the given query as a view. This can be useful if
3740-- you want to share Rel8 queries with other applications.
3841createView :: Selects names exprs
39− => TableSchema names -> Query exprs -> Connection -> IO ()
40−createView (TableSchema name mschema names) query =
41− Hasql.run session >=> either throwIO pure
42+ => Connection -> TableSchema names -> Query exprs -> IO ()
43+createView connection schema query =
44+ Hasql.run session connection >>= either throwIO pure
4245 where
4346 session = Hasql.statement () statement
4447 statement = Hasql.Statement bytes params decode prepare
@@ -46,18 +49,15 @@ createView (TableSchema name mschema names) query =
4649 params = Hasql.noParams
4750 decode = Hasql.noResult
4851 prepare = False
49− sql = "CREATE VIEW " <> title <> " AS " <> select
50− where
51− title = case mschema of
52− Nothing -> quote name
53− Just schema -> quote schema <> "." <> quote name
54− select = fromMaybe fallback $ sqlForQueryWithNames names query
55− where
56− fallback = fold $ sqlForQueryWithNames names emptyTable
57−
58−
59−quote :: String -> String
60−quote string = "\"" <> concatMap go string <> "\""
52+ sql = show (ppCreateView schema query)
53+
54+
55+ppCreateView :: Selects names exprs
56+ => TableSchema names -> Query exprs -> Doc
57+ppCreateView schema query =
58+ text "CREATE VIEW" <+>
59+ ppInto schema $$
60+ text "AS" <+>
61+ fromMaybe fallback (ppSelect query)
6162 where
62− go '"' = "\"\""
63− go c = [c]
63+ fallback = fold (ppSelect (emptyTable `asTypeOf` query))
src/Rel8/Statement/Where.hsnew file+31
@@ -0,0 +1,31 @@
1+{-# language MonoLocalBinds #-}
2+
3+module Rel8.Statement.Where
4+ ( ppWhere
5+ )
6+where
7+
8+-- base
9+import Prelude
10+
11+-- opaleye
12+import qualified Opaleye.Internal.HaskellDB.Sql.Print as Opaleye
13+import qualified Opaleye.Internal.Sql as Opaleye
14+
15+-- pretty
16+import Text.PrettyPrint ( Doc, (<+>), text )
17+
18+-- rel8
19+import Rel8.Expr ( Expr )
20+import Rel8.Expr.Opaleye ( toPrimExpr )
21+import Rel8.Schema.Name ( Selects )
22+import Rel8.Schema.Table ( TableSchema )
23+import Rel8.Table.Opaleye ( attributes )
24+
25+
26+ppWhere :: Selects names exprs
27+ => TableSchema names -> (exprs -> Expr Bool) -> Doc
28+ppWhere schema where_ = text "WHERE" <+> ppExpr condition
29+ where
30+ ppExpr = Opaleye.ppSqlExpr . Opaleye.sqlExpr . toPrimExpr
31+ condition = where_ (attributes schema)
src/Rel8/Table/Name.hs+3−17
@@ -12,7 +12,6 @@
1212module Rel8.Table.Name
1313 ( namesFromLabels
1414 , namesFromLabelsWith
15− , showExprs
1615 , showLabels
1716 , showNames
1817 )
@@ -25,17 +24,11 @@ import Data.List.NonEmpty ( NonEmpty, intersperse, nonEmpty )
2524import Data.Maybe ( fromMaybe )
2625import Prelude
2726
28−-- opaleye
29−import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye
30−
3127-- rel8
32−import Rel8.Expr ( Expr )
33−import Rel8.Expr.Opaleye ( toPrimExpr )
3428import Rel8.Schema.HTable ( htabulate, htabulateA, hfield, hspecs )
3529import Rel8.Schema.Name ( Name( Name ) )
3630import Rel8.Schema.Spec ( Spec(..) )
3731import Rel8.Table ( Table(..) )
38−import Rel8.Table.Cols ( Cols( Cols ) )
3932
4033
4134-- | Construct a table in the 'Name' context containing the names of all
@@ -70,23 +63,16 @@ namesFromLabelsWith f = fromColumns $ htabulate $ \field ->
7063 Spec {labels} -> Name (f (renderLabels labels))
7164
7265
73−showExprs :: Table Expr a => a -> [(String, Opaleye.PrimExpr)]
74−showExprs as = case (namesFromLabels, toColumns as) of
75− (Cols names, exprs) -> getConst $ htabulateA $ \field ->
76− case (hfield names field, hfield exprs field) of
77− (Name name, expr) -> Const [(name, toPrimExpr expr)]
78−
79−
8066showLabels :: forall a. Table (Context a) a => a -> [NonEmpty String]
8167showLabels _ = getConst $
8268 htabulateA @(Columns a) $ \field -> case hfield hspecs field of
83− Spec {labels} -> Const [renderLabels labels]
69+ Spec {labels} -> Const (pure (renderLabels labels))
8470
8571
86−showNames :: forall a. Table Name a => a -> [String]
72+showNames :: forall a. Table Name a => a -> NonEmpty String
8773showNames (toColumns -> names) = getConst $
8874 htabulateA @(Columns a) $ \field -> case hfield names field of
89− Name name -> Const [name]
75+ Name name -> Const (pure name)
9076
9177
9278renderLabels :: [String] -> NonEmpty String
src/Rel8/Table/Opaleye.hs+36−4
@@ -2,7 +2,6 @@
22{-# language DataKinds #-}
33{-# language DisambiguateRecordFields #-}
44{-# language FlexibleContexts #-}
5−{-# language LambdaCase #-}
65{-# language NamedFieldPuns #-}
76{-# language RankNTypes #-}
87{-# language TypeFamilies #-}
@@ -10,17 +9,23 @@
109
1110module Rel8.Table.Opaleye
1211 ( aggregator
12+ , attributes
1313 , binaryspec
1414 , distinctspec
15+ , exprs
16+ , exprsWithNames
1517 , table
1618 , tableFields
1719 , unpackspec
1820 , valuesspec
21+ , view
1922 , castTable
2023 )
2124where
2225
2326-- base
27+import Data.Functor.Const ( Const( Const ), getConst )
28+import Data.List.NonEmpty ( NonEmpty )
2429import Prelude hiding ( undefined )
2530
2631-- opaleye
@@ -46,9 +51,9 @@ import Rel8.Expr.Opaleye
4651 , scastExpr
4752 )
4853import Rel8.Schema.HTable ( htabulateA, hfield, htraverse, hspecs, htabulate )
49−import Rel8.Schema.Name ( Name( Name ), Selects )
54+import Rel8.Schema.Name ( Name( Name ), Selects, ppColumn )
5055import Rel8.Schema.Spec ( Spec(..) )
51−import Rel8.Schema.Table ( TableSchema(..) )
56+import Rel8.Schema.Table ( TableSchema(..), ppTable )
5257import Rel8.Table ( Table, fromColumns, toColumns )
5358import Rel8.Table.Undefined ( undefined )
5459
@@ -64,6 +69,14 @@ aggregator = Opaleye.Aggregator $ Opaleye.PackMap $ \f aggregates ->
6469 inner f ()
6570
6671
72+attributes :: Selects names exprs => TableSchema names -> exprs
73+attributes schema@TableSchema {columns} = fromColumns $ htabulate $ \field ->
74+ case hfield (toColumns columns) field of
75+ Name column -> fromPrimExpr $ Opaleye.ConstExpr $
76+ Opaleye.OtherLit $
77+ show (ppTable schema) <> "." <> show (ppColumn column)
78+
79+
6780binaryspec :: Table Expr a => Opaleye.Binaryspec a a
6881binaryspec = Opaleye.Binaryspec $ Opaleye.PackMap $ \f (as, bs) ->
6982 fmap fromColumns $ unwrapApplicative $ htabulateA $ \field ->
@@ -82,7 +95,20 @@ distinctspec =
8295 toColumns
8396
8497
85−table ::Selects names exprs => TableSchema names -> Opaleye.Table exprs exprs
98+exprs :: Table Expr a => a -> NonEmpty Opaleye.PrimExpr
99+exprs (toColumns -> as) = getConst $ htabulateA $ \field ->
100+ case hfield as field of
101+ expr -> Const (pure (toPrimExpr expr))
102+
103+
104+exprsWithNames :: Selects names exprs
105+ => names -> exprs -> NonEmpty (String, Opaleye.PrimExpr)
106+exprsWithNames names as = getConst $ htabulateA $ \field ->
107+ case (hfield (toColumns names) field, hfield (toColumns as) field) of
108+ (Name name, expr) -> Const (pure (name, toPrimExpr expr))
109+
110+
111+table :: Selects names exprs => TableSchema names -> Opaleye.Table exprs exprs
86112table (TableSchema name schema columns) =
87113 case schema of
88114 Nothing -> Opaleye.Table name (tableFields columns)
@@ -115,6 +141,12 @@ valuesspec :: Table Expr a => Opaleye.ValuesspecSafe a a
115141valuesspec = Opaleye.ValuesspecSafe (toPackMap undefined) unpackspec
116142
117143
144+view :: Selects names exprs => names -> exprs
145+view columns = fromColumns $ htabulate $ \field ->
146+ case hfield (toColumns columns) field of
147+ Name column -> fromPrimExpr $ Opaleye.BaseTableAttrExpr column
148+
149+
118150toPackMap :: Table Expr a
119151 => a -> Opaleye.PackMap Opaleye.PrimExpr Opaleye.PrimExpr () a
120152toPackMap as = Opaleye.PackMap $ \f () ->
src/Rel8/Table/Projection.hs+4−4
@@ -29,13 +29,13 @@ import Rel8.Table.Transpose ( Transposes )
2929-- usable 'Projection'.
3030type Projecting :: Type -> Type -> Constraint
3131class
32− ( Transposes (Field a) (Context a) a (Transpose (Field a) a)
33− , Transposes (Field a) (Context a) b (Transpose (Field a) b)
32+ ( Transposes (Context a) (Field a) a (Transpose (Field a) a)
33+ , Transposes (Context a) (Field a) b (Transpose (Field a) b)
3434 )
3535 => Projecting a b
3636instance
37− ( Transposes (Field a) (Context a) a (Transpose (Field a) a)
38− , Transposes (Field a) (Context a) b (Transpose (Field b) b)
37+ ( Transposes (Context a) (Field a) a (Transpose (Field a) a)
38+ , Transposes (Context a) (Field a) b (Transpose (Field b) b)
3939 )
4040 => Projecting a b
4141
tests/Main.hs+20−19
@@ -185,14 +185,13 @@ testSelectTestTable = databasePropertyTest "Can SELECT TestTable" \transaction -
185185 rows <- forAll $ Gen.list (Range.linear 0 10) genTestTable
186186
187187 transaction \connection -> do
188− void do
189− liftIO $ Rel8.insert connection
190− Rel8.Insert
191− { into = testTableSchema
192− , rows = map Rel8.lit rows
193− , onConflict = Rel8.DoNothing
194− , returning = Rel8.NumberOfRowsAffected
195− }
188+ liftIO $ Rel8.insert connection
189+ Rel8.Insert
190+ { into = testTableSchema
191+ , rows = Rel8.values $ map Rel8.lit rows
192+ , onConflict = Rel8.DoNothing
193+ , returning = pure ()
194+ }
196195
197196 selected <- liftIO $ Rel8.select connection do
198197 Rel8.each testTableSchema
@@ -599,18 +598,19 @@ testUpdate = databasePropertyTest "Can UPDATE TestTable" \transaction -> do
599598 rows <- forAll $ Gen.map (Range.linear 0 5) $ liftA2 (,) genTestTable genTestTable
600599
601600 transaction \connection -> do
602− void $ liftIO $ Rel8.insert connection
601+ liftIO $ Rel8.insert connection
603602 Rel8.Insert
604603 { into = testTableSchema
605− , rows = map Rel8.lit $ Map.keys rows
604+ , rows = Rel8.values $ map Rel8.lit $ Map.keys rows
606605 , onConflict = Rel8.DoNothing
607− , returning = Rel8.NumberOfRowsAffected
606+ , returning = pure ()
608607 }
609608
610− void $ liftIO $ Rel8.update connection
609+ liftIO $ Rel8.update connection
611610 Rel8.Update
612611 { target = testTableSchema
613− , set = \r ->
612+ , from = pure ()
613+ , set = \_ r ->
614614 let updates = map (bimap Rel8.lit Rel8.lit) $ Map.toList rows
615615 in
616616 foldl
@@ -624,8 +624,8 @@ testUpdate = databasePropertyTest "Can UPDATE TestTable" \transaction -> do
624624 )
625625 r
626626 updates
627− , updateWhere = \_ -> Rel8.lit True
628− , returning = Rel8.NumberOfRowsAffected
627+ , updateWhere = \_ _ -> Rel8.lit True
628+ , returning = pure ()
629629 }
630630
631631 selected <- liftIO $ Rel8.select connection do
@@ -643,19 +643,20 @@ testDelete = databasePropertyTest "Can DELETE TestTable" \transaction -> do
643643 rows <- forAll $ Gen.list (Range.linear 0 5) genTestTable
644644
645645 transaction \connection -> do
646− void $ liftIO $ Rel8.insert connection
646+ liftIO $ Rel8.insert connection
647647 Rel8.Insert
648648 { into = testTableSchema
649− , rows = map Rel8.lit rows
649+ , rows = Rel8.values $ map Rel8.lit rows
650650 , onConflict = Rel8.DoNothing
651− , returning = Rel8.NumberOfRowsAffected
651+ , returning = pure ()
652652 }
653653
654654 deleted <-
655655 liftIO $ Rel8.delete connection
656656 Rel8.Delete
657657 { from = testTableSchema
658− , deleteWhere = testTableColumn2
658+ , using = pure ()
659+ , deleteWhere = const testTableColumn2
659660 , returning = Rel8.Projection id
660661 }
661662