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