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