on a90cc0f help sign in
rel8/src/Rel8.hs
671 lines 19 kB history raw
1{-# language DuplicateRecordFields #-}
3module Rel8
4 ( -- * Getting Started
5 -- $setup
7 -- ** Writing Queries
8 -- $guideQueries
10 -- ** Joins
11 -- $guideJoins
13 -- ** Aggregation
14 -- $guideAggregation
16 -- * Database types
17 -- ** @DBType@
18 DBType(..)
20 -- *** Deriving-via helpers
21 -- **** @JSONEncoded@
22 , JSONEncoded(..)
23 , JSONBEncoded(..)
25 -- **** @ReadShow@
26 , ReadShow(..)
28 -- *** @TypeInformation@
29 , TypeInformation(..)
30 , mapTypeInformation
31 , parseTypeInformation
33 -- ** The @DBType@ hierarchy
34 , DBSemigroup(..)
35 , DBMonoid(..)
36 , DBNum
37 , DBFractional
39 -- * Tables and higher-kinded tables
40 , Rel8able
41 , Column
42 , Default, Label
43 , HMaybe
44 , HList
45 , HNonEmpty
46 , HThese
48 , Table
49 , AltTable((<|>:))
50 , EqTable, (==:), (/=:)
51 , OrdTable, ascTable, descTable
52 , lit
53 , bool
54 , case_
56 -- ** @MaybeTable@
57 , MaybeTable
58 , maybeTable, nothingTable, justTable
59 , isNothingTable, isJustTable
60 , optional
61 , catMaybeTable
62 , bindMaybeTable
63 , traverseMaybeTable
65 -- ** @EitherTable@
66 , EitherTable
67 , eitherTable, leftTable, rightTable
68 , isLeftTable, isRightTable
69 , keepLeftTable
70 , keepRightTable
71 , bindEitherTable
72 , bitraverseEitherTable
74 -- ** @TheseTable@
75 , TheseTable
76 , theseTable, thisTable, thatTable, thoseTable
77 , isThisTable, isThatTable, isThoseTable
78 , hasHereTable, hasThereTable
79 , justHereTable, justThereTable
80 , alignBy
81 , keepHereTable, loseHereTable
82 , keepThereTable, loseThereTable
83 , keepThisTable, loseThisTable
84 , keepThatTable, loseThatTable
85 , keepThoseTable, loseThoseTable
86 , bindTheseTable
87 , bitraverseTheseTable
89 -- ** @ListTable@
90 , ListTable
91 , many
93 -- ** @NonEmptyTable@
94 , NonEmptyTable
95 , some
97 -- ** Table schemas
98 , TableSchema(..)
99 , Name
100 , namesFromLabels
102 -- * Expressions
103 , Expr
104 , Sql
105 , litExpr
106 , unsafeCastExpr
108 -- ** @null@
109 , nullify
110 , nullable
111 , isNull
112 , isNonNull
113 , mapNullable
114 , liftOpNullable
115 , catNullable
116 , coalesce
118 -- ** Boolean operations
119 , DBEq
120 , true, false, not_
121 , (&&.), and_
122 , (||.), or_
123 , (==.), (/=.), (==?), (/=?)
124 , in_
125 , boolExpr, caseExpr
127 -- ** Ordering
128 , DBOrd
129 , (<.), (<=.), (>.), (>=.)
130 , (<?), (<=?), (>?), (>=?)
131 , leastExpr, greatestExpr
133 -- ** Functions
134 , Function
135 , function
136 , nullaryFunction
137 , binaryOperator
139 -- * Queries
140 , Query
141 , showQuery
143 -- ** Selecting rows
144 , Selects
145 , each
146 , values
148 -- ** Filtering
149 , filter
150 , where_
151 , whereExists
152 , whereNotExists
153 , distinct
154 , distinctOn
155 , distinctOnBy
157 -- ** @LIMIT@/@OFFSET@
158 , limit
159 , offset
161 -- ** @UNION@
162 , union
163 , unionAll
165 -- ** @INTERSECT@
166 , intersect
167 , intersectAll
169 -- ** @EXCEPT@
170 , except
171 , exceptAll
173 -- ** @EXISTS@
174 , exists
175 , with
176 , withBy
177 , without
178 , withoutBy
180 -- ** Aggregation
181 , Aggregate
182 , aggregate
183 , listAgg
184 , nonEmptyAgg
185 , groupBy
186 , DBMax, max
187 , DBMin, min
188 , DBSum, sum
189 , DBString, stringAgg
190 , count
191 , countStar
192 , countDistinct
193 , countWhere
194 , and
195 , or
197 -- ** Ordering
198 , orderBy
199 , Order
200 , asc
201 , desc
202 , nullsFirst
203 , nullsLast
205 -- * IO
206 , Serializable
208 -- * Running statements
209 -- ** @SELECT@
210 , select
212 -- ** @INSERT@
213 , Insert(..)
214 , OnConflict(..)
215 , insert
216 , Insertion
217 , toInsert
218 , toInsertDefaults
220 -- ** @DELETE@
221 , Delete(..)
222 , delete
224 -- ** @UPDATE@
225 , update
226 , Update(..)
228 -- ** @.. RETURNING@
229 , Returning(..)
231 -- * TODO
232 -- TODO
233 -- These need organizing, but are reachable from Rel8's documentation so we
234 -- do need to export and document them.
235 , Nullabilizes
236 , HTable
237 , Labelable
238 , Context
239 , IsMaybe
240 ) where
242-- base
243import Prelude ()
245-- rel8
246import Rel8.Aggregate
247import Rel8.Expr
248import Rel8.Expr.Aggregate
249import Rel8.Expr.Bool
250import Rel8.Expr.Eq
251import Rel8.Expr.Function
252import Rel8.Expr.Null
253import Rel8.Expr.Ord
254import Rel8.Expr.Order
255import Rel8.Expr.Serialize
256import Rel8.Order
257import Rel8.Query
258import Rel8.Query.Aggregate
259import Rel8.Query.Distinct
260import Rel8.Query.Each
261import Rel8.Query.Either
262import Rel8.Query.Exists
263import Rel8.Query.Filter
264import Rel8.Query.Limit
265import Rel8.Query.List
266import Rel8.Query.Maybe
267import Rel8.Query.Null
268import Rel8.Query.Order
269import Rel8.Query.SQL (showQuery)
270import Rel8.Query.Set
271import Rel8.Query.These
272import Rel8.Query.Values
273import Rel8.Schema.Column
274import Rel8.Schema.Context (Name, Insertion)
275import Rel8.Schema.Context.Label
276import Rel8.Schema.Generic
277import Rel8.Schema.HTable
278import Rel8.Schema.Nullability
279import Rel8.Schema.Table
280import Rel8.Statement.Delete
281import Rel8.Statement.Insert
282import Rel8.Statement.Returning
283import Rel8.Statement.Select
284import Rel8.Statement.Update
285import Rel8.Table
286import Rel8.Table.Aggregate
287import Rel8.Table.Alternative
288import Rel8.Table.Bool
289import Rel8.Table.Either
290import Rel8.Table.Eq
291import Rel8.Table.Insert
292import Rel8.Table.List
293import Rel8.Table.Maybe
294import Rel8.Table.Name
295import Rel8.Table.NonEmpty
296import Rel8.Table.Ord
297import Rel8.Table.Order
298import Rel8.Table.Recontextualize
299import Rel8.Table.Serialize
300import Rel8.Table.These
301import Rel8.Type
302import Rel8.Type.Eq
303import Rel8.Type.Information
304import Rel8.Type.JSONEncoded
305import Rel8.Type.JSONBEncoded
306import Rel8.Type.Monoid
307import Rel8.Type.Num
308import Rel8.Type.Ord
309import Rel8.Type.ReadShow
310import Rel8.Type.Semigroup
311import Rel8.Type.String
312import Rel8.Type.Sum
313import Rel8.Expr.Opaleye (unsafeCastExpr)
316-- $setup
317--
318-- In this section, we'll take a look at using Rel8 to work with a small
319-- database for Haskell packages. We'll take a look at idiomatic usage of Rel8,
320-- mapping tables to Haskell, and then look at writing some simple queries.
321--
322-- The documentation in this library uses the following language extensions:
323--
324-- >>> :set -XBlockArguments
325-- >>> :set -XDeriveAnyClass
326-- >>> :set -XDeriveGeneric
327-- >>> :set -XDerivingStrategies
328-- >>> :set -XDerivingVia
329-- >>> :set -XDuplicateRecordFields
330-- >>> :set -XGeneralizedNewtypeDeriving
331-- >>> :set -XOverloadedStrings
332-- >>> :set -XStandaloneDeriving
333-- >>> :set -XTypeApplications
334-- >>> :set -XTypeFamilies
335--
336-- Before we start writing any Haskell, let's take a look at the schema we'll
337-- work with.
338--
339-- > author: project:
340-- >
341-- > Column │ Type │ Nullable Column │ Type │ Nullable
342-- > ═══════════╪═════════╪══════════ ═══════════╪═════════╪══════════
343-- > author_id │ integer │ not null author_id │ integer │ not null
344-- > name │ text │ not null name │ text │ not null
345-- > url │ text │
346--
347-- Our schema consists of two tables - @author@ and @project@. A @project@
348-- always has an @author@, but not all @author@s have projects. Each @author@
349-- has a name and (maybe) an associated website, and each project has a name.
350--
351-- Now that we've seen our schema, we can begin writing a mapping in Rel8. The
352-- idiomatic way to map a table is to use a record that is parameterised what
353-- Rel8 calls an /interpretation functor/, and to define each field with
354-- 'Column'. For this type to be usable with Rel8 we need it to be an instance
355-- of 'HigherKindedTable', which can be derived with a combination of
356-- @DeriveAnyClass@ and @DeriveGeneric@.
357--
358-- Following these steps for @author@, we have:
359--
360-- > data Author f = Author
361-- > { authorId :: Column f Int64
362-- > , name :: Column f Text
363-- > , url :: Column f (Maybe Text)
364-- > } deriving (Generic, HigherKindedTable)
365--
366-- This is a perfectly reasonable definition, but cautious readers might notice
367-- a problem - in particular, with the type of the @authorId@ field. While
368-- @Int64@ is correct, it's not the best type. If we had other identifier types
369-- in our project, it would be too easy to accidentally mix them up and create
370-- nonsensical joins. As Haskell programmers, we often solve this problem by
371-- creating @newtype@ wrappers, and we can also use this technique with Rel8:
372--
373-- >>> :{
374-- newtype AuthorId = AuthorId { toInt64 :: Int64 }
375-- deriving newtype (DBEq, DBType, Eq, Show)
376-- :}
377--
378-- Now we can write our final schema mapping. First, the @author@ table:
379--
380-- >>> :{
381-- data Author f = Author
382-- { authorId :: Column f AuthorId
383-- , authorName :: Column f Text
384-- , authorUrl :: Column f (Maybe Text)
385-- }
386-- deriving stock Generic
387-- deriving anyclass HigherKindedTable
388-- :}
389--
390-- And similarly, the @project@ table:
391--
392-- >>> :{
393-- data Project f = Project
394-- { projectAuthorId :: Column f AuthorId
395-- , projectName :: Column f Text
396-- }
397-- deriving stock Generic
398-- deriving anyclass HigherKindedTable
399-- :}
400--
401-- To show query results in this documentation, we'll also need @Show@
402-- instances: Unfortunately these definitions look a bit scary, but they are
403-- essentially just @deriving (Show)@:
404--
405-- >>> deriving stock instance f ~ Identity => Show (Author f)
406-- >>> deriving stock instance f ~ Identity => Show (Project f)
407--
408-- These data types describe the structural mapping of the tables, but we also
409-- need to specify a 'TableSchema' for each table. A @TableSchema@ contains the
410-- name of the table and the name of all columns in the table, which will
411-- ultimately allow us to @SELECT@ and @INSERT@ rows for these tables.
412--
413-- To define a @TableSchema@, we just need to fill construct appropriate
414-- @TableSchema@ values. When it comes to the @tableColumns@ field, we
415-- construct values of our data types above, and set each field to the name of
416-- the column that it maps to:
417--
418-- First, @authorSchema@ describes the column names of the @author@ table when
419-- associated with the @Author@ type:
420--
421-- >>> :{
422-- authorSchema :: TableSchema (Author ColumnSchema)
423-- authorSchema = TableSchema
424-- { tableName = "author"
425-- , tableSchema = Nothing
426-- , tableColumns = Author
427-- { authorId = "author_id"
428-- , authorName = "name"
429-- , authorUrl = "url"
430-- }
431-- }
432-- :}
433--
434-- And likewise for @project@ and @Project@:
435--
436-- >>> :{
437-- projectSchema :: TableSchema (Project ColumnSchema)
438-- projectSchema = TableSchema
439-- { tableName = "project"
440-- , tableSchema = Nothing
441-- , tableColumns = Project
442-- { projectAuthorId = "author_id"
443-- , projectName = "name"
444-- }
445-- }
446-- :}
447--
448-- Aside: you might be wondering why this information isn't in the definitions
449-- of @Author@ and @Project@ above. Rel8 decouples @TableSchema@ from the data
450-- types themselves, as not all tables you define will necessarily have a
451-- schema. For example, Rel8 allows you to define helper types to simplify the
452-- types of queries - these tables only exist at query time, but there is no
453-- corresponding base table. We'll see more on this idea later!
454--
455-- With these table definitions, we can now start writing some queries! To
456-- actually run a query, we'll need a database connection. Rel8 uses the
457-- @hasql@ library, and for this documentation we'll get the connection string
458-- from the @$TEST_DATABASE_URL@ environment variable.
459--
460-- >>> connectionString <- System.Environment.getEnv "TEST_DATABASE_URL"
461-- >>> Right c <- Hasql.Connection.acquire (Data.ByteString.Char8.pack connectionString)
462-- >>> Control.Monad.void $ Hasql.Session.run (Hasql.Session.sql "BEGIN") c
464-- $guideQueries
465--
466-- First, we'll take a look at @SELECT@ statements - usually the bulk of most
467-- database heavy applications.
468--
469-- In Rel8, @SELECT@ statements are built using the 'Query' monad. You can
470-- think of this monad like the ordinary @[]@ (List) monad - but this isn't
471-- required knowledge.
472--
473-- To start, we'll look at one of the simplest queries possible - a basic
474-- @SELECT * FROM@ statement. To select all rows from a table, we use 'each',
475-- and supply a 'TableSchema'. So to select all @project@ rows, we can write:
476--
477-- >>> :t each projectSchema
478-- each projectSchema :: Query (Project Expr)
479--
480-- Notice that @each@ gives us a @Query@ that yields @Project Expr@ rows. To
481-- see what this means, let's have a look at a single field of a
482-- @Project Expr@:
483--
484-- >>> let aProjectExpr = undefined :: Project Expr
485-- >>> :t projectAuthorId aProjectExpr
486-- projectAuthorId aProjectExpr :: Expr AuthorId
487--
488-- Recall we defined @projectAuthorId@ as @Column f AuthorId@. Now we have @f@
489-- is @Expr@, and @Column Expr AuthorId@ reduces to @Expr AuthorId@. We'll see
490-- more about @Expr@ soon, but you can think of @Expr a@ as "SQL expressions of
491-- type @a@".
492--
493-- To execute this @Query@, we pass it to 'select':
494--
495-- >>> :t select c (each projectSchema)
496-- select c (each projectSchema) :: MonadIO m => m [Project Identity]
497--
498-- When we @select@ things containing @Expr@s, Rel8 builds a new response table
499-- with the @Identity@ interpretation. This means you'll get back plain Haskell
500-- values. Studying @projectAuthorId@ again, we have:
501--
502-- >>> let aProjectIdentity = undefined :: Project Identity
503-- >>> :t projectAuthorId aProjectIdentity
504-- projectAuthorId aProjectIdentity :: AuthorId
505--
506-- Here @Column Identity AuthorId@ reduces to just @AuthorId@, with no
507-- wrappping type at all.
508--
509-- Putting this all together, we can run our first query:
510--
511-- >>> select c (each projectSchema) >>= mapM_ print
512-- Project {projectAuthorId = 1, projectName = "rel8"}
513-- Project {projectAuthorId = 2, projectName = "aeson"}
514-- Project {projectAuthorId = 2, projectName = "text"}
515--
516-- Cool!
517--
518-- We now know that 'each' is the equivalent of a @SELECT *@ query, but
519-- sometimes we're only interested in a subset of the columns of a table. To
520-- restrict the returned columns, we can specify a projection by using 'Query's
521-- @Functor@ instance:
522--
523-- >>> select c $ projectName <$> each projectSchema
524-- ["rel8","aeson","text"]
526-- $guideJoins
527--
528-- Another common operation in relational databases is to take the @JOIN@ of
529-- multiple tables. Rel8 doesn't have a specific join operation, but we can
530-- recover the functionality of a join by selecting all rows of two tables, and
531-- then using 'where_' to filter them.
532--
533-- To see how this works, first let's look at taking the product of two tables.
534-- We can do this by simply calling 'each' twice, and then returning a tuple of
535-- their results.
536--
537-- >>> :{
538-- mapM_ print =<< select c do
539-- author <- each authorSchema
540-- project <- each projectSchema
541-- return (projectName project, authorName author)
542-- :}
543-- ("rel8","Ollie")
544-- ("rel8","Bryan O'Sullivan")
545-- ("rel8","Emily Pillmore")
546-- ("aeson","Ollie")
547-- ("aeson","Bryan O'Sullivan")
548-- ("aeson","Emily Pillmore")
549-- ("text","Ollie")
550-- ("text","Bryan O'Sullivan")
551-- ("text","Emily Pillmore")
552--
553-- This isn't quite right, though, as we have ended up pairing up the wrong
554-- projects and authors. To fix this, we can use 'where_' to restrict the
555-- returned rows. We could write:
556--
557-- > select c $ do
558-- > author <- each authorSchema
559-- > project <- each projectSchema
560-- > where_ $ projectAuthorId project ==. authorId author
561-- > return (project, author)
562--
563-- but doing this every time you need a join can obscure the meaning of the
564-- query you're writing. A good practice is to introduce specialised functions
565-- for the particular joins in your database. In our case, this would be:
566--
567-- >>> :{
568-- projectsForAuthor :: Author Expr -> Query (Project Expr)
569-- projectsForAuthor a = each projectSchema >>= filter \p ->
570-- projectAuthorId p ==. authorId a
571-- :}
572--
573-- Our final query is then:
574--
575-- >>> :{
576-- mapM_ print =<< select c do
577-- author <- each authorSchema
578-- project <- projectsForAuthor author
579-- return (projectName project, authorName author)
580-- :}
581-- ("rel8","Ollie")
582-- ("aeson","Bryan O'Sullivan")
583-- ("text","Bryan O'Sullivan")
584--
585-- == @LEFT JOIN@s
586--
587-- Rel8 is also capable of performing @LEFT JOIN@s. To perform @LEFT JOIN@s, we
588-- follow the same approach as before, but use the 'optional' query transformer
589-- to allow for the possibility of the join to fail.
590--
591-- In our test database, we can see that there's another author who we haven't
592-- seen yet:
593--
594-- >>> select c $ authorName <$> each authorSchema
595-- ["Ollie","Bryan O'Sullivan","Emily Pillmore"]
596--
597-- Emily wasn't returned in our earlier query because - in our database - she
598-- doesn't have any registered projects. We can account for this partiality in
599-- our original query by wrapping the @projectsForAuthor@ call with 'optional':
600--
601-- >>> :{
602-- mapM_ print =<< select c do
603-- author <- each authorSchema
604-- mproject <- optional $ projectsForAuthor author
605-- return (authorName author, projectName <$> mproject)
606-- :}
607-- ("Ollie",Just "rel8")
608-- ("Bryan O'Sullivan",Just "aeson")
609-- ("Bryan O'Sullivan",Just "text")
610-- ("Emily Pillmore",Nothing)
612-- $guideAggregation
613--
614-- Aggregations are operations like @sum@ and @count@ - operations that reduce
615-- multiple rows to single values. To perform aggregations in Rel8, we can use
616-- the 'aggregate' function, which takes a 'Query' of aggregated expressions,
617-- runs the aggregation, and returns aggregated rows.
618--
619-- To start, let's look at a simple aggregation that tells us how many projects
620-- exist:
621--
622-- >>> error "TODO"
623--
624-- Rel8 is also capable of aggregating multiple rows into a single row by
625-- concatenating all rows as a list. This aggregation allows us to break free
626-- of the row-orientated nature of SQL and write queries that return tree-like
627-- structures. Earlier we saw an example of returning authors with their
628-- projects, but the query didn't do a great job of describing the one-to-many
629-- relationship between authors and their projects.
630--
631-- Let's look again at a query that returns authors and their projects, and
632-- focus on the /type/ of that query.
633--
634-- >>> :{
635-- projectsForAuthor a = each projectSchema >>= filter \p ->
636-- projectAuthorId p ==. authorId a
637-- :}
638--
639-- >>> :{
640-- let authorsAndProjects = do
641-- author <- each authorSchema
642-- project <- projectsForAuthor author
643-- return (author, project)
644-- where
645-- :}
646--
647-- >>> :t select c authorsAndProjects
648-- select c authorsAndProjects
649-- :: MonadIO m => m [(Author Identity, Project Identity)]
650--
651-- Our query gives us a single list of pairs of authors and projects. However,
652-- with our domain knowledge of the schema, this isn't a great type - what we'd
653-- rather have is a list of pairs of authors and /lists/ of projects. That is,
654-- what we'd like is:
655--
656-- > [(Author Identity, [Project Identity])]
657--
658-- This would be a much better type! Rel8 can produce a query with this type by
659-- simply wrapping the call to @projectsForAuthor@ with either 'some' or
660-- 'many'. Here we'll use 'many', which allows for the possibility of an author
661-- to have no projects:
662--
663-- >>> :{
664-- mapM_ print =<< select c do
665-- author <- each authorSchema
666-- projectNames <- many $ projectName <$> projectsForAuthor author
667-- return (authorName author, projectNames)
668-- :}
669-- ("Ollie",["rel8"])
670-- ("Bryan O'Sullivan",["aeson","text"])
671-- ("Emily Pillmore",[])