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