on master help sign in
Add NullTable (#173)

This is an alternative to `MaybeTable` that doesn't use a tag columns. It's less flexible (no `Functor` or `Applicative` instance) and is meaningless when used with a table that has no non-nullable columns (so nesting `NullTable` is redundant). But in situations where the underlying `Table` does have non-nullable columns, it can losslessly converted to and from `MaybeTable`. It is useful for embedding into a base table when you don't want to store the extra tag column in your schema. Co-authored-by: Ollie Charles <ollie@ocharles.org.uk>

commit99e868b84bb2fcabf538772cd7fd6459c6d61118 authorShane <shane.obrien@circuithub.com> dateFri 20 May 2022, 12:34 (4 years ago) parentd0a54b7
6 files changed+216
rel8.cabal+2 src/Rel8.hs+11 src/Rel8/Column/Null.hs+26 src/Rel8/Query/Null.hs+15 src/Rel8/Table/Null.hs+135 src/Rel8/Table/Nullify.hs+27
rel8.cabal+2
@@ -66,6 +66,7 @@ library
6666 Rel8.Column.List
6767 Rel8.Column.Maybe
6868 Rel8.Column.NonEmpty
69+ Rel8.Column.Null
6970 Rel8.Column.These
7071
7172 Rel8.Expr
@@ -167,6 +168,7 @@ library
167168 Rel8.Table.Maybe
168169 Rel8.Table.Name
169170 Rel8.Table.NonEmpty
171+ Rel8.Table.Null
170172 Rel8.Table.Nullify
171173 Rel8.Table.Opaleye
172174 Rel8.Table.Ord
src/Rel8.hs+11
@@ -38,6 +38,7 @@ module Rel8
3838 , HMaybe
3939 , HList
4040 , HNonEmpty
41+ , HNull
4142 , HThese
4243 , Lift
4344
@@ -107,6 +108,14 @@ module Rel8
107108 , catNonEmptyTable
108109 , catNonEmpty
109110
111+ -- ** @NullTable@
112+ , NullTable
113+ , nullableTable, nullTable, nullifyTable
114+ , isNullTable, isNonNullTable
115+ , catNullTable
116+ , nameNullTable
117+ , fromMaybeTable, toMaybeTable
118+
110119 -- ** @ADT@
111120 , ADT, ADTable
112121 , BuildADT, buildADT
@@ -306,6 +315,7 @@ import Rel8.Column.Lift
306315import Rel8.Column.List
307316import Rel8.Column.Maybe
308317import Rel8.Column.NonEmpty
318+import Rel8.Column.Null
309319import Rel8.Column.These
310320import Rel8.Expr
311321import Rel8.Expr.Aggregate
@@ -367,6 +377,7 @@ import Rel8.Table.List
367377import Rel8.Table.Maybe
368378import Rel8.Table.Name
369379import Rel8.Table.NonEmpty
380+import Rel8.Table.Null
370381import Rel8.Table.Opaleye ( castTable )
371382import Rel8.Table.Ord
372383import Rel8.Table.Order
src/Rel8/Column/Null.hsnew file+26
@@ -0,0 +1,26 @@
1+{-# language DataKinds #-}
2+{-# language StandaloneKindSignatures #-}
3+{-# language TypeFamilyDependencies #-}
4+
5+module Rel8.Column.Null
6+ ( HNull
7+ )
8+where
9+
10+-- base
11+import Data.Kind ( Type )
12+import Prelude
13+
14+-- rel8
15+import qualified Rel8.Schema.Kind as K
16+import Rel8.Schema.Result ( Result )
17+import Rel8.Table.Null ( NullTable )
18+
19+
20+-- | Nest a 'Null' value within a 'Rel8able'. @HNull f a@ will produce a
21+-- 'NullTable' @a@ in the 'Expr' context, and a @'Maybe' a@ in the 'Result'
22+-- context.
23+type HNull :: K.Context -> Type -> Type
24+type family HNull context = maybe | maybe -> context where
25+ HNull Result = Maybe
26+ HNull context = NullTable context
src/Rel8/Query/Null.hs+15
@@ -1,5 +1,8 @@
1+{-# language FlexibleContexts #-}
2+
13module Rel8.Query.Null
24 ( catNull
5+ , catNullTable
36 )
47where
58
@@ -9,6 +12,8 @@ import Prelude
912-- rel8
1013import Rel8.Expr ( Expr )
1114import Rel8.Expr.Null ( isNonNull, unsafeUnnullify )
15+import Rel8.Table ( Table )
16+import Rel8.Table.Null ( NullTable, isNonNullTable, unsafeUnnullifyTable )
1217import Rel8.Query ( Query )
1318import Rel8.Query.Filter ( where_ )
1419
@@ -21,3 +26,13 @@ catNull :: Expr (Maybe a) -> Query (Expr a)
2126catNull a = do
2227 where_ $ isNonNull a
2328 pure $ unsafeUnnullify a
29+
30+
31+-- | Filter a 'Query' that might return @nullTable@ to a 'Query' without any
32+-- @nullTable@s.
33+--
34+-- Corresponds to 'Data.Maybe.catMaybes'.
35+catNullTable :: Table Expr a => NullTable Expr a -> Query a
36+catNullTable a = do
37+ where_ $ isNonNullTable a
38+ pure $ unsafeUnnullifyTable a
src/Rel8/Table/Null.hsnew file+135
@@ -0,0 +1,135 @@
1+{-# language DataKinds #-}
2+{-# language FlexibleContexts #-}
3+{-# language FlexibleInstances #-}
4+{-# language MultiParamTypeClasses #-}
5+{-# language ScopedTypeVariables #-}
6+{-# language StandaloneKindSignatures #-}
7+{-# language TypeApplications #-}
8+{-# language TypeFamilies #-}
9+{-# language UndecidableInstances #-}
10+
11+module Rel8.Table.Null
12+ ( NullTable(..)
13+ , nullableTable, nullTable, nullifyTable, unsafeUnnullifyTable
14+ , isNullTable, isNonNullTable
15+ , nameNullTable
16+ , toMaybeTable, fromMaybeTable
17+ )
18+where
19+
20+-- base
21+import Data.Kind ( Type )
22+import Prelude hiding ( null, undefined )
23+
24+-- comonad
25+import Control.Comonad ( extract )
26+
27+-- rel8
28+import Rel8.Expr ( Expr )
29+import Rel8.Expr.Bool ( not_ )
30+import Rel8.Kind.Context ( Reifiable )
31+import qualified Rel8.Schema.Kind as K
32+import Rel8.Schema.Name ( Name )
33+import Rel8.Table
34+ ( Table, Columns, Context, fromColumns, toColumns
35+ , FromExprs, fromResult, toResult
36+ , Transpose
37+ )
38+import Rel8.Table.Bool ( bool )
39+import Rel8.Table.Eq ( EqTable, eqTable )
40+import Rel8.Table.Maybe ( MaybeTable, justTable, maybeTable, nothingTable )
41+import Rel8.Table.Nullify ( Nullify, isNull )
42+import Rel8.Table.Ord ( OrdTable, ordTable )
43+import Rel8.Table.Projection ( Projectable, project )
44+import Rel8.Table.Serialize ( ToExprs )
45+import Rel8.Table.Undefined ( undefined )
46+
47+
48+-- | @NullTable t@ is the table @t@, but where all the columns in @t@ have the
49+-- possibility of being 'Rel8.null'. This is very similar to
50+-- 'Rel8.MaybeTable', except that it does not use an extra tag field, so it
51+-- cannot distinguish between @Nothing@ and @Just Nothing@ if nested. In other
52+-- words, if all of the columns of the @t@ passed to @NullTable@ are already
53+-- nullable, then @NullTable@ has no effect.
54+type NullTable :: K.Context -> Type -> Type
55+newtype NullTable context a = NullTable (Nullify context a)
56+
57+
58+instance Projectable (NullTable context) where
59+ project f (NullTable a) = NullTable (project f a)
60+
61+
62+instance (Table context a, Reifiable context, context ~ context') =>
63+ Table context' (NullTable context a)
64+ where
65+ type Columns (NullTable context a) = Columns (Nullify context a)
66+ type Context (NullTable context a) = Context (Nullify context a)
67+ type FromExprs (NullTable context a) = FromExprs (Nullify context a)
68+ type Transpose to (NullTable context a) = NullTable to (Transpose to a)
69+
70+ toColumns (NullTable a) = toColumns a
71+ fromColumns = NullTable . fromColumns
72+
73+ toResult = toResult @_ @(Nullify context a)
74+ fromResult = fromResult @_ @(Nullify context a)
75+
76+
77+instance (EqTable a, context ~ Expr) => EqTable (NullTable context a) where
78+ eqTable = eqTable @(Nullify context a)
79+
80+
81+instance (OrdTable a, context ~ Expr) => OrdTable (NullTable context a) where
82+ ordTable = ordTable @(Nullify context a)
83+
84+
85+instance (ToExprs exprs a, context ~ Expr) =>
86+ ToExprs (NullTable context exprs) (Maybe a)
87+
88+
89+-- | Check if any of the non-nullable fields of @a@ are 'Rel8.null' under the
90+-- 'NullTable'. Returns 'Rel8.false' if @a@ has no non-nullable fields.
91+isNullTable :: Table Expr a => NullTable Expr a -> Expr Bool
92+isNullTable (NullTable a) = isNull a
93+
94+
95+-- | The inverse of 'isNullTable'.
96+isNonNullTable :: Table Expr a => NullTable Expr a -> Expr Bool
97+isNonNullTable = not_ . isNullTable
98+
99+
100+-- | Like 'Rel8.nullable'.
101+nullableTable :: (Table Expr a, Table Expr b)
102+ => b -> (a -> b) -> NullTable Expr a -> b
103+nullableTable b f ma@(NullTable a) = bool (f (extract a)) b (isNullTable ma)
104+
105+
106+-- | The null table. Like 'Rel8.null'.
107+nullTable :: Table Expr a => NullTable Expr a
108+nullTable = NullTable (pure undefined)
109+
110+
111+-- | Lift any table into 'NullTable'. Like 'Rel8.nullify'.
112+nullifyTable :: a -> NullTable Expr a
113+nullifyTable = NullTable . pure
114+
115+
116+unsafeUnnullifyTable :: NullTable Expr a -> a
117+unsafeUnnullifyTable (NullTable a) = extract a
118+
119+
120+-- | Construct a 'NullTable' in the 'Name' context. This can be useful if you
121+-- have a 'NullTable' that you are storing in a table and need to construct a
122+-- 'TableSchema'.
123+nameNullTable :: a -> NullTable Name a
124+nameNullTable = NullTable . pure
125+
126+
127+-- | Convert a 'NullTable' to a 'MaybeTable'.
128+toMaybeTable :: Table Expr a => NullTable Expr a -> MaybeTable Expr a
129+toMaybeTable = nullableTable nothingTable justTable
130+
131+
132+-- | Convert a 'MaybeTable' to a 'NullTable'. Note that if the underlying @a@
133+-- has no non-nullable fields, this is a lossy conversion.
134+fromMaybeTable :: Table Expr a => MaybeTable Expr a -> NullTable Expr a
135+fromMaybeTable = maybeTable nullTable nullifyTable
src/Rel8/Table/Nullify.hs+27
@@ -1,7 +1,9 @@
11{-# language DataKinds #-}
2+{-# language FlexibleContexts #-}
23{-# language FlexibleInstances #-}
34{-# language LambdaCase #-}
45{-# language MultiParamTypeClasses #-}
6+{-# language NamedFieldPuns #-}
57{-# language ScopedTypeVariables #-}
68{-# language StandaloneKindSignatures #-}
79{-# language TypeApplications #-}
@@ -12,11 +14,13 @@ module Rel8.Table.Nullify
1214 ( Nullify
1315 , aggregateNullify
1416 , guard
17+ , isNull
1518 )
1619where
1720
1821-- base
1922import Control.Applicative ( liftA2 )
23+import Data.Functor.Const ( Const( Const ), getConst )
2024import Data.Functor.Identity ( runIdentity )
2125import Data.Kind ( Type )
2226import Prelude
@@ -27,6 +31,8 @@ import Control.Comonad ( Comonad, duplicate, extract, ComonadApply, (<@>) )
2731-- rel8
2832import Rel8.Aggregate ( Aggregate )
2933import Rel8.Expr ( Expr )
34+import Rel8.Expr.Bool ( (||.), false )
35+import qualified Rel8.Expr.Null as Expr
3036import Rel8.Kind.Context ( Reifiable, contextSing )
3137import Rel8.Schema.Context.Nullify
3238 ( Nullifiability( NAggregate, NExpr )
@@ -45,12 +51,14 @@ import Rel8.Schema.HTable.Nullify
4551 , hproject
4652 )
4753import qualified Rel8.Schema.Kind as K
54+import Rel8.Schema.Null ( Nullity( NotNull, Null ) )
4855import qualified Rel8.Schema.Result as R
4956import Rel8.Table
5057 ( Table, Columns, Context, toColumns, fromColumns
5158 , FromExprs, fromResult, toResult
5259 , Transpose
5360 )
61+import Rel8.Schema.Spec ( Spec( Spec, nullity ) )
5462import Rel8.Table.Eq ( EqTable, eqTable )
5563import Rel8.Table.Ord ( OrdTable, ordTable )
5664import Rel8.Table.Projection ( Projectable, apply, project )
@@ -182,3 +190,22 @@ guard :: (Reifiable context, HTable t)
182190 -> HNullify t context
183191guard tag isNonNull isNonNullExpr =
184192 hguard (guarder contextSing tag isNonNull isNonNullExpr)
193+
194+
195+isNull :: forall a. Table Expr a => Nullify Expr a -> Expr Bool
196+isNull =
197+ maybe false getAny .
198+ getConst .
199+ hunnullify (\Spec {nullity} a -> Const $ case nullity of
200+ NotNull -> Just $ Any $ Expr.isNull a
201+ Null -> Nothing) .
202+ toColumns
203+
204+
205+newtype Any = Any
206+ { getAny :: Expr Bool
207+ }
208+
209+
210+instance Semigroup Any where
211+ Any a <> Any b = Any (a ||. b)