@@ -1,19 +1,7 @@
1{-# language DuplicateRecordFields #-}
1{-# language DuplicateRecordFields #-}
4− ( -- * Getting Started
16− -- * Database types
4+ ( -- * Database types
17 -- ** @DBType@
5 -- ** @DBType@
18 DBType(..)
6 DBType(..)
@@ -256,9 +244,11 @@ import Rel8.Expr.Bool
256import Rel8.Expr.Eq
244import Rel8.Expr.Eq
257import Rel8.Expr.Function
245import Rel8.Expr.Function
258import Rel8.Expr.Null
246import Rel8.Expr.Null
247+import Rel8.Expr.Opaleye (unsafeCastExpr)
259import Rel8.Expr.Ord
248import Rel8.Expr.Ord
260import Rel8.Expr.Order
249import Rel8.Expr.Order
261import Rel8.Expr.Serialize
250import Rel8.Expr.Serialize
251+import Rel8.Kind.Necessity
262import Rel8.Order
252import Rel8.Order
263import Rel8.Query
253import Rel8.Query
264import Rel8.Query.Aggregate
254import Rel8.Query.Aggregate
@@ -278,6 +268,7 @@ import Rel8.Query.These
278import Rel8.Query.Values
268import Rel8.Query.Values
279import Rel8.Schema.Column
269import Rel8.Schema.Column
280import Rel8.Schema.Context.Label
270import Rel8.Schema.Context.Label
271+import Rel8.Schema.Field (Field)
281import Rel8.Schema.Generic
272import Rel8.Schema.Generic
282import Rel8.Schema.HTable
273import Rel8.Schema.HTable
283import Rel8.Schema.Name
274import Rel8.Schema.Name
@@ -307,8 +298,8 @@ import Rel8.Table.These
307import Rel8.Type
298import Rel8.Type
308import Rel8.Type.Eq
299import Rel8.Type.Eq
309import Rel8.Type.Information
300import Rel8.Type.Information
310−import Rel8.Type.JSONEncoded
311import Rel8.Type.JSONBEncoded
301import Rel8.Type.JSONBEncoded
302+import Rel8.Type.JSONEncoded
312import Rel8.Type.Monoid
303import Rel8.Type.Monoid
313import Rel8.Type.Num
304import Rel8.Type.Num
314import Rel8.Type.Ord
305import Rel8.Type.Ord
@@ -316,364 +307,5 @@ import Rel8.Type.ReadShow
316import Rel8.Type.Semigroup
307import Rel8.Type.Semigroup
317import Rel8.Type.String
308import Rel8.Type.String
318import Rel8.Type.Sum
309import Rel8.Type.Sum
319−import Rel8.Expr.Opaleye (unsafeCastExpr)
320−import Rel8.Kind.Necessity
321−import Rel8.Schema.Field (Field)
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.
330−-- The documentation in this library uses the following language extensions:
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
344−-- Before we start writing any Haskell, let's take a look at the schema we'll
347−-- > author: project:
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
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.
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@.
366−-- Following these steps for @author@, we have:
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)
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:
382−-- newtype AuthorId = AuthorId { toInt64 :: Int64 }
383−-- deriving newtype (DBEq, DBType, Eq, Show)
386−-- Now we can write our final schema mapping. First, the @author@ table:
389−-- data Author f = Author
390−-- { authorId :: Column f AuthorId
391−-- , authorName :: Column f Text
392−-- , authorUrl :: Column f (Maybe Text)
394−-- deriving stock Generic
395−-- deriving anyclass HigherKindedTable
398−-- And similarly, the @project@ table:
401−-- data Project f = Project
402−-- { projectAuthorId :: Column f AuthorId
403−-- , projectName :: Column f Text
405−-- deriving stock Generic
406−-- deriving anyclass HigherKindedTable
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)@:
413−-- >>> deriving stock instance f ~ Identity => Show (Author f)
414−-- >>> deriving stock instance f ~ Identity => Show (Project f)
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.
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:
426−-- First, @authorSchema@ describes the column names of the @author@ table when
427−-- associated with the @Author@ type:
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"
442−-- And likewise for @project@ and @Project@:
445−-- projectSchema :: TableSchema (Project ColumnSchema)
446−-- projectSchema = TableSchema
447−-- { tableName = "project"
448−-- , tableSchema = Nothing
449−-- , tableColumns = Project
450−-- { projectAuthorId = "author_id"
451−-- , projectName = "name"
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!
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.
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
474−-- First, we'll take a look at @SELECT@ statements - usually the bulk of most
475−-- database heavy applications.
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.
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:
485−-- >>> :t each projectSchema
486−-- each projectSchema :: Query (Project Expr)
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
492−-- >>> let aProjectExpr = undefined :: Project Expr
493−-- >>> :t projectAuthorId aProjectExpr
494−-- projectAuthorId aProjectExpr :: Expr AuthorId
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
501−-- To execute this @Query@, we pass it to 'select':
503−-- >>> :t select c (each projectSchema)
504−-- select c (each projectSchema) :: MonadIO m => m [Project Identity]
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:
510−-- >>> let aProjectIdentity = undefined :: Project Identity
511−-- >>> :t projectAuthorId aProjectIdentity
512−-- projectAuthorId aProjectIdentity :: AuthorId
514−-- Here @Column Identity AuthorId@ reduces to just @AuthorId@, with no
515−-- wrappping type at all.
517−-- Putting this all together, we can run our first query:
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"}
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:
531−-- >>> select c $ projectName <$> each projectSchema
532−-- ["rel8","aeson","text"]
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.
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
546−-- mapM_ print =<< select c do
547−-- author <- each authorSchema
548−-- project <- each projectSchema
549−-- return (projectName project, authorName author)
552−-- ("rel8","Bryan O'Sullivan")
553−-- ("rel8","Emily Pillmore")
555−-- ("aeson","Bryan O'Sullivan")
556−-- ("aeson","Emily Pillmore")
558−-- ("text","Bryan O'Sullivan")
559−-- ("text","Emily Pillmore")
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:
566−-- > author <- each authorSchema
567−-- > project <- each projectSchema
568−-- > where_ $ projectAuthorId project ==. authorId author
569−-- > return (project, author)
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:
576−-- projectsForAuthor :: Author Expr -> Query (Project Expr)
577−-- projectsForAuthor a = each projectSchema >>= filter \p ->
578−-- projectAuthorId p ==. authorId a
581−-- Our final query is then:
584−-- mapM_ print =<< select c do
585−-- author <- each authorSchema
586−-- project <- projectsForAuthor author
587−-- return (projectName project, authorName author)
590−-- ("aeson","Bryan O'Sullivan")
591−-- ("text","Bryan O'Sullivan")
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.
599−-- In our test database, we can see that there's another author who we haven't
602−-- >>> select c $ authorName <$> each authorSchema
603−-- ["Ollie","Bryan O'Sullivan","Emily Pillmore"]
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':
610−-- mapM_ print =<< select c do
611−-- author <- each authorSchema
612−-- mproject <- optional $ projectsForAuthor author
613−-- return (authorName author, projectName <$> mproject)
615−-- ("Ollie",Just "rel8")
616−-- ("Bryan O'Sullivan",Just "aeson")
617−-- ("Bryan O'Sullivan",Just "text")
618−-- ("Emily Pillmore",Nothing)
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.
627−-- To start, let's look at a simple aggregation that tells us how many projects
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.
639−-- Let's look again at a query that returns authors and their projects, and
640−-- focus on the /type/ of that query.
643−-- projectsForAuthor a = each projectSchema >>= filter \p ->
644−-- projectAuthorId p ==. authorId a
648−-- let authorsAndProjects = do
649−-- author <- each authorSchema
650−-- project <- projectsForAuthor author
651−-- return (author, project)
655−-- >>> :t select c authorsAndProjects
656−-- select c authorsAndProjects
657−-- :: MonadIO m => m [(Author Identity, Project Identity)]
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:
664−-- > [(Author Identity, [Project Identity])]
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:
672−-- mapM_ print =<< select c do
673−-- author <- each authorSchema
674−-- projectNames <- many $ projectName <$> projectsForAuthor author
675−-- return (authorName author, projectNames)
677−-- ("Ollie",["rel8"])
678−-- ("Bryan O'Sullivan",["aeson","text"])
679−-- ("Emily Pillmore",[])