Skip to content

Commit

Permalink
Statements overhaul (support for statement-level WITH) (#250)
Browse files Browse the repository at this point in the history
The motivation behind this PR is to add support for PostreSQL's `WITH` syntax at the statement level, which gives the ability to, e.g., delete some rows from a table and then re-insert those deleted rows into another table, without any round-trips between the application and the database.

To support this, this PR introduces a new type called `Statement`, which represents a single PostgreSQL statement. It has a `Monad` instance which allows sub-statements (such as `DELETE` and `INSERT` statements) to be composed together and their results bound to values that can be referenced in subsequent sub-statements. These "compound" statements are then rendered as a `WITH` statement.

`select`, `insert`, `update` and `delete` have all been altered to produce the `Statement` type described above instead of the `Hasql.Statement` type.

Some changes were necessary to the `Returning` type. `Returning` previously bundled two different concepts together: whether or not to generate a `RETURNING` clause in the SQL for a manipulation statement, and how to decode the returned rows (if any). It was necessary to break these concepts apart because with `WITH` we need the ability to generate manipulation statements with `RETURNING` clauses that are never actually decoded at all (the results just get passed to the next statement without touching the application).

Now, the `Returning` type is only concerned with whether or not to generate a `RETURNING` clause, and the question of how to decode the returned the result of the statement is handled by the `run` functions. `run` converts a `Statement` into a runnable `Hasql.Statement`, decoding the result of the statement as a list of rows. The other variations, `run_`, `runN`, `run1`, `runMaybe` and `runVector` can be used when you want to decode as something other than a list of rows.

This also gains us support for decoding the result of a query directly to a `Vector` for the first time, which brings a performance improvement over lists for those who need it.
  • Loading branch information
shane-circuithub committed Jul 7, 2023
1 parent 0357176 commit 3c0b67f
Show file tree
Hide file tree
Showing 18 changed files with 816 additions and 299 deletions.
29 changes: 19 additions & 10 deletions docs/concepts/insert.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@

While the majority of Rel8 is about building and executing ``SELECT``
statement, Rel8 also has support for ``INSERT``, ``UPDATE`` and ``DELETE``.
These statements are all executed using the ``insert``, ``update`` and
``delete`` functions, all of which take a record of parameters.
These statements are built using the ``insert``, ``update`` and ``delete```
functions, take ``Insert``, ``Update`` and ``Delete`` values respectively,
all of which are records of parameters.

.. note::

This part of Rel8's API uses the ``DuplicateRecordFields`` language
extension. In code that needs to use this API, you should also enable this
language extension, or you may get errors about ambiguous field names.
extension. In code that needs to use this API, you should enable the
``DisambiguateRecordFields`` language extension, or you may get errors
about ambiguous field names.

``DELETE``
----------
Expand Down Expand Up @@ -110,7 +112,7 @@ PostgreSQL has the ability to return extra information after a ``DELETE``,
``INSERT`` or ``UPDATE`` statement by attaching a ``RETURNING`` clause. A common
use of this clause is to return any automatically generated sequence values for
primary key columns. Rel8 supports ``RETURNING`` clauses by filling in the
``returning`` field and specifying a ``Projection``. A ``Projection`` is a row
``returning`` field and specifying a ``Returning``. A ``Returning`` is a row
to row transformation, allowing you to project out a subset of fields.

For example, if we are inserting orders, we might want the order ids returned::
Expand All @@ -119,16 +121,16 @@ For example, if we are inserting orders, we might want the order ids returned::
{ into = orderSchema
, rows = values [ order ]
, onConflict = Abort
, returning = Projection orderId
, returning = Returning orderId
}

If we don't want to return anything, we can use ``pure ()``::
If we don't want to return anything, we can use ``NoReturning``::

insert Insert
{ into = orderSchema
, rows = values [ order ]
, onConflict = Abort
, returning = pure ()
, returning = NoReturning
}

Default values
Expand All @@ -148,7 +150,7 @@ construct the ``DEFAULT`` expression::
{ into = orderSchema
, rows = values [ Order { orderId = unsafeDefault, ... } ]
, onConflict = Abort
, returning = Projection orderId
, returning = Returning orderId
}

.. warning::
Expand All @@ -162,6 +164,13 @@ construct the ``DEFAULT`` expression::

will lead to a runtime crash.

.. warning::
Also note PostgreSQL's syntax rules mean that ``DEFAULT``` can only appear
in ``INSERT``` expressions whose rows are specified using ``VALUES``. This
means that the ``rows`` field of your ``Insert`` record doesn't look like
``values [..]``, then ``unsafeDefault`` won't work.


Reimplement default values in Rel8
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand All @@ -177,5 +186,5 @@ them in Rel8, rather than in your database schema.
{ into = orderSchema
, rows = values [ Order { orderId = nextval "order_id_seq", ... } ]
, onConflict = Abort
, returning = Projection orderId
, returning = Returning orderId
}
6 changes: 6 additions & 0 deletions rel8.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ library
, text
, these
, time
, transformers
, uuid
, vector
default-language:
Haskell2010
ghc-options:
Expand Down Expand Up @@ -151,10 +153,13 @@ library
Rel8.Schema.Spec
Rel8.Schema.Table

Rel8.Statement
Rel8.Statement.Delete
Rel8.Statement.Insert
Rel8.Statement.OnConflict
Rel8.Statement.Returning
Rel8.Statement.Rows
Rel8.Statement.Run
Rel8.Statement.Select
Rel8.Statement.Set
Rel8.Statement.SQL
Expand Down Expand Up @@ -243,3 +248,4 @@ test-suite tests
-Wno-missing-import-lists -Wno-prepositive-qualified-module
-Wno-deprecations -Wno-monomorphism-restriction
-Wno-missing-local-signatures -Wno-implicit-prelude
-Wno-missing-kind-signatures
26 changes: 23 additions & 3 deletions src/Rel8.hs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,12 @@ module Rel8

-- * Running statements
-- $running
, run
, run_
, runN
, run1
, runMaybe
, runVector

-- ** @SELECT@
, select
Expand All @@ -351,6 +357,10 @@ module Rel8
-- ** @.. RETURNING@
, Returning(..)

-- ** @WITH@
, Statement
, showStatement

-- ** @CREATE VIEW@
, createView
, createOrReplaceView
Expand Down Expand Up @@ -421,10 +431,12 @@ import Rel8.Schema.Name
import Rel8.Schema.Null hiding ( nullable )
import Rel8.Schema.Result ( Result )
import Rel8.Schema.Table
import Rel8.Statement
import Rel8.Statement.Delete
import Rel8.Statement.Insert
import Rel8.Statement.OnConflict
import Rel8.Statement.Returning
import Rel8.Statement.Run
import Rel8.Statement.Select
import Rel8.Statement.SQL
import Rel8.Statement.Update
Expand Down Expand Up @@ -470,9 +482,17 @@ import Rel8.Window

-- $running
-- To run queries and otherwise interact with a PostgreSQL database, Rel8
-- provides 'select', 'insert', 'update' and 'delete' functions. Note that
-- 'insert', 'update' and 'delete' will generally need the
-- `DuplicateRecordFields` language extension enabled.
-- provides the @run@ functions. These produce a 'Hasql.Statement.Statement's
-- which can be passed to 'Hasql.Session.statement' to execute the statement
-- against a PostgreSQL 'Hasql.Connection.Connection'.
--
-- 'run' takes a 'Statement', which can be constructed using either 'select',
-- 'insert', 'update' or 'delete'. It decodes the rows returned by the
-- statement as a list of Haskell of values. See 'run_', 'runN', 'run1',
-- 'runMaybe' and 'runVector' for other variations.
--
-- Note that constructing an 'Insert', 'Update' or 'Delete' will require the
-- @DisambiguateRecordFields@ language extension to be enabled.

-- $adts
-- Algebraic data types can be modelled between Haskell and SQL.
Expand Down
8 changes: 7 additions & 1 deletion src/Rel8/Query/SQL.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@ where
-- base
import Prelude

-- opaleye
import qualified Opaleye.Internal.Tag as Opaleye

-- rel8
import Rel8.Expr ( Expr )
import Rel8.Query ( Query )
import Rel8.Statement.Select ( ppSelect )
import Rel8.Table ( Table )

-- transformers
import Control.Monad.Trans.State.Strict (evalState)


-- | Convert a 'Query' to a 'String' containing a @SELECT@ statement.
showQuery :: Table Expr a => Query a -> String
showQuery = show . ppSelect
showQuery = show . (`evalState` Opaleye.start) . ppSelect

0 comments on commit 3c0b67f

Please sign in to comment.